SPI协议详解:从CPOL/CPHA时序到片选与调试避坑指南

发布时间:2026/9/13 1:53:04
SPI协议详解:从CPOL/CPHA时序到片选与调试避坑指南 1. 为什么SPI看起来简单用起来却全是坑SPISerial Peripheral Interface协议在嵌入式领域几乎无处不在Flash、EEPROM、SD卡、各类传感器、LCD屏幕、ADC、DAC、无线模块……只要稍微有数据交换需求的外设大概率都能看到它。很多人把它当成“串口一样的东西”来学觉得不过就是四根线而已结果一到实际调试就懵了——要么读回来的数据全是FF要么数据错位要么偶尔正常偶尔抽风最后只能挠头。SPI名字里有个“Serial”但它是同步串行总线和UART那种异步串行完全不同。它本质上是“主设备生成时钟主从双方按这个时钟的边沿互相交换数据”。所以它没有波特率误差一说也没有起始位停止位只要时钟对上数据就能传。听起来是不是比UART还简单但恰恰因为“没有统一的标准”每家芯片厂商对时序的理解、对引脚的定义、对片选的用法都不太一样你手上的主控和外设之间才容易出现各种让人抓狂的问题。这篇文章我不打算只带你背一遍时序图。咱们从物理连线、时钟极性和相位、一主多从连接方式、硬件片选与软件片选、实际读写Flash的完整流程到软件模拟SPI和调试踩坑一条线串下来。该给代码的给代码该给参数的给参数该讲原理的地方讲原理最后你会发现自己不仅能看懂SPI还能真正把它用好。2. 从时序图读懂SPI的底层逻辑时钟极性与相位2.1 CPOL和CPHA两个比特定义四种模式SPI的时序参数里最核心的就是CPOLClock Polarity时钟极性和CPHAClock Phase时钟相位。很多新手看到这两个缩写就头疼其实用大白话讲就两句话CPOL决定SCLK在空闲状态也就是没有数据传输时到底是高还是低。CPOL0表示空闲低电平CPOL1表示空闲高电平。CPHA决定数据是在SCLK的第一个跳变沿被采样还是在第二个跳变沿被采样。CPHA0表示第一个边沿采样CPHA1表示第二个边沿采样。这里说的“采样边沿”是从主机和从机双方角度看的。SPI是全双工传输主机在SCLK的某个边沿切换数据接收方在另一个边沿采样数据一收一发正好错开避免读到正在变化中的数据。四种模式组合起来是这样模式CPOLCPHA空闲SCLK电平数据采样边沿数据切换边沿Mode 000低上升沿下降沿Mode 101低下降沿上升沿Mode 210高下降沿上升沿Mode 311高上升沿下降沿我最常用的几乎都是Mode 0。很多SPI外设默认也支持Mode 0比如W25Q系列Flash、常见的74HC595移位寄存器、各种SPI接口的LCD驱动IC。但注意“默认支持”不代表“只支持”有些芯片比如某些传感器数据手册上明确写了必须用Mode 3你要是照抄别人工程里的Mode 0大概率读出来一堆乱码。2.2 如何从数据手册快速确定SPI模式看数据手册的时候不要只盯着时序图上画的那些波浪线要重点找两个信息第一看时序图里SCLK标的是“CPOL”还是“Idle Low/Idle High”。如果空闲状态是低电平CPOL就是0如果空闲状态是高电平CPOL就是1。第二看文字描述里有没有类似“Data is latched on the rising edge of SCLK”或者“Data is sampled on the falling edge”这种句子。如果是在上升沿锁存/采样那就要看这个上升沿是第一个边沿还是第二个边沿。和空闲电平一组合就能确定具体模式。我自己更习惯的做法是直接查芯片手册里的“SPI Timing Characteristics”表格里面通常会有Setup Time建立时间和Hold Time保持时间。这两个时间决定了你的时钟频率不能随便往高了拉后面调高速模式的时候会用到。2.3 模式不匹配会有什么真实表现模式配错时最典型的两个现象一是数据全零或者全FF。比如从机在上升沿输出数据而你把主机配成了在上升沿采样主机读到的就是数据线上的“新旧交替”瞬间采样点不稳定最终读回什么都有通常就是0x00或者0xFF。二是数据错位但“看起来像数据”。比如两个字节的顺序乱了或者每个字节里的位顺序反了。这种情况下误判率很高因为通讯并不是完全不通只是数据不对。我曾经调一块SPI接口的ADC读出来的电压值总是在某个区间内循环乱跳一开始还以为是ADC配置有问题后来拿逻辑分析仪抓波形才发现SCLK空闲电平是对的但数据采样边沿选反了ADC输出的是上个周期采到的旧值——症状特别有迷惑性。所以遇到数据不对第一件事永远是把模式和从设备手册核对一遍不要先怀疑硬件。3. 一主多从的组网方式片选的哲学3.1 三种连接方式独立片选、菊花链和普通一主多从SPI物理连接可以是一主一从也可以一主多从。多从设备时最常见的做法是主机的SCLK、MOSI、MISO三条线并联接所有从设备每个从设备单独一根片选线CS/SS主机通过拉低某个从设备的CS来选择跟谁通信。另一种是菊花链连接。这种接法下所有从设备的MOSI和MISO首尾相连像串糖葫芦一样主机的数据位会经过第一个从设备再传给第二个从设备。很多新手一听“菊”字就兴奋觉得省片选线但实际上菊花链要求每个从设备都必须支持这种级联模式而且数据延迟、位宽都会随从设备数量增加累积。除了一些特定芯片比如某些移位寄存器、部分Flash普通SPI外设并不支持这种接法。我建议默认只考虑独立片选方案哪怕片选线多几根调试和扩展都省心。3.2 硬件片选与软件片选不是非黑即白的选择片选信号SPI里叫CSChip Select或者SSSlave Select极大多数情况下低电平有效。这个信号看起来就是把一根GPIO拉低拉高的事但它牵扯到一个经典问题到底用硬件片选由SPI外设自动控制还是软件片选自己用普通GPIO控制对比项硬件片选NSS硬件控制软件片选GPIO控制控制时机由SPI控制器在传输开始/结束时自动拉低/拉高由代码在任意时刻拉低/拉高灵活性低片选行为受控制器内部时序限制高想什么时候拉就什么时候拉连续传输某些控制器会在字节间短暂释放CS容易打断从设备可以一直保持CS低电平适合长连续读复杂度配置简单代码中需要手动管理适用场景简单、标准的单次/多次传输需要精细控制片选时序或兼容特殊从设备以STM32为例CubeMX里有一个“NSS”配置项可以选Hardware NSS Output、Hardware NSS Input或者Software NSS。如果你追求快速验证选Software NSS然后把任意GPIO拉低拉高就行如果选Hardware NSS Output控制器在每次传输开始前自动拉低传输结束自动拉高写代码确实省事但遇到需要“片选一直保持低、中途切换数据方向”的场景它可能提前释放CS导致从设备复位状态。Linux那边的做法更粗暴。很多SPI驱动宁可不用主控自带的片选而是在设备树里配置一个cs-gpios节点用普通GPIO模拟片选。这样做的好处是片选时序完全由驱动控制兼容性最好。缺点是一个字节一个字节传的时候片选拉低的开销会拖慢一点速度但这个代价在绝大多数场景下可以忽略。3.3 多个从设备共用MISO时的隐藏陷阱一主多从还有一个特别容易被忽视的坑所有从设备的MISO都并联在主机同一个MISO引脚上。正常情况下只有被选中CS拉低的那个从设备才允许驱动MISO没被选中的必须把MISO置为高阻态High-Z。如果某个从设备没做这个处理或者它的CS接线有问题你就会看到MISO线被多个设备同时拉扯波形一塌糊涂。很多主控的MISO引脚内部会配置上拉电阻能一定程度缓解这种冲突但不能根治。检查这类问题时优先确认未选中设备的MISO是否高阻以及CS信号是否真的稳定拉到了对应从设备上。4. 用W25Q64串起一条完整的SPI读写链路4.1 W25Q64的基本指令结构W25Q64是一个64Mbit8MB的SPI NOR Flash很多开发板上都带着它。拿它当例子很合适因为SPI Flash的指令模式代表了大多数SPI从设备的交互方式先发指令码再发地址然后是数据。调试时第一个要跑的指令永远是读JEDEC ID0x9F。这个指令不需要地址主机发送0x9F之后从设备会连续返回3个字节的ID比如华邦的W25Q64通常会返回0xEF、0x40、0x17。读ID的意义在于验证物理连接、SPI模式、片选时序都正确而且它不会改写Flash内部状态随便试都不怕。常见的几个指令指令指令码作用Read JEDEC ID0x9F读取芯片ID验证通讯Read Data0x03从指定地址连续读数据Read Status Register0x05读取状态寄存器Write Enable0x06写使能写操作前必须发Page Program0x02页编程一次最多256字节Sector Erase0x20扇区擦除一次4KB这里有个新手必踩的坑写Flash之前必须先发0x06写使能指令。Flash没有收到写使能你发再多的Page Program指令它也不会写进去。而且写使能是一次性的写完一次就会自动清除每次写操作前都得重新发。4.2 STM32CubeMX配置SPI的关键步骤用STM32CubeMX生成SPI工程时主要关注这几个配置Mode选择Full-Duplex Master因为W25Q64是典型的双向全双工设备。如果打算用软件片选把NSS设为SoftwareCubeMX里是Disable或者Software具体版本不同叫法略有差异然后在GPIO配置里单独把CS引脚设为Output Push Pull。Prescaler预分频根据主频和从设备支持的最大时钟算。W25Q64标准模式下可以跑到几十MHz但初次调试别拉满先从较低频率开始比如1MHz左右波形更容易看问题也更少。CPOL/CPHA选Mode 0CPOL0CPHA0。Data Size选8 bitFirst Bit选MSB First绝大多数SPI外设都是MSB在前。CubeMX生成的HAL代码里核心收发函数是HAL_SPI_Transmit、HAL_SPI_Receive和HAL_SPI_TransmitReceive。但有个细节很多人没注意SPI是全双工协议所以HAL_SPI_Receive并非只收不发它在接收数据的同时也在SCLK上发数据只是发出的数据全被忽略了。反过来HAL_SPI_Transmit的接收缓冲被忽略。读ID的代码我一般这样写uint8_t tx_data[4] {0x9F, 0x00, 0x00, 0x00}; uint8_t rx_data[4] {0}; HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, tx_data, rx_data, 4, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);发0x9F之后补了3个0x00因为从设备是在你持续给SCLK时才会回数据。4个字节传完rx_data[1]、rx_data[2]、rx_data[3]就是JEDEC ID。片选必须贯穿整个读写过程一旦CS拉高从设备就认为这次操作结束了。4.3 调试顺序先看ID再看波形最后才谈读写我调试SPI Flash的顺序永远是固定三步第一步读ID。ID不对或者全是FF先不动代码上逻辑分析仪看波形。 第二步抓波形。重点看四件事CS是不是在传输期间一直保持低SCLK频率是否正常SCLK空闲电平是低还是高MOSI/MISO上的数据位是不是按预期出现。 第三步ID和波形都正常后再试Read Data读一段数据然后才碰写操作。为什么要这么保守因为写Flash包含擦除、页编程、状态寄存器轮询等一系列状态机过程一旦通讯本身有问题你会分不清是Flash状态机的问题还是SPI传输的问题。先把最基础的读链路打通后面才有排查的依据。5. 软件模拟SPI什么时候需要、怎么写、怎么控制时序5.1 软件模拟SPI并不是“低级”做法很多人觉得软件模拟SPI是MCU没有硬件SPI时才会用的替代方案其实不完全对。我遇到过的软件模拟需求包括SPI外设数量不够、引脚复用冲突、某些从设备需要特殊片选时序、想要低成本小封装MCU同时挂多颗不同SPI设备……这些场景下用GPIO模拟是更务实的选择。而且软件模拟SPI对理解协议非常有帮助。当你亲手用代码把一个字节一个字节地按时序发出去你对CPOL/CPHA、采样边沿、数据稳定时间的理解会比看十遍数据手册都深刻。5.2 GPIO模拟主机代码以Mode 0为例GPIO模拟的核心思路就是在SCLK的边沿上把MOSI上的数据设置好再把MISO上的数据读进来。#define SPI_SCLK_HIGH() HAL_GPIO_WritePin(SCLK_GPIO_Port, SCLK_Pin, GPIO_PIN_SET) #define SPI_SCLK_LOW() HAL_GPIO_WritePin(SCLK_GPIO_Port, SCLK_Pin, GPIO_PIN_RESET) #define SPI_MOSI_HIGH() HAL_GPIO_WritePin(MOSI_GPIO_Port, MOSI_Pin, GPIO_PIN_SET) #define SPI_MOSI_LOW() HAL_GPIO_WritePin(MOSI_GPIO_Port, MOSI_Pin, GPIO_PIN_RESET) #define SPI_MISO_READ() HAL_GPIO_ReadPin(MISO_GPIO_Port, MISO_Pin) uint8_t soft_spi_transfer_byte(uint8_t tx_data) { uint8_t rx_data 0; for (int i 7; i 0; i--) { // 在SCLK拉高之前把MOSI数据准备好 if (tx_data (1 i)) { SPI_MOSI_HIGH(); } else { SPI_MOSI_LOW(); } delay_us(1); // SCLK上升沿从机采样MOSI SPI_SCLK_HIGH(); delay_us(1); // 此时MISO上是从机已经稳定的数据 if (SPI_MISO_READ()) { rx_data | (1 i); } // SCLK下降沿从机开始切换下一位数据 SPI_SCLK_LOW(); delay_us(1); } return rx_data; }这个代码里最关键的是delay_us。Mode 0的时序要求是MOSI数据必须在SCLK上升沿之前建立稳定主机的MISO采样点必须放在SCLK高电平稳定之后。GPIO翻转本身有延迟如果代码里不延时从机的数据可能还没准备好主机就读了或者主机的数据还没稳定从机就开始采样了。5.3 时序精度软件模拟的上限在哪里软件模拟SPI的速度上限取决于三件事GPIO翻转速度、延时函数精度、代码本身的循环开销。STM32F103这类Cortex-M3核心跑72MHz主频一个空循环延时约1us每bit至少需要2个延时周期半个周期拉高、半个周期拉低算下来一个字节大概需要16~20us对应大概500kHz~1MHz的SCLK频率。如果优化得好一点去掉不必要的判断用查表法代替移位判断能做到更高一些但总体来说软件模拟SPI适合中低速场景高速大吞吐还是要靠硬件外设。还有一点要注意如果你在RTOS环境里做软件模拟延时函数可能会被任务切换打断导致SCLK毛刺变多或者周期不均。稳妥的做法是模拟传输期间禁止调度或者干脆用DWT计数器做高精度延时。6. 实测中的踩坑合集与排查思路6.1 读回全是FF先检查这四件事读到0xFF实际上是SPI调试最常见的现象遇到它不用慌按顺序排查第一片选有没有真正拉低。很多芯片的CS是低有效如果你把GPIO初始化成了高有效或者忘记了拉低CS从设备根本没被激活MISO自然一直保持高电平读回来的就是0xFF。第二SCLK空闲电平对不对。如果从设备要求CPOL1空闲高而你配置成CPOL0很多从设备会直接不工作。第三MISO引脚有没有配置正确。MCU的MISO引脚要配置成输入模式如果有上拉确认上拉不影响信号。有些板子MISO上接了上拉电阻如果没选中设备MISO读到高电平但片选拉低后数据应该是正常的。第四从设备有没有上电稳定。个别Flash芯片初始化需要一段时间上电后立即读ID可能直接返回0xFF。厂家数据手册里会有一个tVSL时间参数表示从电源上电到芯片可操作的时间实测一般几个毫秒就够但如果你上电后立刻操作很容易踩这个坑。我之前调过一块AD7124的ADC手册明确要求上电后必须等待至少几百毫秒才能访问SPI寄存器否则读出来全是0xFF。当时第一反应是怀疑模式配置错了结果完全是初始化太着急。6.2 SCK空闲电平不对波形图比代码更诚实有一个项目里我明明在CubeMX里选了Mode 0但示波器上看到的SCLK空闲电平总是高。代码我反复确认了好几遍没写错。后来才发现CubeMX生成的初始化代码里SCLK引脚被配置成Alternative Function了但引脚复用关系里它默认复用成了别的功能导致波形不对。这种问题光看代码是看不出来的必须上逻辑分析仪。我推荐调SPI时养成一个习惯无论代码看起来多正常都拿逻辑分析仪抓一次波形顺带把CS、SCLK、MOSI、MISO四根线都挂上一看便知空闲电平、时钟频率、数据位顺序是否如你所愿。逻辑分析仪是最便宜的SPI老师。6.3 片选毛刺和硬件NSS的连续传输问题硬件NSS有个非常隐蔽的坑某些主控在连续发送多字节数据时会在字节传输间隙短暂释放CS时间极短但很多从设备会认为这是一次传输结束。表现就是单独读一个字节没问题连续读一串数据时后面的数据全都不对。我遇到过一块SPI接口的显示屏驱动板从设备要求CS必须在整帧数据内保持低我用硬件NSS怎么配都不行最后改成软件GPIO控制CS问题立刻消失。如果你的从设备数据手册里强调CS必须在多字节传输期间“保持低”那最好直接用软件片选。6.4 高速模式下的间歇性故障不一定是代码问题SPI一旦把时钟频率拉高问题就变得玄学了。比如20MHz时钟下偶尔读到某个字节错误降低到1MHz又完全正常。这种间歇性故障往往不是逻辑问题而是信号完整性问题。SPI是同步接口理论上对时钟抖动没那么敏感但数据线、时钟线上的振铃和串扰会导致采样点的建立时间不够。排查手段很简单降频确认是不是频率问题然后用示波器看SCLK和MOSI的上升沿有没有明显过冲、振铃。如果波形不干净可以在时钟线和数据线上加几十欧姆的串阻或者在接收端的采样点设置上留出余量。另外字节序也容易被忽略。有的从设备支持MSB First有的支持LSB First甚至有的支持通过寄存器切换。如果波形和数据都正常但读回来的数字总是反过来比如0x01变成0x80那基本就是位顺序错了。6.5 我排错时常用的顺序表简单整理一下我调SPI的顺序表给你做个参考顺序检查项工具1物理连接四根线是否接对、是否有虚焊万用表2片选是否按预期拉低/拉高逻辑分析仪3SCK空闲电平、频率是否正确逻辑分析仪/示波器4CPOL/CPHA模式与从设备手册是否一致数据手册对比5MOSI/MISO数据位顺序、字节序逻辑分析仪解码6从设备方向上电初始化、使能寄存器、写使能指令逻辑分析仪代码单步九成的问题走到第3步就能找到原因。7. SPI和I2C怎么选选型决策7.1 SPI/I2C/UART的关键差异对比做嵌入式选型时经常需要决定某个外设是该用SPI还是I2C还是UART。这里我放一个自己经常参考的对比表对比项SPII2CUART引脚数4根SCLK、MOSI、MISO、CS2根SCL、SDA2根TX、RX通讯模式全双工半双工全双工传输速率高几十MHz常见中低速常见100kHz/400kHz/1MHz取决于波特率常见到几Mbps应答机制无应答错了需要软件约定校验带ACK应答能检测从机是否收到无应答需协议层约定从设备寻址靠CS片选线无地址概念靠7位/10位地址总线上可挂多个设备点对点或靠协议地址多主机支持不原生支持原生支持不原生支持抗干扰需注意信号完整性开漏上拉走线短距离较稳要看电平标准典型场景Flash、SD卡、LCD、ADCEEPROM、传感器、RTCGPS模块、蓝牙模块、调试串口7.2 结合实际场景给决策建议如果外设需要大吞吐量、高速度比如SPI Flash刷固件、LCD刷图片、SD卡读写SPI几乎是不二之选如果只是偶尔读一个传感器数据对速度不敏感但想省引脚、多挂几个设备在同一组线上I2C更合适如果是对接现成的串口模块、无线模块UART反而最省事。还有一点实际体会SPI虽然快但从设备一多片选线就占引脚而且PCB布线时SCLK和MOSI容易形成干扰I2C虽然慢但两根线能挂一堆设备硬件开销小。很多时候选择不是技术上的“谁更强”而是工程上的“谁更合适”。我自己调过无数SPI外设之后最大的一个感触是SPI协议本身并不难难的是“从设备手册里那一堆没人看的时序图”。只要你肯花十分钟把CPOL/CPHA搞清楚再用逻辑分析仪把波形对照一遍这个协议就能变成你手上最顺手的高速工具。希望这篇经验分享能帮你少走几个我当年走过的弯路。