news 2026/6/20 17:08:10

SPI接口从入门到精通:时序、配置与实战调试全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SPI接口从入门到精通:时序、配置与实战调试全解析

1. 项目概述:为什么SPI接口值得你花时间搞懂?

如果你正在玩单片机、搞嵌入式开发,或者对硬件通信有一点点兴趣,那么“SPI”这个词你一定不陌生。它就像硬件世界里的“方言”,设备之间用它来快速、高效地“说悄悄话”。我见过太多新手,一上来就对着库函数调用SPI.transfer(),数据是能收发了,但心里总是不踏实:时钟相位和极性到底怎么设?为什么我的从设备没反应?全双工和半双工有啥区别?这些问题不搞清楚,调起程序来就像在摸黑走路,一个不小心就掉坑里。

这篇内容,就是要把SPI这层窗户纸彻底捅破。我们不只讲“是什么”,更要深挖“为什么”和“怎么用”。从最基础的4根线讲起,到时钟极性和相位的四种组合模式,再到实际项目中如何选型、配置、调试,最后分享一堆我踩过的坑和总结的“骚操作”。目标很明确:让你读完以后,不仅能看懂数据手册里的SPI时序图,更能独立设计、调试出一个稳定可靠的SPI通信系统。无论你是学生、工程师还是爱好者,这篇近万字的干货,都能帮你把SPI从“会用”提升到“精通”的层次。

2. SPI接口的核心设计思路与底层逻辑

2.1 从“主从对话”理解SPI的本质

SPI的全称是Serial Peripheral Interface,串行外设接口。这个名字听起来有点官方,我们可以把它想象成一场主设备(Master)和从设备(Slave)之间严格的“问答游戏”。

核心角色

  • 主设备 (Master):这场对话的发起者和节奏控制者。它产生时钟信号(SCLK),就像乐队的指挥,决定什么时候开始、什么时候结束、以及节奏快慢。
  • 从设备 (Slave):对话的响应者。它不主动发言,只在主设备提供的时钟节拍下,接收或发送数据。一个主设备可以同时“指挥”多个从设备,但同一时刻通常只与一个从设备进行有效数据交换。

为什么是“串行”?与并口一次传输8位、16位甚至32位数据不同,SPI是逐位(bit by bit)传输的。这样做牺牲了单次传输的带宽吗?表面上是的。但其优势在于极大的简化了物理连接(只需要少数几根线),降低了PCB布线的复杂度与成本,并且通过提高时钟频率(动辄几十MHz),其实际数据传输率可以非常高,完全能满足大多数外设(如Flash、传感器、显示屏)的需求。这是一种典型的“用时间换空间”的设计思路。

2.2 四线制基础与全双工优势

SPI最经典、最完整的形态是四线制。这四根线各司其职,缺一不可:

  1. SCLK (Serial Clock)串行时钟线,由主设备产生。这是整个通信系统的“心跳”。每一个时钟脉冲的上升沿或下降沿,都定义了一位数据的采样或输出时刻。没有时钟,数据就失去了传输的基准。
  2. MOSI (Master Out Slave In)主设备输出,从设备输入线。顾名思义,这是主设备向从设备发送数据的通道。
  3. MISO (Master In Slave Out)主设备输入,从设备输出线。这是从设备向主设备返回数据的通道。
  4. SS/CS (Slave Select / Chip Select)从设备选择线(或称片选线)。这是最关键的一根控制线。它通常是低电平有效。主设备通过将某条SS线拉低,来“选中”与之对应的那个从设备,告诉它:“接下来我要和你通话了”。未被选中的从设备必须将其MISO线置于高阻态,以避免总线冲突。

这四根线构成了一个全双工(Full-Duplex)的同步通信通道。全双工意味着数据可以同时在MOSI和MISO线上传输,主设备在发送一个字节的同时,也能接收一个字节。这一点非常重要,它使得SPI的效率很高。很多SPI设备的数据手册中,主设备发送的命令字(Command),其本身也作为时钟,同时从设备中“挤”出对应的数据字节(Data)。这是一个“一问一答”同时完成的过程。

注意:SS线有时也被称为CS线。在一些简单的单从设备系统中,如果从设备允许被永久选中,这根线甚至可以硬接地。但在多从设备系统或需要节能的系统中,必须由主设备GPIO来控制,以实现设备的选通与休眠。

2.3 时钟极性(CPOL)与相位(CPHA):时序的灵魂

这是SPI中最令人困惑,也最核心的概念。时序不对,通信全废。CPOL和CPHA共同定义了数据位相对于时钟沿的采样和建立关系。

  • CPOL (Clock Polarity) - 时钟极性

    • CPOL=0:时钟空闲状态为低电平。第一个时钟沿是上升沿。
    • CPOL=1:时钟空闲状态为高电平。第一个时钟沿是下降沿。
    • 你可以简单地记:看SCLK线在不传输数据时(空闲时)的状态。
  • CPHA (Clock Phase) - 时钟相位

    • CPHA=0:数据在第一个时钟边沿被采样(捕获),在第二个时钟边沿发生切换(输出)。
    • CPHA=1:数据在第二个时钟边沿被采样,在第一个时钟边沿发生切换。
    • 关键理解:“采样”是对输入方而言的(比如主设备采样MISO线),“切换”是对输出方而言的(比如从设备在MISO线上输出新数据位)。

两者组合,形成四种SPI模式,这是你必须刻在脑子里的:

模式CPOLCPHA空闲时钟采样边沿输出边沿常见应用
Mode 000低电平上升沿下降沿最常用,如很多SPI Flash
Mode 101低电平下降沿上升沿
Mode 210高电平下降沿上升沿
Mode 311高电平上升沿下降沿如SD卡(部分变体)

如何确定设备用哪种模式?没有捷径,必须查数据手册!在从设备的数据手册(Datasheet)的SPI接口或时序图(Timing Diagram)部分,一定会明确标注。你需要找到类似“CPHA=0, CPOL=0”的描述,或者直接看时序图,分析第一个数据位是在时钟的哪个边沿开始有效(这通常对应输出边沿),以及在哪个边沿被稳定采样。

实操心得:我习惯准备一个逻辑分析仪。当通信失败时,抓取SCLK、MOSI、MISO的波形,对照数据手册的时序图,一眼就能看出是模式设错了,还是数据位对齐有问题。这是最直接的调试方法。

3. SPI核心细节解析与高级工作模式

3.1 数据帧格式与位序(MSB/LSB)

除了时钟模式,数据帧的格式也需要主从双方约定一致。

  • 数据位宽:通常为8位(一个字节),但SPI协议本身不限制,可以是4位、12位、16位等。这需要根据从设备的要求来配置主设备的SPI控制器。
  • 位序 (Bit Order)
    • MSB First:最高有效位先传输。这是最常见的默认设置。例如,发送字节0xB5(二进制10110101),会先发送最高位的1
    • LSB First:最低有效位先传输。有些设备(如某些型号的OLED屏)会使用这种模式。

配置错误会导致数据解析完全错误。例如,主设备按MSB发送0xB5,从设备按LSB解读,收到的就会变成0xAD,风马牛不相及。

3.2 多从设备连接拓扑

如何让一个主设备连接多个SPI从设备?有三种主流方法:

  1. 标准SPI(独立片选)

    • 方法:主设备的SCLK、MOSI、MISO并联到所有从设备。每个从设备独占一条SS线连接到主设备的一个GPIO。
    • 优点:逻辑简单,通信独立,速度可以各自最优。
    • 缺点:占用主设备GPIO资源多(N个从设备需要N个GPIO做片选)。
    • 适用场景:从设备数量不多,且对GPIO资源不敏感的场景。
  2. SPI菊花链(Daisy Chain)

    • 方法:所有从设备共用一组SCLK和一条SS线。从设备A的MISO连接到设备B的MOSI,设备B的MISO连接到设备C的MOSI,以此类推,最后一个设备的MISO接回主设备的MISO。数据像链条一样依次穿过所有设备。
    • 优点:极大地节省了连线(只需要4根线)和GPIO(只需要1个片选)。
    • 缺点
      • 所有设备必须支持菊花链模式(并非所有SPI设备都支持)。
      • 通信效率低。主设备想读取链中最后一个设备的数据,必须发送足够多的时钟周期,让数据位依次“移位”通过前面所有设备。读写操作变得复杂。
      • 链中任何一个设备故障,可能导致整个链路通信中断。
    • 适用场景:多个完全相同的、支持菊花链的设备,如级联的LED驱动芯片(如TLC5940)、移位寄存器等。
  3. 软件模拟SPI

    • 方法:不使用MCU硬件SPI控制器,而是用普通GPIO口,通过软件精确控制电平变化来模拟SCLK、MOSI,并读取MISO,实现SPI时序。
    • 优点:极度灵活,不受硬件SPI引脚限制,可以任意指定GPIO。可以模拟任何特殊的、非标准的SPI变种时序。
    • 缺点:占用大量CPU时间,通信速度慢,且时序精度受软件中断、任务调度影响。
    • 适用场景:硬件SPI引脚被占用;需要与一个时序非常特殊的旧设备通信;在极其简单的MCU(无硬件SPI外设)上使用。

选型建议优先使用硬件SPI。它的时序由硬件保证,精确且不占用CPU,效率极高。只有在引脚冲突或特殊需求时,才考虑软件模拟。

3.3 SPI的变体:3线制、半双工与QSPI

基础四线全双工SPI是基石,但实际应用中会遇到它的各种“变体”。

  • 3线制SPI(半双工)

    • 有些设备为了节省引脚,将MOSI和MISO合并为一条双向数据线(SIO)。同一时刻,这根线只能用于发送接收,因此是半双工。
    • 操作逻辑:主设备需要先配置数据线的方向(输出模式)来发送命令,然后再切换方向(输入模式)来读取数据。通信协议中通常会有一个“方向切换位”来指示。
    • 常见设备:一些温湿度传感器、小容量的SPI EEPROM。
  • 双线制SPI

    • 更进一步,只有SCLK和一条双向数据线,甚至没有专用的片选(可能通过命令字寻址)。这更接近I2C,但在协议层仍是SPI的时序思想。较为少见。
  • QSPI (Quad SPI)

    • 这是SPI的“性能增强版”,旨在应对大容量Flash等需要高速读写的场景。
    • 核心变化:将数据线从1条(MOSI+MISO)扩展到了4条(IO0, IO1, IO2, IO3)。这4条线在时钟驱动下可以同时发送或接收数据,瞬间将数据带宽提升4倍。
    • 工作模式
      • 标准SPI模式:仅使用IO0和IO1(相当于MOSI和MISO)。
      • 双线输出模式:同时使用IO0和IO1发送数据。
      • 四线输出模式:同时使用全部4条线发送数据。
      • 四线I/O模式:4条线全部用作双向数据线,实现全双工四通道高速通信。
    • 应用:主要用于外接大容量串行NOR Flash,作为MCU的代码存储器(XIP, Execute In Place)或数据存储。现代很多高性能MCU都集成了QSPI控制器。

理解要点:这些变体都是基于基础SPI的时钟同步思想,在数据线上做文章。只要理解了最核心的时钟与数据沿的关系,这些变体都是触类旁通的。

4. SPI接口的完整实操配置与驱动编写

4.1 硬件连接检查清单

在写第一行代码之前,确保硬件连接万无一失:

  1. 电源与地:这是最基础也最易错的。确保主从设备共地!电平匹配(如3.3V设备与5V设备连接需电平转换)。
  2. 四线连接:SCLK, MOSI, MISO, SS一一对应连接。特别注意MOSI接MOSI,MISO接MISO,不要交叉!
  3. 上拉电阻:对于开漏输出的MISO线或片选线,可能需要上拉电阻(通常4.7kΩ-10kΩ)确保空闲状态稳定。很多MCU的GPIO内部可配置上拉,可优先使用。
  4. 走线考虑:对于高速SPI(>10MHz),尽量缩短走线长度,避免平行走线以减少串扰,必要时在SCLK和MOSI上串联小电阻(22-33欧姆)阻尼反射。

4.2 基于STM32的硬件SPI配置详解(以HAL库为例)

我们以STM32的硬件SPI外设为例,展示一个完整的配置流程。假设我们连接一个Mode 0, MSB First的SPI Flash。

// 1. SPI外设句柄定义 SPI_HandleTypeDef hspi1; // 2. SPI初始化配置函数 void SPI1_Init(void) { hspi1.Instance = SPI1; // 使用SPI1外设 hspi1.Init.Mode = SPI_MODE_MASTER; // 主模式 hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 全双工(两线) hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 数据帧8位 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0,空闲低电平 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0,第一个边沿采样 // 注意:HAL库中 SPI_PHASE_1EDGE 对应 CPHA=0, SPI_PHASE_2EDGE 对应 CPHA=1 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件控制NSS(即我们手动控制GPIO作片选) hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64; // 波特率预分频 // 计算:SPI时钟 = APB2总线时钟 / 预分频值。若APB2=72MHz,则SPI时钟=1.125MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // MSB先行 hspi1.Init.TIMode = SPI_TIMODE_DISABLE; // 禁用TI模式(标准SPI模式) hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; // 禁用CRC hspi1.Init.CRCPolynomial = 10; // 即使不用CRC也需设置一个值 if (HAL_SPI_Init(&hspi1) != HAL_OK) { Error_Handler(); // 初始化失败处理 } } // 3. 片选GPIO控制函数(以PA4为例) #define SPI_FLASH_CS_PIN GPIO_PIN_4 #define SPI_FLASH_CS_PORT GPIOA void SPI_FLASH_CS_LOW(void) { HAL_GPIO_WritePin(SPI_FLASH_CS_PORT, SPI_FLASH_CS_PIN, GPIO_PIN_RESET); } void SPI_FLASH_CS_HIGH(void) { HAL_GPIO_WritePin(SPI_FLASH_CS_PORT, SPI_FLASH_CS_PIN, GPIO_PIN_SET); } // 4. 基础数据收发函数(阻塞式) uint8_t SPI_ReadWriteByte(uint8_t TxData) { uint8_t RxData; HAL_SPI_TransmitReceive(&hspi1, &TxData, &RxData, 1, 1000); // 超时1秒 return RxData; } // 5. 读取Flash ID的示例(Flash命令:0x9F) uint32_t SPI_Flash_ReadID(void) { uint32_t ID = 0; SPI_FLASH_CS_LOW(); // 拉低片选,开始通信 SPI_ReadWriteByte(0x9F); // 发送读ID命令 ID |= (SPI_ReadWriteByte(0xFF) << 16); // 读制造商ID(如0xEF) ID |= (SPI_ReadWriteByte(0xFF) << 8); // 读存储器类型 ID |= SPI_ReadWriteByte(0xFF); // 读容量ID SPI_FLASH_CS_HIGH(); // 拉高片选,结束通信 return ID; }

配置要点解析

  • SPI_NSS_SOFT:强烈建议使用软件控制NSS(即用普通GPIO作片选)。硬件NSS模式在某些场景下时序控制不够灵活。
  • BaudRatePrescaler:通信速度需根据从设备支持的最高时钟和你的布线情况设置。从低速(如125kHz)开始调试,稳定后再逐步提高。
  • HAL_SPI_TransmitReceive:这是全双工传输的核心函数。即使你只想发送(忽略接收的数据),或只想接收(需要发送哑元数据,如0xFF),也最好使用这个函数,因为它符合SPI全双工的工作机制。

4.3 软件模拟SPI的实现

当硬件SPI不可用时,软件模拟是救星。以下是模拟Mode 0的示例:

// 定义GPIO引脚(以STM32 HAL库为例) #define SIM_SPI_SCK_PIN GPIO_PIN_5 #define SIM_SPI_SCK_PORT GPIOA #define SIM_SPI_MOSI_PIN GPIO_PIN_6 #define SIM_SPI_MOSI_PORT GPIOA #define SIM_SPI_MISO_PIN GPIO_PIN_7 #define SIM_SPI_MISO_PORT GPIOA #define SIM_SPI_CS_PIN GPIO_PIN_4 #define SIM_SPI_CS_PORT GPIOA // 初始化GPIO void SIM_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; // SCK, MOSI, CS 配置为推挽输出 GPIO_InitStruct.Pin = SIM_SPI_SCK_PIN | SIM_SPI_MOSI_PIN | SIM_SPI_CS_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(SIM_SPI_SCK_PORT, &GPIO_InitStruct); // MISO 配置为输入 GPIO_InitStruct.Pin = SIM_SPI_MISO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; HAL_GPIO_Init(SIM_SPI_MISO_PORT, &GPIO_InitStruct); // 设置初始状态:SCK空闲低(CPOL=0),CS高(不选中) HAL_GPIO_WritePin(SIM_SPI_SCK_PORT, SIM_SPI_SCK_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(SIM_SPI_CS_PORT, SIM_SPI_CS_PIN, GPIO_PIN_SET); } // 软件模拟SPI传输一个字节 (Mode 0, MSB first) uint8_t SIM_SPI_TransferByte(uint8_t txData) { uint8_t rxData = 0; // 拉低片选,开始传输 HAL_GPIO_WritePin(SIM_SPI_CS_PORT, SIM_SPI_CS_PIN, GPIO_PIN_RESET); // 短暂延时,满足从设备建立时间 // __NOP(); 或 for(int i=0;i<5;i++); for (int i = 0; i < 8; i++) { // 1. 设置MOSI输出当前最高位 (MSB First) if (txData & 0x80) { HAL_GPIO_WritePin(SIM_SPI_MOSI_PORT, SIM_SPI_MOSI_PIN, GPIO_PIN_SET); } else { HAL_GPIO_WritePin(SIM_SPI_MOSI_PORT, SIM_SPI_MOSI_PIN, GPIO_PIN_RESET); } txData <<= 1; // 左移,准备下一位 // 2. 产生时钟上升沿 (CPOL=0, CPHA=0: 数据在上升沿被采样) HAL_GPIO_WritePin(SIM_SPI_SCK_PORT, SIM_SPI_SCK_PIN, GPIO_PIN_SET); // 此处可加微小延时,确保数据稳定 // 3. 在时钟高电平期间,读取MISO if (HAL_GPIO_ReadPin(SIM_SPI_MISO_PORT, SIM_SPI_MISO_PIN)) { rxData = (rxData << 1) | 0x01; } else { rxData = (rxData << 1) | 0x00; } // 4. 产生时钟下降沿,为下一位数据输出做准备 HAL_GPIO_WritePin(SIM_SPI_SCK_PORT, SIM_SPI_SCK_PIN, GPIO_PIN_RESET); // 此处可加微小延时 } // 拉高片选,结束传输 HAL_GPIO_WritePin(SIM_SPI_CS_PORT, SIM_SPI_CS_PIN, GPIO_PIN_SET); return rxData; }

软件模拟的关键

  • 时序精确性__NOP()或微小延时循环是必须的,它保证了SCLK高低电平的宽度和数据的建立/保持时间满足从设备要求。这个延时需要根据你的MCU主频和从设备速度来调整。
  • 可移植性:这段代码逻辑清晰,可以轻松移植到任何平台的GPIO操作上。

5. SPI调试实战:常见问题与排查技巧

5.1 问题现象与排查路径速查表

问题现象可能原因排查步骤与工具
完全无通信,从设备无响应1. 电源/地未接好
2. 片选(SS)信号错误(常高或常低)
3. 时钟(SCLK)无输出
4. 从设备损坏
1. 万用表检查电源、地电压。
2. 逻辑分析仪/示波器观察SS、SCLK波形。
3. 确认SS引脚是否被其他功能复用。
4. 替换从设备或主设备测试。
能通信但数据错误1. SPI模式(CPOL/CPHA)不匹配
2. 数据位序(MSB/LSB)不匹配
3. 时钟速度过快
4. 电气噪声干扰
1.首要检查:用逻辑分析仪捕获波形,对照数据手册看时序。
2. 检查主从设备位序配置。
3. 降低SPI时钟频率再试。
4. 检查PCB布线,在SCLK和MOSI上加串联电阻。
通信不稳定,时好时坏1. 时序裕量不足(建立/保持时间)
2. 电源纹波大
3. 长线传输阻抗不匹配
4. 软件中断干扰
1. 降低时钟频率。
2. 用示波器检查电源和信号质量。
3. 缩短走线或端接匹配电阻。
4. 在SPI关键操作段禁用全局中断。
多从设备时相互干扰1. 未选中从设备的MISO未置高阻
2. 片选切换时序不当
1. 确认从设备MISO是否为三态输出。
2. 确保在SCLK空闲时切换片选,避免产生额外时钟边沿。
软件模拟SPI工作不正常1. GPIO输入/输出模式配置错误
2. 延时时间不准确
3. 片选控制逻辑错误
1. 确认MISO为输入,其他为输出。
2. 用逻辑分析仪校准延时,确保时序满足要求。
3. 严格在字节传输前后控制片选。

5.2 核心调试工具:逻辑分析仪的使用技巧

一个几十块钱的USB逻辑分析仪(配合Sigrok/PulseView软件)是调试SPI的神器。它能非侵入式地捕获并显示多路数字信号波形。

使用步骤

  1. 连接:将探针连接到SCLK、MOSI、MISO、SS线上。
  2. 设置:在软件中添加“SPI”解码器,指定各通道对应的信号线,并设置正确的SPI模式(CPOL, CPHA)。
  3. 触发:通常设置为SS下降沿触发。
  4. 捕获:启动主设备通信,软件会捕获波形并自动将二进制数据解码成十六进制字节显示

如何分析

  • 看SS:是否在每次传输前拉低,传输后拉高?
  • 看SCLK:时钟频率是否与配置一致?空闲电平是否正确(CPOL)?
  • 看MOSI/MISO:数据位是在时钟的哪个边沿变化(输出)?哪个边沿稳定(采样)?这与CPHA设置是否一致?
  • 对比数据:软件解码出的字节,是否与你代码中发送/期望接收的数据一致?

实操心得:我习惯在初始化后,先让主设备发送一个简单的已知命令(如读ID命令0x9F),然后用逻辑分析仪抓取。一眼就能看出时序模式对不对、数据对不对。这比盲目修改代码高效一百倍。

5.3 软件层面的高级调试与优化

  1. DMA传输:对于需要连续收发大量数据的场景(如读写SD卡、刷新显示屏),使用DMA可以解放CPU。

    • 配置:使能SPI的TX和RX DMA请求流。
    • 优势:CPU只需设置好传输的起始地址和长度,即可处理其他任务,传输完成后由DMA产生中断通知CPU。极大提高系统效率。
    • 注意:需要处理好缓存一致性问题(Cache Coherence),特别是在有Cache的MCU上。
  2. 中断模式:相比阻塞式等待,中断模式可以提高CPU利用率。

    • 发送:填充数据到SPI数据寄存器,启动发送,等待发送完成中断。
    • 接收:在RXNE(接收缓冲区非空)中断中读取数据。
    • 注意:中断服务函数要尽量短小快出,避免嵌套过深或处理时间过长。
  3. 错误处理

    • 溢出错误 (OVR):数据被新数据覆盖前未被读取。检查你的接收代码是否及时。
    • 模式错误 (MODF):在多主设备系统中,当NSS被意外拉低时发生。在单主系统中通常可忽略。
    • CRC错误:如果使能了CRC校验,则需检查。
    • 良好的驱动代码应该检查并处理这些错误标志位。

6. SPI在典型嵌入式场景中的应用实例

6.1 驱动SPI Flash存储器(如W25Q64)

SPI Flash是SPI最经典的应用之一,用于存储固件、配置参数或数据日志。

操作特点

  • 需要命令字:任何操作都以一个1字节的命令码开始,如写使能0x06,页编程0x02,扇区擦除0x20,读数据0x03
  • 有状态寄存器:通过读状态寄存器0x05来查询设备是否忙(BUSY位),在写或擦除操作后必须等待。
  • 分页编程和扇区擦除:写入前必须先擦除(位只能由1变0),擦除以扇区(通常4KB)为单位,写入则以页(通常256字节)为单位。

关键代码片段(读数据)

void SPI_Flash_ReadData(uint32_t addr, uint8_t *pBuffer, uint32_t size) { SPI_FLASH_CS_LOW(); SPI_ReadWriteByte(0x03); // 发送读数据命令 SPI_ReadWriteByte((addr >> 16) & 0xFF); // 发送24位地址的高字节 SPI_ReadWriteByte((addr >> 8) & 0xFF); SPI_ReadWriteByte(addr & 0xFF); while(size--) { *pBuffer++ = SPI_ReadWriteByte(0xFF); // 循环读取,发送哑元时钟 } SPI_FLASH_CS_HIGH(); }

6.2 连接SPI接口的传感器(如IMU MPU-6050)

一些传感器也提供SPI接口,通常比I2C速度更快,抗干扰能力更强。

操作特点

  • 寄存器映射:传感器内部功能通过寄存器控制。主设备通过SPI读写这些寄存器。
  • 地址位:SPI帧中通常包含一个读写位(R/W)和寄存器地址。例如,MPU-6050的SPI帧格式为:[R/W(1bit) | 6位寄存器地址],后面跟数据。
  • 连续读:很多传感器支持连续读多个寄存器,只需发送起始寄存器地址,然后持续产生时钟即可。

关键代码片段(读取加速度计数据)

// 假设MPU-6050的加速度计数据寄存器起始地址为0x3B void MPU6050_ReadAccel(int16_t *accelData) { uint8_t buffer[6]; SPI_MPU_CS_LOW(); SPI_ReadWriteByte(0x80 | 0x3B); // 最高位1表示读,后7位是寄存器地址 for(int i=0; i<6; i++) { buffer[i] = SPI_ReadWriteByte(0xFF); // 连续读6个字节 } SPI_MPU_CS_HIGH(); accelData[0] = (buffer[0]<<8) | buffer[1]; // AX accelData[1] = (buffer[2]<<8) | buffer[3]; // AY accelData[2] = (buffer[4]<<8) | buffer[5]; // AZ }

6.3 驱动SPI TFT显示屏(如ILI9341)

SPI屏为了节省引脚,常工作在“3线SPI+1根DC命令/数据选择线”模式。

操作特点

  • DC线:这是一根额外的控制线,用于区分发送的是命令(Command)还是数据(Data)。DC=0写命令,DC=1写数据。
  • 双帧缓存:为了快速刷新,常在MCU内部RAM开辟一块和屏幕分辨率匹配的帧缓冲区(Frame Buffer),所有绘图操作在缓冲区中进行,完成后一次性通过SPI DMA传输到屏幕的显存(GRAM)。
  • 优化技巧:设置屏幕的“窗口”(行列地址),然后连续发送像素数据,可以避免每次画点都重复发送地址命令,极大提升填充速度。

关键代码片段(初始化与画点)

#define LCD_DC_PIN GPIO_PIN_2 #define LCD_DC_PORT GPIOA void LCD_WriteCommand(uint8_t cmd) { HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET); // DC=0: 命令 SPI_ReadWriteByte(cmd); } void LCD_WriteData(uint8_t dat) { HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET); // DC=1: 数据 SPI_ReadWriteByte(dat); } void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_WriteCommand(0x2A); // 列地址设置命令 LCD_WriteData(x1>>8); LCD_WriteData(x1&0xFF); LCD_WriteData(x2>>8); LCD_WriteData(x2&0xFF); LCD_WriteCommand(0x2B); // 行地址设置命令 LCD_WriteData(y1>>8); LCD_WriteData(y1&0xFF); LCD_WriteData(y2>>8); LCD_WriteData(y2&0xFF); LCD_WriteCommand(0x2C); // 写GRAM命令 } // 在(x,y)处画一个红色点 void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { LCD_SetWindow(x, y, x, y); LCD_WriteData(color>>8); // 发送颜色高字节 LCD_WriteData(color&0xFF); // 发送颜色低字节 }

7. 进阶话题与性能调优

7.1 SPI时钟频率与系统性能的权衡

SPI的时钟频率(SCLK)是性能的关键,但并非越高越好。

  • 理论极限:受限于主从设备SPI控制器的最高时钟、PCB走线质量、信号完整性。
  • 从设备限制必须严格遵守数据手册中给出的最大SCLK频率。例如,一个Flash芯片标称最大50MHz,你跑80MHz就可能出错。
  • 信号完整性:频率越高,信号边沿越陡,反射和串扰越严重。长距离或布线不佳时,高速信号会畸变。
  • 实际调优
    1. 从低速开始:调试阶段先用一个较低的分频(如系统时钟/256),确保通信基本功能正常。
    2. 逐步提高:在功能正常的基础上,逐步提高频率,并用逻辑分析仪观察波形是否干净(过冲、振铃小),数据是否稳定。
    3. 加入匹配:在高速(>10MHz)或走线较长时,在SCLK和MOSI输出端串联一个22-100欧姆的小电阻,可以显著改善信号质量,阻尼反射。

7.2 中断与DMA的应用策略

  • 何时用阻塞模式:传输数据量极小(如读写几个寄存器)、对实时性要求不高的简单任务。代码简单直观。
  • 何时用中断模式:数据传输有一定量,且主程序有其他事情要做,不希望被长时间阻塞。例如,一边通过SPI读取传感器,一边刷新UI。
  • 何时用DMA模式:大数据量、连续传输的场景是DMA的主场。如图像数据刷屏、音频数据流、大文件读写Flash。DMA+SPI的组合能几乎零CPU开销完成数据传输,是提升系统整体性能的利器。
    • 配置陷阱:启用DMA后,要确保源/目标内存地址是DMA可访问的(如在SRAM中),并且注意缓存对齐问题。传输完成中断中要及时处理数据或启动下一次传输。

7.3 多主设备与总线仲裁(高级话题)

标准SPI是单主设备协议。但在一些特殊的多MCU系统中,可能需要多主设备共享SPI总线。

  • 硬件支持:需要主设备的SPI支持多主模式(通常通过硬件NSS管理)。
  • 仲裁机制:SPI本身没有总线仲裁。一种常见的软件方案是,将SPI总线上的所有设备(包括主设备)的MISO线通过一个与门(AND Gate)连接,并上拉到VCC。任何设备想发送数据前,先输出一个“0”到自己的MISO并读取总线状态。如果读到的是“0”,说明总线空闲,可以占用;如果读到的是“1”,说明有其他设备正在发送,则等待。这需要复杂的软件协议来支持。
  • 现实建议尽量避免设计多主SPI系统。如果必须,考虑使用其他自带仲裁的通信方式,如CAN、I2C(多主),或者用一个MCU作为主设备,其他MCU通过UART等方式与其通信。

搞懂SPI,绝不仅仅是记住四根线和四种模式。它是一套关于同步、时序和可靠性的完整工程思维。从最基础的波形识别,到复杂的系统优化,每一步都需要理论和实践紧密结合。我最深的体会是,手边备一个逻辑分析仪,敢于去抓波形、看时序,很多纸上谈兵的疑惑都会迎刃而解。当你能够根据一个陌生芯片的数据手册,独立配置好SPI并成功驱动它时,那种成就感,就是嵌入式开发最纯粹的乐趣之一。希望这篇长文能成为你手边一份可靠的SPI实战指南,在下次遇到SPI问题时,能帮你更快地找到方向。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/20 17:07:58

Mission Planner终极教程:从零开始掌握专业无人机地面站软件

Mission Planner终极教程&#xff1a;从零开始掌握专业无人机地面站软件 【免费下载链接】MissionPlanner Mission Planner Ground Control Station for ArduPilot (c# .net) 项目地址: https://gitcode.com/gh_mirrors/mi/MissionPlanner Mission Planner是一款功能强大…

作者头像 李华
网站建设 2026/6/20 17:05:56

告别51单片机!STC15W4K32S4上手初体验:内置时钟/复位,Type-C下载真方便

从51到STC15W4K&#xff1a;硬件精简与开发效率的飞跃 第一次拿到STC15W4K32S4开发板时&#xff0c;最直观的感受是板子上元件少得惊人——没有晶振、没有复位电路、甚至连传统的串口芯片都不见了。这种极简设计背后&#xff0c;是国产单片机在集成度与易用性上的重大突破。对于…

作者头像 李华
网站建设 2026/6/20 17:08:08

5分钟掌握:Windows电脑直接运行安卓应用的神器APK安装器

5分钟掌握&#xff1a;Windows电脑直接运行安卓应用的神器APK安装器 【免费下载链接】APK-Installer An Android Application Installer for Windows 项目地址: https://gitcode.com/GitHub_Trending/ap/APK-Installer 想在Windows电脑上直接安装和运行安卓应用吗&#…

作者头像 李华
网站建设 2026/6/20 17:07:49

Light Chaser:现代数据可视化设计平台的技术探索与实践指南

Light Chaser&#xff1a;现代数据可视化设计平台的技术探索与实践指南 【免费下载链接】light-chaser light chaser is a lightweight data visualization designer tool 项目地址: https://gitcode.com/gh_mirrors/li/light-chaser 在当今数据驱动的时代&#xff0c;高…

作者头像 李华