资讯动态

SPI协议从原理到实战:架构、工作模式与调试技巧

发布时间:2026/9/13 5:24:40 来源:尧图企业网站定制
1. 想玩转嵌入式SPI协议绕不开做嵌入式的早晚得跟SPISerial Peripheral Interface串行外设接口打交道。它是由摩托罗拉在八十年代推出的一种高速、全双工、同步串行通信总线至今仍是板级通信的绝对主力。你可能用过它驱动Flash存储芯片、读传感器数据、刷屏幕、配无线模块甚至跟FPGA或另一颗MCU高速换数据——凡是需要“快”和“稳”的场合SPI几乎都是首选。这篇东西就是把SPI协议从里到外拆开揉碎讲清楚。我会结合自己多年调试板子的经验把总线架构、四种工作模式、片选信号的硬件/软件两种处理方式、读写Flash的完整流程还有一堆只有在实际调板时才会遇到的坑都摊开来讲。适合刚入门的学生、做嵌入式开发的工程师以及所有想搞懂“SPI到底是咋工作”的人。读完你能明白协议本身其实不复杂复杂的是你用的那款芯片会怎么“折腾”你。2. 先弄懂SPI的总线构成四条线和一个移位寄存器2.1 四根信号线各自分工明确SPI通信靠四根线SCLK串行时钟、MOSI主出从入、MISO主入从出、CS/SS片选低电平有效。主机产生时钟数据在时钟边沿按位传输。CS拉低表示选中某个从机主机可以同时挂多个从机但同一时刻只能有一个从机被“点名”。这里要特别提醒实际项目中不同芯片手册给MISO/MOSI起名五花八门比如DO/DI、SDO/SDI、Dout/Din。很多人第一次接反了线调半天没数据就是因为没仔细看从机手册按主机习惯硬套。先查从机引脚定义再连线能省很多时间。2.2 核心思想两边各一个移位寄存器互相倒数据SPI本质上是两个移位寄存器首尾相连。主机把一个字节写进自己的移位寄存器同时把寄存器里的旧数据推出去从机那边也在同步往自己的寄存器里推数据。每来一个时钟边沿两边各移一位一个时钟周期内主机发一位、收一位所以是全双工。举个形象的例子主机和从机各端着一杯水SCLK是“喊口令的人”每喊一声两人同时把自己杯里的水倒给对方一勺。8个时钟过后双方杯子里的水就交换了一遍。注意SPI没有类似UART的起始位/停止位概念也没有应答机制主机不给时钟从机就完全静止。这也是为什么SPI协议本身非常简单、容易用逻辑分析仪抓波形分析的原因。这里建议第一次接触SPI的人别急着调代码先拿逻辑分析仪看一次主机主动发0x55的波形把SCLK、MOSI、CS的变化过程看清楚比看十遍理论都管用。2.3 为什么SPI速度快、又适合板内通信SPI速率可以轻松跑到几十MHz甚至上百MHz远超I2C的几百kHz到几MHz比UART更是高出几个数量级。而且SPI没有地址帧、应答位这些额外开销时钟多少数据就传多少效率极高。不过要注意速度快是有代价的SPI没有流控机制主机不知道从机是否“忙完了”。如果从机处理速度慢主机就得在软件层加延时或者等从机用IO口拉低表示“我忙”否则数据就会丢。这一点在驱动NRF24L01这类无线模块时特别明显。另外板内通信距离短线长一点信号就变差这属于硬件设计范畴后面我会细说。3. 四种工作模式CPOL/CPHA是“必考题”3.1 CPOL决定时钟空闲电平CPHA决定采样边沿SPI协议定义了时钟极性CPOL和时钟相位CPHA。CPOL0表示空闲时SCLK为低电平CPOL1表示空闲时SCLK为高电平。CPHA0表示在第一个跳变沿采样数据CPHA1表示在第二个跳变沿采样数据。合起来就是模式0、1、2、3。模式0CPOL0CPHA0和模式3CPOL1CPHA1最常用绝大多数Flash、SD卡、屏幕都支持这两种。有很多人记混这两个参数。我的记忆方法是先看空闲电平再看第一个边沿。CPOL决定空闲啥样CPHA决定第一个边沿是否采样。调程序时如果数据全是乱的首选就是检查主机和从机的模式是否一致。这个错误很容易犯因为从机手册通常写“支持模式0和模式3”但你没注意它默认是模式几主机的配置恰好和它不一样结果就翻车了。对比一下各模式的采样/移位边沿见下表模式CPOLCPHA采样边沿移位边沿典型用途模式000上升沿下降沿W25Q64、OLED、NRF24L01模式101下降沿上升沿部分ADC、传感器模式210下降沿上升沿部分外设需要模式311上升沿下降沿SD卡默认、很多无线芯片3.2 从机手册才是最终判决依据有些芯片的手册会直接标明“Data is sampled on the rising edge of SCLK”那这就是模式0或模式3具体看空闲电平。如果写了“Data is shifted out on the falling edge of SCLK”也可以推断出模式。最好画出时序波形图对着图配置别靠猜。我遇到过一个案例某颗温湿度传感器手册只说支持SPI模式没细写。我按模式0配置读出来全是0xFF。后来用逻辑分析仪抓从机的MISO波形才发现从机是在下降沿输出数据实际是模式1。所以遇到不熟悉的芯片不要直接套STM32CubeMX里的默认参数一定要先看手册。3.3 CubeMX里怎么配置工作模式STM32CubeMX是很多人的入门工具。配置SPI时在“Parameter Settings”里把Baud Rate设成合适的值Clock Polarity选Low或HighClock Phase选1st Edge或2nd Edge就能匹配四种模式。注意实际生成代码后寄存器值由HAL_SPI_Init配置调试时直接改结构体参数也是可以的不一定非得重新生成工程。个人习惯先用低速比如1MHz验证通信正确再逐步提高时钟频率。很多人一上来就拉满速率结果全是乱码还以为是代码问题。SPI这玩意儿速率太高时信号完整性问题会非常突出这点后面专门讲。4. 片选信号硬件片选与软件片选之争4.1 硬件片选看似省心坑却不少很多MCU的SPI控制器自带片选引脚比如STM32的NSS引脚。配置为硬件片选后每次传输前硬件自动拉低CS传输完自动拉高。听起来很省心但实际上硬件片选的坑特别多。第一个坑是“提前抬线”。某些MCU在使能SPI外设时NSS会先拉高一下导致从机以为一次传输结束如果从机对CS时序敏感就会丢数据。第二个坑是速率切换不灵活——如果你在同一总线上挂了两个不同速率的从机硬件片选往往只能设置一个全局速度切换起来不方便。第三个坑是缓冲区里的帧间隔硬件片选在连续发送多个字节时可能不会在字节之间拉高CS而很多从机比如Flash擦写命令要求CS在整个命令期间保持低电平这就需要特殊处理。4.2 软件片选用GPIO手动拉灵活可控软件片选就是不用MCU自带的NSS引脚而是随便找一个普通GPIO在每次传输之前手动拉低传输结束再拉高。这样做的好处非常明显帧边界完全可控想啥时候拉高就啥时候拉高。同一总线上挂多个从机时可以任意切换、任意配置不同的波特率。不受硬件NSS引脚数量限制IO口够用就行。软件片选的实现也很简单在HAL库下就是HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi, txBuf, rxBuf, len, timeout); HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);注意CS信号必须在传输前拉低传输完成后再拉高。有些从机对CS拉低到SCLK第一个上升沿之间的时间有要求如果拉低后立刻就开始时钟从机可能来不及准备。这时需要加一个微小的延时几微秒就够具体看从机手册的tCSSU参数。实际经验我调试W25Q64时把CS拉低后立刻调用HAL_SPI_Transmit发送命令偶尔会出现读回的数据是0xFF的情况。后来加了2us延时就好了。这种问题在示波器上不一定看得出来但确实会影响稳定性。4.3 软件拉片选在Linux下也用得上有网友搜“linux spi 软件拉片选”说明Linux用户也常遇到这个问题。Linux内核的SPI框架里可以通过设备树或者驱动代码把片选设置成GPIO控制。在设备树中spixxx节点下可以这样写spi0 { cs-gpios gpio0 10 GPIO_ACTIVE_LOW; status okay; flash0 { compatible jedec,spi-nor; reg 0; spi-max-frequency 40000000; }; };这里cs-gpios就是用GPIO做软件片选内核会在每次SPI传输前自动控制它。对于某些SoC硬件控制的片选在CLK空闲时状态不够理想用GPIO反而更稳。5. 实战用SPI读写W25Q64把时序彻底跑通5.1 W25Q64基本操作和命令集W25Q64是一颗8MB的SPI NOR FlashSPI接口支持标准SPI、Dual SPI、Quad SPI。要想读写它得先跟芯片“对上暗号”。它的基本命令集中在指令集里常用的有命令地址/数据功能0x9F无读JEDEC ID制造商和设备ID0x06无写使能WREN0x203字节地址扇区擦除4KB0x023字节地址 数据页编程最多256字节0x033字节地址读数据每次操作之前先拉低CS发命令字节再发地址读数据、擦除、编程需要读ID不需要然后视命令发送或接收数据。命令结束后拉高CS。特别注意擦除和编程命令发出后芯片需要时间内部操作你可以在代码里延时也可以读状态寄存器0x05命令轮询BUSY位。后者更可靠。这里插一句很多人一开始觉得SPI Flash命令好多记不住。其实完全不用记手册上有一张命令表用的时候查就行了。真正要理解的是“一次传输的帧结构”——命令、地址、数据怎么排列以及片选什么时候拉高拉低。理解了这两点任何SPI芯片的命令集你都能快速上手。5.2 初始化与IO口配置用STM32的HAL库初始化SPI其实就几行代码SPI_HandleTypeDef hspi; hspi.Instance SPI1; hspi.Init.Mode SPI_MODE_MASTER; hspi.Init.Direction SPI_DIRECTION_2LINES; hspi.Init.DataSize SPI_DATASIZE_8BIT; hspi.Init.CLKPolarity SPI_POLARITY_LOW; hspi.Init.CLKPhase SPI_PHASE_1EDGE; hspi.Init.NSS SPI_NSS_SOFT; hspi.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; hspi.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi);这里NSS配置成SOFT就是为了用软件片选。GPIO方面SCLK和MOSI配成推挽输出MISO配成浮空输入或上拉输入CS配成推挽输出。配置成浮空还是上拉看从机MISO引脚有没有内部上拉需求一般浮空就行。时钟频率用APB总线时钟除以预分频值。比如APB时钟是72MHzprescaler16时SCLK是4.5MHz。这个速度对W25Q64来说很安全。我自己会先用低速验证确定没问题后再调高比如跑到36MHz甚至更高前提是PCB走线质量够好。5.3 读ID、擦除、写页、读数据完整流程读ID是最简单的自检步骤代码大致是uint8_t cmd[4] {0x9F, 0, 0, 0}; uint8_t buf[2]; CS_LOW(); HAL_SPI_TransmitReceive(hspi, cmd, buf, 4, 100); CS_HIGH(); // buf[2]是Manufacturer IDW25Q64应为0xEFbuf[3]是Device IDW25Q64应为0x4017扇区擦除的过程是拉低CS - 发0x06写使能 - 拉高CS - 拉低CS - 发0x20 3字节地址 - 拉高CS - 等待BUSY位清零。页编程的过程是拉低CS - 发0x06 - 拉高CS - 拉低CS - 发0x02 3字节地址 最多256字节数据 - 拉高CS - 等待BUSY位清零。读取数据就简单了拉低CS - 发0x03 3字节地址 - 连续读任意字节 - 拉高CS。读操作不需要先写使能也不影响BUSY状态。注意页编程一次最多写256字节跨页写要自己处理分页逻辑否则数据会回绕到本页开头把你不想覆盖的地方覆盖掉。这属于Flash操作的经典坑代码里务必做边界判断。5.4 软件模拟SPI不依赖硬件外设的备选方案有时候MCU的硬件SPI引脚被占用或者芯片不支持硬件SPI那就得软件模拟。软件模拟的核心就是GPIO口按时序翻转。模式0的读一个字节可以这样写uint8_t soft_spi_read_byte(void) { uint8_t val 0; for (int i 0; i 8; i) { while (SCLK 1); // 等待SCLK为低 SCLK 1; val 1; if (MISO) val | 1; SCLK 0; } return val; }写一个字节就是把SCLK拉高前沿MOSI摆好电平再产生跳变。这个写法特别直观也方便调时序——你可以在任何两个bit之间插入延时满足从机的建立/保持时间要求。缺点是速度慢、占CPU。对于几MHz以下的低速外设完全够用但想让软件模拟跑到20MHz以上就很难了因为GPIO翻转速度有极限。需要高速传输的场合还是得用硬件SPI。另外软件模拟时务必注意把所有相关GPIO配成正确的输入/输出方向防止把引脚配置成开漏导致电平拉不动。6. 调试SPI通信高频问题与排查思路6.1 MISO一直为高或一直为低第一个要检查的是从机到底有没有被“叫醒”。CS信号有没有真正拉低如果CS没拉低从机根本不会理你MISO自然没有输出。我用万用表量过很多次CS引脚发现经常是配错了GPIO或者片上外设复用没开。第二个检查点是时钟有没有送到从机。如果SCLK没翻转从机自然不会输出数据。用示波器探头量SCLK波形是最直接的方法别靠猜。第三个检查点是主机有没有把MOSI设成输出模式。很多人复用配置没做对MOSI一直是输入发出去的数据根本没到引脚上从机当然收不到指令MISO也就没反应。6.2 读回来的数据全是0xFF或0x00数据全是0xFF通常是MISO线上拉得太强或者从机根本没驱动MISO。数据全是0x00则是MISO被拉低可能是接错线、短路或者从机内部故障。这种问题先断开从机直接量MISO引脚电平判断是硬件问题还是软件问题。如果断开后MISO还有确定电平大概率是被什么拉住了如果MISO悬空状态不定说明从机没工作。另外主机MISO配置成浮空输入时如果从机MISO是三态输出在CS没有选中的情况下从机不驱动MISOMISO就会悬空数据可能随机。这时在MISO加一个上拉电阻通常能改善稳定性。6.3 时钟极性和相位不匹配导致的数据错位典型现象是能收到数据但每个字节的位顺序是错的或者最高位变成了最低位。这就是主机和从机模式不匹配。解决方法是重新确认从机手册的采样边沿。实在不清楚可以用逻辑分析仪同时抓SCLK和MISO看数据在哪个沿是稳定的反推模式。另外一个常见的隐蔽问题SPI的位序MSB first还是LSB first配置错误。绝大多数SPI设备都默认MSB first但有些传感器偏偏用LSB first。如果你发现读回的数据bit顺序反了除了查CPOL/CPHA也要查FirstBit参数。6.4 时钟频率过高导致的数据乱码当SCLK频率太高信号在PCB走线、连接线上反射严重从机采样到的数据就会出错。尤其是在杜邦线连接开发板的情况下几十MHz的方波走过10cm长的杜邦线波形早就畸变得不成样子。遇到这种情况先把SPI预分频调到最低速比如几百kHz确认低速通信正常后再逐步加快。如果是产品开发高速SPI一定要用短走线最好加串联电阻22Ω~33Ω来减小振铃MISO和MOSI要尽量短。还有一点CS信号也要做好滤波或整形很多从机最敏感的就是CS边沿。6.5 逻辑分析仪是调试SPI的神器我调试SPI大部分时间都是接一个几十块钱的逻辑分析仪把SCLK、MOSI、MISO、CS四路信号抓下来软件能自动解析SPI帧。配合命令解码一眼就能看出主机发的命令对不对、从机回的响应对不对。比示波器好用特别是看长帧、看连续传输时角色颠倒的细节。抓波形时采样率要设得比SCLK高至少4倍否则波形会失真。比如SCLK是10MHz逻辑分析仪的采样率至少设到40MHz最好是80MHz以上。如果只有24MHz采样率的老旧逻辑分析仪那就先把SPI降速再抓。7. 进阶话题从DSPI/QSPI到SPI转CAN7.1 标准SPI之外的两种高速变体DSPI和QSPI很多Flash芯片支持Dual SPI和Quad SPI。所谓DSPI是指用MOSI和MISO两根线双向传输数据QSPI则是用四根IO线IO0~IO3同时传数据能成倍提升吞吐率。以W25Q64为例标准SPI读速率为40MHz8MB读取要一秒多开启QSPI后同样时钟下读取速度可以提升到原来的4倍左右对需要快速启动的嵌入式系统特别重要。需要注意DSPI/QSPI的使用不是调用标准SPI外设就行而是要用MCU专门支持的QSPI控制器比如STM32的QUADSPI以及芯片对应的命令模式。有些MCU没有硬件QSPI那就只能靠软件用GPIO模拟但速度上不去。对于准备做GUI刷屏、Flash XIP片上执行项目的同学QSPI是绕不开的。7.2 SPI转CAN跨总线桥接的典型方案在一些工控场景中需要把SPI接口的MCU接到CAN总线上常见方案是SPI转CAN芯片比如MCP2515。这种桥接方案的结构是MCU通过SPI往MCP2515写CAN帧MCP2515负责CAN协议收发再通过SPI读取收到的帧。核心要注意的是SPI只是“搬运工”真正的时间关键点在CAN控制器侧。用CubeMX配置SPI驱动MCP2515时建议把SPI速率设置在10MHz以下MCP2515的手册上限一般是10MHz并且把帧间隔设置好。如果SPI通信没问题但CAN帧总发不出去这时别折腾SPI部分了先检查CAN收发器比如TJA1050的供电、终端电阻和波特率配置。很多人在这个桥接方案上卡了好几天最后发现是波特率没对上。7.3 FPGA实现SPI时序透明的另一种玩法FPGA做SPI的好处是时序完全由你说了算。你可以写一个状态机来控制SCLK的边沿、CS信号和数据的移位想怎么调整就怎么调整。对于需要极低延时或特殊协议变体的场景FPGA是唯一选择。在FPGA里实现SPI主机核心就是一个移位寄存器加一个时钟分频器always (posedge clk) begin if (start) begin cs 1b0; shift_reg tx_data; end else if (bit_cnt 8) begin if (sclk_falling) mosi shift_reg[7]; if (sclk_rising) begin shift_reg {shift_reg[6:0], miso}; bit_cnt bit_cnt 1; end end else begin cs 1b1; rx_data shift_reg; end end别小看这段代码它就是SPI协议的全部精髓。掌握了它你才算真正“掌握”了SPI而不是只会调用库函数。我也建议所有想深入嵌入式的人有空用FPGA或Verilog写一次SPI主机哪怕只是仿真收获也很大。7.4 跑在Linux上的SPI从设备树到用户态访问在Linux下操作SPI通常是写一个内核驱动或者用spidev接口在用户态直接用ioctl。设备树里把SPI控制器和从设备描述清楚内核就会自动匹配驱动。用户态使用spidev典型的流程是open设备节点设置模式、速度、位序然后通过ioctl的SPI_IOC_MESSAGE发送完整事务。这种方式调试很快适合快速验证硬件。在设置模式时很容易踩的坑是spidev默认把CS当成硬件控制拉高/拉低时序不合你的预期。解决办法就是在设备树里用cs-gpios配置成软件片选前面已经说过了。另外spidev一次传输的buffer大小受内核限制传输大块数据时要分块否则会收到EINVAL错误。这些细节没有真正在Linux上跑过SPI的人是不会知道的。8. 踩过这么多坑说几句实在话SPI协议本身不复杂但它跟硬件强相关调试时不要只盯代码要养成“用波形说话”的习惯。我调试SPI时最常用的工具就是逻辑分析仪其次是示波器。一个CS信号的毛刺、一个SCLK的振铃都可能导致通信不稳定而这些靠肉眼看代码是看不出来的。另外配置SPI时一定要养成查手册的习惯从从机手册确认模式、速率、片选时序再动手写代码。别嫌麻烦这个习惯能帮你躲掉八成以上的低级错误。如果你用的是STM32CubeMX这类工具生成代码之后也要学着看懂HAL库里SPI的收发流程不要只会点点点。底层原理懂了换任何芯片平台都能很快上手。最后再说一个小技巧调试SPI尤其是点屏或者读Flash时先用读ID这种最简单的命令验证通路ID对了再开始操作存储区。这样能迅速把“硬件连接问题”和“业务逻辑问题”分开排查效率翻倍。这个习惯我用了十年推荐你也养成。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价