
简介面向STM32开发者与嵌入式初学者的串口通信实战工程基于正点原子mini板与精英板通过USART3实现两个开发板之间的双向收发并借助USART1在串口助手上打印输出代码调试完全可用。整套资源共321个文件以h头文件和c源文件为主配合uvprojx工程配置、axf/hex可执行镜像、map/lst编译列表等Keil开发环境产物压缩包仅7.35MB便于直接载入工程学习。已有4375人学习使用内容涵盖串口寄存器配置、中断接收、LCD屏幕显示回显及LED状态指示等关键环节接收端精英板在串口3收到数据时翻转LED10串口1发送成功时翻转LED1。若缺少mini板或LCD屏可借助串口助手完成替代验证或注释相关初始化代码快速适配降低硬件门槛。整体方案结构清晰适合作为串口通信原理验证、外设联动调试及基础项目二次开发的参考模板。 做嵌入式这几年串口通信应该是我用得最多、也最“保命”的外设功能之一。不管是调试打印、传感器数据读取还是两块STM32之间的数据交互串口的出场率几乎百分之百。就拿STM32之间的通信来说很多人第一反应是I2C、SPI但真到了项目里反而是串口最靠谱、最省事排错也最直观。这篇文章我就以“STM32之间的串口通信”为主线把从硬件连接、CubeMX配置、协议设计到实际工程的整套流程完整梳理一遍里面包含了我在实际项目里踩过的坑和总结出来的套路希望能帮你少走弯路。如果你正准备做毕业设计、参加电赛或者刚接触多板通信这篇文章应该都能直接派上用场。即使你只是想把两块开发板打通搞清楚TX、RX到底怎么接、代码怎么收发下面的内容也够用了。1. 整体设计思路与原理拆解1.1 为什么板间通信优先选串口很多初学者会纠结两块STM32之间通信到底用串口、I2C还是SPI更合适我的建议是除非有明确的高速传输需求否则串口优先。串口USART/UART是全双工异步通信收发各用一根线只需要TX、RX和GND三根线就能跑起来。相比I2C需要上拉电阻、SPI需要4根线还要管理片选串口的接线成本和协议复杂度都最低。另一个原因是调试方便。两块板子通信出问题了你可以随时用USB转TTL模块把其中一块的TX、RX接到电脑上用串口助手看它到底在发什么数据。I2C和SPI的数据是一帧帧协议化的逻辑分析仪不离手根本没法快速定位问题而串口裸发一个字节就能看出门道。所以从工程效率角度讲串口也是最优解。1.2 串口通信的关键概念回顾串口通信的原理并不复杂但在写代码之前有几个概念必须搞清楚否则后面配置多半要出问题。第一是帧格式。UART是异步通信没有时钟线收发双方靠约定的波特率采样。空闲时TX线保持高电平发送开始时拉低一个位时间作为起始位然后依次发送数据位通常8位最后是停止位拉高。一般我们说的“8N1”就是指8个数据位、无校验、1个停止位这是最常用的配置。第二是波特率。波特率是每秒传输的码元数常见的有9600、115200等。两个设备的波特率必须一致即使偏差超过约1.5%也可能产生乱码。对STM32来说波特率由外设时钟和波特率寄存器BRR共同决定CubeMX会自动计算但前提是你的时钟树配置是对的。这个我在后面的排查部分会重点讲。第三是逻辑电平。STM32的串口引脚输出的是3.3V TTL电平。如果两块STM32都是3.3V系统可以直接对接如果一端是5V的51单片机或Arduino就需要电平转换否则轻则通信异常重则烧坏引脚。这个细节在混合开发时特别容易踩。2. 硬件连接与初始化配置2.1 硬件接线与引脚选择STM32的串口外设叫USART很多型号有多个串口比如STM32F103C8T6有USART1、USART2、USART3。做双机通信前首先要明确用哪个串口、对应哪些引脚。以最常用的USART1为例TX在PA9、RX在PA10。两块板子连接时必须遵循“TX接对方RXRX接对方TXGND接GND”的交叉原则。很多新手第一次接串口把两块板的TX接在一起、RX接在一起结果收不到数据就是没弄明白这个交叉关系。再强调一遍A板PA9TX1接B板PA10RX1A板PA10RX1接B板PA9TX1再接一条GND共地。GND不接的话信号没有参考电平通信完全不可靠。如果两板之间的距离比较远或者现场电磁干扰大建议不要直接使用TTL电平而是通过MAX3485转成RS485差分信号走双绞线传输。RS485是半双工但抗干扰能力强传输距离可达上百米。近距离实验直接TTL对连就行不需要上RS485。2.2 CubeMX串口初始化要点用STM32CubeMX初始化串口效率比手写寄存器高得多。创建工程时在Pinout Configuration里找到USART1Mode选择Asynchronous异步模式然后在Parameter Settings里确认波特率、数据位、停止位、校验位。波特率选多少如果是两块STM32之间的短距离通信115200是推荐值传输速度够快配置也标准。如果通信距离较长或者现场干扰较重建议降到9600稳定性明显更好。我自己做产品原型时板间通信一般默认115200接外设模块比如GPS、蓝牙则看模块手册要求很多老模块只支持9600。在NVIC Settings里建议开启USART1 global interrupt。如果不开启中断就只能用阻塞方式收发而阻塞接收会一直卡住CPU这在双机通信场景下基本上没法用。所以从一开始就把中断打开后面用中断方式接收会方便很多。2.3 HAL库收发接口怎么选STM32的HAL库提供了三套收发方式阻塞、中断和DMA。三者在代码写法上差别不大但使用场景完全不一样。阻塞方式对应HAL_UART_Transmit和HAL_UART_Receive特点是一次调用必须等收发完成才返回。发送一般用阻塞问题不大比如定时上报一帧数据阻塞发送那几百微秒完全可以接受。但阻塞接收会一直死等如果对方迟迟不发数据程序就卡在那里对需要处理其他任务的系统来说这就是灾难。中断方式对应HAL_UART_Transmit_IT和HAL_UART_Receive_IT。其中最常用的是接收中断调用后函数立即返回数据到达时硬件触发中断收完一个字节后回调HAL_UART_RxCpltCallback。要注意的是ST的HAL库实现里HAL_UART_Receive_IT每调用一次只接收指定长度的数据而且接收完成后不会自动重新打开接收必须在回调函数里再次调用HAL_UART_Receive_IT否则只收到一个字节就停了。这是我最初特别容易忽略的雷。DMA方式对应HAL_UART_Transmit_DMA和HAL_UART_Receive_DMA适合大批量数据传输。配合串口的IDLE空闲中断可以实现不定长数据接收效果非常好。具体做法下一章详细讲。3. 自定义通信协议与双机工程实现3.1 为什么要自己定一套帧协议如果两块STM32只是互相发一个“1”或“0”这种单字节信号那确实不需要协议直接发就行。但实际项目中通信内容往往是多字节的比如传感器数据、控制指令、状态信息而且必须保证接收方收到的数据是准确的。没有协议约束的裸数据接收方根本不知道一串字节从哪里开始、到哪里结束也没法判断数据是否被干扰。所以工程上必须自定义一套简单的帧协议。我在项目里常用的一种极简但可靠的帧格式是这样的字节位置内容说明00xAA帧头1固定值10x55帧头2固定值2Cmd命令字比如0x01表示数据上报3Len数据区长度4~(4Len-1)Data实际数据末尾Sum累加和校验取低8位选0xAA 0x55做帧头是有讲究的。0xAA是101010100x55是01010101这两个值在示波器上看有非常明显的交替电平特征容易识别而且出现重复的概率低。累加和校验就是把从帧头到数据区最后一字节所有值加在一起取低8位作为Sum。接收方按同样规则算一遍对比收到的Sum一致说明数据大概率没被篡改。3.2 不定长数据接收的两种方案双机通信最常见的难点就是接收方不知道对方一帧要发多少字节。这里有两种方案按项目复杂度选择。第一种是纯中断状态机解析。每收到一个字节就进入HAL_UART_RxCpltCallback回调通过一个状态机判断当前处于帧头、命令、长度还是数据阶段逐字节拼帧。优点是逻辑清晰、容易理解和修改缺点是每个字节都触发一次中断在高速率下CPU开销较大但115200波特率下每字节约87微秒对STM32来说完全能承受。第二种是DMA空闲中断IDLE。这种方案更高级也更省CPU。思路是开启DMA接收让数据自动存进缓冲区当串口总线上一个字节时间没有新数据时硬件触发IDLE中断此时从DMA计数器就能算出这次收到了多少个字节。一帧数据直接以整块形式出现在缓冲区里CPU只需要去缓冲区解析即可。这个方案配合状态机处理效率非常高。3.3 双机通信完整示例与代码解析下面我给出一个可以直接复制的双机通信示例。假设A板USART1每秒向B板USART1发送一帧温度和电压数据B板收到数据后解析并把解析结果通过串口2打印到调试助手。两块板子都用STM32F103系列CubeMX里按前面说的方法配置好USART1和USART2的中断即可。先看A板发送端的代码。发送函数的核心是构造帧缓冲区// 发送一帧数据 // cmd: 命令字 data: 数据指针 data_len: 数据长度 void UART_SendFrame(uint8_t cmd, uint8_t *data, uint8_t data_len) { uint8_t frame[64]; uint8_t len 0; uint8_t sum 0; uint8_t i; frame[len] 0xAA; // 帧头1 frame[len] 0x55; // 帧头2 frame[len] cmd; // 命令字 frame[len] data_len; // 数据长度 sum 0xAA 0x55 cmd data_len; for (i 0; i data_len; i) { frame[len] data[i]; sum data[i]; } frame[len] sum; // 校验字节 HAL_UART_Transmit(huart1, frame, len, 100); }主函数里这样调用uint8_t sensor_data[2]; sensor_data[0] 25; // 温度 sensor_data[1] 330; // 电压拆成低8位传 while (1) { UART_SendFrame(0x01, sensor_data, 2); HAL_Delay(1000); }这里要注意一个细节电压值可能是16位的但串口一个字节最多承载8位所以要么拆成两个字节分别传要么按比例缩放成8位。初学时容易忽略这种数据类型转换问题导致接收端数据对不上。再看接收端的解析代码。在CubeMX里已经开启了USART1接收中断定义一个全局接收缓冲区和接收状态机uint8_t rx_byte; // 当前收到的字节 uint8_t rx_buf[64]; // 帧数据区 uint8_t rx_state 0; // 解析状态 uint8_t rx_cmd 0; uint8_t rx_len 0; uint8_t rx_data_cnt 0; // 接收中断回调 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { switch (rx_state) { case 0: // 等待帧头1 if (rx_byte 0xAA) rx_state 1; break; case 1: // 等待帧头2 if (rx_byte 0x55) rx_state 2; else rx_state 0; // 帧头不匹配重新开始 break; case 2: // 接收命令字 rx_cmd rx_byte; rx_state 3; break; case 3: // 接收数据长度 rx_len rx_byte; rx_data_cnt 0; if (rx_len 0) rx_state 4; else rx_state 5; // 长度为0直接等校验 break; case 4: // 接收数据 if (rx_data_cnt rx_len) { rx_buf[rx_data_cnt] rx_byte; } if (rx_data_cnt rx_len) rx_state 5; break; case 5: // 接收校验并判断 // 实际工程中在这里做累加和校验并处理数据 if (ProcessFrame(rx_cmd, rx_buf, rx_len) 0) { // 帧有效可以更新全局变量或置标志位 frame_ok 1; } rx_state 0; break; } // 重新使能单字节接收中断 HAL_UART_Receive_IT(huart1, rx_byte, 1); } }ProcessFrame函数负责累加和校验以及按命令字分发数据。你可以根据你的项目需要把接收到的数据写到全局结构体里主循环检测到frame_ok标志后进行处理这样不会在中断回调里做耗时操作。这种“状态机单字节中断”的写法是串口协议解析的基础功。理解了它不管以后换什么协议、加什么功能逻辑都不变。如果你追求更高效率可以在此基础上把接收改为DMAIDLE方式解析逻辑完全复用只是拿到整包数据的时机不一样。4. 常见问题与排查技巧实录4.1 乱码问题的排查思路串口通信最经典的问题就是乱码。数据能收到但全是错乱字符或者字节对不上。排查时按下面顺序走第一步检查波特率是否一致。A板115200B板9600收到的必然是乱码。确认两块板子CubeMX里配置的波特率完全一致。第二步检查时钟树。这是最容易忽略的地方。很多开发板默认使用外部晶振但如果你用的板子没有外部晶振或者CubeMX里时钟配置选错了HSE/HSI实际波特率会偏离设定值。比如8MHz的内部RC时钟精度本身不高用来跑115200可能就产生误码了。规范做法是确认你的板子实际贴了多少频率的晶振然后在CubeMX的Clock Configuration里把HSE填成对应频率让系统时钟跑在最高主频上。第三步检查共地。两板之间除了TX、RX必须再连一根GND线。缺少共地时信号电压没有参考点偶尔能通但极不稳定表现就是时好时坏、偶发乱码。4.2 收不到数据怎么一步步定位如果完全收不到数据先做回环自测把本板USART1的TX引脚和RX引脚直接用杜邦线短接然后串口助手发送数据看能不能自己收到。如果能收到说明串口外设和引脚配置没问题问题在两块板之间的连接上。然后再检查RX和TX是否接反了。这个问题真的很多见尤其是用过J-Link、ST-Link调试线之后人容易惯性思维。串口连接的标准是交叉A_TX到B_RXA_RX到B_TX。如果连接也没问题就看看引脚配置是否正确。用CubeMX的话一般不会出现复用配置错误但如果你自己手写寄存器或者改过GPIO就要确认AFIO的复用设置没有覆盖掉别的功能。4.3 中断接收丢数据、卡死的处理用了中断接收之后有时会发现数据丢字节甚至程序直接卡死。这里有两个常见原因。第一个是接收函数没有重新使能。前面说过HAL_UART_Receive_IT只接收一次如果在回调里忘了再次调用串口收一个字节后就再也不进回调了。表现就是“只收到第一个字节后面全没了”。第二个是ORE溢出错误。如果数据到达速度太快而CPU正在处理更高优先级的中断来不及读取接收数据寄存器硬件就会报Overrun Error。HAL库检测到ORE错误后如果没有正确处理后续接收会一直处在错误状态。解决方法是在HAL_UART_ErrorCallback里调用__HAL_UART_CLEAR_OREFLAG(huart1)清除错误标志然后重新启动接收或者直接调用HAL_UART_AbortReceive再重新开启。还有一种情况容易忽略在中断回调里调用HAL_Delay或者printf。HAL_Delay依赖SysTick中断如果中断优先级配置不当在UART中断里调用HAL_Delay会死等看起来就是程序卡死了。printf重定向到串口也要慎重如果发送缓冲区没弄好回调和主循环互相阻塞整个系统就假死。我的习惯是中断回调里只做数据搬移和标志置位所有需要耗时的操作都放到主循环里处理。4.4 调试器连接失败代码烧不进去怎么办做串口通信调试时大概率会遇到烧录器连不上芯片的情况。我印象最深的是第一次用ST-Link连着板子代码里还把PA13、PA14给复用了结果程序一跑SWD调试口直接被占掉Keil报出“error: no stm32 target found! if your product embeds debug authentication, pl...”这个报错。一时间还以为是芯片烧了后来排查才发现是引脚复用冲突。遇到这个报错先检查ST-Link的SWDIO、SWCLK、GND和3.3V四根线是否接对再确认目标板有没有独立供电。然后按住板子上的复位键不放点击Keil下载在松开的瞬间让程序烧录进去。如果还是不行用STM32 ST-LINK Utility或者STM32CubeProgrammer的“connect under reset”模式连接把芯片的读保护解除注意会整片擦除Flash基本都能救回来。值得注意的是如果你在代码里把SWD引脚配置成了普通GPIO输出程序一旦跑起来调试口就失效了。因此只要不是量产阶段我都建议在初始化代码里保留SWD功能或者至少不要占用SWD引脚做其他功能。我把这些年在串口通信上遇到的问题汇总成一个速查表方便你写代码和调试时对照现象可能原因解决办法完全收不到数据TX/RX接反、未共地交叉连接补GND线收数据乱码波特率不一致、时钟源不对统一波特率检查时钟树配置只收到一个字节中断接收未重新使能在回调中再次调用HAL_UART_Receive_IT运行一段时间卡死ORE溢出或中断里做了耗时操作清除溢出标志回调里只置标志收到数据校验失败干扰或帧格式不匹配使用帧头累加和校验必要时加CRC调试器报no targetSWD引脚占用或供电问题按复位下载connect under reset还有一些调试中的小技巧也很实用。比如在串口助手显示十六进制这样不会因为ASCII码显示问题误判协议帧发送自定义协议帧时先把一帧数据用串口助手手工发一遍验证接收端解析正常后再让代码自动发送这样能把问题范围缩小到发送端或接收端。最后再分享一点我的个人习惯串口通信这个技术点看起来基础却贯穿了几乎每一个STM32项目。我自己的体会是千万不要觉得串口简单就跳过原理直接抄代码尤其是中断接收和协议解析这两块理解不深的话出问题只能干瞪眼。我现在的习惯是只要是两块STM32之间做通信不管功能多简单都统一用帧协议格式帧头固定、带长度、带校验。虽然只有一两个字节的数据时这么做显得“重”但项目后期功能一扩展帧协议带来的规范性和可维护性会让你少很多事。另外我还会在板子上留一个USB转TTL的调试接口主串口跑通信调试串口接电脑两边数据一目了然。这个配置花不了几块钱但排障效率能翻倍。后续如果你想把这套通信方案做得更完善可以考虑加入超时重发、应答确认机制或者把XMODEM这类成熟的文件传输协议移植进来用于远程固件升级。串口这个外设本身不复杂复杂的是怎么用好它。希望这篇文章能给你的项目带来一点帮助。本文还有配套的精品资源点击获取