
简介一套基于正点原子开发板的STM32串口通信演示工程面向嵌入式初学者或需要处理板间数据交互的开发人员。工程包含mini板与精英板两端完整代码通过USART3PB10、PB11实现双机收发精英板同时借助USART1将接收结果输出至串口助手并支持在LCD屏上显示接收内容通过LED翻转直观反馈串口收发状态。压缩包共321个文件以.c/.h源码、Keil工程文件uvprojx/uvoptx、编译生成的axf/hex固件以及map/lst链接信息为主另含少量调试配置文件与工程清理脚本整体大小7.35MB。已有4375人学习下载工程经过实际调试验证可直接运行目录结构清晰有助于读者快速理解STM32串口收发配置、数据流向与调试方法也可在此框架上继续扩展通信协议。 串口在STM32开发里属于第一课级别的内容但越是基础的东西越容易出各种奇奇怪怪的问题。尤其是两块STM32之间通信看起来无非就是TX接RX、RX接TX实际做起来才发现乱码、丢帧、死锁、收发不同步……每一个坑都能卡掉你半天时间。这篇文章不打算讲太泛的概念直接围绕STM32对STM32这个具体场景把硬件连接、软件实现、帧协议设计、排障思路完整过一遍给出一套能直接抄作业的方案。1. 为什么双机通信首选串口先看清串口的定位1.1 常见板级通信方式的核心差异STM32片内外设很丰富板间通信能用I2C、SPI、CAN、USB也能用串口UART。但如果你只是想让两块STM32互相传数据串口往往是综合成本最低的选择。先看一张对比表通信方式线数通信模式典型速率适用距离串口UART2TX/RX全双工异步9600bps~2Mbps板内/短距离I2C2SCL/SDA半双工同步100k~1Mbps板内SPI3~4全双工同步可达数十Mbps板内CAN2CANH/L半双工异步最高1Mbps远距离强干扰环境USB2D/D-主从异步12Mbps以上短距离串口最大的优势是简单可靠不需要时钟线双方约定好波特率就能通信数据格式透明用逻辑分析仪或者串口助手一眼就能看出问题调试成本极低。I2C、SPI虽然速率高但要么是半双工要么需要额外的CS/时钟引脚而且协议状态机比UART复杂不少。CAN抗干扰强但需要收发器和复杂的帧ID规划对简单点对点通信来说有些大材小用。1.2 双STM32通信的典型业务场景我实际做过的项目中STM32之间走串口最常见的几类场景主控板与功能板解耦一块主控板负责逻辑决策另一块负责采集传感器数据比如心电、血氧、温湿度中间用串口把数据打包上传。热搜词里的STM32心率血氧就是这种典型结构。双机冗余备份设备运行中主控故障时备用板通过心跳包感知异常并接管控制。这种场景下串口的心跳机制设计特别重要。协议转换网关一块板子跑Modbus从站把数据通过串口透传给另一块板子由后者完成LCD显示或者Wi-Fi上传。Bootloader升级通道上位机通过串口把固件分包发给STM32再由Bootloader写入Flash实现不拆机升级。这些场景有一个共同点数据量不大、实时性要求中等、但对稳定性要求很高。串口在单片机之间天然就适合干这个活。2. 硬件连接RX/TX交叉只是入门真正决定成败的是电平和地2.1 接线前的三个基础确认两块STM32之间用串口直连时第一件事是确认引脚定义。以最常用的STM32F103系列为例USART1的TX是PA9、RX是PA10USART2的TX是PA2、RX是PA3。接线规则很简单A的TX接B的RXA的RX接B的TXGND接GND。注意是交叉连接很多第一次做的人在这里踩坑以为TX对TX、RX对RX就能通结果数据全丢。第二个确认是电平标准。STM32的串口引脚是TTL电平高电平约3.3V低电平0V。两块ST32之间电平一样可以直接连不需要MAX232这种电平转换芯片。但如果另一端是51单片机或者5V供电的传感器模块就要做电平兼容处理最简单的办法是在TX线上串一个330Ω~1kΩ的电阻分压。第三个确认是共地。这个问题在单板调试时不容易暴露一旦两块板子分别用独立的电源适配器供电不共地的后果就是通信完全失败或者随机乱码。UART是异步通信收发双方靠电压差来识别电平如果没有参考地接收端的0V和发送端的0V根本不是同一个电位数据自然就乱了。2.2 板间距离稍远时的方案调整两块板子如果只是同一块PCB上或者间距十几厘米TTL直连完全够用。但如果两块板子距离超过1米或者要穿过电柜、电机附近这种干扰较大的环境建议改用RS485方案。做法是每端接一个MAX34853.3V供电的485收发器A对A、B对B连接通过DE/RE引脚控制收发方向。RS485是差分信号抗共模干扰能力强传输距离可以达到几百米甚至上千米。如果手头只有USB转TTL模块想临时测试两块STM32的串口通信也可以把两块板子的TX、RX分别接同一个USB转TTL的两个通道如果有的话但注意这只能用于观察数据不能替代真实的板间连接。实际操作中我更推荐直接交叉连接后用逻辑分析仪抓波形这样能看到原始字节流排查问题快得多。3. 软件实现轮询、中断、DMA空闲中断三档方案代码详解3.1 基础轮询收发最适合先跑通链路刚接触的时候先用最简单的轮询方式把通信链路跑通是关键。下面以STM32CubeIDE HAL库为例给出一个最小可用的双机收发代码。/* 发送端代码 */ uint8_t tx_buf[] Hello STM32!\r\n; HAL_UART_Transmit(huart1, tx_buf, strlen((char*)tx_buf), 1000);/* 接收端代码 */ uint8_t rx_byte; HAL_UART_Receive(huart2, rx_byte, 1, 1000);这段代码的逻辑很清楚发送端调用HAL_UART_Transmit把整段数据发出去接收端的HAL_UART_Receive阻塞等待一个字节。两个函数都有超时参数单位是毫秒。轮询方式的优点是逻辑简单、代码容易理解适合确认接线和波特率配置是否正确。但它有两个明显缺点一是阻塞程序在等待发送或接收完成期间无法处理其他任务二是效率低如果系统里还要跑屏幕刷新、按键扫描轮询方式会把CPU时间全占掉。所以轮询只能用来做链路自检真正的项目基本不会只用这一种方式。3.2 中断接收解决边收边处理的需求中断方式让串口在后台接收数据收到一个字节就触发一次中断主循环可以继续干别的事。代码框架如下/* 在main.c中启动中断接收 */ uint8_t rx_buffer[64]; uint8_t rx_index 0; HAL_UART_Receive_IT(huart1, rx_byte, 1); /* 在stm32f1xx_it.c的中断回调函数中处理 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx_buffer[rx_index] rx_byte; /* 每收到一个字节重新启动下一次中断接收 */ HAL_UART_Receive_IT(huart1, rx_byte, 1); } }这里有个关键细节HAL库的HAL_UART_Receive_IT是一次性的接收完指定字节数后就不会再接收了所以必须在回调里重新调用一次否则程序只能收到第一个字节。这个重新触发的动作淘汰了很多新手我见过不少人把HAL_UART_Receive_IT放在主循环里调结果中断频繁重入程序直接卡死。中断方式的局限在于每收一个字节就中断一次如果波特率是115200每秒约11520个字节这意味着CPU每秒要被中断一万多次大量时间花在上下文切换上。对资源紧张的MCU来说这不是最优解。3.3 DMA空闲中断工程项目的稳健方案工程上真正推荐的做法是DMA 空闲中断(IDLE)。思路是DMA负责把串口收到的数据自动搬运到内存缓冲区CPU完全不用管当串口在一段时间内没有收到新数据时硬件触发IDLE中断这时CPU才来处理一整包数据。/* 定义接收缓冲区 */ #define RX_BUF_SIZE 256 uint8_t dma_rx_buf[RX_BUF_SIZE]; /* 启动DMA接收 */ HAL_UART_Receive_DMA(huart1, dma_rx_buf, RX_BUF_SIZE); /* 空闲中断回调中处理一帧数据 */ void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { __HAL_UART_CLEAR_IDLEFLAG(huart1); /* 计算本次收到的数据长度 */ uint16_t len RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); /* 此时dma_rx_buf[0...len-1]就是一帧完整数据 */ process_frame(dma_rx_buf, len); /* 重启DMA接收 */ HAL_UART_Receive_DMA(huart1, dma_rx_buf, RX_BUF_SIZE); } }这段代码的逻辑是DMA一直在往缓冲区写数据写到缓冲区最大值后自动回绕循环模式。当串口空闲下来时__HAL_DMA_GET_COUNTER返回的是DMA剩余未传输的数量用缓冲区大小减去这个数就能得到本次一帧数据的实际长度。要注意HAL_UART_IdleCpltCallback并不是HAL库自带的标准回调名称需要在工程中手动实现并用__HAL_UART_CLEAR_IDLEFLAG清除标志位。CubeMX生成代码时不会自动生成这个回调函数需要自己在stm32f1xx_it.c里写。完整写法一般是void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); if (RESET ! __HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); HAL_UART_IdleCpltCallback(huart1); } }DMA空闲中断这套组合在处理不定长数据帧时优势非常明显CPU几乎零负担数据也不会丢。我用这方案跑过一天一夜72万帧数据一帧都没丢过。4. 通信帧设计只有串口收发远远不够协议才是双机通信的分水岭4.1 为什么裸收发的串口数据不可用串口本质上是字节流通道它只负责把字节按顺序发出去不关心这些字节是什么意思。两块STM32之间如果只是收到数据其实没有任何业务意义——你需要知道数据从哪开始、到哪结束、内容是什么、有没有传错。举个实际例子A板想把温度数据25.6发给B板。如果直接HAL_UART_Transmit(huart1, (uint8_t*)25.6, 4),B板收到的是25.6四个字节。看似能显示出来但一旦数据变成255.6变成5个字节接收端就不知道这一帧什么时候结束再或者中间出现一个干扰字节整个字符串就错位了。所以裸传输只适合调试观察不能用于真正的数据交互。4.2 三种常用帧格式设计对比方案实现方式优点缺点适用场景分隔符法在数据末尾加\r\n或0xAA实现简单调试直观数据内容中不能出现相同字符需转义指令简单、数据量小定长帧法每次固定发N个字节解析简单不需要找帧头灵活性差扩字段要改协议数据结构固定、字段不变帧头长度校验帧头数据长度数据区校验和通用性最强任意长度数据实现稍复杂需要状态机绝大多数工程场景我个人的建议是只要不是临时调试一律用第三种帧头长度校验的结构。虽然代码量多几十行但换来的稳定性是前两种完全比不了的。4.3 一个可以落地的帧解析状态机以最常见的帧格式为例帧头(0xAA 0x55) | 数据长度(1字节) | 命令字(1字节) | 数据区(N字节) | 校验和(1字节)解析端用状态机逐字节处理代码结构如下typedef enum { FRAME_STATE_HEAD1, FRAME_STATE_HEAD2, FRAME_STATE_LEN, FRAME_STATE_CMD, FRAME_STATE_DATA, FRAME_STATE_CHECK } frame_state_t; frame_state_t state FRAME_STATE_HEAD1; uint8_t frame_data[256]; uint8_t data_index 0; uint8_t frame_len 0; uint8_t frame_cmd 0; uint8_t sum 0; void uart_parse_byte(uint8_t byte) { switch (state) { case FRAME_STATE_HEAD1: if (byte 0xAA) state FRAME_STATE_HEAD2; break; case FRAME_STATE_HEAD2: if (byte 0x55) { state FRAME_STATE_LEN; } else if (byte 0xAA) { /* 连续的0xAA也不算错保持HEAD2状态 */ } else { state FRAME_STATE_HEAD1; } break; case FRAME_STATE_LEN: frame_len byte; data_index 0; sum 0; state (frame_len 0) ? FRAME_STATE_CMD : FRAME_STATE_CHECK; break; case FRAME_STATE_CMD: frame_cmd byte; sum byte; state FRAME_STATE_DATA; break; case FRAME_STATE_DATA: frame_data[data_index] byte; sum byte; if (data_index frame_len - 1) { state FRAME_STATE_CHECK; } break; case FRAME_STATE_CHECK: if (byte sum) { process_frame(frame_cmd, frame_data, data_index); } state FRAME_STATE_HEAD1; break; default: state FRAME_STATE_HEAD1; break; } }这段状态机代码的精髓在于不依赖帧长度也能找到帧边界。即使中间丢了一个字节下一帧的帧头依然能被识别因为状态机不会因为状态错乱而卡死。收到错误帧头就丢弃继续重新找0xAA 0x55这是通信协议鲁棒性的关键。校验方式这里用的是最简单的累加和把命令字和数据区所有字节加起来低8位放在帧尾。如果项目对数据完整性要求更高可以换成CRC16或者CRC32代码库可以搜到现成的查表实现这里不再展开。5. 实测高频故障与完整排查链路5.1 数据乱码先查波特率再查时钟最后查电平乱码是串口通信最常见的问题没有之一。我的排查顺序非常固定第一步确认两边的波特率完全一致。注意不是差不多就行9600和9600必须完全一致。如果A板跑的是HAL_UART_Init里配置的115200B板就必须也是115200中间隔一个比特的偏差都会导致采样错位。第二步查时钟树。这一步经常被忽略。STM32的USART波特率由PCLK1或PCLK2分频而来如果CubeMX里改了系统时钟比如从8MHz外部晶振改成72MHz主频但USART的时钟源配置没跟着更新实际波特率会和预期偏差很大。排查方法是先看SystemClock_Config()里RCC_HCLK_CONFIG的设置再确认USART挂在哪个总线上别让APB1和APB2的时钟频率搞混。第三步查电平适配。如果两端有电平转换模块比如接了MAX3485或者MAX232检查转换芯片的供电和方向控制引脚是否正常。我用过的一款485模块DE/RE引脚悬空时收发都异常接上控制脚并拉低为接收模式后立刻恢复正常。5.2 只收不发、发几次就卡死中断标志位和DMA配置的陷阱这类问题的典型表现是上电后第一帧数据正常第二帧开始就完全没反应了。原因多半是中断标志位没有清除或者DMA传输完成后没有重新初始化。HAL库中HAL_UART_Transmit发送完成后TC发送完成标志位需要清除。标准写法是__HAL_UART_CLEAR_FLAG(huart1, UART_FLAG_TC);如果不做这一步极少数情况下下一次发送会卡在HAL_UART_Transmit里出不来。热搜词里那个stm32延时函数delay卡死的问题有一部分就是发送函数里的超时等待没有正确退出造成的。DMA方式的坑主要在轮询模式上。DMA接收如果用的是普通模式而非循环模式传输完指定字节数后DMA会自动关闭此时必须重新调用HAL_UART_Receive_DMA重新启动。如果漏了这一步硬件会一直收数据但CPU完全感知不到。5.3 调试器连不上目标板ST-LINK连接失败的处理思路热搜词里有一条error: no stm32 target found! if your product embeds debug authentication这是ST-LINK连接失败时常见的提示。做双机通信开发时如果两块板子都接了ST-LINK或者某块板子的串口引脚和调试引脚复用冲突就可能出现这个报错。排查顺序先看ST-LINK和板子之间的SWDIO、SWCLK、GND三条线是否接对很多报错是因为GND没共地。再看目标板供电是否正常SWD调试接口对电源要求很敏感。最后检查代码里是否把SWD引脚复用成了别的功能比如把PA13/PA14配置成了GPIO输出这会直接断掉调试链路解决方案是用ISP模式烧录一段恢复程序或者按住复位键在连接瞬间松手这种方式不一定每次都能成功但值得一试。6. 优化与扩展从双机互联到多机组网6.1 半双工总线型网络引入RS485两块STM32之间的串口是点对点全双工但如果现场有三块、五块甚至更多板子需要相互通信点对点的接法就会变得异常混乱。这时候应该考虑RS485总线组网。RS485是半双工的同一时刻只能有一个节点发送其他节点全部处于接收状态。应用层需要设计一套仲裁机制常见做法是用地址轮询主机依次向从机发送查询命令从机收到后回复未收到命令的从机保持静默。这种主从模式在Modbus协议里已经封装得很完善如果项目复杂度不高直接移植FreeModbus就够了——热搜词里就有freemodbus stm32移植确实是一条成熟的路。多机通信中还需要解决一个问题RS485的方向切换。发送数据前要把DE引脚拉高发送完成后必须拉低否则总线一直被发送端占用其他节点无法回应。方向切换的时机要格外小心如果拉低的时机太早最后一个字节可能还没发完对方收到的数据就会丢尾。6.2 具体的优化方向波特率自适应上电后先发一个固定字节0x55接收端根据这个字节的脉宽反推波特率。0x55的二进制是01010101在示波器上能看到非常清晰的方波是最常用的波特率训练字节。数据压缩如果一帧数据里大部分字段都是0或者重复值可以用简单的RLE压缩对控制类指令这种特征明显的场景效果很好。日志与心跳分离把调试用的日志输出单独走一个串口比如USART3把业务数据通信的串口单独留出来避免调试代码污染正式通信链路。这个做法在项目后期维护时能省下大量排查时间。以我个人的实际经验来说串口通信的成功不在于把波特率配好、把线接对也不在于第一次就能把数据收发跑通而在于整套机制的设计——硬件上有没有预留隔离和滤波位置软件上有没有把帧解析、错误处理、超时重发这三个环节做到位出现故障时有没有一套固定的排查方法来快速定位。把握住这些点两块STM32之间通信这件事基本就稳了。本文还有配套的精品资源点击获取