☰
STM32 SPI通信协议详解:原理、配置与主从实验
2026/10/14 20:14:55 网站建设 项目流程

做嵌入式这几年,我最大的体会就是:很多同学学STM32卡住,不是卡在GPIO和串口上,而是卡在通信接口上。I2C动不动就死在应答信号上,UART速度上去之后又容易丢帧,这时候回头看SPI,反而觉得它是三类常用接口里最容易上手、也最实用的一个。这篇文章是我《STM32学习心得》系列的第三十篇,专门把SPI接口的原理、配置方法和实验过程完整梳理一遍,希望帮正在学SPI的朋友少走弯路。

SPI的全称是Serial Peripheral Interface,串行外设接口,最早由半导体行业提出并沿用至今。它只需要四根线就能完成全双工数据交换,速度远高于UART和I2C,而且协议本身非常自由,没有复杂的地址帧、应答帧,想怎么传就怎么传。从Flash存储、SD卡到各类传感器和显示屏,甚至ADC芯片,都能看到它的身影。不管你是刚把GPIO和UART玩明白的新手,还是想系统复习SPI底层细节的熟手,这篇文章应该都能帮到你。

1. SPI协议原理与硬件架构拆解

1.1 为什么嵌入式设备里离不开SPI

先从一个最实际的角度说:总线的速度。UART异步通信要收发双方约定波特率,一旦时钟误差积累,高速下就容易错位;I2C虽然有时钟同步机制,但每个字节后面都要跟一个应答位,从机拉不住时钟线就会卡死整条总线。SPI则是同步通信,主机自己产生时钟,时钟沿一到,收发同时完成一位,不受波特率误差影响,也没有应答机制拖后腿,所以在需要高吞吐的场合,SPI基本是第一选择。

串行NOR Flash、串行NAND Flash这类存储芯片尤其依赖SPI。你想一下,刷一个固件动辄几百KB,如果用UART得传好久,用I2C更是慢得让人着急,而SPI在这种场景下能轻松跑到几十兆波特率,速度差距是数量级的。另外,SPI的接线非常简单,主机的MOSI接从机的MOSI,主机的MISO接从机的MISO,SCK对SCK,CS对CS,四根线就能搞定。对于硬件工程师来说,PCB布线也友好得多。

还有一个容易被忽略的点是SPI的“协议自由”。I2C有严格的起始、停止、地址、应答格式,而SPI除了时钟极性和相位必须匹配之外,数据内容怎么编排完全由芯片手册决定。这意味着你可以针对不同从机设计非常灵活的通信帧,这也是它能在那么多不同芯片上存活到今天的原因。

1.2 四根信号线是如何协作的

SPI的四根线分别是SCK、MOSI、MISO和CS。SCK是串行时钟,由主机产生,决定了一位一位数据在什么时刻被移入和移出;MOSI全称Master Out Slave In,数据从主机流向从机;MISO全称Master In Slave Out,数据从从机流向主机;CS是片选信号,低电平有效,主机把它拉低,就代表总线上的这个从机“被选中”,可以开始通信了。

要理解SPI的数据交换,最好的类比是把主机和从机各想象成一个移位寄存器,一根线把主机的输出端连到从机的输入端,另一根线把从机的输出端连回主机的输入端。每次SCK来一个跳变沿,两个移位寄存器就同时左移一位,主机吐出一个bit给从机,从机也吐出一个bit给主机。所以SPI是全双工的——发送和接收在同一时刻完成,你要想从从机读一个字节,就得先发一个字节出去,哪怕发的是无效数据。

这也解释了为什么SPI的读写经常是同一个操作:主机调用一次收发函数,发出一个字节,同时收到一个字节。很多初学的人会纠结“这不是读操作吗,为什么还要先写数据”,其实就是这个原因。CS信号则保证了总线上多个从机不会互相干扰——同一时刻只有被拉低CS的那个从机响应时钟,其他从机的输出处于高阻状态。

1.3 CPOL/CPHA与四种工作模式

SPI最容易把人绕晕的地方就在这里:CPOL和CPHA。CPOL决定SCK空闲时的电平,CPOL=0表示空闲时SCK为低电平,CPOL=1表示空闲时SCK为高电平。CPHA决定数据在哪个边沿被采样:CPHA=0表示在第一个跳变沿采样,CPHA=1表示在第二个跳变沿采样。这两个参数组合起来就是SPI的四种Mode,从Mode 0到Mode 3。

SPI模式CPOLCPHASCK空闲电平数据采样沿
Mode 000低第一个边沿(上升沿)
Mode 101低第二个边沿(下降沿)
Mode 210高第一个边沿(下降沿)
Mode 311高第二个边沿(上升沿)

市面上大多数从机芯片默认支持Mode 0,也就是CPOL=0、CPHA=0,所以很多人的SPI例程收到数据都能跑通。但千万别觉得Mode 0够用就行——有些传感器和存储器对时序有特别要求,只支持Mode 2或者Mode 3。判断依据永远是芯片数据手册里的时序图,它会明确告诉你数据是在上升沿还是下降沿建立、在哪个边沿采样。你和从机模式不一致,最典型的现象就是数据能通但全是乱的,没有规律。

这里我再补一个理解技巧:主机的CPOL和CPHA必须和从机完全一致,因为SPI没有额外的握手协议,时钟怎么跳、数据怎么采,全靠双方约定。通信前先把两个设备的手册各自翻开,确认模式,再动手接线。

1.4 数据帧格式与传输速率

SPI的数据帧一般是8位或者16位,但在STM32的标准外设库里最常用的是8位。存储芯片、传感器这类从机,绝大多数都按字节组织寄存器,所以8位帧足够用。数据的位序也有讲究,STM32支持MSB先行和LSB先行,通信双方必须一致,否则会出现数据左右颠倒的情况——比如发出去的0x01变成了0x80。

传输速率方面,SPI的时钟频率由主机决定,等效公式很简单:SPI时钟 = 外设总线时钟 / 分频系数。在STM32F1系列里,SPI1挂在APB2总线上,APB2最高72MHz;SPI2和SPI3挂在APB1总线上,APB1最高36MHz。分频系数可以从2到256取2的整数次幂,所以SPI1理论上最高能到36MHz,SPI2最高18MHz。听起来很高,但实际能不能跑满,还要看从机支持的最高时钟频率和PCB走线质量。

2. STM32的SPI外设配置要点解析

2.1 STM32的SPI资源分布与时钟来源

STM32F1系列常见的SPI外设有SPI1、SPI2、SPI3三个,引脚复用关系固定,不能随便换。我整理了一个最常用的映射表,方便接线时查:

SPI外设时钟总线SCKMISOMOSI常用NSS
SPI1APB2PA5PA6PA7PA4
SPI2APB1PB13PB14PB15PB12
SPI3APB1PB3PB4PB5PA15

不同型号的引脚映射有差异,比如某些大封装芯片上SPI1的引脚还能重映射到PB3/PB4/PB5,查手册的“Alternate function mapping”表最靠谱。时钟总线这一点一定要记牢,因为它直接决定分频后的最大频率。很多人把SPI2的分频写成2,以为能到36MHz,实际上APB1只有36MHz,分频2之后最高18MHz,所以在追求极限速度的场合,优先用SPI1。

2.2 GPIO复用配置

SPI使能之前,先得把引脚配置对。SCK和MOSI由主机输出时钟和数据,所以要配成复用推挽输出(GPIO_Mode_AF_PP);MISO是从机的输出、主机的输入,所以要配成浮空输入或者带上拉的输入模式;CS如果走软件控制,就配成普通推挽输出,通信前手动拉低。这三个角色一旦搞混——比如把MISO配成了输出,读数据就会出问题。

为什么SCK和MOSI是“复用推挽”而不是“普通推挽”?因为在STM32里,外设信号和GPIO是分时复用同一组引脚的,普通推挽模式下引脚由GPIO数据寄存器控制,而改成复用推挽之后,引脚的控制权才交给SPI外设,这样SPI移位寄存器才能把数据送到引脚上。理解了“复用”这两个字,后面看USART、定时器通道的配置也就一通百通了。

另外,GPIO速度这里建议直接配到最高(50MHz),尤其SCK频率高的场景。GPIO速度设置太低,会让引脚边沿变缓,数据采样点不稳定,高速通信时就容易偶发错位。虽然50MHz对大多数场景都有余量,但把这个配好能省去很多奇怪的时序问题。

2.3 SPI结构体参数逐项拆解

标准外设库里SPI初始化就靠一个SPI_InitStructure,逐项设置之后调用SPI_Init()。我按实际配置顺序逐个说:

  • SPI_Direction:选择双线全双工(SPI_Direction_2Lines_FullDuplex),这是最多见的用法;单线发送、单线接收只在特殊从机时才用。
  • SPI_Mode:主机还是从机。一个总线里只能有一个主机,其他都是从机。
  • SPI_DataSize:8位还是16位。绝大多数从机用8位,按手册来。
  • SPI_CPOL和SPI_CPHA:前面说的四种模式,全看从机手册。
  • SPI_NSS:硬件NSS还是软件NSS。我强烈建议新手用软件NSS。
  • SPI_BaudRatePrescaler:分频系数取值为2、4、8、16、32、64、128、256。
  • SPI_FirstBit:MSB还是LSB先行。

这里插一句,用HAL库的同学会发现这些参数在CubeMX里都能看到,名称也差不多,只是把CPOL/CPHA合并成SPI模式选项,初始化流程更傻瓜化,但底层逻辑和标准库完全一致。不管你用哪种库,搞清楚这些参数的含义才是关键。

2.4 硬件NSS与软件NSS的选择

NSS这根线在STM32里支持两种玩法:硬件NSS和软件NSS。硬件NSS模式下,外部引脚的变化会直接影响SPI外设的工作状态,主机的NSS引脚拉低,SPI才认为总线可用;软件NSS模式下,NSS引脚不参与SPI外设仲裁,只需要通过一个普通GPIO去控制从机的片选即可。

我为什么建议新手用软件NSS?因为硬件NSS牵扯到SPI外设内部状态自动切换,比如多从机场景下切换不及时,可能出现主机误以为总线忙,导致第一个字节发送异常这类诡异问题。软件NSS就是普通GPIO控制,拉低选中、拉高释放,逻辑直接,排查问题也简单。唯一要注意的是在软件NSS模式下,SPI_InitStructure里的SPI_NSS仍要设置成SPI_NSS_Soft,并且主机模式下还要把SSI位置1,告诉外设“片选内部有效”——这个位漏配的话,SPI会一直处于未选中状态,数据根本发不出去。

2.5 使能与收发流程

初始化完成之后,最后一步是SPI_Cmd使能外设,这一步很基础但特别容易被漏掉。使能之后,核心操作就是收发一体的读写函数:先检查TXE标志(发送缓冲区空),空的就把要发的字节填入SPI数据寄存器;接着芯片会自动产生时钟把数据移出去,同时把从机移回来的数据放进接收缓冲区;再检查RXNE标志(接收缓冲区非空),非空就把数据读回来。这个流程里,TXE和RXNE的顺序不能反,很多人收到乱码,就是在一个字节还没移完时又强行发下一个,导致数据覆盖。

收发函数里还有一个很多人忽略的点:读接收数据必须在RXNE置位之后进行,否则会读到上一次的残留数据。从机模式下同样要走这套收发流程,只是时钟由主机提供,从机程序需要做的就是把数据提前准备好,等待主机的时钟来把数据带走、把新的数据送进来。

3. SPI通信实验:主从收发与波形分析

3.1 实验硬件准备与接线

这个实验我选了两块开发板对接:一块当主机,一块当从机,实现双向数据交换。实验的目的是让你亲眼看到SPI的时序:主机发什么、从机回什么、波形长什么样。硬件准备如下:两块STM32F1系列开发板,杜邦线至少四根,逻辑分析仪或者示波器一台,没有逻辑分析仪的话用串口打印也能验证数据对不对。先把两块板的GND接一起,否则电平参考不一致,什么都白搭。

接线表如下,注意MOSI和MISO是对接的:

主机引脚功能从机引脚
PA5SCKPA5
PA7MOSIPA6
PA6MISOPA7
PA4CSPA4
GND地GND

这里有一个极其常见的坑:很多人会把主机的MOSI接到从机的MISO,结果两边都在输出,总线打架。记住一点,MOSI这个名字是站在主机角度命名的,从机那边的对应引脚虽然手册上也叫MOSI,但实际方向是输入。所以主机MOSI就是要连从机的MOSI,引脚名一样,方向相反,用逻辑分析仪抓数据时别被两边的同名引脚绕晕。

3.2 主机模式初始化代码

主机代码我用标准外设库写,这样寄存器逻辑比较直观。SCK用PA5、MISO用PA6、MOSI用PA7、CS用PA4,全部挂在SPI1上。分频系数根据实验先选64,72MHz除以64得到1.125MHz的SCK,这个速度对杜邦线连接来说非常稳,不容易出现信号质量问题。

void SPI1_Init_Master(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // PA5=SCK, PA7=MOSI -> 复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA6=MISO -> 浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA4=CS -> 软件控制片选,普通推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS默认拉高,不选中从机 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // Mode 0 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }

这段代码里我特意把CS初始状态设为高,这样从机在上电到初始化完成的这段时间内不会被误触发。SCK和MOSI配成复用推挽,MISO配成浮空输入,这些前面已经解释过,就不再重复了。

3.3 从机模式初始化代码

从机那边只需把SPI_Mode改成SPI_Mode_Slave,其余参数必须和主机保持一致,特别是CPOL和CPHA。从机的SCK和MOSI方向相对主机正好相反:SCK是输入、MOSI是输入,但MISO是输出。对应到GPIO配置上,从机的MISO要配成复用推挽,SCK和MOSI配成浮空输入。

void SPI1_Init_Slave(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // PA5=SCK, PA7=MOSI -> 输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA6=MISO -> 复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA4=CS -> 输入,由主机控制,这里不参与SPI仲裁 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Slave; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }

从机模式下不需要配置波特率分频,因为时钟完全由主机提供,这个参数在从机模式下意义不大。有些芯片手册会要求从机在片选拉低后的若干个时钟周期内准备好数据,所以从机代码通常要在主循环里提前调用收发函数,把要返回的数据预先放进发送缓冲区,等主机的时钟一过来就能移出去。如果你的项目后面要做真正的多从机或严格帧同步,建议把从机的CS接回SPI硬件NSS功能,或者自己用外部中断感知CS下降沿,这里为了让实验最简单,先不处理这些。

3.4 收发一体的核心读写函数

无论主机还是从机,单字节收发都可以用同一个函数,区别只在于数据内容由谁决定。下面是SPI1的读写函数:

uint8_t SPI1_ReadWriteByte(uint8_t txData) { uint8_t rxData; while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 等待发送缓冲区空 SPI_I2S_SendData(SPI1, txData); // 填入要发送的数据 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); // 等待接收缓冲区非空 rxData = SPI_I2S_ReceiveData(SPI1); // 取回从机返回的数据 return rxData; }

这个函数的核心思路就是“发一个、收一个”。第一次调用时,从机还没返回数据,你收到的可能是0x00或者0xFF,这都正常,因为从机还没来得及初始化。主机要做的往往是在正式读取从机数据之前,先发送一个“读取命令”字节,从机收到命令后在下一个字节的时钟里把数据返回。理解了这一点,你就明白了那些复杂的Flash读写时序图是怎么来的。

3.5 逻辑分析仪看波形

代码跑起来之后,把逻辑分析仪接到主机的PA5(SCK)、PA7(MOSI)、PA6(MISO)、PA4(CS)上,逻辑分析仪的GND也一定要和板子的GND接在一起,否则采样全是乱的。正常应该能看到这样的时序:CS先是高电平,通信开始瞬间被拉低;SCK开始输出一串方波,每个字节8个脉冲;MOSI在SCK的上升沿附近输出数据位,MISO也同步返回数据。在逻辑分析仪里选择SPI协议解码器,把参数设为CPOL=0、CPHA=0对应的Mode 0,就能直接看到MOSI和MISO线上解出来的十六进制数据。

这里有个操作习惯很值得养成:抓波形时别只抓一两个字节,把连续几个字节的波形都抓下来。如果发现SCK波形中间有断续,说明两个字节之间存在间隙,这在某些高速从机上是不能接受的;如果发现MISO数据在SCK边沿附近跳变,说明时序裕量不够,要么降速,要么检查接线长度。

3.6 实验结果与现象

主机程序里写一个简单的测试逻辑:CS拉低,依次发送0xA5、0x5A、0x01、0x02四个字节,每发一个字节把返回值存下来,然后CS拉高,把返回值和发送值通过串口打印。我的实测结果是:从机把接收到的0xA5、0x5A、0x01、0x02原样返回,主机读回这四个值,收发一一对应。

再把实验扩展一下,把从机换成常见的SPI Flash芯片,比如W25Q64这类,用同样的主机代码发0x9F(读ID命令),然后连读三个字节,返回的通常是厂家ID和芯片ID。这个实验的意义在于:Flash芯片不需要你写从机程序,它就是一个现成的SPI从机,任何SPI时序问题都会被它无情地暴露出来。读ID能读到正确值,说明你的SPI主机配置从GPIO到模式全部正确;读不到,回头按第四节的排查表逐步查。

4. 常见问题与排查技巧实录

4.1 读回来全是0xFF,怎么回事?

这是SPI调试中出现频率最高的问题,没有之一。现象是通信流程正常,但MISO上读回来的数据永远是0xFF。看到这个现象,第一反应不是怀疑代码,而是按下面顺序排除:先检查CS有没有被真正拉低,很多人用万用表测CS在通信过程中确实是低电平,但拉低的时机不对,数据移出去了CS才变化;再查MISO引脚是不是配成了输入模式、有没有虚焊或杜邦线松动;最后确认从机是不是上电了、复位引脚有没有被拉死。

如果从机是Flash这类芯片,还有一个容易忽略的点:某些芯片在读取命令之后需要等待一小段“转换时间”,主机必须在命令字节和读数据字节之间插入几个空时钟周期或延时。这类参数在数据手册里会以tRD或状态寄存器的RDY位给出,不看手册就闷头调试,很容易在这里卡住。实际上,很多接了Flash的裸机程序第一次读ID失败,都是因为这里没留意。

4.2 数据错位、第一位丢失

字节错位,比如主机发了4个字节,从机读到的却是3个字节,经常是片选建立时间不够。很多从机要求CS拉低之后,再过一段时间才能收到第一个SCK边沿,这段最小时间在手册里叫tCSS。如果主机在CS拉低的瞬间就立刻产生时钟,从机还没准备好,第一个字节就会丢。

解决思路有两种:一种是在CS拉低后加一小段延时再开始写数据;另一种是每次通信前先检查SPI的BSY标志,确认总线空闲。如果你在做多字节传输,还要注意CS在整个传输过程中必须一直保持低电平,不能在两个字节之间被拉高,否则从机会认为一次事务已经结束。这类问题靠逻辑分析仪最容易确认:看CS拉低和第一个SCK上升沿之间的时间差,再对照手册里的tCSS参数。

4.3 时钟极性和相位不匹配

模式不匹配的典型症状是:数据能收到,而且“准乱”。所谓准乱,就是错的规律性很强,比如收到的字节永远是发送字节的左右镜像,或者数据整体偏移了一位。出现这种规律性错误,基本可以确定是CPOL或CPHA配错了,而不是接线或硬件问题。

排查方法很简单:看从机数据手册的时序图,确认数据是在上升沿采样还是下降沿采样,空闲时钟是高还是低,然后回头改SPI_CPOL和SPI_CPHA。如果手头有逻辑分析仪,直接用解码功能选不同模式试,哪种模式解出来的数据有意义就用哪种。这里提醒一句:主机的CPOL/CPHA和从机的CPOL/CPHA必须一致,但MOSI和MISO两条线上的数据建立时间可能不同,个别高速芯片还会有数十纳秒的延迟参数,遇到极高速率下的偶发错误,要考虑数据信号延时,而不是一味怪模式。

4.4 连续发送时时钟之间有间隔

很多从机对“一次传输中SCK必须连续”有要求,但你会发现用上面的SPI1_ReadWriteByte连续调用时,SCK波形在两个字节之间出现了停顿。原因在于每个字节传输完成后,CPU需要检查TXE标志、再写入新的数据,这个过程的时间是随机的,取决于CPU时钟和代码执行路径。对于大多数从机来说,这种字节间隙都能容忍,但有些对时序敏感的芯片就会出错。

解决办法最有效的是DMA。把要发送的数据放入DMA缓冲区,配置好SPI的发送DMA通道,让DMA连续补充发送数据寄存器,CPU只需要在开始时触发一次,后面的事情全部交给DMA,SCK就不会因为CPU执行速度而出现不可控的间隙。另一种办法是关闭SPI后再发送多个字节,但那样会把CS时序搞得很复杂,不如DMA干净。

4.5 DMA传输的几个坑

用DMA做SPI收发时,常见问题有两个。第一个是DMA缓冲区的数据宽度配置错误:SPI的8位数据对应DMA的字节模式,如果你把DMA配成了半字模式,数据会错位;反过来,SPI设置16位数据帧时,DMA要配成半字模式。第二个问题出现在收发都开DMA的场景:发送和接收用同一个DMA通道时要分时使用,或者用两个不同的DMA通道,否则数据会串。

还有一个容易被忽略的坑是DMA结束后的外设关闭顺序。正确做法是:先关闭DMA对应的通道,再关闭SPI外设,最后清标志位。如果顺序反了,SPI控制器里可能残留未完成的传输状态,下一次开启DMA时会收到一个脏数据。这些经验都是踩过坑之后才总结出来的,写下来希望你能跳过。

4.6 问题排查速查表

把上面这些经验浓缩成一张表,遇到问题对着查:

现象可能原因排查方向
读回全是0xFFCS未有效拉低、MISO配置错误、从机未就绪测CS时序、查GPIO模式、查从机供电复位
数据规律性错乱CPOL/CPHA不匹配、位序不一致核对从机手册时序图,检查FirstBit
第一个字节丢失CS建立时间不够CS拉低后延时,检查tCSS
SCK字节间有间隙CPU软件填充速度不稳定改用DMA传输
DMA数据错位数据宽度和帧宽度不匹配8位对应字节模式,16位对应半字模式
偶发性乱码接线过长、无共地、速度过快降低分频、缩短杜邦线、确认共地

这张表只覆盖了最高频的问题,SPI世界里还有从机NSS硬件冲突、多从机总线争用这类进阶问题,等你的主从通信跑顺之后,再遇到新现象时,用“逻辑分析仪+手册对照”的方法基本都能定位。

最后再分享一个我自己的习惯:SPI调试永远从慢速开始。先把分频系数调到最大,跑通了再逐步提速,每一步提速后都观察波形和数据的稳定性。很多人一上来就追求几十兆的高频率,结果在硬件问题上浪费大量时间,其实只要时序逻辑正确,速度只是参数问题。另一个实用技巧是写一个SPI回环测试:把MOSI和MISO用一根线短接,主机发什么就能读回什么,这是检验SPI外设配置是否正确的最快方法,不需要任何从机芯片。我在实际项目中排查SPI问题时,第一步永远是回环测试,第二步才是接真实从机。希望这篇心得能让你在SPI调试的路上少走几个弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询