STM32与Arduino USART串口通信实战:电平、时钟与协议对齐
简介这份资源面向嵌入式初学者与跨平台串口调试开发者聚焦STM32F103ZET6与Arduino之间USART通信的完整实现解决两者供电电压不同3.3V与5V时的电平匹配与数据收发问题。压缩包共77个文件约278KB以35个.h头文件与34个.c源文件为主体涵盖USART初始化、中断收发、SysTick延时及命令解析等模块另含uvproj、uvopt工程文件与map、lst编译产物便于直接导入Keil查看工程结构。内容围绕波特率、数据位、停止位、奇偶校验与硬件握手等关键参数展开并涉及SoftwareSerial库使用与电平转换思路可帮助读者理解跨平台串口通信的配置要点与常见乱码、丢包问题的排查方向。目前已有2261人学习下载适合作为STM32与Arduino联合调试的实践参考。1. stm32_arduino_usart通信两块板子怎么把一句话说清楚手头同时有 STM32 和 Arduino 的人早晚会撞上同一个需求让这两块板子互相说话。STM32 跑实时控制、采样、算力活Arduino 接传感器、驱动舵机、做简单人机交互两边各干各的中间用一根串口线连起来。听起来简单真上手就会发现电平、波特率、时钟、收发时序随便哪个环节没对齐接收端就是一堆乱码或者干脆没反应。stm32_arduino_usart通信这件事本质不是「接线通电就能通」而是把两套完全不同的开发体系在 USART 这一层对齐。它适合谁适合已经能单独点亮 STM32 和 Arduino、但没让两者协同工作过的嵌入式开发者也适合做多板系统、想把任务拆到不同 MCU 上的人。核心要解决的问题就三个电平能不能对上、时钟能不能算准、收发逻辑会不会互相踩。把这三件事讲透剩下的就是照着参数填、照着代码改。下面按「先立住原理、再动手复现、最后排坑」的顺序走每一步都给到能直接抄的参数和代码。2. USART 底层对齐电平、时钟与帧格式三件事2.1 先分清 USART 和 UART别在名字上翻车很多人把 USART 和 UART 混着叫接线时才发现同步时钟脚没接。UART 是异步收发只有 TX、RX 两根信号线靠双方约定的波特率对齐每一位USART 是通用同步异步收发器多了一根时钟线可以工作在同步模式。STM32 的片上外设叫 USARTArduino 的硬件串口本质是 UART。做 stm32_arduino_usart通信时绝大多数场景用的是异步模式也就是只接 TX、RX、GND 三根线同步时钟脚悬空不用。名字叫 USART实际跑的是异步这一点先记住能省掉一堆「为什么时钟脚没接也能通」的困惑。异步模式下双方没有共享时钟全靠各自的波特率发生器在约定时刻采样。发送方按波特率把每一位依次推出去接收方在起始位下降沿之后按同样的波特率在每个位周期的中间采样。两边波特率只要偏差超过大约 2% 到 3%采样点就会漂到相邻位收到的东西自然错乱。所以第一件事不是写代码是把两边的波特率算准、设成一样。2.2 电平匹配3.3V 和 5V 之间那道坎STM32 的 IO 一般是 3.3V 电平Arduino Uno 这类板子是 5V 电平。直接把 STM32 的 TX 接到 Arduino 的 RXSTM32 输出 3.3V 高电平Arduino 的 5V 输入阈值通常在 0.6×VCC 即 3.0V 左右3.3V 勉强能识别为高短距离、低波特率下常常能通但这是踩在边缘上温度一变、线一长就可能翻车。反过来 Arduino 的 TX 输出 5V 进 STM32 的 RX如果 STM32 引脚不是 5V 容忍的长期灌 5V 会伤 IO。稳妥做法是加电平转换。常见方案有两种一是用双向电平转换模块基于 MOS 管加两只上拉电阻成本低、速率够用二是用电阻分压Arduino TX 到 STM32 RX 之间串 1kΩ、再对地接 2kΩ把 5V 分到约 3.3V但分压只适合单向、低速且会拖慢上升沿。我一般会先查 STM32 那款芯片的引脚是否标了 FT5V 容忍是 FT 就可以直连 Arduino TX不是 FT 就必须转换。这一步查数据手册的引脚定义表别凭感觉。2.3 帧格式数据位、停止位、校验位必须逐项对齐异步串口一帧的组成是1 个起始位低电平、5 到 9 个数据位、可选的校验位、0.5 到 2 个停止位。STM32 和 Arduino 默认都是 8 数据位、无校验、1 停止位也就是常说的 8N1。只要两边都保持默认这一项通常不用动。但如果你在 STM32 侧改过校验位或者停止位Arduino 侧没跟着改收到的就是持续错位的数据。下面这张表把两边常见配置项列出来方便逐项核对配置项STM32 侧常见取值Arduino 侧常见取值是否必须一致波特率9600 / 1152009600 / 115200必须数据位88必须校验位NoneNone必须停止位11必须流控NoneNone必须电平3.3V5V需转换或确认容忍2.4 波特率误差从哪来时钟树和分频系数STM32 的 USART 波特率来自外设时钟经过一个分频寄存器得到。以常见的 72MHz 外设时钟、目标 115200 为例分频系数是 72e6/(16×115200)39.0625取整数部分 39小数部分 0.0625 写进小数寄存器实际波特率是 72e6/(16×39.0625)115200误差接近零。但如果外设时钟是 36MHz分频系数变成 19.53125取整后实际波特率会有偏差偏差大到一定程度就收不对。Arduino 侧用 16MHz 晶振115200 的分频是 16e6/(16×115200)8.68取整 8 后实际波特率约 125000误差约 8.5%这已经超过安全范围。所以 Arduino 在 16MHz 下跑 115200 本身就偏短距离能通长线或干扰环境下容易出错。更稳的选择是 9600 或 19200误差小得多。这也是为什么很多教程默认用 9600——不是它慢是它稳。提示如果必须用 115200优先确认 STM32 外设时钟能否整除出低误差分频Arduino 侧可考虑换用带 16MHz 晶振但支持更好分频的库或者干脆降到 57600 试。3. 动手接线与最小收发代码3.1 接线TX 对 RXGND 必须共地先把线接对再谈代码。异步串口是交叉连接STM32 的 TX 接 Arduino 的 RXSTM32 的 RX 接 Arduino 的 TX两边的 GND 必须连在一起。GND 不共地两边对「高」和「低」的参考点不同收到的全是噪声。如果用了电平转换模块模块的 HV 接 5V 侧、LV 接 3.3V 侧信号线对应穿过模块。接线顺序建议先断电接 GND再接信号线最后上电。带电插拔信号线容易在瞬间产生尖峰打坏 IO。接好后先别急着跑代码用万用表量一下两边 GND 是否真的导通这一步花十秒能省掉后面半小时的排查。3.2 STM32 侧用 HAL 库初始化 USART 并发送下面这段是 STM32 用 HAL 库初始化 USART1、以 9600 波特率发送一个字符串的最小代码。假设外设时钟 72MHz引脚 PA9 为 TX、PA10 为 RX。// stm32_usart_tx.c // 假设已由 CubeMX 生成 GPIO 和 USART1 时钟使能 UART_HandleTypeDef huart1; void USART1_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 9600; // 与 Arduino 侧保持一致 huart1.Init.WordLength UART_WORDLENGTH_8B; // 8 数据位 huart1.Init.StopBits UART_STOPBITS_1; // 1 停止位 huart1.Init.Parity UART_PARITY_NONE; // 无校验 huart1.Init.Mode UART_MODE_TX_RX; // 收发都开 huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; // 无硬件流控 huart1.Init.OverSampling UART_OVERSAMPLING_16; // 16 倍过采样 if (HAL_UART_Init(huart1) ! HAL_OK) { // 初始化失败检查时钟和引脚配置 Error_Handler(); } } void USART1_SendString(uint8_t *str, uint16_t len) { // 阻塞发送超时 100ms适合调试阶段 HAL_UART_Transmit(huart1, str, len, 100); }这段代码的关键在huart1.Init这几个字段BaudRate必须和 Arduino 完全一致WordLength、StopBits、Parity三项决定帧格式默认 8N1 就别乱改OverSampling选 16 倍是常规做法采样更稳。HAL_UART_Transmit是阻塞发送调一次发一批调试阶段够用正式产品里如果发送频繁建议换成中断或 DMA避免占着 CPU 空等。3.3 Arduino 侧Serial 初始化与接收解析Arduino 侧用内置Serial对象初始化时把波特率设成和 STM32 一样然后在loop里读数据。// arduino_usart_rx.ino void setup() { Serial.begin(9600); // 必须与 STM32 侧一致 while (!Serial) { } // 等待串口就绪部分板子需要 } void loop() { if (Serial.available() 0) { // 读到换行符为止组成一条完整消息 String msg Serial.readStringUntil(\n); msg.trim(); // 去掉首尾空白和回车 if (msg.length() 0) { // 这里处理收到的内容比如点亮 LED 或回发确认 Serial.print(ACK:); Serial.println(msg); } } }Serial.begin(9600)里的 9600 是两边约定的速率改一边就必须改另一边。Serial.available()返回接收缓冲区里还有多少字节没读大于 0 说明有数据。readStringUntil(\n)会一直读到换行符适合按行发送的文本协议。trim()去掉末尾的\r和空白避免解析时多出看不见的字符。回发ACK:是为了让 STM32 侧能确认对方收到了调试时非常有用。3.4 收发联调先单向再双向先慢后快第一次联调建议先只做单向STM32 每隔一秒发一句hello\nArduino 收到后原样回发。STM32 侧用HAL_UART_Receive阻塞接收超时设 200ms收到就打印。确认单向通了再改成双向轮流发。波特率先用 9600通了再试 19200、38400最后才碰 115200。每换一次波特率两边同时改改完重新上电不要热改。联调时如果 Arduino 串口监视器能看到 STM32 发来的内容但 STM32 收不到 Arduino 的回发先查 Arduino 的 TX 有没有真的接到 STM32 的 RX再查 STM32 的接收中断或轮询有没有开。很多时候问题不在代码在那一根线插错了排针。4. 避坑与排查五个让通信翻车的典型场景4.1 收到稳定乱码波特率或时钟对不上现象Arduino 串口监视器里全是ÿ、?之类的乱码换行也不对。原因两边波特率不一致或者 STM32 外设时钟配置和预期不符导致实际波特率偏离。解决先用示波器或逻辑分析仪量 STM32 TX 上一位的宽度算出实际波特率和设定值对比。没有仪器就先把两边都降到 9600确认能通再往上加。STM32 侧重点检查SystemCoreClock和 USART 时钟源CubeMX 里时钟树配错是高频翻车点。4.2 完全没反应GND 没共地或 TX/RX 接反现象两边都上电了发什么都没人收。原因最常见的是 GND 没连其次是 TX 接了 TX、RX 接了 RX。解决万用表量两边 GND 是否导通把信号线对调试一次。如果用了电平转换模块确认 HV 和 LV 没接反模块本身也要共地。4.3 偶尔丢字节阻塞发送和接收缓冲区溢出现象大部分数据对偶尔少一两个字节或整条丢失。原因STM32 用阻塞发送时如果同时有接收任务CPU 被占住接收寄存器溢出或者 Arduino 侧loop里处理太慢接收缓冲区默认 64 字节被填满后新数据覆盖旧数据。解决STM32 侧改用中断或 DMA 收发Arduino 侧缩短单次处理时间或者把Serial.setTimeout调小、及时读走数据。发送长数据时加帧头帧尾和校验接收端按帧解析别指望一次read拿全。4.4 上电瞬间收到一个错误字节现象每次上电接收端都会先收到一个 0x00 或 0xFF。原因上电瞬间引脚电平未稳定TX 线被拉低一下接收端把它当成起始位采到一个错误字节。解决初始化时先把 TX 引脚设为高电平或复用推挽输出并置高再切到 USART 功能接收端在正式解析前先丢弃第一帧或者用帧头同步收到合法帧头才开始组包。4.5 长线通信不稳定地电位差和干扰现象短线一切正常线一拉长到一两米就时好时坏。原因两地电位不完全相等加上长线引入的干扰采样点漂移。解决缩短线长用双绞线并把 GND 和信号线绞在一起降低波特率在接收端加适当的上拉或下拉条件允许时改用差分信号方案。普通杜邦线超过半米就要警惕别用「刚才还好好的」来判断稳定性。5. 进阶把 USART 通信做成可靠协议5.1 从裸字节到带帧头帧尾的协议裸发字符串只适合调试。真正要稳定得定义帧结构。我一般用这样的格式帧头0xAA 0x55、长度字节、命令字、数据区、校验和。接收端状态机逐字节扫描遇到帧头才开始收收到长度后按长度取数据最后校验。这样即使中间丢了字节也能在下一个帧头重新同步不会一直错位。// 简易接收状态机STM32 侧放在 USART 中断里逐字节喂入 typedef enum { WAIT_H1, WAIT_H2, WAIT_LEN, WAIT_CMD, WAIT_DATA, WAIT_SUM } RxState; RxState state WAIT_H1; uint8_t buf[64]; uint8_t len, idx, cmd, sum; void RxByte(uint8_t b) { switch (state) { case WAIT_H1: if (b 0xAA) state WAIT_H2; break; case WAIT_H2: state (b 0x55) ? WAIT_LEN : WAIT_H1; // 第二个头不对就重来 break; case WAIT_LEN: len b; idx 0; sum b; state WAIT_CMD; break; case WAIT_CMD: cmd b; sum b; state WAIT_DATA; break; case WAIT_DATA: if (idx len) { buf[idx] b; sum b; } if (idx len) state WAIT_SUM; break; case WAIT_SUM: if (b sum) { /* 校验通过处理 cmd 和 buf */ } state WAIT_H1; // 无论对错都回到找帧头 break; } }状态机的核心是「任何异常都回到找帧头」这样一次错位不会拖垮后续所有帧。sum用累加和是最简单的校验要求高可以换 CRC8 或 CRC16。len要限制上限防止越界写buf。5.2 用超时和重发兜住偶发丢包再稳的物理层也会偶发丢字节。协议层加一个确认和重发机制发送方发完一帧启动定时器规定时间内没收到 ACK 就重发重发超过三次报错。接收方校验通过后回一帧 ACK校验失败就丢弃、不回。这样上层业务不用关心底层丢没丢只管调发送接口。超时时间怎么定按一帧的传输时间乘 2 到 3 倍。9600 波特率下一个字节约 1ms20 字节的帧约 20ms超时设 60ms 比较合适。设太短会误重发设太长丢包后恢复慢。这个值要按实际帧长和波特率算别照抄。5.3 验证方法用逻辑分析仪看时序用压力测试看稳定性调通之后别急着收工。拿逻辑分析仪抓一段 TX 和 RX 的波形确认起始位、数据位、停止位的宽度和预期一致确认没有毛刺。然后做压力测试连续发一万帧统计丢帧率和误码率。我一般会写个小脚本让 STM32 循环发递增序号Arduino 侧记录收到的序号最后统计缺了哪些。丢帧率在千分之一以内算可用超过就要回去查线、查波特率、查缓冲区。注意压力测试时把串口监视器关掉它本身会拖慢 Arduino 的接收速度导致误判。5.4 一个我常犯的错忽略接收缓冲区的实际大小早期做 stm32_arduino_usart通信时我总以为「发了就一定能收到」直到有一次 Arduino 侧在处理一个耗时任务接收缓冲区 64 字节被填满后面的数据全丢了而 STM32 侧还在傻发。后来养成习惯发送前先确认对方有空或者协议里加流控字段接收方忙就回一个「稍等」发送方降速。这个习惯比任何调试技巧都管用。缓冲区大小、处理耗时、发送速率这三者要一起算别只盯着波特率。希望帮到你。本文还有配套的精品资源点击获取