资讯动态

STM32与Arduino USART串口通信实战:接线、配置与避坑指南

发布时间:2026/10/14 18:10:13 来源:尧图企业网站定制
简介这份资源面向嵌入式初学者与跨平台串口通信实践者聚焦STM32F103ZET6与Arduino之间USART通信的完整实现解决两者供电电压不同3.3V与5V时的数据交换与电平适配问题。压缩包共77个文件约278KB以35个.h头文件与34个.c源文件为主体涵盖USART初始化、中断收发、SysTick延时、命令解析等模块另含uvproj工程文件、map映射表与lst列表文件便于直接编译与调试。资源中给出了STM32端与Arduino端的固件代码框架涉及波特率、数据位、停止位、奇偶校验及硬件握手等关键参数配置并包含电平转换与信号线连接的调试思路可帮助读者理解跨电压通信的常见故障排查方法。目前已有2261人学习下载适合作为STM32与Arduino串口联调的参考范例。1. stm32_arduino_usart通信两块板子怎么把一句话说清楚手头同时有 STM32 和 Arduino 的人早晚会撞上同一个需求让这两块板子互相说句话。可能是 STM32 负责采集传感器数据Arduino 负责驱动一块屏幕也可能是 Arduino 接了一堆现成模块想把数据甩给 STM32 做更重的运算。这时候绕不开的就是 USART 串口通信。它不挑线、不挑协议栈、两根信号线加一根地线就能跑是嵌入式里最省心的板间对话方式。但真动手就会发现翻车点不在“接线”而在两边的时钟、电平、帧格式、收发时序对不上。STM32 的 USART 配置项多Arduino 的Serial封装得又太舒服很多人第一次连就是一堆乱码或者干脆收不到。这篇就把 stm32_arduino_usart通信 从选型、接线、参数、代码到排错讲透适合刚上手双板协作的新手也适合想确认边界参数的老手。2. 先把 USART 的物理层和帧格式对齐为什么你的第一根线大概率接错USART 通信能不能通八成取决于接线和参数而不是代码写得多漂亮。这一章先把物理层和帧格式这两件“地基”讲清楚后面写代码才有意义。2.1 交叉接线TX 接 RX 不是口诀是电气约束USART 是全双工异步串口发送端和接收端是两套独立电路。A 板的发送引脚 TX 输出电平必须进到 B 板的接收引脚 RX反之亦然。所以 STM32 的 TX 接 Arduino 的 RXSTM32 的 RX 接 Arduino 的 TX这就是常说的交叉接线。很多人第一次接成 TX 对 TX、RX 对 RX然后盯着代码找 bug找了半天。血泪经验是接完线先用万用表确认一下或者干脆先只接一根 TX→RX用单向发送验证通了再接另一根。还有一点容易被忽略GND 必须共地。两块板子如果各自用不同的电源没有公共参考地电平判断就没有基准收到的就是玄学乱码。共地是底线不是可选项。2.2 电平匹配3.3V 和 5V 之间那点事STM32 的 IO 通常是 3.3V 电平Arduino 这边分两种经典 Uno、Nano 是 5V 电平而 3.3V 版本的板子是 3.3V。这就带来一个方向性问题。STM32 的 TX3.3V接到 5V Arduino 的 RX一般没问题因为 5V 板子的高电平判定门限通常在 0.6×VCC 左右3.3V 能被识别为高。但反过来5V Arduino 的 TX 直接进 STM32 的 RX5V 打到 3.3V 引脚上长期看是伤芯片的。稳妥做法是加一个电平转换或者至少串一个分压电阻网络。发送方接收方是否直连建议STM32 3.3V TXArduino 5V RX可直连观察稳定性Arduino 5V TXSTM32 3.3V RX不建议加分压或电平转换STM32 3.3V TXArduino 3.3V RX可直连最省心任意 TX任意 RX必须共地GND 对接提示不确定板子电平的时候先查原理图或丝印别凭感觉。烧一个 RX 引脚的成本远高于查一次资料。2.3 帧格式三件套波特率、数据位、校验位异步串口没有时钟线靠双方约定好的波特率来采样。波特率对不上收到的就是乱码这是最常见的“通信失败”。除了波特率还有数据位、停止位、校验位合起来叫帧格式。常见配置是 8 数据位、1 停止位、无校验简称 8N1。STM32 和 Arduino 默认都支持这套。只要两边都设成 8N1 加同一个波特率帧格式就对齐了。波特率常用 9600、115200短距离板间通信 115200 完全够用也更快。需要留意的是STM32 的 USART 波特率是从总线时钟分频出来的如果时钟树配错实际波特率会有偏差。偏差超过 2%~3%通信就开始不稳。Arduino 这边晶振比较准所以问题通常出在 STM32 侧。3. STM32 侧 USART 配置从时钟树到中断收发的最小可用代码STM32 的 USART 配置比 Arduino 复杂但复杂换来的是可控。这一章用 HAL 库走一遍最小可用流程把关键参数和中断收发讲清楚。3.1 时钟树先配对否则波特率全是错的在 CubeMX 里配 USART 之前先把时钟树确认好。假设系统时钟 72MHzUSART1 挂在 APB2 上那 USART1 的时钟就是 72MHz。HAL 库会根据这个时钟和你要的波特率自动算分频系数。如果时钟树配错比如实际 APB2 是 36MHz但你以为是 72MHz那算出来的波特率就是目标值的一半通信必挂。所以配置顺序是先时钟树再外设。/* USART1 初始化8N1115200收发模式 */ UART_HandleTypeDef huart1; void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; // 波特率两边必须一致 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; if (HAL_UART_Init(huart1) ! HAL_OK) { Error_Handler(); // 初始化失败检查时钟和引脚 } }这段代码里BaudRate、WordLength、StopBits、Parity四项就是帧格式必须和 Arduino 侧完全一致。Mode设成收发都开HwFlowCtl在只接两根线的情况下必须关掉否则会等一个永远不来的流控信号。3.2 中断接收别用轮询会丢数据发送可以用阻塞式HAL_UART_Transmit简单直接。但接收如果也用轮询主循环一忙就会漏掉字节。板间通信推荐用中断接收每来一个字节进一次中断把数据存进缓冲区。#define RX_BUF_SIZE 64 uint8_t rx_byte; // 单字节接收缓存 uint8_t rx_buffer[RX_BUF_SIZE]; // 环形或线性缓冲区 volatile uint16_t rx_index 0; /* 在 main 初始化后启动一次中断接收 */ HAL_UART_Receive_IT(huart1, rx_byte, 1); /* 接收完成回调每收到一个字节触发一次 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { if (rx_index RX_BUF_SIZE - 1) { rx_buffer[rx_index] rx_byte; // 存入缓冲区 } HAL_UART_Receive_IT(huart1, rx_byte, 1); // 重新武装接收 } }关键点是回调最后必须再次调用HAL_UART_Receive_IT否则只收一个字节就停了。这是新手最常踩的坑之一。rx_index用volatile修饰因为它在中断和主循环之间共享。3.3 发送与协议约定加个帧头帧尾更稳裸发字节在短距离下能用但一旦有干扰或者接收方启动晚了一点就会错位。常见做法是加一个简单帧结构比如帧头 0xAA、长度、数据、校验和。/* 发送一帧帧头 长度 数据 校验和 */ void USART1_SendFrame(uint8_t *data, uint8_t len) { uint8_t checksum 0; uint8_t header 0xAA; HAL_UART_Transmit(huart1, header, 1, 100); // 帧头 HAL_UART_Transmit(huart1, len, 1, 100); // 长度 HAL_UART_Transmit(huart1, data, len, 100); // 数据 for (uint8_t i 0; i len; i) { checksum data[i]; // 累加校验 } HAL_UART_Transmit(huart1, checksum, 1, 100); // 校验和 }HAL_UART_Transmit的最后一个参数是超时时间单位毫秒。115200 波特率下发几十字节很快100ms 足够。校验和用最简单的累加够用且好实现。接收方按同样的规则解析遇到帧头才开始收校验不过就丢弃整帧。4. Arduino 侧对接Serial 的默认行为和你想的不一样Arduino 的Serial用起来简单但它的默认行为和 STM32 的 USART 有些差异不注意就会对不上。4.1 波特率设置与 begin 的时机Arduino 用Serial.begin(115200)设置波特率。注意这个调用要放在setup()里而且越早越好。如果接收方启动晚发送方已经开始发数据前面的字节就丢了。稳妥做法是两边上电后都先延时一小段或者发送方等收到对方的握手信号再开始发。void setup() { Serial.begin(115200); // 必须和 STM32 侧一致 while (!Serial) { } // 部分板子等串口就绪Uno 上可省略 delay(500); // 给对面留启动时间避免丢首帧 } void loop() { // 发送示例 Serial.write(0xAA); // 帧头 Serial.write(0x02); // 长度 Serial.write(0x01); // 数据1 Serial.write(0x02); // 数据2 Serial.write(0x03); // 校验和 0x01 0x02 delay(1000); }Serial.write发的是原始字节Serial.print发的是 ASCII 文本。板间通信建议统一用write避免数字被转成字符串带来的歧义。比如Serial.print(100)发出去的是字符 1、0、0 三个字节而Serial.write(100)发的是一个字节 0x64。4.2 接收解析按帧头同步别假设对齐Arduino 接收同样建议按帧解析。用一个状态机先找帧头再读长度再读数据和校验。#define BUF_SIZE 64 uint8_t buf[BUF_SIZE]; uint8_t idx 0; uint8_t state 0; // 0找帧头 1读长度 2读数据 3读校验 uint8_t len 0; uint8_t sum 0; void loop() { while (Serial.available()) { uint8_t b Serial.read(); switch (state) { case 0: if (b 0xAA) state 1; // 等到帧头才开始 break; case 1: len b; if (len BUF_SIZE) { state 0; break; } // 长度异常复位 idx 0; sum 0; state 2; break; case 2: buf[idx] b; sum b; if (idx len) state 3; break; case 3: if (b sum) { /* 校验通过处理 buf */ } state 0; // 无论对错都复位等下一帧 break; } } }状态机的好处是即使中间丢了几个字节也能靠帧头重新同步不会一直错位下去。len BUF_SIZE这个判断是防越界必须加。4.3 软串口备用方案当硬件串口被占用时有些 Arduino 板子只有一个硬件串口还被 USB 调试占着。这时候可以用SoftwareSerial在普通 IO 上模拟一个串口。#include SoftwareSerial.h SoftwareSerial mySerial(10, 11); // RX10, TX11 void setup() { Serial.begin(115200); // USB 调试 mySerial.begin(115200); // 和 STM32 通信 }软串口在 115200 下稳定性一般建议降到 9600 或 19200。而且软串口接收时会占用 CPU高波特率下容易丢字节。能用硬件串口就别用软的。5. 避坑与排查通信不通时按这个顺序查通信出问题别一上来就改代码。按物理层到协议层的顺序排查效率最高。现象一完全收不到任何数据。原因通常是接线错或没共地。先确认 TX→RX 交叉再确认 GND 对接。用示波器或逻辑分析仪看发送引脚有没有波形没有就是发送方没发出来有就是接收方没收到。现象二收到一堆乱码。原因基本是波特率不一致或者 STM32 时钟树配错导致实际波特率偏差。先核对两边begin和BaudRate的值再检查 STM32 的系统时钟和 APB 时钟。用示波器量一个字节的位宽反推实际波特率一比就知道差多少。现象三能收到但偶尔丢字节。原因是接收方处理太慢缓冲区溢出。STM32 侧检查是否用了中断接收并重新武装Arduino 侧检查loop里有没有delay阻塞太久。把接收缓冲区加大或者提高处理速度。现象四第一帧总是丢。原因是接收方启动晚于发送方。解决方法是发送方上电后延时或者加一个握手流程接收方就绪后先发一个字节通知发送方开始。现象五通信一段时间后死掉。原因可能是中断里卡死或者缓冲区索引越界。检查HAL_UART_RxCpltCallback里有没有阻塞操作检查rx_index和idx的边界判断。加一个看门狗超时复位是最后的后悔药。注意排查时一次只改一个变量。同时改波特率和接线通了也不知道是哪个起的作用。6. 进阶技巧用 DMA 加空闲中断做不定长接收前面用中断逐字节接收在 115200 下够用但如果数据量大或者主循环忙逐字节中断的 CPU 开销就上来了。更优雅的做法是 DMA 加空闲中断DMA 负责把数据搬进缓冲区空闲中断负责判断一帧结束。STM32 的 USART 支持空闲线检测总线在一帧结束后会空闲一个字节时间触发 IDLE 中断。配合 DMA 的循环或普通模式就能实现不定长接收。#define DMA_RX_SIZE 128 uint8_t dma_rx_buf[DMA_RX_SIZE]; /* 启动 DMA 接收 空闲中断 */ void USART1_StartDMAReceive(void) { __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 开空闲中断 HAL_UART_Receive_DMA(huart1, dma_rx_buf, DMA_RX_SIZE); } /* 在 USART1_IRQHandler 里处理空闲中断 */ void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); uint16_t recv_len DMA_RX_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx); /* recv_len 就是这一帧的字节数处理 dma_rx_buf 前 recv_len 个字节 */ HAL_UART_DMAStop(huart1); USART1_StartDMAReceive(); // 重新启动准备下一帧 } HAL_UART_IRQHandler(huart1); }__HAL_DMA_GET_COUNTER返回 DMA 还剩多少没搬总数减去剩余就是已接收长度。这个方案的好处是 CPU 几乎不参与搬运只在帧结束时处理一次。适合数据量较大或者对实时性有要求的场景。验证方法很简单让 Arduino 每隔 100ms 发一帧长度递增的数据STM32 侧打印recv_len看是否和发送长度一致。如果长度总是 DMA_RX_SIZE说明空闲中断没触发检查 IDLE 标志清除顺序如果长度偶尔偏大说明两帧粘在一起了需要在协议层用帧头重新切分。我自己调这个方案时最大的教训是空闲中断里必须先清标志再停 DMA顺序反了会丢帧。这个顺序没有文档明说是踩出来的。另外 DMA 缓冲区别开太小128 字节在 115200 下大约 11ms 就填满如果主循环处理慢建议开到 256 或 512。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑