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最经典、最完整的形态是四线制。这四根线各司其职,缺一不可:
- SCLK (Serial Clock):串行时钟线,由主设备产生。这是整个通信系统的“心跳”。每一个时钟脉冲的上升沿或下降沿,都定义了一位数据的采样或输出时刻。没有时钟,数据就失去了传输的基准。
- MOSI (Master Out Slave In):主设备输出,从设备输入线。顾名思义,这是主设备向从设备发送数据的通道。
- MISO (Master In Slave Out):主设备输入,从设备输出线。这是从设备向主设备返回数据的通道。
- 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模式,这是你必须刻在脑子里的:
| 模式 | CPOL | CPHA | 空闲时钟 | 采样边沿 | 输出边沿 | 常见应用 |
|---|---|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低电平 | 上升沿 | 下降沿 | 最常用,如很多SPI Flash |
| Mode 1 | 0 | 1 | 低电平 | 下降沿 | 上升沿 | |
| Mode 2 | 1 | 0 | 高电平 | 下降沿 | 上升沿 | |
| Mode 3 | 1 | 1 | 高电平 | 上升沿 | 下降沿 | 如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 First:最高有效位先传输。这是最常见的默认设置。例如,发送字节
配置错误会导致数据解析完全错误。例如,主设备按MSB发送0xB5,从设备按LSB解读,收到的就会变成0xAD,风马牛不相及。
3.2 多从设备连接拓扑
如何让一个主设备连接多个SPI从设备?有三种主流方法:
标准SPI(独立片选):
- 方法:主设备的SCLK、MOSI、MISO并联到所有从设备。每个从设备独占一条SS线连接到主设备的一个GPIO。
- 优点:逻辑简单,通信独立,速度可以各自最优。
- 缺点:占用主设备GPIO资源多(N个从设备需要N个GPIO做片选)。
- 适用场景:从设备数量不多,且对GPIO资源不敏感的场景。
SPI菊花链(Daisy Chain):
- 方法:所有从设备共用一组SCLK和一条SS线。从设备A的MISO连接到设备B的MOSI,设备B的MISO连接到设备C的MOSI,以此类推,最后一个设备的MISO接回主设备的MISO。数据像链条一样依次穿过所有设备。
- 优点:极大地节省了连线(只需要4根线)和GPIO(只需要1个片选)。
- 缺点:
- 所有设备必须支持菊花链模式(并非所有SPI设备都支持)。
- 通信效率低。主设备想读取链中最后一个设备的数据,必须发送足够多的时钟周期,让数据位依次“移位”通过前面所有设备。读写操作变得复杂。
- 链中任何一个设备故障,可能导致整个链路通信中断。
- 适用场景:多个完全相同的、支持菊花链的设备,如级联的LED驱动芯片(如TLC5940)、移位寄存器等。
软件模拟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 硬件连接检查清单
在写第一行代码之前,确保硬件连接万无一失:
- 电源与地:这是最基础也最易错的。确保主从设备共地!电平匹配(如3.3V设备与5V设备连接需电平转换)。
- 四线连接:SCLK, MOSI, MISO, SS一一对应连接。特别注意MOSI接MOSI,MISO接MISO,不要交叉!
- 上拉电阻:对于开漏输出的MISO线或片选线,可能需要上拉电阻(通常4.7kΩ-10kΩ)确保空闲状态稳定。很多MCU的GPIO内部可配置上拉,可优先使用。
- 走线考虑:对于高速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的神器。它能非侵入式地捕获并显示多路数字信号波形。
使用步骤:
- 连接:将探针连接到SCLK、MOSI、MISO、SS线上。
- 设置:在软件中添加“SPI”解码器,指定各通道对应的信号线,并设置正确的SPI模式(CPOL, CPHA)。
- 触发:通常设置为SS下降沿触发。
- 捕获:启动主设备通信,软件会捕获波形并自动将二进制数据解码成十六进制字节显示。
如何分析:
- 看SS:是否在每次传输前拉低,传输后拉高?
- 看SCLK:时钟频率是否与配置一致?空闲电平是否正确(CPOL)?
- 看MOSI/MISO:数据位是在时钟的哪个边沿变化(输出)?哪个边沿稳定(采样)?这与CPHA设置是否一致?
- 对比数据:软件解码出的字节,是否与你代码中发送/期望接收的数据一致?
实操心得:我习惯在初始化后,先让主设备发送一个简单的已知命令(如读ID命令0x9F),然后用逻辑分析仪抓取。一眼就能看出时序模式对不对、数据对不对。这比盲目修改代码高效一百倍。
5.3 软件层面的高级调试与优化
DMA传输:对于需要连续收发大量数据的场景(如读写SD卡、刷新显示屏),使用DMA可以解放CPU。
- 配置:使能SPI的TX和RX DMA请求流。
- 优势:CPU只需设置好传输的起始地址和长度,即可处理其他任务,传输完成后由DMA产生中断通知CPU。极大提高系统效率。
- 注意:需要处理好缓存一致性问题(Cache Coherence),特别是在有Cache的MCU上。
中断模式:相比阻塞式等待,中断模式可以提高CPU利用率。
- 发送:填充数据到SPI数据寄存器,启动发送,等待发送完成中断。
- 接收:在RXNE(接收缓冲区非空)中断中读取数据。
- 注意:中断服务函数要尽量短小快出,避免嵌套过深或处理时间过长。
错误处理:
- 溢出错误 (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就可能出错。
- 信号完整性:频率越高,信号边沿越陡,反射和串扰越严重。长距离或布线不佳时,高速信号会畸变。
- 实际调优:
- 从低速开始:调试阶段先用一个较低的分频(如系统时钟/256),确保通信基本功能正常。
- 逐步提高:在功能正常的基础上,逐步提高频率,并用逻辑分析仪观察波形是否干净(过冲、振铃小),数据是否稳定。
- 加入匹配:在高速(>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问题时,能帮你更快地找到方向。