2026/9/7 2:19:00

嵌入式SPI协议实战避坑指南:时序、片选与多从机设计

嵌入式SPI协议实战避坑指南:时序、片选与多从机设计 搞嵌入式的没有不跟SPI打交道的但恰恰是这个看起来最简单的协议坑了我很久。直线接线没问题、逻辑分析仪上波形也有屏就是不亮换一个传感器模块读出来的数据全是0xFF把某个外设往总线上多挂一路原来的设备直接罢工。这些问题我当年一个个都踩过回头排查才知道根子上全是SPI的细节理解不到位。这篇文章我把自己用SPI这几年踩过的坑、总结出的规律梳理一遍。从四根线到底怎么分工到CPOL/CPHA时序模式怎么看数据手册再到硬件片选和软件片选怎么选、一根总线挂多个从机要注意什么、硬件SPI不够用时怎么用GPIO模拟最后把SPI和IIC、UART放在一起做个选型对比。内容全是实战里磨出来的经验适合正在用STM32、ESP32、Arduino做项目或者在调SPI屏幕、Flash、SD卡、各种传感器的朋友。1. 先搞清楚SPI在解决什么问题再去背四根线的名字很多人学SPI上来就背SCLK、MOSI、MISO、CS背完就懵。我建议反过来想SPI是被什么需求逼出来的。1.1 为什么需要一种新的串行协议早年的板级通信UART是主力但UART有个先天性缺陷——异步通信收发两边各用各的时钟。你要发数据就得提前约定波特率两边时钟稍微有点偏差数据就错位了。而且UART只能点对点一台主机想同时跟多个外设通信得接多路串口管脚根本不够用。后来IIC出现了用两根线挂一堆设备省管脚还有地址机制。但IIC速度上不去标准模式100kbps快速模式也就400kbps到了1Mbps已经属于高速模式对时序要求还特别苛刻。我当年试图用IIC去读一颗200万像素的摄像头寄存器光初始化配置就得小一秒根本没法用。在这个背景下SPI就是冲着两个核心诉求去的一是快二是简单粗暴。它把时钟线单独拉出来主机产生时钟、从机跟着时钟走没有波特率对齐的问题也没有地址帧、应答位的开销。时钟能拉到多高取决于PCB走线和从机能跑多快。几十MHz非常常见。这就是SPI至今在中低速板级通信里不可替代的根本原因。1.2 四根线各管什么事SPI是主从结构一个主机可以挂多个从机最少只需要四根线但注意——这个最少是有条件的你挂的从机越多片选线就越多。SCLK串行时钟由主机产生从机只能被动接收。时钟的频率就是数据传输速率的核心参数之一。MOSIMaster Out Slave In主机输出、从机输入。主机要发给从机的数据通过这根线一位一位传过去。MISOMaster In Slave Out从机输出、主机输入。从机回给主机的数据走这根线。CS/SS片选信号通常是低电平有效。主机把某个从机的CS拉低等于告诉它我要跟你说话其他CS保持高电平的从机就闭嘴MISO输出呈高阻态。这里有个新手最容易忽略的点SPI是全双工。MOSI和MISO是两根独立的线主机发数据的同时从机也在往MISO上放数据。所以一次SPI传输读和写是同时完成的。这个特性直接影响后面对代码和时序的理解——很多人不理解为什么SPI读Flash要先发一个空字节其实那个空字节就是时钟的载体没有MOSI上的数据SCLK也得继续跑从机才能把数据推出来。1.3 主从结构带来的一个隐含约束因为是主机负责产生时钟所以SPI总线上的所有活动必须由主机主动发起。从机没有时钟它永远不会主动说话。这个特点既是优点也是限制优点是好控制主机想什么时候通信就什么时候通信限制是如果某个从机有紧急事件需要通知主机它只能靠另外一根中断线拉个引脚SPI协议本身没有从机主动上报的机制。设计系统架构时这一点要先想清楚别等到做项目了才发现某个传感器需要实时上报结果SPI总线这边还在轮询。2. 时序模式是SPI一半的坑CPOL、CPHA与数据手册该怎么读说实话SPI通信里我见过最多的问题不是接线接错而是时序模式对不上。主机和从机一个按模式0工作一个按模式2工作波形看起来都有数据就是乱码。这一章我把这块彻底讲透。2.1 从波形图看懂CPOL和CPHASPI的时序模式由两个参数决定CPOLClock Polarity时钟极性和CPHAClock Phase时钟相位。CPOL决定的是SCLK的空闲电平——就是不传数据的时候时钟线停在什么电平。CPOL0空闲低电平CPOL1空闲高电平。CPHA决定的是数据在哪个边沿被采样。CPHA0在第一个边沿采样CPHA1在第二个边沿采样。这里第一个边沿和第二个边沿要结合CPOL来理解如果CPOL0时钟空闲是低电平那第一个有效边沿就是上升沿如果CPOL1时钟空闲是高电平第一个有效边沿反而是下降沿。两个参数一组合就是四种模式模式CPOLCPHA空闲电平第一个边沿采样边沿Mode 000低电平上升沿上升沿Mode 101低电平上升沿下降沿Mode 210高电平下降沿下降沿Mode 311高电平下降沿上升沿2.2 绝大多数设备用的都是Mode 0或Mode 3实战里最常用的就是Mode 0CPOL0, CPHA0其次Mode 3CPOL1, CPHA1。为什么是这两个因为SPI协议规范里对默认状态推荐的是极性和相位都为0或都为1这两种模式足够覆盖绝大多数Flash芯片、SD卡、屏幕驱动、传感器模块。Mode 1和Mode 2不是没有但少见得多。所以如果你手里有一颗不认识的芯片又没有数据手册先试Mode 0不行就试Mode 3大概率能通。2.3 数据手册里怎么定位时序模式很多数据手册不会直接写Mode 0这几个字需要你自己看图。这里我教一个很实用的方法找到手册里的SPI时序图SPI Timing Diagram看三样东西。第一看SCLK空闲时是低还是高就能确定CPOL。第二看数据的稳定区出现在第一个边沿还是第二个边沿附近如果数据在上升沿处稳定、下降沿处切换说明是在上升沿采样CPHA0反之则是CPHA1。第三很多手册还会给一张表格里面有CPOL和CPHA或对应模式的说明直接对照就行。我见过最坑的一颗芯片是某国产陀螺仪手册翻译得稀碎时序图画成了Mode 1结果实际是Mode 3折腾了我半天。后来我学聪明了直接用逻辑分析仪抓从机在已知频率下回的数据换个模式试哪种能解出正确ID就锁定哪种。提示买模块时优先选明确标注了SPI模式的成品模块那种连时序模式都不标的模块要么自己拿逻辑分析仪测要么换一家买别浪费项目时间。但如果必须用可以按先Mode 0后Mode 3、再Mode 2最后Mode 1的顺序试最快定位。3. 片选决定成败硬件NSS与GPIO软件片选怎么选片选是SPI里最容易被低估的信号。很多人以为就是把CS拉低再拉高结果遇到了各种莫名其妙的问界。3.1 STM32硬件NSS的坑STM32的SPI外设提供了硬件NSS功能一种是NSS引脚由硬件自动控制发送数据前自动拉低、发完后自动拉高另一种是NSS输出模式外设自己控制引脚状态。听起来很方便对吧我当年也是这么想的直到踩了坑。坑在哪儿硬件NSS的自动拉低/拉高时机不是你想的那样干净利落。它在SPI传输完成但配置未清除时可能会提前释放片选导致从机认为本次传输结束后面补的字节被当成新的一帧开头而且它跟DMA、FIFO配合时行为更复杂。多层板子、高频时钟、长距离飞线这些情况下硬件NSS很容易出诡异问题。另外一个坑是引脚复用功能冲突。STM32的NSS脚经常和别的功能复用你在CubeMX里开了硬件NSS可能就把某个用得好好的GPIO或者别的外设引脚给顶掉了。3.2 GPIO软件片选简单、可控、推荐我的建议是除非有特殊需求否则用普通GPIO做软件片选。逻辑上就是传输前把对应引脚拉低传输完成后拉高。// 软件片选控制示例HAL库风格 #define CS_LOW() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET) #define CS_HIGH() HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET) // 读Flash ID uint8_t spi_read_flash_id(void) { uint8_t cmd 0x9F; // JEDEC ID命令 uint8_t id 0x00; CS_LOW(); HAL_SPI_TransmitReceive(hspi1, cmd, id, 1, 100); CS_HIGH(); return id; }软件片选的好处有三个。第一时机完全可控你想在什么时候拉低、什么时候拉高自己说了算不会受外设状态机影响。第二不占用专用NSS引脚随便找个GPIO都能当片选布局布线自由度大。第三挂多个从机时每个从机一个GPIO逻辑一清二楚。要注意的是两个从机之间切换片选时最好加一点间隔比如几个微秒不要一个CS刚拉高立刻拉低另一个。从机芯片内部需要时间完成当前帧的收尾太快切换可能导致残留数据错位。我之前调一块屏幕和一颗Flash共用总线时切换太快就会出现Flash偶尔读错地址的现象。3.3 多从机场景的接线策略一根SPI总线上挂多个从机所有从机的SCLK、MOSI并在一起MISO也并在一起片选各自独立。这里有几个关键点必须注意。MISO线是所有从机共用的但同一时刻只能有一个从机在驱动这根线。所以从机在不被选中时MISO必须呈高阻态不能死顶着输出。绝大多数SPI从机芯片都是这么设计的但个别廉价模块会在CS拉高后还拖个尾巴。遇到这种情况要么在MISO上加个串联电阻缓冲要么给每个从机MISO串一个小的三态缓冲器比如74LVC1G125成本不高但能保平安。另外SCLK和MOSI在切换从机时尽量保持在同一状态。很多从机是在CS下降沿才真正检测时钟边沿如果CS拉低时SCLK刚好是一个毛刺可能产生误触发。4. 从CubeMX到逻辑分析仪跑通一次真实SPI传输这一章我带你把一次完整的SPI通信走通从STM32CubeMX配置开始到代码实现再到用逻辑分析仪验证波形。这套流程我每次做新板子都会完整走一遍能省掉后面80%的调试时间。4.1 CubeMX里的关键配置项打开CubeMX选择一个SPI外设你会看到一堆配置项。每项怎么填对应什么底层逻辑下面这个表格捋清楚了配置项常见取值说明ModeTransmit Only / Full-Duplex等根据外设需求选读多写多选Full-DuplexPrescaler4/8/16/32/64等分频系数决定实际SCLK频率Clock PolarityLow / High对应CPOL看从机手册Clock Phase1st Edge / 2nd Edge对应CPHA看从机手册Data Size8 Bit为主个别传感器用16位必须匹配First BitMSB First为主极少数从机要求LSB FirstNSSSoftware按上一章建议优先软件片选CRCDisable极少用后文说明Prescaler的计算逻辑很容易被忽略。STM32的SPI时钟源来自APB外设时钟不同型号、不同总线配置APB时钟不一样。如果你用的主频是170MHzAPB2外设时钟可能也是170MHzPrescaler填8SPI时钟就是170/8约21MHz。有些屏幕模块最高只支持20MHz你填个4倍分频就是42MHz就会概率性花屏。所以务必先算清楚实际频率再对一下从机的最高时钟限制。4.2 基础收发代码框架CubeMX生成初始化代码后最常用的是HAL库的阻塞式收发接口// 写操作发送buffer、不接收数据 HAL_StatusTypeDef status; status HAL_SPI_Transmit(hspi1, tx_buffer, len, timeout); // 读操作先发命令/地址再接收数据 HAL_StatusTypeDef status; status HAL_SPI_Receive(hspi1, rx_buffer, len, timeout);这个Receive接口内部其实也是全双工的发送部分是空字节不需要你手动处理。它的本质就是主机输出空数据产生时钟从机在时钟驱动下把数据放到MISO上主机把它采进来。不过要注意阻塞式收发在传输过程中会一直占用CPU如果你的系统里还有其他实时任务比如PID控制、WiFi协议栈建议使用中断或DMA方式。中断方式用HAL_SPI_Transmit_ITDMA方式用HAL_SPI_Transmit_DMA本质逻辑一样只是数据传输不占CPU。用DMA时记得开启SPI的DMA请求中断否则你不知道什么时候传输完成。4.3 逻辑分析仪验证波形写代码从来不是终点验证波形才是。我强烈建议每一个做嵌入式的人搞一个24MHz以上采样率的逻辑分析仪二三十块钱已经够用调试SPI的效率能翻倍。接线很简单把逻辑分析仪的CH0接SCLK、CH1接MOSI、CH2接MISO、CH3接CS共地接好然后用配套软件打开SPI协议解码器设置CPOL和CPHA与你的配置一致抓一次通信。第一次抓波形重点看三件事。第一CS是不是在传输期间全程拉低有没有中途抖动第二SCLK频率对不对跟预想差多少第三MOSI上的数据和解码器解出来的数据是否跟你的发送buffer一致。把这些确认好了再去解码MISO上的回包往往问题瞬间水落石出。我遇到过最典型的一个问题主板SPI走线太长SCLK信号过冲逻辑分析仪上显示频率正确但屏就是偶尔白屏。后来在SCLK上串了一个33Ω电阻问题直接消失这就是信号完整性问题在SPI上的经典表现。高频走线时串阻抑制振铃不是玄学是必要的工程手段。5. 一根总线上挂多个从机屏幕与SD卡共享SPI的实战经验ESP32、STM32这类主控的SPI外设数量是有限的常常需要让一块屏幕和一张SD卡共用一根SPI总线。这个需求非常典型但也非常容易出问题。5.1 为什么两个器件能共享一根总线本质原因就是前面讲的片选机制。SPI是主从之间点对点通信但通过片选信号可以实现时分复用。同一时刻只有一个从机的CS是低电平其他从机的CS都是高电平它们的MISO处于高阻态不会影响主机的MISO采样。所以屏幕和SD卡共享SCLK、MOSI、MISO各自用独立的CS脚逻辑上完全可行。我在做一个小型数据记录仪时就是让ILI9341屏幕和MicroSD卡共用ESP32的VSPI总线。屏幕负责显示SD卡负责存储。两个器件按时间片轮流占用总线互不干扰实测很稳。5.2 共享总线时必须满足的三个条件第一个条件片选必须独立且软件可控。这个前面已经说过硬件NSS在这种场景下基本没法用。第二个条件两个从机的时序模式必须一致或者至少都兼容同一个模式。屏幕和SD卡绝大多数都支持Mode 0或Mode 3如果你选的屏幕只支持Mode 2SD卡只支持Mode 0那就要在两个器件切换时动态切换SPI的CPOL和CPHA增加复杂度不说还容易出错。第三个条件MISO线必须在从机未选中时是高阻否则两个从机同时驱动MISO电平冲突直接烧引脚。第三个条件不是所有模块都满足。我踩过一个坑某TFT屏幕模块在CS拉高后MISO依然被驱动为低电平SD卡正常工作时这根线被它拽住了导致SD卡的数据读取随机失败。排查了很久才定位到最后在屏的MISO上串了一个1kΩ电阻问题解决。所以买模块前或者画板前最好先看下模块原理图确认MISO是否有三态控制。5.3 共享总线时的速率与负载问题屏幕和SD卡对时钟频率的要求通常不一样。屏幕为了刷新率高能跑到20MHz甚至更高SD卡就娇贵一些虽然标称支持25MHz但在飞线场景下跑到10MHz以上就容易出错。我建议的做法是把总线时钟设成两个器件都支持的较低值然后优先保证SD卡的稳定性。屏幕刷新率不够可以通过优化显示缓冲、只更新局部区域来弥补而不是硬拉高SPI时钟。ESP32的SPI外设支持按每次传输设置不同的时钟频率这个功能很实用——操作SD卡时用低速比如10MHz刷新屏幕时用高速比如26MHz两边需求都照顾到了。如果主控没有这种特性简单粗暴地统一用较低频率反而是最稳的。还有一个很多人不重视的问题——总线负载。SCLK和MOSI是共享的走线分支多、器件引脚电容叠加会让信号边沿变缓。SPI频率越高越明显。如果板子上挂了三个以上从机SCLK上串个22-47Ω电阻做阻抗匹配非常重要同时尽量让分支走线等长且短。6. 硬件SPI不够用时的Plan BIO口模拟三线SPI不是所有场景都适合用硬件SPI。当你遇到资源不足、时序特殊、或者手头主控根本没有硬件SPI时用GPIO模拟SPI是一个必须掌握的技能。6.1 什么时候真的需要软件模拟我知道很多人听到软件模拟SPI会觉得低级其实不是。真实场景里软件模拟SPI反而是救命稻草。一种典型场景是引脚不够。某些封装很小的MCU比如TSSOP16、SOIC8里塞的芯片硬件SPI模块虽然有但引脚都在特定位置跟你实际布局对不上。这时候用IO口模拟三线SPI就能把SPI放在任意GPIO上布线的自由度一下子大很多。另一种是外设过多一颗STM32F103C8T6只有两个SPI一块板子上挂了一颗屏幕、一块Flash、一个CAN控制器、一个编码器硬件SPI完全不够分剩下的设备就用软件模拟。还有一种特殊场景某个从机的时序非常特殊硬件SPI的模式、位宽、片选行为都满足不了它只能靠GPIO一点一点抠时序。6.2 模拟SPI的代码框架软件模拟SPI的核心就是两个动作拉高拉低SCLK模拟时钟在合适的边沿往MOSI写数据或者从MISO读数据。以三线SPISCLK、MOSI、MISO为例一个简单的发送函数可以写成这样#define SCLK_HIGH() HAL_GPIO_WritePin(SCLK_GPIO_Port, SCLK_Pin, GPIO_PIN_SET) #define SCLK_LOW() HAL_GPIO_WritePin(SCLK_GPIO_Port, SCLK_Pin, GPIO_PIN_RESET) void spi_soft_write_byte(uint8_t data) { for (int i 7; i 0; i--) { // MSB First if (data (1 i)) { MOSI_HIGH(); } else { MOSI_LOW(); } SCLK_HIGH(); // 上升沿数据被采样 SCLK_LOW(); // 时钟恢复低电平 } }这是Mode 0空闲低、上升沿采样的写法。如果是Mode 3空闲高、上升沿采样初始化时要把SCLK拉到高电平数据变换后在SCLK下降沿前保持稳定然后下降沿让从机采样。读操作跟写操作的差别在于主机不需要关心MOSI上的数据只需要在采样边沿把MISO的电平读进来。uint8_t spi_soft_read_byte(void) { uint8_t data 0; for (int i 7; i 0; i--) { data 1; SCLK_HIGH(); // 上升沿从机把数据推到MISO data | HAL_GPIO_ReadPin(MISO_GPIO_Port, MISO_Pin); SCLK_LOW(); } return data; }6.3 软件模拟的时序与性能权衡软件模拟的最大瓶颈是速度。一次简单的GPIO翻转加判断循环在72MHz的STM32F103上大概要花几十纳秒一个字节8个位一帧可能还要加片选操作算下来整体速率能跑到几MHz已经不错了。对SPI Flash的读操作这种纯数据搬运场景软件模拟确实偏慢但对传感器寄存器配置、LCD初始化这类低频操作完全够用。延时怎么加也有讲究。有些器件对SCLK的最小高低电平宽度有要求比如手册写了最小SCK周期是200ns那你翻转时钟后最好加一小段延迟用for循环空转或者__NOP()否则连续翻转太快从机跟不上表现为偶发数据错乱。加多长按手册的时序参数来留50%以上余量最稳妥。软件模拟还有一个容易被忽视的细节中断关闭。模拟SPI传输过程中如果来了一个中断GPIO电平状态在中断返回后可能跟期望的不一致导致某一比特翻转丢失。所以在传输关键帧比如Flash页写入期间临时关闭中断或临界区保护是很有必要的否则偶尔的乱码会让你怀疑人生。7. 别再用快来区分SPI和IIC三组选型对比每次一聊SPI就有人拿IIC和UART一起来问。我总结了一套自己的对比框架用物理层差异倒推应用场景比单纯背SPI快、IIC慢、UART简单要靠谱得多。7.1 物理层差异决定一切UART是异步串行收发双方各管各的时钟所以必须约定波特率才能通信。它只需要TX和RX两根线全双工但没有时钟线也就没有主从同步的概念只能点对点。只要波特率一致、空闲电平都对数据就能传这是它协议最简单的一面。IIC是同步串行但它只有两根线——SCL时钟和SDA数据。数据线是双向的半双工同一时刻只能一个方向传输。它用地址机制解决多设备共享问题每次传输主机发一个7位或10位地址选出从机然后再传数据。少了片选线代价就是传输效率低、时序要求严苛。SPI是最奢侈的时钟、发、收、片选四根线起步全双工、速度快、协议开销几乎没有。但它的地址能力为零多个从机完全靠片选线区分从机数量越多占用的GPIO越多。7.2 什么时候用谁一张表说清楚对比维度SPIIICUART线数4根起步SCLK/MOSI/MISO/CS2根SCL/SDA2根TX/RX通信方式同步全双工同步半双工异步全双工多从机能力靠片选从机多占管脚靠地址从机多不占管脚点对点不支持速率数十MHz级别标准100k/400k快速1M高速3.4M取决于波特率常用115200bps典型场景屏幕、Flash、SD卡、ADC/DAC高速采样传感器小数据量、RTC、EEPROM、温度计GPS模块、蓝牙模块、串口调试、工业设备协议开销几乎没有裸数据有地址帧、应答位效率打折有起始位、停止位、校验位UART虽然慢但在设备互联、长线传输、无线模块对接上仍然是最方便的因为不需要共享时钟双方各收各的收发天然解耦。7.3 数据手册上的指纹帮你快速判断拿到一颗未知芯片怎么快速判断它该用哪个协议我以前的经验是看引脚数量和名字。如果引脚里有SCL和SDA那就是IIC如果有SCLK/SCK加MOSI/MISO加CS那就是SPI如果只有RX和TX那就是UART。这三组名字就是协议最清晰的指纹几乎不会混淆。稍微特殊一点的是有些传感器同时支持IIC和SPI通常通过某个引脚的电平或地址引脚选择模式看手册里It is possible to select I2C or SPI mode by...这样的描述就能定位。还有一个小提示IIC的SCL和SDA外部必须接上拉电阻SPI一般不需要从机内部可能自带弱上拉或者直接推挽驱动UART的TX/RX之间注意电平匹配、不要直接接RS232的电平。这几个细节在新手阶段最容易翻车接口一接错波形抓都抓不出来。SPI这个协议说实话核心概念就那么几个时钟、相位、片选、全双工。但每个细节展开都有实际工程的坑。最终我自己养成的习惯是无论多简单的外设上电后第一件事就是拿逻辑分析仪抓波形确认时序模式无论多赶的项目片选切换之间都留足余量无论多贵的器件都要核对一下最高SCLK限制。希望大家看完这篇文章能在SPI这条路上少走一些我当年走过的弯路。