
如果你玩过单片机一定会遇到这个名字SPI。它可能是你点亮一块屏幕、读写一块Flash、驱动一个传感器时最亲密的伙伴。这篇博客我就来彻底拆解一遍SPI通信协议从物理层信号到实际项目中的配置细节把时钟极性、片选方式、数据时序这些概念一次性讲清楚再用STM32这种主流平台带你完整跑通一个读写Flash的例子最后聊聊我在多设备共享总线、DMA传输时踩过的那些坑。1. SPI核心架构与工作流程1.1 四根线的物理连接逻辑SPI全称是Serial Peripheral Interface串行外设接口。它由Motorola在1979年提出至今仍是板级通信的绝对主力之一。很多人觉得SPI难懂其实它本质上就是一个环形移位寄存器并没有多高深。一个标准的SPI主从连接最少需要四根线SCLK串行时钟由主机产生所有从设备共享。MOSIMaster Output Slave Input主机输出数据、从机接收数据。MISOMaster Input Slave Output从机输出数据、主机接收数据。CS/SS片选信号低电平有效由主机控制用来选中要通信的从设备。这个“低电平有效”的片选逻辑特别重要。实际项目中CS脚在通信前必须拉高通信开始时拉低通信结束后再拉高。如果你把CS一直拉低在多个设备挂载同一条总线上时所有设备都会认为自己被选中那总线就直接乱掉了。SPI的数据收发是同步的主机在时钟的上升沿或下降沿输出一位数据同时在另一个边沿采样接收数据。收发同时进行所以SPI本质上是一个全双工协议这一点和UART、I2C都不一样。全双工意味着主机发数据的时候如果从机MISO上没有数据返回主机仍然会接收到垃圾数据但只要你不去理会接收寄存器里的值就行。1.2 数据流的方向与移位寄存器机制SPI的核心是一个左右移位的寄存器链。主机和从机内部各有一个8位或16位的移位寄存器它们通过MOSI和MISO首尾相连构成一个环。每来一个时钟脉冲主机和从机各自移动一位数据主机通过MOSI把最高位送出去同时通过MISO把从机的最高位收进来。这样做带来的好处很直观一次时钟周期执行了两个操作既发送了一位又接收了一位。如果你要读取一个从机寄存器往往是先通过MOSI发送寄存器地址再通过MOSI发一个无效字节从机就会在这段时间内把目标寄存器内容放到MISO线上送回主机。在写驱动时很多人会困惑“为什么读操作还要先写一个0x00”其实这个0x00就是用来推动时钟的。SPI通信的时钟必须由主机打出来从机不能主动产生时钟所以即使想让从机说话主机也得先把时钟打起来。这个机制理解透了后面调试I2C移植SPI或者模拟SPI时序的时候会轻松很多。2. SPI四种工作模式与时序选择2.1 CPOL和CPHA到底在决定什么SPI有四种模式分别是Mode 0、Mode 1、Mode 2、Mode 3。模式的区分实际上就两个参数CPOLClock Polarity和CPHAClock Phase。CPOL决定时钟空闲电平。CPOL0时空闲状态SCLK为低电平CPOL1时空闲状态SCLK为高电平。CPHA决定数据采样的时刻。CPHA0时在第一个时钟边沿采样数据CPHA1时在第二个时钟边沿采样数据。四种模式可以做成一张表模式CPOLCPHA采样沿输出沿Mode 000上升沿下降沿Mode 101下降沿上升沿Mode 210下降沿上升沿Mode 311上升沿下降沿选择哪种模式取决于从设备的数据手册。比如最常见的W25Q系列Flash支持Mode 0和Mode 3ST7789屏幕控制器默认是Mode 0有些传感器只支持Mode 3。新手最容易犯的错就是把所有设备都配成Mode 0结果某个外设不能正常通信测量SCLK波形又看不出明显异常其实问题就出在采样边沿和输出边沿对不上。2.2 什么时候用Mode 0什么时候用Mode 3从波形上看Mode 0和Mode 3的数据输出、采样时刻其实是完全一致的区别只在于空闲时SCLK是高还是低。为什么会出现两种空闲状态不同的模式因为不同厂家的芯片在设计内部移位寄存器时对时钟的依赖方式不一样。有的芯片认为时钟空闲低、第一个沿开始传输比较自然有的芯片则认为时钟空闲高、拉低作为起始标志更稳妥。实际选型时强烈建议先查数据手册里的时序图找到“Serial Clock”那一栏上面会画明CPOL和CPHA的具体参数。如果手册字迹不清或者根本找不到就用示波器抓一下该设备在示例驱动代码下SCLK的波形或者直接逐个尝试四种模式看哪个模式下能读到正确的设备ID这也是硬件调试中最直接的办法。我在项目中一般会固定使用Mode 0或者Mode 3。因为它们对8位数据传输来说数据建立时间足够长不容易出现通信异常。如果真的遇到那种“单独读都对、连续读偶尔出错”的情况多半就是模式没配对建议优先排查CPHA设置。3. 硬件片选与软件片选的选择3.1 硬件片选NSS的工作机制STM32等MCU上SPI外设通常会有一个硬件片选引脚叫做NSS。启用硬件片选后只要往SPI的数据寄存器里写数据硬件会自动把NSS输出为低电平发送完成自动拉高。听上去很智能实际使用时却有两个麻烦。第一个麻烦是“多从设备仲裁”。如果总线上挂了两个从设备两片Flash共享SCLK、MOSI、MISO但各自用独立的CS脚硬件片选只能管理一个固定的NSS引脚。你要切换访问另一个设备就必须重新初始化SPI外设的NSS配置甚至要操作GPIO寄存器去强制拉高、拉低非常折腾。第二个麻烦是时序不可控。有些从设备对CS的建立时间要求比较严格比如CS拉低之后需要等待一小段时间才能输出时钟。硬件片选自动拉低后紧接着就开始产生时钟如果MCU的主频很高建立时间可能不够导致从机还没来得及准备数据就发出去了。3.2 软件片选的实际使用建议我个人的习惯是除非系统里只有一个SPI从设备且时序要求极其固定否则一律使用软件片选。软件片选就是把片选引脚换成普通GPIO通信前手动拉低通信结束后手动拉高。代码里就是多两行GPIO_WriteLow和GPIO_WriteHigh的事情换来的灵活性却大得多。软件片选最大的价值在多从设备共存的场景。A设备处理完事务把A的CS拉高再拉低B的CS整个过程完全由你控制甚至可以插入延时来配合某些奇葩芯片的时序要求。这个做法在Linux内核的SPI驱动框架里也是默认的理念一个SPI控制器可以挂多个device每个device都有自己独立的GPIO片选。要注意的是用软件片选时必须关掉SPI外设的硬件NSS功能否则引脚会处于冲突状态。在STM32的初始化结构体里对应的是NSS SPI_NSS_Soft然后调用SPI_SSOutputCmd或SPI_NSSInternalSoftwareConfig之类的函数确保外设不会自动输出片选信号。4. SPI与I2C的选型对比4.1 速度、线路数量与协议的差异拿SPI和I2C对比是最常见的问题因为两者都是板级通信的常客。先看结论SPI适合高速、大数据量、单主多从且片选管脚够用的场景I2C适合低速、省引脚、挂接多从设备的场景。两者的差异可以从几个维度来看对比项SPII2C引脚数量4N每个从机多一个CS2根SDASCL通信速率可达几十MHz常见10MHz-50MHz标准100kHz快速400kHz高速3.4MHz通信模式全双工半双工寻址方式硬件片选7位或10位软件地址数据帧无固定帧格式自由定义有起始、停止、ACK/NACK机制多主支持一般单主支持多主仲裁在速率这一点上SPI优势非常明显。一块常见的SPI NOR Flash读操作跑到40MHz很轻松而I2C接口的EEPROM比如AT24C02最快也就400kHz左右。如果你要刷屏幕、传音频数据、记录日志SPI是首选。4.2 从实际项目角度如何选总线项目里选型时我会先问自己三个问题数据量有多大如果只是偶尔读取几个字节的传感器数据I2C完全够用省引脚还少走线。实时性要求高不高如果要做屏幕刷新或者音频流SPI几乎是必须的I2C的速率完全不够看。从设备数量和MCU引脚资源如何I2C只需要两根线就能挂十几个地址不同的设备SPI要一个设备占用一个CS脚设备一多引脚压力就大了。还有一种混合方案一个MCU上同时挂SPI和I2C总线传感器用I2C大容量存储和屏幕用SPI这种组合在实际产品里非常多见。5. STM32平台SPI完整配置流程5.1 基于CubeMX的引脚与参数初始化我用STM32写SPI驱动时通常会先用CubeMX做图形化配置然后在其生成的初始化代码基础上微调。这里拿STM32F103或者F407系列为例步骤几乎一模一样。首先在Pinout视图中选择要使用的SPI引脚。比如SPI1的SCLK、MOSI、MISO分别映射到PA5、PA7、PA6CS脚用软件控制就随便选一个空闲的GPIO比如PA4或PB0配置为GPIO_Output。注意CubeMX里要把NSS设置为软件模式也就是在SPI参数界面里将NSS Type改为Disable或者Software。接着配置SPI参数需要确认以下几点Direction一般选择Full-Duplex Master这是最常见的模式。如果只写不读比如驱动OLED屏幕也可以选Transmit Only Master。Hardware NSS SignalDisable把片选完全交给软件。Baud Rate Prescaler根据系统时钟和SPI时钟需求来选择分频系数。Clock Polarity (CPOL)根据设备手册设置默认Low。Clock Phase (CPHA)根据设备手册设置默认1Edge。Data Size默认8位即可少数设备需要16位。First BitMSB First绝大多数SPI设备都遵循高位先出。5.2 分频系数与时钟频率的计算方法分频系数的选择决定了SPI实际通信速率。STM32F103的APB1最高36MHzAPB2最高72MHz。SPI1挂在APB2上SPI2和SPI3挂在APB1上。如果你把SPI1的时钟源设为72MHz选择4分频那么SPI时钟就是72/4 18MHz。如果你的外围设备最高支持20MHz这个值没问题。如果设备最高只支持10MHz18MHz就太快了需要继续分频到8也就是9MHz。计算代码如下uint32_t spi_clock 72000000; // APB2时钟 uint32_t baudrate spi_clock / 4; // 18MHz具体分频值可以直接在CubeMX界面里看到配置界面右侧会自动计算实际波特率。选择一个等于或略低于设备最大速率的档位即可。5.3 软件片选模式下的代码写法在软件片选模式下代码就非常简单了。以W25Q128 Flash为例片选引脚用PA4写一个读取设备ID的函数#define FLASH_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define FLASH_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) uint8_t SPI_TransferByte(uint8_t byte) { uint8_t received 0; HAL_SPI_TransmitReceive(hspi1, byte, received, 1, HAL_MAX_DELAY); return received; } uint32_t W25Q_ReadID(void) { uint32_t id 0; FLASH_CS_LOW(); SPI_TransferByte(0x9F); // JEDEC ID命令 id | (uint32_t)SPI_TransferByte(0x00) 16; id | (uint32_t)SPI_TransferByte(0x00) 8; id | (uint32_t)SPI_TransferByte(0x00); FLASH_CS_HIGH(); return id; }HAL库的HAL_SPI_TransmitReceive阻塞式调用会等待传输完成同时完成收发。这个函数在简单读写场景下够用但效率不高后面我会讲DMA方案。6. 常见通信故障与排查技巧6.1 波形正常但读回全0xFFSPI调试中最常见的问题就是读回数据全是0xFF这通常意味着MISO线上没有数据返回。排查顺序可以这样来先用万用表确认MISO物理连接是否正确特别是刚画好的板子容易出现虚焊或者连错焊盘。检查从设备有没有上电。有些外设支持低功耗模式片选拉低之前需要先唤醒。确认从机的VCC和GND是否接好部分模块需要额外的电平转换芯片3.3V和5V直连可能会烧坏或者不工作。检查主机和从机的SPI模式是否一致。如果主机是Mode 0从机被设成Mode 3双方时钟极性和采样沿对不上就会出现数据错乱但示波器上看波形依然是正常的。还有一种情况CS片选没有完全拉低主机的GPIO配置成了开漏输出而不是推挽输出。开漏输出在没有上拉电阻的情况下输出低电平实际上是微弱拉低从机的片选检测阈值可能不过关导致从机根本不知道自己被选中。6.2 时钟频率过高引起的偶发数据错误在某些项目里SPI通信出现“时好时坏”的现象查了半天发现是SPI速率太接近从设备的极限。从设备数据手册上标称最高20MHz你把SPI配成18MHz看起来没问题但实际PCB走线过长或者电平转换芯片有延迟信号经过反射和充电时间之后有效建立时间已经不满足要求了。解决办法很简单把SPI时钟降到8MHz或者4MHz多半问题就消失了。大量实际测试表明SPI稳定通信的最大速率远远低于理论值因为还要考虑走线电感、上拉电阻阻值、串联终端电阻等等因素。如果你的系统必须跑高速我建议优先检查MOSI和MISO走线长度是否相近然后在SCLK线上串联一个22欧姆到33欧姆的终端电阻减少信号过冲和振铃。6.3 多设备共享总线造成的相互干扰当多个SPI设备共享SCLK、MOSI、MISO时最常见的故障就是访问设备A时设备B也会“响应”。原因是设备B的片选引脚悬空了或者片选引脚在初始化时没有拉高。设备B的CS在未选中状态下必须保持高电平。如果某个GPIO在芯片复位阶段默认是浮空输入在各种噪声干扰下可能误判为低电平导致设备B被意外选中。要解决这个问题所有片选引脚在初始化阶段就要明确拉高同时建议在PCB设计时给CS脚加上10kΩ上拉电阻让它的默认状态更加牢固。MISO线的处理同样重要。多个从机的MISO输出引脚直接并联在一起如果某个从机在未被选中时MISO引脚输出并不是高阻态而是持续驱动就会和当前被选中的从机争抢总线。解决方法是确认每个从机MISO引脚支持三态输出或者在片选无效时配置为输入模式。好在大多数SPI从设备都设计为片选无效时MISO输出高阻如果真遇到例外就得在中间加总线缓冲器。7. 拓展DMA传输与FPGA实现要点7.1 用DMA减轻CPU负担的SPI读取SPI传输大量数据时如果用阻塞式HAL_TransmitReceiveCPU会被占用很久。以W25Q128为例读取一页数据需要256字节以18MHz速率计算光传输时间就在110微秒以上期间CPU被HAL库的空循环霸占别的任务全卡住了。用DMA可以把传输过程完全交给硬件CPU只需配置好内存地址和传输长度然后去处理其他事情传输完成时触发中断即可。在CubeMX里将SPI的TX和RX请求都配置为DMA并开启SPI全局中断。在代码里定义一个缓冲区uint8_t tx_buffer[256]; uint8_t rx_buffer[256]; HAL_SPI_TransmitReceive_DMA(hspi1, tx_buffer, rx_buffer, 256);然后添加传输完成回调函数void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // 传输完成处理rx_buffer } }DMA模式下要注意缓冲区生命周期。DMA传输是异步的如果主函数在DMA尚未完成时就修改tx_buffer内容会造成数据错乱。因此需要定义一个标志位传输完成中断里置位主循环检测到标志位后才允许再次修改缓冲区。7.2 SPI在FPGA中的Verilog实现思路FPGA项目中自己写SPI控制器也是常见需求尤其是做高云、赛灵思或者Intel平台时。SPI主机在FPGA里可以拆成几个模块时钟分频模块生成SCLK。移位寄存器模块控制MOSI和MISO的数据移位。状态机模块管理片选信号、发送字节计数和通信阶段。一个最小SPI主机的状态机可以设计为IDLE - TX_START - TX_BIT0~TX_BIT7 - TX_STOP。在TX_BIT状态中根据SCLK边沿逐位移出数据。核心伪代码如下always (posedge clk) begin if (state TX_BIT) begin if (sclk_falling_edge) begin mosi tx_data[bit_count]; end if (sclk_rising_edge) begin rx_data[bit_count] miso; end end endFPGA实现SPI的好处是时序完全可以自己控制你可以随意调整SCLK频率和相位甚至在SCLK的半周期插入延时来满足从设备的建立保持时间要求。坏处是代码量比MCU大得多还要自己处理跨时钟域问题。如果只是简单的SPI Flash读写很多FPGA厂商已经有现成的SPI控制器IP核不需要重复造轮子。但如果你想彻底理解SPI时序手写一个Verilog控制器绝对值得尝试它能让你真正明白每一个时钟边沿到底是干什么用的。7.3 高速屏与SD卡共享SPI总线的仲裁方案很多嵌入式项目里屏幕和SD卡都挂在同一条SPI总线上比如ESP32或者STM32上接ST7789屏幕和TF卡模块。这种架构省引脚但存在一个隐患两者可能配置了不同的SPI模式。ST7789常见是Mode 0TF卡最好在Mode 0或Mode 3下工作。如果它们模式不同怎么办切换模式需要重新初始化SPI外设。我的建议是优先选择支持相同Mode的设备组合否则每次切换都要执行SPI外设的DeInit和Init既慢又容易出问题。另一个隐患是两者分频系数不同。屏幕刷新可能需要10MHz以上的时钟而TF卡在初始化阶段需要低速率约400kHz等初始化完成后再切回高速模式。这种动态切换在SD卡协议里是标配流程写代码时要额外留意。我在一个实际项目里就遇到过这种问题屏幕刷新一切正常一挂上TF卡就花屏。后来排查发现每次从TF卡切回屏幕时SPI外设还保留着SD卡初始化阶段的低分频配置屏幕刷新率被拖慢导致显示撕裂。解决办法是在每次切换设备前根据目标设备重新配置分频系数。8. 调试工具与实测心得8.1 逻辑分析仪是SPI调试最好的朋友SPI调试逻辑分析仪的优先级高于示波器。因为SPI是数字协议逻辑分析仪可以直接解码出MOSI和MISO上的数据内容不用人眼一个个去数波形。拿一个8通道逻辑分析仪举例把CH0接到SCLKCH1接到MOSICH2接到MISOCH3和CH4分别接两个设备的CS。采样率至少大于SPI时钟的4倍推荐8倍以上否则解码器可能误判。在软件里设置好SPI协议解析参数CPOL和CPHA要选择正确。连接好之后触发条件一般设置成“CS下降沿”因为一次通信的开始标志就是CS拉低。然后发送一个读ID命令逻辑分析仪屏幕上会直接显示你发出的0x9F和从机返回的设备ID字节整个过程一目了然。如果你看到MOSI上的数据和代码预期不一致先检查代码里发送的顺序和位序设置如果MISO上始终是空的再回头查从机侧的问题。逻辑分析仪能帮你把主机和从机的边界隔开排查效率翻倍。8.2 实际项目中的SPI速率选择经验汇总结合多个量产项目的经验我总结了一个SPI速率选择的参考表应用场景建议SPI时钟说明OLED屏幕SSD13061MHz-4MHz屏更新率不高速率太高也没用TFT屏幕ST7789/ST773510MHz-40MHz屏幕分辨率越高速率需求越高SPI NOR FlashW25Q系列10MHz-40MHz读操作可高写操作受Flash内部限制SD卡SPI模式400kHz初始化 / 10-25MHz读写初始化必须低速传感器如MAX318651MHz-5MHz多数传感器速率不高FPGA从机25MHz-50MHz取决于FPGA内部逻辑和走线质量这个表不是绝对的具体还要看PCB布局和线长。如果走线很短小于3cm可以按表中的上限来如果走线很长或者经过排线连接建议直接降到表里的下限保证稳定性。8.3 两个调通SPI的土办法万一没有示波器和逻辑分析仪怎么办有两个土办法可以帮你判断通信是否正常。第一个办法是用GPIO翻转法。在SPI通信开始前把一个GPIO拉高通信结束后拉低然后用另一块MCU或者音频线加电脑声卡录音通过观察方波频率推断通信时间。精度不高但能确认SPI有没有在运行。第二个办法是回环测试。把MOSI和MISO直接短路主机发送0xAA如果接收到的也是0xAA说明SPI外设本身的收发链路没问题。如果回环测试正常但外设通信失败问题就出在外部连线或从机配置上。这两个方法虽然原始但在没有专业仪器的环境下真的能救命。我早期调试一块传感器板子时就是靠回环测试才发现是MISO引脚虚焊和从机一点关系都没有。SPI协议本身不复杂但真正用好的关键在于细节模式对不对、速率合不合理、片选时序有没有留够、多设备时总线会不会冲突。把这些基础问题想透了再难的SPI外设也能轻松调通。