拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32 SPI双机通信实战:主从模式、CubeMX配置与从机收不到数据排查

简介面向嵌入式开发者的STM32 SPI双机通信工程资源覆盖主从模式与主主模式下的协议设计、时钟相位配置、CS信号控制及双向数据收发重点提供“把主机改成查询接收”的示例代码帮助理解主机发送查询命令、从机响应的典型交互流程同时给出硬件连接与软件配置的对应关系。资源包共51个文件压缩包仅121KB包含C语言源文件、头文件、Keil工程配置文件、编译生成的HEX/MAP/LST文件、备份文件及说明文档等源码与工程层级清晰便于对照阅读。已有1895人下载学习。通过学习该工程可掌握SPI通信的初始化流程、主从切换策略、中断与DMA可选加速方式以及示波器/逻辑分析仪下的调试排查思路适合正在实现单片机高速通信或需要快速搭建SPI双机链路的开发者参考。 做嵌入式的人大概率都有这种经历手里两块STM32想让它们说上话第一反应就是用串口或I2C。串口简单但速度慢I2C又总有设备地址和时钟同步这些额外的破事。SPI呢速度够快四根线一接就能跑但真到了“两块MCU面对面通信”的场景很多人会发现自己照着读写W25Q64那套思路来第一轮调试就碰了一鼻子灰。这篇内容就是给准备用STM32 SPI做双机通信的人写的。我会把这个场景里最容易被忽略的主从机逻辑差异、CS引脚处理、CubeMX配置、HAL库收发代码以及从机收不到数据的排查顺序全部过一遍。别的文章通常只教你“SPI怎么初始化”我这里重点解决“两块STM32怎么在一个SPI总线体系里不出bug地互相传数据”。这个需求在实战里特别常见一块主板做采集和运算另一块做显示或电机控制或者一块跑复杂算法另一块专门管外设。SPI双机通信一旦跑通了比串口省心因为时钟在主机手里不用两个人各自去对齐波特率。1. SPI双机通信和读写Flash的最大不同从机是被动的先说一个很多人没转过弯来的点。你在单片机上挂过一个Flash芯片、SD卡模块、OLED屏幕用SPI都是主机模式你发命令对方被动响应。这块经验拿到双机通信里只能保留一半。为什么因为两块STM32的地位从来不是对等的哪怕你心里想的是“双机”物理上SPI协议强制分出了时钟主人。SPI是同步通信SCK这根时钟线只能由一方产生。负责产生SCK的叫主机另一方就是从机。主机什么时候发数据、发多少字节、SCK跑到多快全由主机说了算。从机唯一能做的事就是在自己的MISO线上准备好数据等主机的SCK脉冲到来时把数据一位一位移出去。这就带来一个非常反直觉的结果从机“想”发送数据它自己说了不算必须等主机发起一次传输。从机端哪怕往SPI的数据寄存器里写好了字节只要主机不产生SCK这些数据就永远躺在寄存器里不出去。这和两个用串口的人对话完全不同串口两边都能随时开口SPI从机就像一个只能被点名发言的人。所以“双机通信”这四个字很容易让人产生误解。如果你仔细想一下通信模型它本质上仍然是主从架构主机定时发起轮询从机借主机给的时钟窗口把应答数据吐出来。你要是确实需要两边随时平等互发就得在SPI之上加一条通知线或者用“主机高频轮询”这种近乎无赖但可靠的方式。还有一点容易被忽略双机通信时从机的MISO并不是永远有效的。主机在发数据的同时从机的MISO在SCK下降沿或上升沿取决于模式会同步输出数据但如果从机没有把数据准备好MISO上就是高阻或垃圾数据。这就是为什么双机通信的报文协议必须设计成“一问一答”不能像串口一样想发就发。理解了从机是被动的一方后面所有配置和排查思路都好办了。很多人的问题不是代码写得不对而是脑子里对“从机被动”这个概念没有立起来。2. 接线与角色分配四根线的接法决定通信成败从硬件上看SPI双机通信很简单四根线就能连SCK、MOSI、MISO、CS。但“简单”不代表不会接错。标准的接法是交叉对接主机的MOSI接从机的MOSI主机的MISO接从机的MISOSCK对SCKCS对CS。注意这里MOSI和MISO不是交叉的因为SPI的MOSI和MISO的定义是相对主机的主机输出到从机走MOSI从机输出到主机走MISO。所以主机的MOSI引脚接从机的同名校脚主机的MISO引脚也接从机的同名校脚别把它当成UART的TX和RX那样交叉接。很多人第一次接线就是在这里翻的车。具体连接关系如下表主机引脚连接目标从机说明PA5SCKPA5SCK时钟由主机产生从机接收PA7MOSIPA7MOSI主机数据输出从机数据输入PA6MISOPA6MISO从机数据输出主机数据输入PA4CSPA4CS主机拉低选中从机从机检测电平看到这里你可能会问两块板子引脚名字一样怎么区分主机从机答案是看CubeMX里把SPI配成Master还是Slave。同一个引脚在不同板子上既可以做MOSI也可以做MISO完全是软件配置决定的。所以最好的接线习惯是两块板子跑同一个工程只是主机编译一份、从机编译一份。CS引脚是另一个重灾区。主机侧的CS可以配成硬件NSS也可以配成普通GPIO的推挽输出程序里手动拉低拉高。但从机侧的CS强烈建议直接配成普通GPIO输入然后用程序读这个电平来判断主机是否选中了自己。为什么因为从机的SPI外设如果工作在硬件NSS模式下NSS引脚的输入信号会被内部逻辑直接用来控制SPI的状态机很多初学者配置不到位片选信号就废了。还有一点非常重要的实战细节两块板子必须共地。SPI是逻辑电平通信不像RS485那样有差分信号两边的GND不在同一电位上SCK、MOSI上读到的电平就会乱跳表现为“偶尔通一次换了板子就完全不通”。这个问题排查起来极其隐蔽因为示波器看波形时探头的地已经帮你共地了波形看着完美一拔示波器就死给你看。如果两块板子之间的距离超过十厘米建议把SCK频率降下来不要一上来就干到18MHz或36MHz。MCU对MCU通信和MCU对Flash通信不一样Flash芯片的高速特性是专门优化过的STM32作为从机时对信号质量没那么宽容。我自己的实践是双机通信频率控制在1到4MHz最稳数据量不够再往上加。如果你想省一根线有一种“三线SPI”的做法只用SCK、MOSI、CS从机不回复数据这种模式适合你只需要单向通知的场景比如主机把传感器数据广播给从机。但多数双机通信需要从机回数据所以完整四线才是常态。3. STM32CubeMX配置从机侧的坑最多CubeMX配置SPI的大框架不难难的是细节。我先把主机和从机的关键配置区别列出来然后逐条解释为什么从机侧的坑最多。主机侧的核心配置SPI模式Full-Duplex Master帧格式Motorola标准SPI数据大小8 Bit按实际协议需求我习惯用8位时钟极性/相位CPOL0CPHA0即SPI Mode 0预分频16分频或32分频先保证跑稳软件NSS使能NSS SoftwareCRC计算关闭从机侧的核心配置SPI模式Full-Duplex Slave帧格式Motorola数据大小8 Bit时钟极性/相位与主机完全一致必须同为Mode 0软件NSS使能CRC计算关闭从机侧第一坑NSS软件模式和硬件模式的区别。CubeMX里默认NSS是Software模式这个模式适合大多数双机通信场景。但如果选了Hardware模式从机只有在NSS引脚上检测到低电平时才允许收发数据而这个引脚的内部滤波和同步逻辑在不同的芯片版本上行为略有差异容易踩到“明明拉低了却不生效”的诡异问题。用软件NSS模式从机的SPI外设就不再关心NSS引脚电平而你的CS检测逻辑可以完全交给普通GPIO输入去判断可控性高得多。从机侧第二坑时钟极性和相位必须与主机一致。CubeMX里SPI Mode 0CPOL0CPHA0是绝大多数场景的默认选择双机通信两边都选Mode 0基本不会出问题。问题经常出在代码复用上——有人从原来驱动W25Q64的工程里改而W25Q64通常工作在Mode 0或Mode 3改的时候只改了设备名忘了同步模式结果主机模式0、从机模式3通信数据全是错的。这里稍稍解释一下CPOL和CPHACPOL决定SCK空闲时的电平CPOL0表示空闲时SCK为低CPHA决定数据采样的边沿CPHA0表示在第一个边沿采样。两块MCU只要这两项配置一致并且物理上满足建立时间和保持时间数据就不会错。不用背复杂的时序图记住“两端一致”四个字就够了。从机侧第三坑别忘了打开软件从管管理。CubeMX的SPI Slave配置界面里有一个“NSS Signal Type”选项选Software后代码初始化时会自动设置SPI_CR1寄存器的SSM和SSI位。但如果你用的是标准外设库或自己手写的寄存器初始化这两位一旦没置位从机的SPI状态机会一直停留在“未选中”状态数据完全接收不到。还有个容易被忽略的配置是“Baud Rate”在从机下其实没有意义因为时钟是主机给的。有人从Master模式复制配置过来看到Baud Rate Prescaler就顺手又选了一遍其实这个参数在Slave模式下完全不影响运行只是CubeMX界面里还会显示出来。别在从机上纠结这个东西它不参与任何判决。如果你是老工程用标准库配置思路完全一样只是寄存器操作方式不同。从机初始化时重点看SPI_CR1和SPI_CR2确保SSM1、SSI1、MSTR0、SPE1顺序不能反先配好SSM和SSI再使能SPE否则外设状态默认为主模式直接改SPE会导致错误帧产生。最后提醒一下DMA和中断的选择。CubeMX里SPI Slave可以单独开启接收中断我的建议是从机端用SPI_Receive_IT或DMA接收主机端用TransmitReceive阻塞或中断都行。但主机和从机的数据长度必须严格一致这一点等会儿在代码部分详细展开。4. 主机与从机的HAL库代码按帧收发才是正确姿势配置做对了代码就顺理成章了。先说主机的发送逻辑。我这里采用最直观的做法CS引脚手动拉低调用收发函数再手动拉高。HAL库的主机发送有三种方式可选阻塞、中断、DMA双机通信最简单的起步方式是阻塞式TransmitReceive代码清晰、调试方便。// 主机发送并接收一帧阻塞方式 uint8_t txBuffer[4] {0x01, 0x02, 0x03, 0x04}; uint8_t rxBuffer[4] {0}; // 选中从机 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); // 发送4字节并同时接收4字节 HAL_SPI_TransmitReceive(hspi1, txBuffer, rxBuffer, 4, 100); // 释放片选 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);注意HAL_SPI_TransmitReceive是同时收发不是先发后收。这一点和很多人直觉不一样SPI是全双工的主机每输出一个字节的同时也从MISO线上读回一个字节。从机那边如果提前准备好了应答数据主机发送完一帧后rxBuffer里自然就有内容了。从机侧的接收代码建议用中断方式。因为从机不知道主机什么时候发起通信阻塞式等待显然不现实而中断方式可以让从机在后台随时准备接收。// 从机接收缓冲区 uint8_t rxBuffer[4] {0}; // 启动中断接收从机进入待接收状态 HAL_SPI_Receive_IT(hspi1, rxBuffer, 4); // 接收完成回调HAL库自动调用 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { // 收到一帧完整数据处理业务逻辑 processFrame(rxBuffer); // 准备下一次接收 HAL_SPI_Receive_IT(hspi1, rxBuffer, 4); } }这里有几个关键点必须讲透。第一从机端不能只用HAL_SPI_Transmit_IT去“发送”数据。为什么会有人这么想因为他觉得从机也该有发送操作。但实际上从机无法自己产生时钟HAL_SPI_Transmit_IT虽然会往数据寄存器里写数据但这些数据只能通过主机的SCK才能移出。正确做法是从机提前把应答数据准备好然后用TransmitReceive模式或HAL_SPI_Receive与主机的SCK协同工作。第二收发长度必须一致。如果主机发4字节从机只启动了接收2字节的中断从机在收到第2字节后就会触发回调并停掉接收剩下的2个SCK脉冲虽然还会继续移位但数据要么丢弃要么写到意想不到的位置下一帧数据进来时状态就乱了。这是“通信偶尔错乱”最常见的元凶。所以建议每次启动接收时直接按协议规定的最大帧长去收不要收到几个算几个。第三帧协议必不可少。两块MCU之间通信不是发几个裸字节就完事的。最常见的做法是定义这样一个帧结构帧头、长度、数据、校验。比如字段长度说明帧头2字节固定值 0xAA 0x55用于同步数据长度1字节后面数据的字节数数据区N字节实际业务数据校验和1字节前面所有字节累加取低8位主机每次发送时按这个格式组帧从机在回调里先检查帧头再核对长度和校验这样即使之前有数据错位也能通过校验失败主动丢弃并重新同步。没有这一层原始字节流里任何一位错位都会让你后续所有数据全部解释错误排查起来非常痛苦。第四从机建议直接上DMA而不是只靠中断。如果你的通信频率较高或帧长较大每字节一次中断会占不少CPU开销尤其是当从机本身还在跑别的任务时。DMA方式的初始化代码稍微多一点但原理一样开SPI的DMA接收请求配置好内存地址和长度剩下的搬运工作交给DMA完成接收完毕后再进一次回调。大数据量场景下这是必须的选择。第五主机端多帧传输要防止CS信号撕裂数据。主机在拉低CS之后SCK产生的每一个字节都是数据的一部分直到拉高CS为止。所以CS操作和发送操作必须作为一个整体中间不能插入耗时的调度或任务切换。如果一个大帧中间被RTOS切走几毫秒再切回来继续发波形上相当于两个SCK脉冲串之间多了个“空洞”有的从机程序会因为这个空洞误判为CS有效期间的异常跳变。解决方式有两个阻塞收发优点是不会被打断、或者发送全程关中断优点是RTOS友好度更高。5. 从机收不到数据的完整排查链路这是双机通信里最常见的问题也是最考验耐心的问题。我把实际排查的经验整理成一条链路从物理层开始逐层往上照着这个顺序走一遍大部分问题能在半小时内定位。第一步查硬件连接和共地。用万用表确认SCK、MOSI、MISO、CS四根线两边导通正常并确认两块板子的GND确实连在一起。我遇到过一次最离奇的情况两块板子用的是同一个电源适配器供电但用了两个不同的USB口接电脑其中一块板子的GND和电脑的GND之间有几十毫伏压差导致SPI信号高电平只有3.1V左右从机死活不认。后来换了一个共用地线问题直接消失。第二步用示波器或逻辑分析仪抓波形。这一步能回答很多问题SCK有没有脉冲CS有没有被拉低MOSI的数据位像不像有效波形不要凭感觉去猜直接看波形。没有示波器的话用逻辑分析仪也行便宜的那种就够用。重点看三个东西SCK的频率和个数对不对、CS的低电平时间是否覆盖了完整数据帧、MISO是否有回波。如果SCK根本没有说明主机侧根本没调发送函数或引脚初始化被别的代码覆盖了。第三步确认两端的CPOL和CPHA完全一致。用示波器看CS拉低后SCK的第一个边沿如果SCK空闲是低电平、第一个边沿是上升沿说明是CPOL0CPHA0如果是高电平、下降沿采样那就是模式3。对着波形数一下和CubeMX里配置比对不一致就改配置这是纯配置错误和数据内容无关。第四步检查从机的软件NSS有没有使能。这是排查过程里“看起来配置都对但就是不通”的头号原因。很多人用寄存器版代码时SPI_CR1的SSM、SSI没置位或者置位的顺序不对。SSM和SSI要在SPE1之前写好顺序反了外设内部状态机会不接受这个配置导致从机一直认为自己没被选中。HAL库用CubeMX生成会自动处理但你要是自己手写初始化就必须注意。第五步排除从机溢出导致的假死。从机接收溢出有一个典型症状第一帧数据正常第二帧开始全部是0xFF或固定异常值。原因是主机发了10字节而从机第一次只启动了接收5字节的中断后续5个字节把数据挤出了接收缓冲区还会置上溢出错误标志位。HAL库里有溢出错误后如果不调用__HAL_SPI_CLEAR_OVR_FLAG清除后续所有接收都会被跳过。这也是为什么我一直强调按最大帧长去收而不是按实际帧长去收。第六步查CS时序是否太短。有的主机会在SPI数据发送结束后立刻拉高CS这在Flash芯片上没事但对MCU从机来说从机内部处理最后一位数据需要一点点时间如果CS上升沿来太快最后的字节可能还没被锁存到接收缓冲区里。解决方式主机在发送结束后加一个几个微秒的延时再拉高CS或者在从机的接收回调里先做数据拷贝再做CS相关判断。这一条看起来玄学但实测中确实会碰到。第七步不要忽视复位状态下的引脚电平。有的板子连接了外部的SPI Flash或LCD这两个外设的CS引脚默认被拉高和双机通信的CS信号存在冲突甚至两个设备同时被选中MISO上两个驱动源打架。排查时最好确认一下板子上除了SPI1外有没有别的设备挂在同一组引脚上。如果有优先换一个可用的SPI外设比如从SPI1换到SPI2不要硬挤在一组引脚上。还有一个经常被忽略的细节主机在循环发送时CS引脚每次拉高拉低的间隔如果太短从机的GPIO输入检测可能来不及响应。GPIO本身有输入滤波如果CubeMX里给CS引脚开了大量滤波或者配置了模拟输入模式电平变化要经过几个时钟周期才能被采样到。建议把CS引脚配置成高速推挽如果是输出或浮空输入/上拉输入如果是输入不要开内部大电容滤波。排查到这里基本上能覆盖99%的收不到数据场景。剩下那种1%的情况往往是代码在CubeMX之外又手动改了寄存器或者是DMA配置冲突——比如某个DMA通道已经被ADC占了却不自知。遇到这种把CubeMX重新生成一遍和当前代码做一下diff很快就露馅。回到开头那句话双机通信的难点不在SPI本身而在角色认知。你先把“从机永远被动”这条铁律刻在脑子里再动手配置后面所有的坑都只是细节问题。通信帧协议建议从上电第一天就加上帧头和校验别等数据错乱了再补到那时你连是谁发错的数据都说不清。你要是一上来就被“两块MCU平等互发”这个想法带偏后面无论怎么调都别扭。我在实际项目里给双机通信加了一条“事件线”作辅助从机有主动数据要上报时把一个普通GPIO拉高主机检测到高电平后立即发起一次SPI传输把数据提走。这样既保留了SPI同步通信的高速率又绕开了“从机不能主动发送”的天然限制。这个方案我推荐给所有被双机通信被动模式折磨过的人成本只多一根线但通信模型会舒服很多。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门