ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SPI通信协议详解:从原理到实战,嵌入式工程师的避坑指南

2026/9/6 14:48:31 拓冰建站 浏览量
SPI通信协议详解:从原理到实战,嵌入式工程师的避坑指南 做嵌入式这些年天天和通信协议打交道UART、I2C、SPI、CAN轮番上阵但要选一个“看似简单、坑却不少”的我第一个提名SPI。SPI全称Serial Peripheral Interface串行外设接口由Motorola在八十年代提出到今天仍是板级通信的主力军。它不像UART那样需要波特率对齐、也不像I2C那样需要地址仲裁四条线拉出来就能跑速率还高所以Flash、屏幕、SD卡、传感器、ADC这类外设几乎清一色支持SPI。这篇就聊聊SPI通信协议那些事从原理到时序、从硬件到软件再把我这些年调试SPI踩过的坑一并交代清楚。无论你是刚入门MCU开发还是正在搞STM32、ESP32、FPGA的板级通信这篇都能帮你把SPI这条线理得明明白白至少能让你少走几步弯路。1. SPI协议的核心原理与工作方式1.1 SPI到底是什么为什么它这么快SPI是一种高速、全双工、同步的串行通信总线。它至少需要四根线SCLK时钟线、MOSI主出从入、MISO主入从出、CS片选线。有的地方叫法不同比如SCK、SDO/SDI、NSS但作用都一样。关键点是“同步”。SPI的时钟由主机产生所有数据位的发送和接收都跟着这根时钟走。正因为有时钟线做同步SPI不需要像UART那样精确约定波特率也不需要像I2C那样在数据线上做时钟同步和仲裁时序天然就干净利落速率也就能轻松跑上几十MHz。我举个例子你就懂了。UART像两个人不在一个房间打电话必须提前约好说话速度否则听不清I2C像一根线上连了十个人大家要靠地址喊话才能对上号SPI更像是工地上的“指挥传令兵”指挥主机拿个喇叭时钟喊口号传令兵MOSI/MISO听到口号就同步迈步谁也不会乱。这也是为什么SPI在嵌入式里是“效率优先”的代表。1.2 四根线各管什么听谁的SCLKSerial Clock主机产生决定通信速率。上升沿或下降沿采样由模式决定。MOSIMaster Out Slave In主机发送数据给从机。MISOMaster In Slave Out从机发送数据给主机。CSChip Select片选线低电平有效居多拉低表示选中某个从机。主机是唯一的时钟源所有从机都围绕主机转。片选线相当于“点名”拉低哪根CS哪个从机就进入工作状态不拉低的时候从机的MOSI/MISO一般处于高阻态不会干扰总线。多从机时通常每个从机单独一根CS线主机用GPIO分别控制。如果设备支持也可以菊花链连接但实际项目里独立CS更常见因为逻辑简单、时序可控。1.3 SPI的四种模式为什么时钟极性和相位能坑死人SPI没有标准的通信协议格式通信双方必须提前约定好时钟极性和相位。这两个参数就是CPOLClock Polarity和CPHAClock Phase。CPOL决定空闲时SCLK是低电平还是高电平。CPOL0空闲低CPOL1空闲高。CPHA决定数据在哪个边沿被采样。CPHA0第一个边沿采样CPHA1第二个边沿采样。组合起来就是Mode 0到Mode 3具体关系如下SPI模式CPOLCPHA采样边沿以空闲低为例Mode 000上升沿采样下降沿移位Mode 101下降沿采样上升沿移位Mode 210下降沿采样上升沿移位Mode 311上升沿采样下降沿移位绝大多数SPI设备默认支持Mode 0或Mode 3但总有例外。如果你发现读回来全是0xFF或者全是错位数据第一个要查的就是模式匹配。我自己的习惯是拿到一颗新芯片先翻数据手册找到“SPI Timing Characteristics”看时序图里SCLK空闲电平、采样沿是哪个再回MCU配置里选对应模式。比如在CUBEMX里就是CPOL和CPHA两个下拉框的事选错了几分钟就能让你怀疑人生。这块儿真不是吓唬你我见过太多人在Mode 0和Mode 3之间反复横跳最后发现手册上写的是Mode 2。2. SPI时序图与数据收发机制2.1 一张时序图怎么读很多新手看到SPI时序图就头大密密麻麻的波形线和标注感觉像天书。其实拆开就四个动作拉低CS、产生时钟边沿、按位收发、拉高CS结束。以Mode 0为例整个过程是这样主机把CS拉低从机被“唤醒”。SCLK开始在空闲低电平基础上产生高电平脉冲。在每个上升沿主机把MOSI上的数据位发出去从机在同一个上升沿采样MISO上从机也在时钟配合下把数据返回给主机。字节发完主机把CS拉高一次事务结束。一个字节的收发主机会先发最高位MSB也可以配置为LSB先行看具体芯片支持。比如W25Q系列Flash支持MSB先行但某些EEPROM就支持LSB先行模式必须按手册来。数据不是“我发完你再发”而是同时进行的。主机每输出一个bit从机也同步返回一个bit这就是全双工。你发0x55的时候可能从机刚好回你0xAA。很多初学者不理解为什么我读Flash一个地址要先“写”一个命令字节就是因为读操作本质上也依赖主机产生时钟从机才能把数据放到MISO上你写出去的那个字节其实是在“借时钟”。2.2 通信数据帧与全双工机制SPI的数据位宽一般是8位但很多MCU支持4到16位可编程比如STM32的字长可以设成8位或16位。选16位时要注意很多从机虽然寄存器是16位但期望的是连续两个8位事务而不是一个16位帧这点最容易在驱动SD卡、LCD屏时踩坑。发送和接收是同一根时钟线驱动的移位寄存器过程。你可以把两个设备的移位寄存器想象成两个对接的传送带主机推一个bit出去从机推一个bit进来时钟每跳一次两边各走一格8个时钟后完成一个字节的双向交换。这也解释了为什么SPI读操作往往要先“写”一个寄存器地址甚至要“写”一个空字节才能把数据读回来。所谓读本质还是全双工交换主机发送的字节通常是命令或填充字。比如SPI Flash的读取命令是0x03后面跟3字节地址然后你才能读到数据字节这个过程中发给从机的“数据”其实全是从机不在乎的填充值。2.3 片选时序对整个事务的重要性片选线不只是“选人”那么简单它还决定了事务的边界。对于很多从机来说CS拉高意味着一次命令结束、内部状态机复位CS拉低则意味着命令开始。如果在一次命令中途CS被意外拉高从机会认为命令被中断可能丢弃已经收到的部分数据。所以软件片选必须注意两点一是在SPI传输开始前先拉低CS二是在整条命令的所有字节全部传输完成后再拉高CS。很多人在用HAL库时把CS操作放在每个字节传输函数外面结果每传一个字节CS就抖一次导致从机只认第一个字节后面的命令全被丢弃。我自己的排查经验是如果SPI通信时灵时不灵先拿示波器看CS的波形确认CS在整个事务期间有没有毛刺或提前拉高。3. 硬件设计要点与应用场景解析3.1 硬件片选与软件片选怎么选这是被问很多的问题。硬件片选是MCU的SPI外设自带NSS引脚可以由硬件自动控制拉低拉高软件片选则是用一个普通GPIO在通信前手动拉低、通信完手动拉高。硬件片选的优点是响应快、配合DMA时不容易漏操作但它有个前提控制器要能精确地在传输开始和结束时驱动NSS。如果MCU的硬件NSS行为和你预期不一致比如提前拉高、或者必须复用NSS引脚不能接其他功能反而容易出问题。在某些MCU上硬件NSS只能和特定的SPI引脚绑定灵活性很差。软件片选是我个人更常用的方案。原因有三个一个SPI外设可以接多个从机用不同GPIO分别控制CS灵活度极高调试时可以在代码里加打印肉眼看到CS状态不受控制器NSS模式限制不容易踩硬件自动控制的隐性坑。唯一要注意的是软件片选在高频通信时GPIO翻转速度可能跟不上或者频繁翻转导致时序毛刺增大。所以真正跑高速比如几十MHz且对时序苛刻的场景才会专门去用硬件NSS。但对于绝大多数项目软件片选完全够用而且好排查问题。3.2 多从机总线设计独立CS还是菊花链多从机方案通常是两种独立CS和菊花链。独立CS就是每个从机一根CS线主机各用一根GPIO控制。优点是访问任意从机互不影响时序独立开发排错直观缺点是CS线多IO占用多。菊花链则是把多个从机的MISO/MOSI串起来从机A的MISO接到从机B的MOSI这样可以只保留一根MOSI和一根MISO通过时钟把数据一级一级往下传。适合需要同时更新多个相同类型设备的场景比如LED驱动、多路DAC但延迟会积累而且软件模型复杂。如果从机数量不多、类型不同独立CS永远是最省心的。别为了省引脚搞出复杂电路调试成本远高于省的那几个IO。我记得有个项目为了省两个引脚把两个不同厂家的传感器挂在同一个CS上做菊花链结果两个传感器时序要求完全不同最后不得不飞线改板得不偿失。3.3 SPI Flash、SPI屏幕、SD卡这些常见外设SPI Flash是协议最典型的应用之一比如W25Q系列。读速度高、接口简单适合存字库、存配置、做OTA镜像。注意写Flash要按扇区擦除后再写不是直接覆盖写很多人第一次用SPI Flash就卡在这里读取全是FF。SPI屏幕在Arduino里特别常见比如ST7789、ILI9341这类TFT屏。数据和命令都走SPI初始化序列很长但只要配置好Mode和频率刷屏速度通常够用。SPI接口屏幕的刷新率受限于SPI时钟和像素数用320x240的屏在40MHz SPI下刷新率大概也就几十帧实际会受驱动IC和初始化参数影响。这里有个经验刷屏想要流畅尽量用DMA搬运像素数据而不是CPU一字节一字节地塞。SD卡走SPI是兼容模式不是原生SDIO模式。SPI模式的好处是硬件简单、几乎所有MCU都支持缺点是速度低适合做日志存储、参数存储这类中等速度需求的场景。如果追求高速尽量用SDIO。3.4 电气特性与信号完整性要点SPI虽然协议简单但物理层也不能随便造。几点值得记住电平匹配3.3V设备不能直接接5V设备需要电平转换否则长期高压可能烧IO或者损坏芯片。上拉/下拉CS、SCLK线上的上下拉电阻防止悬空误触发。尤其CS要在空闲时保持确定电平避免从机误选。走线长度高频下20MHz走线要尽量短避免长线反射。实在要长线降低速率比加电阻更有效。地线连续信号线附近必须有完整的地平面否则噪声会让时序抖动。多处从机时注意总线电容容性大时上升沿变缓要么降速率要么加驱动。实际调板子时如果SPI在低频率下没问题、高频就出错优先怀疑信号完整性而不是软件配置。先降频验证功能再根据波形和余量逐步提频。4. 软件实现与MCU实操4.1 CUBEMX配置SPI的完整过程拿STM32做例子在CUBEMX里配置SPI其实五分钟搞定但每个选项都有含义Mode选Transmit Only、Receive Only还是Transmit Receive。全双工选Full-Duplex Master。Hardware NSS Signal这里选Disable用软件片选IO自己控制。Clock Prescaler分频系数决定SPI时钟频率。比如APB2时钟84MHz4分频就是21MHz。Clock Polarity和Clock Phase对应上面的Mode选择查从机手册填。Data Size一般8 Bits。16位要看芯片是否支持。MSB/LSB First多数设备MSB先行少数要LSB。配置完生成代码后通常用HAL_SPI_Transmit、HAL_SPI_Receive、HAL_SPI_TransmitReceive这三个函数。传参时要指定句柄、数据指针、长度、超时时间。这里有个容易犯的错HAL_SPI_Transmit是阻塞式的如果SCLK配置错误或者从机没响应时钟翻转就能把数据发完并不会返回错误。很多人的代码卡在传输完成标志上其实不是单片机问题而是SPI外设一直处于Busy状态没被正确复位调用传输函数前要先调用HAL_SPI_DeInit再重新Init或者检查一下是否有未完成的DMA传输。4.2 阻塞、中断与DMA怎么选SPI三种传输方式各有适用场景阻塞方式适合短数据、低频次、不在乎CPU占用。简单粗暴调试方便。中断方式适合中等长度数据CPU可以去做别的事但要处理中断优先级和数据完整性。DMA方式适合大块数据搬运比如刷屏、读大块Flash。CPU只负责启动DMA和收DMA完成中断效率极高。DMA是SPI传输的大杀器但它也有自己的坑缓存必须是连续的内存块不能是局部变量栈上那种临时buffer除非明确对齐并且内容在DMA期间不改变。DMA发送时如果CPU还在修改buffer会出现数据撕裂。用HAL库时DMA传输完成回调里要清标志避免二次传输污染。有些MCU的DMA和Cache之间需要做一致性问题比如带Cortex-M7内核且开了CacheDMA读SRAM可能读到旧数据需要做clean/invalidate操作。关于DMA和Cache的问题我多说一句。Cortex-M7这类带Cache的内核CPU写数据写到Cache里DMA却直接去读SRAM两边数据不一致是真实存在的大坑。解决办法是DMA发送前调用SCB_CleanDCacheDMA接收完成后调用SCB_InvalidateDCache。忘了这个操作你会在DMA接收数据时发现自己收到的还是旧数据百思不得其解。4.3 GPIO模拟SPI什么时候需要手撸GPIO模拟SPI听起来像走回头路但真到某些场景是绕不开的MCU的硬件SPI接口数量不够多个从机抢接口从机对时序有特殊要求比如需要很慢的时钟、需要中途改变CS状态硬件SPI和某个外设引脚复用冲突纯软件平台或FPGA里临时代码验证。手撸GPIO模拟SPI的核心就是四个GPIOSCK、MOSI、MISO、CS。以Mode 0为例伪代码如下uint8_t spi_gpio_read_write(uint8_t tx_byte) { uint8_t rx_byte 0; for (int i 7; i 0; i--) { if (tx_byte (1 i)) MOSI_HIGH(); else MOSI_LOW(); SCK_HIGH(); delay_us(1); if (MISO_READ()) rx_byte | (1 i); SCK_LOW(); delay_us(1); } return rx_byte; }注意这里我用的是标准写顺序先置数据、再拉时钟、再采样、再拉低时钟。GPIO模拟时时序完全由delay控制先搞清楚从机手册里要求的建立时间和保持时间再写delay数值别随手写个delay_us(1)就以为万事大吉。对于低频从机这个代码够用但如果从机要求几十MHz时钟GPIO模拟基本没戏老老实实用硬件SPI。4.4 FPGA接SPI思路不太一样FPGA的SPI实现和MCU不同没有现成外设全要靠Verilog搭状态机。核心思路是用一个计数器产生SCLK状态机控制CS拉低、逐bit收发、CS拉高。写Verilog SPI从机时要注意跨时钟域问题——SCLK来自外部和FPGA内部时钟往往不同源必须在采样前打拍同步否则亚稳态会让你怀疑人生。这也是面试和项目中常考的时序收敛点。我自己写FPGA SPI的经验是先把发送和接收分开写各写一个模块调试时在FPGA内部加一个计数器或LED看CS、SCLK有没有到再用逻辑分析仪抓总线波形比对数据和时序。FPGA调试最大的优势是波形可以随便抓比自己猜寄存器快得多。如果用Verilog做SPI主机SCLK产生逻辑要特别注意占空比均匀不要出现一个周期宽一个周期窄的情况否则从机容易采样错位。4.5 共用SPI总线屏幕和传感器如何共存热搜里有一条“ESP32屏幕与SD卡共享SPI哪个好”这也是实际项目里经常遇到的。比如一块屏幕和一个SD卡模块都走SPI总线。它们能共存的前提是总线空闲时所有从机的MISO都是高阻态不会互相干扰而CS则由主机分别控制同一时刻只能有一个从机被选中。很多人在这种共享总线设计时犯的错是两个设备虽然CS不同但时序模式不同比如屏幕要求Mode 0SD卡要求Mode 3。这时你不能在初始化时配一次就完了得在切换设备时动态改SPI模式。如果MCU的SPI外设不支持运行中改CPOL/CPHA就得靠软件模拟或舍一取一。另外屏幕和SD卡共享总线还有一个小技巧如果屏幕读写频率很高而SD卡只是偶尔存日志可以把屏幕的CS接硬件NSSSD卡的CS接普通GPIO然后用屏幕的DMA传输为主SD卡操作时切到低速模式。这样能兼顾速度和稳定性。5. 常见问题与调试经验实录5.1 为什么读回来全是0xFF或0x00这是SPI调试里出场率最高的问题原因往往很朴素读回来全是0xFF大概率从机根本没被选中CS没拉低或者CS引脚配置错误也可能是MISO没接对从机数据根本传不回来。读回来全是0x00大概率主机发送的数据没对或者从机收到空字节后返回空数据也可能是SPI模式不对边沿采样错位导致数据错乱。排查顺序我建议是先查接线再查GPIO配置再查SPI模式再查寄存器地址和命令字。别一上来就怀疑是芯片坏了大多数时候是人坏了。我自己就经历过一次查了整整一下午最后发现是MISO线本来应该接PB4结果焊成了PB5。5.2 时钟极性和相位不匹配的表现如果你SPI通信能通一半、偶尔错位、接收数据高低字节对调大概率就是CPHA或CPOL不匹配。CPOL不对可能看到整个波形反相空闲电平反了。CPHA不对通常表现为数据错半拍读回来的字节就像左移或右移了一位。这种问题在示波器上特别明显。你把SCLK和MOSI波形同时抓下来数一下采样沿和数据变化沿的相对位置错没错一眼就能看出来。手里没有示波器时可以先把频率降到1MHz以下再用逻辑分析仪抓逻辑分析仪成本低做协议解析比示波器还好用。5.3 DMA传输时数据不完整或乱码DMA模式下最常见的有三类情况传输长度没配对DMA传输长度和SPI数据宽度不匹配比如设置成8位数据但DMA用了16位宽度数据错位。Cache一致性问题带Cache的MCU在DMA搬运后读到的还是Cache里的旧数据。解决办法是在DMA写内存后做Cache Invalidate在CPU准备给DMA发数据前做Cache Clean。传输未完成就操作buffer代码里发完数据立刻改了发送buffer内容但DMA还没搬完就会出现“发一半变串数据”的诡异现象。正确做法是等DMA传输完成回调触发后再修改buffer。5.4 常见问题速查表现象可能原因解决方向读回全0xFFCS未拉低/接线错误/MISO开路查CS配置、查硬件连接读回全0x00MOSI无数据/模式错误查发送数据、查SPI模式数据错位或左右移位CPHA配置错误查数据手册按实际采样沿配置时钟有输出但波形奇怪分频系数不对/走线太长降频率、缩短走线DMA传输不完整长度或数据宽度不匹配核对DMA配置和SPI字长偶发干扰导致误码地线噪声/电平不匹配加强地、加滤波、降速传输卡死无响应SPI外设处于Busy状态检查是否未调用DeInit/Reset5.5 我踩过的一些典型坑最经典的坑是芯片手册上写“SCLK idle high”我却看图看反了配成Mode 0结果芯片理都不理我排查了两个小时最后发现是CPOL选错。还有一个坑是SD卡SPI模式下初始化时必须要先给至少74个时钟脉冲才能响应命令。这个脉冲必须在发CMD0之前给否则卡片直接不回应。这块逻辑没处理好会让人误以为SPI驱动有问题其实是初始化顺序不对。再有就是SPI的CS带毛刺。GPIO在开机上电时如果默认输出低会误选从机导致从机进入异常状态。解决方法是上电初始化时先把CS引脚拉高或者加外部上拉电阻保证系统起来之前总线是安全的。6. 一些真正实用的心得做SPI通信这几年我越来越觉得SPI的难点不在协议本身而在“知道你的对手是谁”。所谓对手就是你手里的那颗从机芯片。它的时序要求、寄存器定义、命令序列都得读数据手册而不是靠猜。我自己有个习惯拿到新板子不管什么外设第一件事不是写业务代码而是先写一个SPI回环测试。把MOSI和MISO短接发一串已知数据读回来比对通了再往下走。这个动作能过滤掉至少一半的底层配置问题省下的时间远比写的几行测试代码多。另外调试SPI时逻辑分析仪真的是神器。我一般会把SCLK、CS、MOSI、MISO四根线全接上抓一段波形对照数据手册的时序图核对。很多时候你以为的软件问题其实在波形里一秒就能看出来。哪怕是几十块钱的逻辑分析仪也比拿万用表猜半天强。还有一点SPI外设配置完成后建议先把频率降到最低验证功能再逐步提高频率。这在排查问题时有奇效能帮你把“逻辑错误”和“物理层不稳定”清晰地区分开来。SPI这个协议看着简单但工程上覆盖了电气、时序、软件、驱动、调试方方面面。希望大家看完这篇后能少走点弯路。如果后面还有机会我想再单独细聊聊SPI Flash的驱动流程和SPI屏幕的刷新优化那个坑也不少。先把这篇消化掉比什么都强。