news 2026/10/3 1:02:42

STM32串口USART实战:从基础配置到中断接收的避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32串口USART实战:从基础配置到中断接收的避坑指南

搞过STM32的都知道,串口这玩意儿是入门的第一道坎,也是往后几年打交道最多的外设。不管是调试打印、和传感器模块通信,还是板间数据传输,USART基本都绕不开。我见过不少朋友在GPIO点上跑马灯玩得很溜,一到USART收发数据就翻车:要么乱码,要么收不到,要么进中断就卡死。这篇文章就是把我自己踩过的坑和验证过的方案整理一遍,从外设概念讲到代码落地,最后附一份排查清单,适合刚把USART用起来的新手,也适合经常在串口调试上浪费时间的同学当速查手册。

1. 串口基础认知:USART与UART、外设架构

1.1 USART和UART到底差在哪

很多初学者对USART和UART这两个缩写一头雾水,实际上STM32芯片上几乎都集成了USART,全称是Universal Synchronous/Asynchronous Receiver/Transmitter,翻译过来就是通用同步/异步收发器。UART(Universal Asynchronous Receiver/Transmitter)则只支持异步模式。两者最大的区别是USART多了一个同步时钟输出能力,可以在特定协议(比如智能卡接口、红外通信、LIN总线)下输出SCLK时钟信号,而UART没有。日常做调试串口、蓝牙模块、GPS模块、TTL电平传感器交互时,我们都在用它们的异步模式,所以你可以把USART当成一个功能更强的UART来用。

这里我还是要多说一句:USART、UART、SPI、I2C这四类总线,很多人会放在一起对比。USART/UART擅长点对点或简单的多点通信,帧格式灵活,波特率可调;SPI是主从环形结构,速度快、全双工、四根线;I2C是开漏加外部上拉,支持多主机总线仲裁,设备挂一条线上靠地址区分。实际选型时我的习惯很简单:芯片间近距离高速传数据选SPI,多设备低速挂总线选I2C,跟PC、无线模块、工控设备打交道选USART。串口的优势是通用性极强,几乎所有单片机、电脑、嵌入式Linux板卡都留了串口调试通路,调试信息往串口一丢,问题定位会快很多。

1.2 STM32串口外设的整体架构

刚接触STM32时,我把USART当成一个黑盒子,直到有一次排查数据错位问题,才静下心把参考手册的框图啃了一遍。USART模块主要由波特率发生器、发送移位寄存器、接收移位寄存器、控制状态寄存器和数据寄存器DR组成。我们程序里写一个字节到USART_DR,硬件会自动把这个字节从发送移位寄存器按位挪到TX引脚上,按配置好的波特率一位一位发出去;接收方向正好相反,RX引脚上的电平按位组合成一个字节,硬件置位RXNE标志,告诉我们"DR里的数据可以读了"。

理解这套流程对后面写中断非常重要。TXE标志表示发送数据寄存器空,也就是DR可以写入下一个字节;TC表示整个帧(含停止位)已经全部送出去。RXNE标志表示接收数据寄存器非空,有数据到了。这三个标志是串口编程的第一批"好朋友",后面所有收发逻辑都围绕它们转。另外,STM32的USART没有硬件FIFO,只有一个字节的DR缓冲,数据进来后你不及时读走,下一个字节一来就会覆盖掉,所以收发时序设计必须心里有数。

2. 环境准备与初始化:引脚、时钟、代码

2.1 引脚复用和AFIO的坑

初始化USART第一步不是写代码,而是翻数据手册确认引脚。以最常用的STM32F103为例,USART1_TX默认在PA9,USART1_RX默认在PA10,这两个引脚默认功能是GPIO,必须通过AFIO重映射和GPIO配置复用为串口功能。很多新手会栽在这个环节:明明串口配置全对,就是不工作,检查下来发现GPIO模式设成了普通推挽输出,而不是复用推挽。F4系列更细一点,每个引脚对应一组AF编号,比如PA9的AF7是USART1_TX,配置时要把GPIO_AF选择为GPIO_AF7_USART1,选错AF编号同样不工作。

F1系列还有一个隐蔽的坑:当你想重映射USART1到PB6/PB7这类非默认引脚时,必须开启AFIO时钟并调用GPIO_PinRemapConfig,否则引脚根本不会切换过去。就算用默认引脚PA9/PA10,只要工程里其他地方用到了重映射功能(比如JTAG禁用、TIM2重映射),AFIO时钟也建议开启。时钟树和复用关系这两块内容,不要凭印象,直接看参考手册的Alternate function mapping表最靠谱。初始化代码里把RCC、GPIO、AFIO、USART四者的时钟全部使能,能避免七八成底层怪问题。

2.2 标准库初始化代码:从GPIO到USART

我早期用的标准库,现在用HAL的也大有人在,但核心配置流程是一样的。下面是F103的USART1初始化模板:

void uart1_init(uint32_t baud) { GPIO_InitTypeDef gpio; USART_InitTypeDef usart; NVIC_InitTypeDef nvic; // 1. 开启时钟:GPIOA、AFIO、USART1全部使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO | RCC_APB2Periph_USART1, ENABLE); // 2. 配置TX(PB6/PA9)为复用推挽,RX(PA10)为浮空输入 gpio.GPIO_Pin = GPIO_Pin_9; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_10; gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); // 3. 串口参数:波特率、8位数据、无校验、1停止位 usart.USART_BaudRate = baud; usart.USART_WordLength = USART_WordLength_8b; usart.USART_StopBits = USART_StopBits_1; usart.USART_Parity = USART_Parity_No; usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None; usart.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &usart); // 4. 使能串口 USART_Cmd(USART1, ENABLE); }

注意几个细节:TX是STM32输出给外部设备,要配成复用推挽;RX是外部信号输入,F1配成浮空输入即可,F4之后可以配成复用开漏加内部上拉,或者干脆用GPIO_PinAFConfig。串口参数里8位数据、无校验、1停止位是业界最常用的8N1格式,PC端串口助手默认也按这个来,双方不一致就会出现乱码。如果你要用校验位,数据位就要相应调整成7位或9位,这个细节很容易搞混。

2.3 波特率怎么算出来的

波特率不是随便填的,它由USARTDIV决定,核心公式是:USARTDIV = PCLK / (16 × 波特率)。F103的USART1挂在APB2总线上,典型频率72MHz;USART2/3挂在APB1上,典型频率36MHz。F4系列USART1也在APB2上(通常84MHz),USART2/3在APB1上(通常42MHz)。举个例子,F103的USART1跑115200波特率,USARTDIV = 72MHz / (16 × 115200) = 39.0625。BRR寄存器整数部分写39(0x27),小数部分0.0625乘以16等于1,最终BRR写入0x271。

如果波特率除不尽怎么办?实际工作中我们不会手算BRR,但碰到乱码要能判断是不是误差太大。USART的容错能力大致在正负2%以内,你算出的小数部分四舍五入到4位,误差不超过半个量化单位,一般都能正常工作。比如36MHz下跑115200,USARTDIV=19.53,取19后误差约1.4%,实测多数STM32能扛住,但恶劣环境或者对端设备要求严格时,最好凑一个能整除的频率,比如把串口时钟配到36.864MHz或把波特率选成精确的128000。现在CubeMX会自动算好这些,但不妨碍我们理解原理,排查问题的时候,先怀疑波特率,再怀疑接线,这个顺序能省很多时间。

3. 查询模式收发:第一版能跑的代码

3.1 轮询发送:一个字节和字符串

查询模式是最直观的收发方式。发送一个字节,思路就是"等TXE标志变为1,写入DR"。TXE的意思是DR已经空了,可以接受新数据,它比TC更早置位,所以轮询TXE发送效率更高。很多教科书示例会直接用TC,也能跑,但会白白多等一个停止位的时间。我的封装如下:

void uart1_send_byte(uint8_t ch) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, ch); } void uart1_send_string(uint8_t *str) { while (*str) { uart1_send_byte(*str++); } }

字符串发送这里有个小坑:不要把结尾的'\0'也发出去。上面这个写法遇到'\0'就停,正好满足多数场景。如果你要发送的是二进制数据,可能本身包含0x00,那就不能用字符串函数,要传入长度参数,按长度循环发送。发送长数据时还有个现实问题:对方接收缓冲区可能不够大,发太快会丢数据。我们自己在STM32上做串口转发时,会按对方能力加上简单的流控,比如RTS/CTS硬件流控,或者协议层做一个"收到再发下一帧"的握手,这样虽然慢一点,但不会丢。

3.2 轮询接收:读一个字节

接收同样可以用轮询方式。检测RXNE标志,为1说明DR里有数据,调用USART_ReceiveData读出来。这里顺序不能反过来,必须先读SR再读DR,如果直接读DR,标志位清理的时机不对,下一帧数据来了还可能读到同一个字节。我的实现:

uint8_t uart1_recv_byte(uint8_t *ch) { if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET) { *ch = (uint8_t)USART_ReceiveData(USART1); return 1; } return 0; }

轮询接收的致命弱点是:主循环忙其他事的时候,串口数据悄悄来了,DR里的数据没被及时读走,下一个字节一到就覆盖了,导致丢包。而且一旦发生溢出,ORE标志会置位,之后的数据接收都会受影响。所以轮询模式只适合演示或者数据量极小的项目,真正上工程必须用中断或者DMA。后面我会详细讲中断方案,那才是实际项目该用的方式。

3.3 一个自收发测试:验证工程和硬件通路

写完全部初始化代码,别急着搞复杂逻辑,先做一个最简单的回环测试。把STM32的TX引脚和RX引脚用杜邦线短接,或者直接用带回环功能的调试器/串口助手,发送一串字符,看板子是否原样返回。这一步通过,说明时钟、GPIO、串口参数、波特率全链路没问题,后面再加业务逻辑才有意义。

调试助手的参数要仔细核对:波特率115200,数据位8,停止位1,无校验,无流控。如果收到的全是乱码,优先检查波特率是否一致,然后用示波器或者逻辑分析仪看TX引脚的波形,量一下实际波特率,这是最直接的排查手段。没有逻辑分析仪的话,也可以用串口助手连续发0x55(二进制01010101),在示波器上数脉冲宽度,能估算实际波特率,这个方法我试过很多次,简单有效。

4. 中断接收与帧协议:实际项目能用的收发架构

4.1 为什么接收必须用中断

实际项目里,主循环要跑按键扫描、OLED刷新、电机控制、传感器采集等等,如果串口接收还用轮询,任何一个阻塞延时都会造成数据丢失。串口的数据什么时候来,我们无法预测,所以接收端必须让硬件在数据到达时主动通知CPU,这就是中断存在的意义。发送方向倒是可以视场景选择:低速调试数据用查询就够了,大流量数据传输建议也用中断或者DMA,否则CPU会一直卡在while循环里。

很多初学者在中断服务函数里写复杂代码,比如直接在中断里做协议解析、处理业务逻辑,这是非常糟糕的习惯。中断里只应该做"把数据搬走"这类极短操作,耗时操作放回主循环。原因很简单:中断优先级过高会抢占主循环,长时间停在中断里,其他任务(比如定时器中断、实时性要求高的外设)全部会被饿死。我自己吃过的亏是,在USART中断里跑了一个半秒的flash擦除,结果系统直接"假死",排查半天才意识到是中断服务工作太长了。

4.2 环形缓冲区:中断和数据之间的桥梁

中断来了,把字节读出来放在哪?最自然的答案是环形缓冲区。环形缓冲用数组加两个下标实现:写下标由中断服务函数维护,读下标由主循环维护,读写互不干扰。数组长度要根据数据流量定,我一般起步用256字节,高频场景用1024或更大,满了之后可以选择丢弃新数据或者丢旧数据,实际项目里我习惯丢弃新数据、保留旧数据,因为协议帧解析通常对连续性要求高,老数据还没被消费,新数据来了不一定重要。

#define RX_BUF_SIZE 256 volatile uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_write = 0; volatile uint16_t rx_read = 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint16_t next = (rx_write + 1) % RX_BUF_SIZE; if (next != rx_read) { rx_buf[rx_write] = (uint8_t)USART_ReceiveData(USART1); rx_write = next; } else { // 缓冲区满,读走数据并丢弃 (void)USART_ReceiveData(USART1); } } }

主循环里判断rx_read != rx_write,用同样的下标去取数据。这里有个老生常谈的细节:中断服务函数里用了环形缓冲,要保证rx_buf和下标在极端情况下不同时被主循环和中断写,所以主循环读数据时可以先关中断,或者用Cortex-M的排他指令,简单项目里先关中断最稳妥,在临界区操作不超过几十个周期,影响可以忽略。

4.3 数据包结构:串口天生没有帧边界

串口协议本身是一串字节流,它不像以太网数据包自带长度,单片机收到数据后必须自己判断"这一帧从哪开始,到哪结束"。最基本的做法是固定帧长:约定每帧6个字节,收到6个字节就算一帧,解析完再等下一帧。但实际项目里数据长度经常变化,所以更通用的方案是帧头+长度+数据+校验,比如0xAA 0x55 LEN DATA... CRC16。接收端先找帧头,再根据LEN字段确定后续字节数,凑够长度后做校验,校验通过再交给业务层。

帧头选择要注意避免和数据内容冲突。比如你的数据里也可能出现0xAA 0x55,接收端就会误判,解决思路有两种:一是把数据做转义处理,类似PPP协议的0x7E转义;二是选择更长的帧头,比如4字节魔数,降低冲突概率。实际做通信模块时,我最常用的是超时断帧方案:收到一个字节后,如果超过一定时间(通常3到5个字符间隔)没有后续数据,就认为当前帧结束。这个"超时"可以靠定时器实现,也可以用STM32的IDLE空闲中断。

IDLE中断是接收串口数据非常省心的特性。总线上一个字节之后空闲下来,硬件会置IDLE标志,我们在这个中断里把已有的缓冲区当成一帧完整数据处理。配置IDLE中断的代码不复杂,但要注意清标志的姿势:先读SR再读DR,顺序错了标志清不掉,中断会一直进来。HAL库的版本在HAL_UART_IRQHandler回调里也带空闲检测,配合DMA用非常舒服。

5. 实战避坑清单:串口问题排查实录

5.1 乱码、丢字节、卡死,一眼定位

工作时间长了,我养成了"串口有问题先排查底层、再排查协议"的习惯。下面是这几年遇到最多的几个问题:

现象可能原因排查和解决办法
全是乱码波特率不一致、时钟频率配错核对两个设备波特率,检查RCC时钟树,用示波器量实际波特率
偶尔丢字节接收中断优先级低、缓冲太小、有长临界区提高串口中断优先级,加大环形缓冲,缩短关中断段耗时
收一帧就卡死ORE溢出标志未处理、IDLE中断未清标志读SR再读DR清标志,或者检查中断里是否有死循环等待
引脚电平不对复用功能配置错误、AFIO未开对照手册重新配置GPIO模式和AF编号,确保AFIO时钟使能
TX自发自收正常,接设备不通共地问题、TX/RX接反确认两个设备GND连一起,检查TX对RX的接线方向
程序下载后串口没反应时钟没配、复位后引脚被复用确认RCC使能USART时钟,确认GPIO没被其他外设抢占配置

这里要特别说一个F1的老坑:AFIO时钟忘开。USART1默认引脚PA9/PA10时,很多人不开AFIO也能跑,因为默认功能刚好匹配;一旦你用重映射功能,比如把USART1重映射到PB6/PB7,不开AFIO时钟,串口就死活不工作。F4以后换成了SYSCFG时钟,思路上是一样的,建议初始化时统一把AFIO或SYSCFG时钟打开,避免今天没事、明天换个引脚就黑屏。

5.2 必须认识的三个错误标志:ORE、FE、NE

USART_SR寄存器里除了TXE/RXNE/TC,还有ORE(溢出错误)、FE(帧错误)、NE(噪声错误)三个标志。ORE是最常见的元凶:接收数据到达时DR里的数据还没被读走,新数据一来就溢出置位,硬件会停止接收,如果中断里没清掉这个标志,接收功能会一直处于异常状态。所以在接收中断里要养成习惯:发生ORE就先读SR再读DR,把错误状态清掉。

FE帧错误通常是波特率不匹配或者信号质量差导致的,检测到停止位电平不对,接收数据仍然会放进DR,但会被标记为错误。NE噪声错误同理,主要来自电磁干扰或者地电平不稳定。如果现场环境恶劣,我一般会在代码里定期检查这三个标志,一旦出现就做串口重新初始化或者丢弃当前缓冲区数据,防止错误数据进入协议解析层。很多人在开发板上跑串口从没遇过这些问题,一上真实设备就各种诡异现象,其实就是缺了这一步错误处理。

5.3 TXE和TC的区别,以及DMA发送的坑

发送端也有一个高频困惑点:到底该查TXE还是TC?TXE表示DR空,可以往DR写下一字节,这时候上一字节其实还在移位寄存器里慢慢发;TC表示整个字节包括停止位都发完了。查询发送用TXE效率高,但如果你需要确保数据全部离开引脚(比如发完立刻切换GPIO复用、立刻进入低功耗模式),就必须查TC。我自己做低功耗项目时就被坑过:程序里发完最后一帧,立刻进入STOP模式,结果数据还在移位寄存器里没发完,对端永远收不到结尾,后来改成查TC再休眠才解决。

DMA发送也有类似的坑:DMA传输完成中断表示的是"数据从内存搬到了DR",并不代表数据已经通过TX引脚全部发出去了。如果你在DMA完成中断里马上关串口或者切换引脚功能,最后几个字节照样会丢。正确做法是关DMA后等待TC标志,或者开启TC中断再处理后续动作。这一点在高速串口和bootloader升级场景尤其重要,丢一个字节都可能让固件报废。

6. 几个亲身踩过的坑,与给新手的建议

串口的坑,说到底就是时钟、引脚、标志位三件事。我刚开始做STM32项目时,有一次在工业现场调试设备,串口接上变频器怎么都通不了,数据时好时坏,排查了两天才发现是通信线缆太长、地线压差过大,加上波特率设太高,造成信号边沿变形。后来我把波特率从115200降到38400,硬件上加强共地,问题直接消失。这件事给我的教训是:串口调试不光要看代码,还要看物理链路,波特率不是越快越好,稳定压倒一切。

还有一次做Bootloader,用串口下载固件时不断失败,查了很久发现是接收中断里做帧解析,帧还没收完就开始处理,数据半包被当成完整包使用,逻辑直接错乱。后来我加上了超时断帧,等整个包收完再统一解析,问题就再没出现。如果你想做串口通信方面的项目,我建议你按这个顺序练:先用查询模式跑通回环,再改成中断加环形缓冲,再加上数据包结构和校验,最后用DMA加IDLE中断做成高吞吐方案。每一步都有清晰的验证标准,踩坑也容易定位。

最后再分享一个实用小技巧:调试串口接收时,不要只依赖串口助手的显示界面,可以在关键节点加一个Linux下用minicom或者Python pyserial做自动化收发测试,这样能模拟高频连续数据,更容易暴露丢包和时序问题。工程上稳妥的串口程序,标志位检查、错误恢复、超时机制一样都不能少,代码看起来多,但都是保命的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 1:02:42

DRV8818+PIC18LF46K22工业级步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:01:51

TM4C129L+DRV8818步进电机驱动方案:硬件、加减速与抗干扰实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:01:51

Nuttx实时操作系统核心原理与硬实时应用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:01:38

棒球赛夺冠可能性的最大流建模与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 1:00:43

别再问“AI写作软件哪个第一”了,药管专业写论文真的要分工着用 ✨

先自报家门:我关注的是食品药品与粮食大类 / 药品与医疗器械类 / 药事服务与管理专业的同学。这个专业很特别,既要懂药品、药事法规、药房流程,又要会做服务质量、患者依从性、合理用药这类研究,写毕业论文时常常不是“不会写字”…

作者头像 李华
网站建设 2026/10/3 0:39:30

全能型 AI论文工具星级排名(2026 终极指南)

基于功能完整性、学术适配性、用户反馈及操作便捷性,本文对当前主流AI论文写作工具进行深度测评,按综合使用价值从高到低进行排名,并详细解析各工具的核心优势与适用人群。🏆 第一梯队:全流程学术解决方案(…

作者头像 李华