简介这是一份面向嵌入式初学者与STM32开发者的SPI双机通信例程聚焦两块STM32板子之间通过SPI协议完成主从数据交互的完整实现并附带液晶显示相关代码帮助读者理解同步串行通信在实际项目中的落地方式。资源包共60个文件约450KB包含c与h源码、uvproj工程文件、map与lst编译列表、hex烧录文件及readme说明等覆盖从工程配置到编译输出的完整链路代码中配有详细注解便于对照寄存器配置理解每一步操作。内容涉及SPI工作模式选择、CPOL与CPHA极性相位配置、CR1/CR2/SR/DR等关键寄存器使用以及GPIO初始化、数据收发、错误处理与液晶屏命令数据传输等环节。目前已有3086人学习下载适合希望从零掌握STM32 SPI主从通信、并借助注解快速上手调试的读者参考实践。1. 两块板子之间的 STM32 SPI 通信从「接线能通」到「数据不丢」的完整落地手头有两块 STM32 开发板想把它们用 SPI 连起来跑通通信这个需求在工控、传感器组网、双核协作场景里非常常见。但很多人第一次上手就翻车主机发数据从机收不到或者收到一堆 0xFF示波器上看波形又「好像没问题」。问题往往不在 SPI 本身而在片选时序、时钟极性、DMA 配置和中断优先级这些细节上。这篇笔记按「先讲清 SPI 主从到底怎么握手再给一套能直接复现的双板例程最后把踩过的坑摊开」的顺序写目标读者是已经会点灯、会串口打印、但还没把 SPI 主从跑稳的嵌入式开发者。整套方案基于常见 STM32F1/F4 系列用 HAL 库实现主机用查询或 DMA 发送从机用中断接收代码可以直接搬到你的工程里改引脚就能跑。2. SPI 主从通信的底层逻辑与硬件选型2.1 为什么 SPI 主从不能「随便接四根线就通」SPI 是全双工同步串行总线四根线分别是 SCK、MOSI、MISO、CS。很多人以为只要把主机的 MOSI 接从机的 MOSI、MISO 接 MISO 就行这是第一个翻车点。正确接法是主机 MOSI 接从机 MOSI主机输出、从机输入主机 MISO 接从机 MISO主机输入、从机输出SCK 对 SCKCS 由主机控制拉低选中从机。如果两块板子都配置成主机或者都配成从机通信必然失败。更隐蔽的问题是时钟极性 CPOL 和时钟相位 CPHA。SPI 有四种模式主机和从机必须完全一致否则采样点错位收到的数据就是乱的。常见做法是主机用模式 0CPOL0CPHA0从机也配模式 0。如果你在 CubeMX 里配了主机模式 0、从机模式 3示波器上看 SCK 有波形但数据就是错的这种玄学问题排查起来很费时间。还有一个容易被忽略的点从机的 NSS 引脚。如果用硬件 NSS从机在 CS 拉低时自动使能如果用软件 NSS从机需要手动在接收前把 NSS 内部拉低。HAL 库里从机模式通常建议把 NSS 配成「硬件输入」或者直接用软件管理具体看你的芯片手册。我一般会在从机初始化后先调一次HAL_SPI_Receive_IT让从机处于等待接收状态否则主机发数据时从机还没准备好第一帧必丢。2.2 双板 SPI 的引脚分配与时钟参数怎么定以两块 STM32F103C8T6 为例主机用 SPI1从机用 SPI1引脚分配如下表。注意从机的 SCK 和 MOSI 是输入MISO 是输出CS 是输入。信号主机引脚从机引脚方向SCKPA5PA5主机输出从机输入MISOPA6PA6主机输入从机输出MOSIPA7PA7主机输出从机输入CSPA4PA4主机输出从机输入时钟参数方面SPI 波特率预分频值决定 SCK 频率。主机如果跑 72MHz预分频 8 就是 9MHz预分频 16 就是 4.5MHz。从机不需要设波特率它跟随主机时钟。但要注意从机的 SPI 时钟不能超过芯片手册规定的最大值F1 系列 SPI 最高 18MHzF4 系列可以到 42MHz 以上。实际调试时建议先把主机波特率降到 1MHz 左右确认通信正常后再往上加这样能排除时序余量不足导致的偶发错误。数据帧格式默认 8 位也可以配 16 位。如果传的是结构体或浮点数建议统一按字节流处理在应用层做打包和解包不要依赖 SPI 的 16 位帧否则大小端和对齐问题会让你很头疼。2.3 主机查询发送与从机中断接收的职责划分SPI 是主机驱动的总线从机不能主动发起传输。所以典型架构是主机定时或事件触发发送从机一直处于接收就绪状态。主机可以用查询方式发送代码简单但会阻塞 CPU也可以用 DMA 发送适合大数据块。从机必须用中断或 DMA 接收因为从机不知道主机什么时候发数据查询方式会漏帧。我一般这样分工主机用HAL_SPI_Transmit查询发送短命令用HAL_SPI_Transmit_DMA发长数据包从机用HAL_SPI_Receive_IT逐字节接收收满一帧后在回调里置标志位主循环处理。这样从机不会丢数据主机也不会被长数据阻塞。3. 手把手搭建双板 SPI 通信例程3.1 主机端初始化与发送代码主机端 CubeMX 配置SPI1 模式选 Full-Duplex Master硬件 NSS 选 Disable预分频先设 16CPOL LowCPHA 1 Edge。生成代码后在 main 里加发送逻辑。// 主机发送缓冲区前两字节是帧头后面是数据 uint8_t tx_buf[8] {0xAA, 0x55, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; // 手动控制 CS 引脚PA4 配成 GPIO 输出 void SPI_CS_Low(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } void SPI_CS_High(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); } // 查询方式发送一帧 void SPI_SendFrame(void) { SPI_CS_Low(); HAL_SPI_Transmit(hspi1, tx_buf, sizeof(tx_buf), 100); // 超时 100ms SPI_CS_High(); }逻辑说明CS 拉低选中从机HAL_SPI_Transmit会依次把 tx_buf 里的 8 个字节通过 MOSI 发出去同时 SCK 输出 8 个时钟脉冲。超时参数 100 是毫秒防止总线异常时死等。发送完拉高 CS从机收到帧结束信号。注意HAL_SPI_Transmit是阻塞式如果主机主循环里还有其他任务建议改用 DMA 版本。参数说明sizeof(tx_buf)是发送字节数必须是 uint8_t 数组。超时时间根据你的波特率算8 字节在 1MHz 下不到 100us100ms 足够。如果从机没接好这里会返回 HAL_TIMEOUT可以在返回值里判断。3.2 从机端中断接收与回调处理从机 CubeMX 配置SPI1 模式选 Full-Duplex SlaveNSS 选 Hardware Input 或者 SoftwareCPOL LowCPHA 1 Edge和主机一致。生成代码后在 main 里启动接收中断。// 从机接收缓冲区 uint8_t rx_buf[8]; volatile uint8_t rx_flag 0; // 接收完成标志 // 在 main 初始化后调用启动第一次接收 void SPI_Slave_StartReceive(void) { HAL_SPI_Receive_IT(hspi1, rx_buf, sizeof(rx_buf)); } // 接收完成回调HAL 库自动调用 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { rx_flag 1; // 置标志主循环处理 // 重新启动接收准备下一帧 HAL_SPI_Receive_IT(hspi1, rx_buf, sizeof(rx_buf)); } } // 主循环里检查标志 void Main_Loop(void) { if (rx_flag) { rx_flag 0; // 处理 rx_buf 里的数据比如校验帧头 if (rx_buf[0] 0xAA rx_buf[1] 0x55) { // 有效帧解析后续数据 } } }逻辑说明从机调用HAL_SPI_Receive_IT后SPI 外设进入接收状态每来一个 SCK 脉冲就移入一位收满 8 字节后触发回调。回调里置标志并重新启动接收保证连续帧不丢。主循环检测标志后处理数据处理完清标志。注意回调里不要做耗时操作否则会阻塞下一次接收。参数说明HAL_SPI_Receive_IT的第三个参数是接收字节数必须和主机发送字节数一致。如果主机发 8 字节、从机收 4 字节从机会在收满 4 字节后触发回调剩下的 4 字节丢失。所以双方要约定好帧长度或者用变长协议在帧头里带长度字段。3.3 用逻辑分析仪验证时序的四个关键点代码跑起来后如果通信不正常先别改代码用逻辑分析仪抓 SCK、MOSI、CS 三根线。重点看四个点第一CS 拉低到第一个 SCK 上升沿之间有没有足够建立时间一般要大于 50ns第二SCK 空闲电平是不是和 CPOL 配置一致模式 0 空闲低第三MOSI 数据在 SCK 上升沿是否稳定如果数据跳变和时钟沿重合说明 CPHA 配错了第四CS 拉高前最后一个 SCK 沿之后数据是否保持足够时间。我遇到过一种情况主机发送 8 字节逻辑分析仪上 SCK 只有 7 个脉冲最后一字节没发出去。原因是HAL_SPI_Transmit超时设太短发送到第 7 字节时超时返回了。把超时从 1ms 改成 100ms 就好了。所以超时参数不要抠太紧留足余量。4. 避坑指南双板 SPI 调试中最容易翻车的五个问题4.1 从机收到全 0xFF 或全 0x00现象主机发送正常从机 rx_buf 里全是 0xFF 或 0x00帧头校验永远不过。原因全 0xFF 通常是从机 MISO 线悬空或从机没使能SPI 外设读到的都是高电平全 0x00 是从机时钟没接对或者从机 SPI 根本没启动。还有一种可能是从机 NSS 配置错误硬件 NSS 模式下 CS 没拉低从机不工作。解决先检查 MISO 线是否接好用万用表测从机 MISO 引脚在空闲时是不是高阻。然后确认从机HAL_SPI_Receive_IT是否在主机发送前就调用了。最后检查从机 NSS 配置如果 CubeMX 里选了 Hardware InputCS 必须接对如果选了 Software需要在接收前手动把 NSS 内部拉低或者直接用__HAL_SPI_ENABLE使能。4.2 第一帧数据总是丢失现象主机发第一帧从机没反应发第二帧开始正常。原因从机启动接收中断需要时间主机发第一帧时从机还没进入接收状态。或者从机在回调里重新启动接收的时机太晚主机第二帧已经发完了。解决从机初始化后先调一次HAL_SPI_Receive_IT再进主循环。主机发送前加 10ms 延时等从机就绪。更稳妥的做法是主机先发一个无效帧「预热」从机收到后丢弃从第二帧开始正式通信。另外回调里重新启动接收要放在最前面不要等处理完数据再启动。4.3 DMA 发送时数据错位或只发了一半现象主机用 DMA 发送长数据从机收到的数据错位或者只收到前半部分。原因DMA 传输完成中断和 SPI 传输完成中断的优先级没配好DMA 传完就关了 CS最后一个字节还没移出。或者 DMA 缓冲区是局部变量函数返回后内存被回收DMA 读到的是垃圾数据。解决DMA 发送时CS 拉高要放在 DMA 传输完成回调里不要放在HAL_SPI_Transmit_DMA后面直接拉高。缓冲区用全局变量或静态变量确保 DMA 访问期间内存有效。如果数据错位检查 DMA 的传输宽度是不是配成了 Half-Word 而 SPI 是 8 位两者要一致。4.4 高速通信时偶发误码现象波特率降到 1MHz 时通信正常升到 9MHz 后偶尔收到错误数据。原因杜邦线太长、没有屏蔽高速时钟下信号反射和串扰导致误码。或者从机中断优先级太低收数据时被其他中断打断SPI 移位寄存器溢出。解决缩短接线长度最好在 10cm 以内SCK 和 MOSI 走线尽量短。从机 SPI 中断优先级设到最高或者用 DMA 接收减少中断次数。如果还不行在 SCK 和地之间并一个 33pF 电容滤高频毛刺。实在要长距离通信加一片 SPI 缓冲芯片或者改用差分方案。4.5 从机回调里重新启动接收导致死锁现象从机跑一段时间后不再接收数据复位才能恢复。原因回调里调用HAL_SPI_Receive_IT时如果上一次接收还没完全关闭HAL 库会返回 HAL_BUSY导致接收没有重新启动。或者回调里调用了其他 HAL 函数和 SPI 中断产生竞争。解决在回调里先检查HAL_SPI_GetState状态确认是 READY 再启动。或者用HAL_SPI_Receive_IT的返回值判断如果返回 HAL_BUSY在主循环里重试。更简单的办法是用 DMA 接收DMA 完成回调里重新启动DMA 的竞争问题比中断少。5. 进阶技巧用双缓冲和帧校验把 SPI 通信做稳前面讲的例程能跑通基本通信但实际项目里往往要连续传大量数据还要保证不丢帧、不错帧。我一般会在从机端加双缓冲和帧校验主机端加应答重传。这套组合拳打下来SPI 通信的稳定性会提升一个档次。双缓冲的思路是从机准备两个接收缓冲区 A 和 B当前用 A 接收时主循环处理 B 里的上一帧数据A 收满后切换到 B 接收主循环处理 A。这样接收和处理互不阻塞高速连续帧也不会丢。实现上可以用 DMA 的双缓冲模式或者手动在回调里切换缓冲区指针。// 从机双缓冲定义 uint8_t rx_buf_a[32]; uint8_t rx_buf_b[32]; uint8_t *active_buf rx_buf_a; // 当前接收缓冲区 uint8_t *ready_buf NULL; // 待处理缓冲区 volatile uint8_t frame_ready 0; // 启动接收绑定当前 active_buf void SPI_Slave_StartReceive(void) { HAL_SPI_Receive_IT(hspi1, active_buf, 32); } // 回调里切换缓冲区 void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { ready_buf active_buf; // 当前缓冲区标记为待处理 active_buf (active_buf rx_buf_a) ? rx_buf_b : rx_buf_a; // 切换 frame_ready 1; HAL_SPI_Receive_IT(hspi1, active_buf, 32); // 用新缓冲区继续收 } } // 主循环处理 ready_buf void Main_Loop(void) { if (frame_ready) { frame_ready 0; // 校验帧头、长度、CRC if (ready_buf[0] 0xAA ready_buf[1] 0x55) { uint8_t len ready_buf[2]; uint8_t crc ready_buf[3 len]; // 计算校验并处理数据 } } }帧校验我一般用「帧头 长度 数据 CRC8」的格式。帧头用 0xAA55 这种不容易出现在数据里的值长度字段告诉从机这一帧有多少字节CRC8 校验数据完整性。主机发送前算好 CRC从机收到后重新算一遍不一致就丢弃并请求重传。重传机制可以用一个简单的应答帧从机收到有效帧后回一个 ACK主机收到 ACK 才发下一帧超时没收到就重发。验证这套机制是否可靠我通常用两个指标连续发送 10000 帧统计丢帧率和误码率。丢帧率应该为 0误码率在 CRC 校验下也应该为 0。如果还有误码说明硬件接线或时钟余量有问题先降波特率再查线。另外可以在主机端加一个计数器每发一帧计数加一从机收到后把计数回传主机对比发送和回传的计数就能知道有没有丢帧。最后说一个我自己的习惯每次调 SPI 通信先不写业务代码只写一个最简单的「主机发 0xAA、从机收到后翻转 LED」的测试例程。这个例程跑通了再往上加协议和业务逻辑。这样一旦出问题能快速定位是底层配置问题还是应用层问题。SPI 这东西底层通了上层就是体力活底层不通上层怎么写都是白搭。希望帮到你。本文还有配套的精品资源点击获取