公司动态

SPI通信协议全解析:从硬件时序到STM32实战应用

📅 2026/8/28 19:38:21
SPI通信协议全解析:从硬件时序到STM32实战应用
1. 从“一根线”到“四根线”为什么SPI总能在嵌入式江湖里站稳脚跟如果你玩过单片机或者捣鼓过各种传感器、屏幕、存储芯片那你一定绕不开SPI这个名字。它不像UART那样一根线发一根线收简单直接也不像I2C那样用两根线就能挂一堆设备听起来很省事。SPI偏偏要用三根、四根甚至更多线看起来有点“铺张浪费”。但奇怪的是从8位单片机到32位ARM再到FPGA几乎所有的硬件平台都把它当作标配无数的Flash芯片、OLED屏幕、以太网PHY、ADC/DAC都首选SPI作为通信接口。这背后到底有什么魔力简单来说SPISerial Peripheral Interface串行外设接口是一种全双工、同步、高速的串行通信总线。它的核心魅力在于“简单粗暴”和“速度至上”。说它简单是因为它的协议层几乎没有“规矩”——没有复杂的地址帧、没有应答机制、没有总线仲裁主设备说发就发从设备说收就收一切时序都由主设备的时钟SCLK严格掌控。这种主从之间“一对一”的紧密耦合虽然牺牲了总线扩展的便利性却换来了极高的通信效率和极简的软件驱动实现。说它高速是因为它没有像I2C那样的开漏输出和上拉电阻带来的速度瓶颈在短距离、板级通信的场景下轻松跑到几十甚至上百MHz是家常便饭。所以当你需要一个快速、可靠、实时性高的方式在板子上跟某个特定的芯片“说悄悄话”时SPI往往是那个最不会让你失望的选择。无论是初始化一块OLED屏幕还是从Flash芯片里快速读取固件或是高速采集传感器的数据SPI都能提供一种近乎“直连”的畅快体验。接下来我们就抛开那些枯燥的教科书定义从一个硬件工程师和嵌入式软件工程师的实操视角彻底拆解SPI的里里外外。2. SPI的“四梁八柱”硬件连线与核心信号深度解析很多人一提到SPI就脱口而出“四根线”SCLK、MOSI、MISO、CS。这个说法对但不全对。这四根线构成了SPI通信最经典、最基础的物理骨架但实际应用中变体非常多。理解每一根线的职责和它们之间的配合是玩转SPI的第一步。2.1 核心四线制标准配置下的分工协作我们来逐一拆解这四根核心信号线SCLK (Serial Clock串行时钟)这是SPI总线的“心跳”由主设备Master产生并完全控制。所有数据的发送和接收都严格对齐这个时钟的边沿。SCLK的频率直接决定了通信速率。这里有一个关键点SCLK在空闲时即没有数据传输时的电平状态以及数据在时钟的哪个边沿被采样是由SPI的“模式”决定的我们后面会详细讲。MOSI (Master Output Slave Input主出从入)数据从主设备流向从设备的通道。主设备通过这根线一位一位地把命令或数据“推”给从设备。MISO (Master Input Slave Output主入从出)数据从从设备流向主设备的通道。从设备通过这根线一位一位地把响应或数据“送”回给主设备。注意MOSI和MISO是同时工作的这使得SPI实现了真正的全双工通信。主设备在发送一个字节的同时也能接收到一个字节。这个特性非常有用例如在读取Flash芯片时主设备先发送一个读命令比如0x03紧接着发送地址与此同时从设备就会通过MISO线开始回送数据。CS/SS (Chip Select / Slave Select片选)这是SPI总线架构的灵魂所在。它通常是一个低电平有效的信号即CS为低电平时从设备被选中。每个从设备都需要独占一根来自主设备的CS线。主设备通过拉低某根特定的CS线来“点名”要与哪个从设备通信。当CS为高时对应的从设备必须将其MISO引脚置于高阻态从而与总线隔离。这就是为什么SPI理论上可以挂很多设备但需要大量GPIO口来做片选的原因。注意CS信号的控制是SPI通信可靠性的基石。一个常见的低级错误是CS信号时序混乱比如在时钟还没稳定时就拉低CS或者在数据传输未完成时就拉高CS这极易导致从设备锁死或数据错位。稳妥的做法是在SCLK处于空闲电平根据模式确定时先拉低CS等待至少一个时钟周期后再开始产生SCLK通信结束后先确保最后一个SCLK边沿完成再拉高CS。2.2 硬件片选 vs. 软件片选成本与灵活性的权衡这是新手和老手的一个分水岭。所谓“硬件片选”就是使用主控芯片硬件SPI外设自带的、专用于片选的GPIO引脚。很多MCU的SPI模块会提供1到2个专用的NSSNegtive Slave Select引脚。使用硬件片选时SPI控制器硬件会自动在数据传输前后控制NSS引脚的电平软件无需干预不仅省事而且时序精准。而“软件片选”则是用任意一个普通的GPIO口来模拟CS信号的功能。你需要手动在代码里控制这个GPIO的电平。这是最常用的方式因为硬件片选引脚数量有限。当你需要连接多个SPI从设备时几乎必然要使用多个GPIO来做软件片选。两者的核心区别与选择建议硬件片选时序精准不占用CPU资源适合对时序要求极其严格或主设备非常繁忙的场景。缺点是引脚固定数量有限。软件片选灵活不受引脚限制想接多少设备理论上就分配多少GPIO。缺点是控制时序需要软件介入会引入微小的延迟并且在多任务或中断环境中如果CS控制不当容易引发冲突。对于绝大多数应用软件片选完全足够。你需要做的就是在驱动代码里把CS引脚的控制封装成独立的函数确保在任何SPI数据传输函数中都成对地调用CS_Low()和CS_High()。2.3 超越四线HOLD、WP与多设备拓扑在实际芯片中你经常会看到除了四根基础线之外还有别的引脚比如Flash芯片上的HOLD和WPWrite Protect。HOLD引脚当这个引脚被拉低时它会暂停当前正在进行的SPI通信就像按了暂停键但不会终止通信。主设备可以在忙时先Hold住从设备去处理更高优先级的任务然后再恢复通信。这在早期的SPI Flash中用于实现简单的多主机共享但现在更高速的器件通常不建议使用因为会打断连续的数据流。现代设计中这个引脚通常直接上拉到VCC使其无效除非你有非常特殊的流控需求。WP引脚写保护。拉低此引脚可以禁止对存储器的写和擦除操作防止误操作。在产品化代码中通常会在初始化后将其拉高以允许写入。关于拓扑SPI本质上是一种点对点或一主多从独立片选的总线。它不支持像I2C那样的多主或多从共用片选仲裁。有一种“菊花链”Daisy-Chain模式可以将多个从设备的MISO和MOSI串接起来数据像移位寄存器一样在所有设备间传递。但这种模式需要从设备硬件支持并不通用常见于一些特定的数字电位器或LED驱动芯片中。对于大多数通用器件老老实实用“一主多从独立片选”是最稳妥的方案。3. 时序是灵魂深入理解SPI的四种模式与波形如果说硬件连线是SPI的躯体那时序就是它的灵魂。SPI通信的可靠性几乎完全取决于对时序的精确理解和控制。而时序的核心就体现在CPOL和CPHA这两个参数上它们共同定义了SPI的四种工作模式。3.1 CPOL与CPHA定义时钟的“性格”CPOL (Clock Polarity时钟极性)定义SCLK线在空闲时的电平。CPOL0SCLK空闲时为低电平。CPOL1SCLK空闲时为高电平。CPHA (Clock Phase时钟相位)定义数据在SCLK的第几个边沿被采样捕获。CPHA0数据在SCLK的第一个边沿即从空闲状态跳变到相反状态的第一个边沿被采样。对于CPOL0第一个边沿是上升沿对于CPOL1第一个边沿是下降沿。CPHA1数据在SCLK的第二个边沿即第一个边沿之后的下一个边沿被采样。组合起来就得到了SPI的四种模式Mode 0 (CPOL0 CPHA0) Mode 1 (CPOL0 CPHA1) Mode 2 (CPOL1 CPHA0) Mode 3 (CPOL1 CPHA1)。3.2 模式0与模式3为什么它们最常见让我们以Mode 0 (CPOL0 CPHA0)为例画出它的波形并理解其背后的硬件行为空闲时SCLK为低(CPOL0)CS为高未选中。当CS被拉低选中从设备。在第一个SCLK边沿上升沿到来之前主设备必须将要发送的第一位数据通常是最高位MSB放到MOSI线上并保持稳定。同时从设备也会将其要发送的第一位数据放到MISO线上。第一个SCLK上升沿到来此时主设备和从设备同时采样对方的输入线。主设备在上升沿采样MISO得到第一位数据从设备在上升沿采样MOSI得到第一位数据。这就是“数据在第一个边沿被采样”(CPHA0)。在第一个上升沿之后主从双方根据要发送的下一位数据更新各自的输出线MOSI/MISO。第一个SCLK的下降沿到来但这个边沿在Mode 0下仅用于数据线的更新和稳定不进行采样。第二个SCLK上升沿到来再次采样如此循环8次完成一个字节的传输。Mode 3 (CPOL1 CPHA1)是另一个极端但逻辑对称空闲时SCLK为高数据在SCLK的第二个边沿对于CPOL1第一个边沿是下降沿第二个边沿是上升沿被采样。你可以发现Mode 0和Mode 3有一个共同点数据采样的边沿对于主从设备都是同一个边沿发生在SCLK脉冲的中间位置这给了数据线充足的时间半个时钟周期来达到稳定状态抗干扰能力最强。因此绝大多数SPI器件如Flash、EEPROM、传感器都支持Mode 0或Mode 3并且常将Mode 0作为默认模式。Mode 1和Mode 2则相对少见它们的数据采样边沿更靠近数据更新的边沿稳定时间窗口更小对硬件时序要求更苛刻。实操心得如何确定从设备的SPI模式永远不要猜第一选择是仔细阅读数据手册Datasheet的时序图章节。如果手册找不到一个实用的方法是如果器件是常见的存储器或传感器先尝试Mode 0。如果通信失败读回的数据全是0xFF或0x00再尝试Mode 3。可以在代码中灵活配置主设备的SPI模式进行测试。用逻辑分析仪抓取波形是最直接的调试方法可以清晰地看到时钟和数据的关系。3.3 用逻辑分析仪“看”到时序以读取Flash ID为例理论说再多不如看一次真实的波形。假设我们要与一个SPI Flash芯片支持Mode 0通信读取它的制造商和设备ID。标准命令是0x9F。我们设置主设备为Mode 0时钟频率设为1MHz初期调试建议用较低频率。拉低CS。主设备通过MOSI发送命令字节0x9F二进制1001 1111MSB先发。与此同时从设备在MISO上回送数据。发送0x9F的同时主设备收到的第一个字节可能是无效的因为从设备还没准备好但紧接着从设备就会开始回送ID。通常连续读回3个字节制造商ID如0xEF、存储器类型如0x40、容量ID如0x17。拉高CS。用逻辑分析仪抓取这段通信你会看到CS拉低后SCLK开始产生脉冲。在每一个SCLK上升沿MOSI和MISO线上的数据电平被捕获。将MOSI线上的电平序列翻译出来就是0x9F将MISO线上的电平序列翻译出来就是0xEF0x400x17。这个可视化的过程能让你对“全双工”、“边沿采样”有最直观的认识。4. 从寄存器到代码STM32 HAL库驱动SPI Flash实战理解了硬件和时序我们进入实战环节。以STM32F103系列MCU和一块W25Q16JV SPI Flash芯片为例使用STM32CubeMX和HAL库完成从初始化到读写擦除的全流程代码解析。4.1 硬件连接与CubeMX配置假设连接如下STM32 SPI1 (主设备)PA5 - SPI1_SCK - Flash CLKPA6 - SPI1_MISO - Flash DO (Data Output)PA7 - SPI1_MOSI - Flash DI (Data Input)STM32 GPIOPA4 (任意GPIO) - Flash CS(可选) PB0 - Flash HOLD (上拉)(可选) PB1 - Flash WP (上拉)在CubeMX中启用SPI1模式选择“Full-Duplex Master”。配置参数Prescaler (时钟分频) 先设置为/256得到低速约280kHz便于调试稳定后可提高。CPOL Low 对应Mode 0/1CPHA 1 Edge 对应Mode 0/2。这里“1 Edge”即第一个边沿与CPHA0对应。这是CubeMX里最容易搞混的地方它的“1 Edge”指的是数据在第一个边沿捕获即CPHA0。如果要配置Mode 3需要CPOLHigh CPHA2 Edge第二个边沿。Data Size 8 bitsFirst Bit MSB First 绝大多数器件都是MSB先传NSS Disabled 因为我们使用软件片选GPIO配置PA4为GPIO_Output并设置初始输出电平为高CS无效。生成代码。4.2 软件驱动层封装核心函数实现生成的代码提供了HAL_SPI_TransmitReceive等基础函数但我们需要封装更适合Flash操作的函数。// spi_flash.c #include spi_flash.h // 定义CS引脚操作宏 #define FLASH_CS_LOW() HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_RESET) #define FLASH_CS_HIGH() HAL_GPIO_WritePin(FLASH_CS_GPIO_Port, FLASH_CS_Pin, GPIO_PIN_SET) // 1. 基础字节读写函数 static uint8_t SPI_Flash_SendByte(uint8_t byte) { uint8_t rx_data; // HAL_SPI_TransmitReceive是阻塞式全双工函数同时完成发送和接收 HAL_SPI_TransmitReceive(hspi1, byte, rx_data, 1, HAL_MAX_DELAY); return rx_data; } // 2. 读取Flash状态寄存器重要 uint8_t SPI_Flash_ReadStatusReg(void) { uint8_t cmd 0x05; // 读状态寄存器1命令 uint8_t status; FLASH_CS_LOW(); SPI_Flash_SendByte(cmd); status SPI_Flash_SendByte(0xFF); // 发送哑元数据以产生时钟来接收 FLASH_CS_HIGH(); return status; } // 3. 等待Flash空闲检查BUSY位 void SPI_Flash_WaitForReady(void) { while ((SPI_Flash_ReadStatusReg() 0x01) 0x01) // 检查状态寄存器第0位BUSY { // 可以加入超时机制防止死等 } } // 4. 写使能 void SPI_Flash_WriteEnable(void) { uint8_t cmd 0x06; FLASH_CS_LOW(); SPI_Flash_SendByte(cmd); FLASH_CS_HIGH(); } // 5. 读取数据标准读 void SPI_Flash_ReadData(uint32_t addr, uint8_t *pBuffer, uint32_t size) { uint8_t cmd 0x03; // 标准读命令 FLASH_CS_LOW(); SPI_Flash_SendByte(cmd); SPI_Flash_SendByte((addr 16) 0xFF); // 发送24位地址高位在前 SPI_Flash_SendByte((addr 8) 0xFF); SPI_Flash_SendByte(addr 0xFF); for(uint32_t i0; isize; i) { pBuffer[i] SPI_Flash_SendByte(0xFF); // 连续读取 } FLASH_CS_HIGH(); } // 6. 页编程写数据一次最多256字节 void SPI_Flash_PageProgram(uint32_t addr, uint8_t *pData, uint32_t size) { uint8_t cmd 0x02; // 页编程命令 if(size 256) size 256; // 页编程不能跨页 SPI_Flash_WaitForReady(); // 等待上次操作完成 SPI_Flash_WriteEnable(); // 写使能 FLASH_CS_LOW(); SPI_Flash_SendByte(cmd); SPI_Flash_SendByte((addr 16) 0xFF); SPI_Flash_SendByte((addr 8) 0xFF); SPI_Flash_SendByte(addr 0xFF); for(uint32_t i0; isize; i) { SPI_Flash_SendByte(pData[i]); } FLASH_CS_HIGH(); SPI_Flash_WaitForReady(); // 等待编程完成 } // 7. 扇区擦除4KB void SPI_Flash_SectorErase(uint32_t addr) { uint8_t cmd 0x20; // 扇区擦除命令 SPI_Flash_WaitForReady(); SPI_Flash_WriteEnable(); FLASH_CS_LOW(); SPI_Flash_SendByte(cmd); SPI_Flash_SendByte((addr 16) 0xFF); SPI_Flash_SendByte((addr 8) 0xFF); SPI_Flash_SendByte(addr 0xFF); FLASH_CS_HIGH(); SPI_Flash_WaitForReady(); // 擦除耗时较长需等待 }4.3 避坑指南HAL库SPI驱动中的那些“坑”时钟分频与极性问题CubeMX里SPI的时钟分频是基于APB总线时钟的。务必核对最终生成的时钟频率是否在你的从设备支持范围内。极性和相位CPOL/CPHA必须与从设备严格匹配这是通信失败的首要排查点。阻塞式HAL_SPI_TransmitReceive的超时这个函数的最后一个参数是超时时间毫秒。如果SPI通信因硬件问题卡住这个超时会触发错误回调。务必在代码中实现HAL_SPI_ErrorCallback函数至少打印个错误信息否则程序会死等。DMA与中断模式对于高速、大数据量传输如读写大量Flash数据强烈建议使用DMA或中断模式避免阻塞主循环。HAL库提供了HAL_SPI_TransmitReceive_DMA和HAL_SPI_TransmitReceive_IT函数。使用DMA时需要注意内存对齐和缓存一致性问题如果开了D-Cache。CS信号的手动控制当使用软件CS时HAL库函数不会控制CS引脚。你必须像上面代码那样在调用传输函数前后手动拉低和拉高CS。确保在每次传输前后都有完整的CS控制序列这是很多“时灵时不灵”问题的根源。Flash的写保护与状态寄存器在写或擦除Flash前必须先发送写使能命令0x06且该命令在一次写/擦除操作后会自动清除。每次操作后必须等待BUSY位清零才能进行下一步操作。忽略这个等待直接发起下一次操作是导致数据写入失败的最常见原因。5. 当硬件SPI不够用软件模拟SPI的灵活性与代价不是所有MCU都有足够的硬件SPI外设或者你的硬件SPI引脚可能被其他功能占用。这时“软件模拟SPI”Bit-Banging就成了救命稻草。其核心思想很简单用普通的GPIO口通过代码精确控制电平翻转的时序来模拟出SCLK、MOSI、MISO和CS的行为。5.1 软件SPI的实现骨架以STC15系列单片机51内核为例模拟Mode 0的SPI// 定义GPIO引脚 sbit SOFT_SPI_SCLK P1^0; sbit SOFT_SPI_MOSI P1^1; sbit SOFT_SPI_MISO P1^2; sbit SOFT_SPI_CS P1^3; // 初始化将所有引脚设为准双向口CS置高 void Soft_SPI_Init(void) { SOFT_SPI_CS 1; SOFT_SPI_SCLK 0; // Mode 0 空闲低电平 } // 模拟发送/接收一个字节 (MSB first) uint8_t Soft_SPI_ReadWriteByte(uint8_t txData) { uint8_t i, rxData 0; SOFT_SPI_CS 0; // 拉低CS // 此处可加微小延时等待从设备准备特别是高速时 for(i0; i8; i) { SOFT_SPI_SCLK 0; // 确保从空闲状态开始 // 准备数据位 if(txData 0x80) // 先发送最高位 SOFT_SPI_MOSI 1; else SOFT_SPI_MOSI 0; txData 1; // 短暂延时建立时间(Tsu) _nop_(); _nop_(); // 产生上升沿从设备在此刻采样MOSI主设备也应在此刻采样MISO SOFT_SPI_SCLK 1; rxData 1; // 左移为接收下一位做准备 if(SOFT_SPI_MISO) rxData | 0x01; // 短暂延时保持时间(Th) _nop_(); _nop_(); SOFT_SPI_SCLK 0; // 下降沿数据线可以变化 } SOFT_SPI_CS 1; return rxData; }5.2 软件SPI的致命弱点与适用场景软件SPI的最大问题在于速度慢且不稳定。它的速度受限于CPU执行指令的速度和延时函数的精度。即使使用_nop_()空操作来延时在中断频繁或任务调度复杂的系统中时序也极易被打乱导致通信错误。适用场景对速度要求极低几十KHz以下的场合。调试初期快速验证硬件连接和从设备基本功能。MCU硬件SPI引脚被占用且通信速率要求不高的补充方案。绝对避免的场景需要高速通信1MHz。需要长时间、大数据量稳定传输。系统实时性要求高中断频繁。经验之谈软件SPI的延时_nop_()个数需要根据主频精确计算和测试。一个粗略的方法是用逻辑分析仪抓取波形调整_nop_()数量使得SCLK周期、数据建立和保持时间满足从设备数据手册的要求。这是一个非常耗时且不精确的过程再次印证了硬件SPI的优越性。6. 进阶与对比SPI在FPGA、ESP32与高速系统中的变体当项目需求走向高端SPI也在不断进化衍生出更复杂、更强大的变体。6.1 FPGA中的SPI从状态机到AXI接口在FPGA如Xilinx Zynq中实现SPI给了你最大的灵活性。你可以用Verilog/VHDL编写一个精确符合你需求的SPI控制器。SPI Master状态机设计一个典型的SPI Master核心是一个状态机包含IDLE、LOAD、SHIFT、DONE等状态。在SHIFT状态根据CPOL和CPHA在正确的时钟边沿对移位寄存器进行移位操作并控制MOSI输出和采样MISO输入。与处理器交互在Zynq这样的SoC FPGA中你通常通过AXI总线将自定义的SPI IP核挂载到PS处理器系统上。PS端的Linux驱动或裸机程序通过读写IP核的寄存器如控制寄存器、数据寄存器、状态寄存器来发起SPI传输。Vivado的IP Integrator可以帮你快速搭建这样的系统甚至可以直接使用Xilinx提供的AXI Quad SPI IP核。速度优势FPGA可以实现极高速度的SPI数百MHz因为时钟和数据生成都由硬件并行完成不受软件指令流限制。这对于需要超高速数据流如图像传感器接口的应用至关重要。6.2 ESP32-S3的SPI外设灵活性与性能的平衡ESP32-S3的SPI外设非常强大它支持多种工作模式除了标准的SPI模式还支持Dual SPI和Quad SPIQSPI。在QSPI模式下数据线从1根MOSI/MISO增加到4根IO0-IO3可以同时传输4位数据理论带宽提升4倍。这正是很多高速SPI Flash如ESP32用于存储程序和数据的片外Flash所采用的模式。内存映射模式对于QSPI FlashESP32-S3可以将其映射到处理器的内存地址空间。这意味着你可以像读取内部RAM一样直接使用指针访问Flash中的内容无需调用复杂的读函数极大提升了执行效率。DMA支持内置的GDMA通用DMA可以接管SPI的数据搬运让CPU在SPI传输大量数据时得以解脱处理其他任务。6.3 SPI vs. I2C vs. UART如何选择这是嵌入式通信协议选择的经典问题。我们来做一个快速对比特性SPII2CUART线数3-4 (SCLK, MOSI, MISO, CS*N)2 (SDA, SCL)2 (TX, RX) 或 1速度高(MHz级别甚至百MHz)中 (标准模式100kbps快速模式400kbps高速模式3.4Mbps)低到中 (常见115200bps最高可达数Mbps)通信方式全双工同步半双工同步全双工异步拓扑一主多从独立片选点对点多主多从总线式点对点多设备需软件协议协议复杂度极简(几乎无协议)中等 (地址、ACK/NACK、仲裁)简单 (起始位、数据位、停止位)硬件开销主设备IO口需求多主从设备IO口需求少主从设备IO口需求少典型应用Flash 显示屏 ADC/DAC 高速传感器温度传感器 EEPROM IO扩展芯片 低速设备调试串口 蓝牙/Wi-Fi模块 GPS模块选择建议追求速度、实时性、点对点通信选SPI。比如驱动TFT屏、读写大容量Flash。设备多、引脚资源紧张、速度要求不高选I2C。比如连接一堆传感器到MCU。简单、通用、距离稍远、无需时钟线选UART。比如和电脑通信、连接无线模组。最后关于网络热词中提到的“SPI中ACK是什么角色”这里需要澄清标准的SPI协议中没有ACK应答机制。这是SPI与I2C的一个根本区别。SPI主设备发出时钟和数据从设备被动接收和发送主设备默认从设备总是“在线”且“准备好”的。通信的可靠性依赖于严格的时序和硬件连接。一些特定的SPI器件可能会在数据帧内定义自己的软件ACK机制但这属于器件自定义行为而非SPI协议本身的规定。