资讯动态

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

发布时间:2026/10/3 1:02:47 来源:尧图企业网站定制
搞过STM32的都知道串口这玩意儿是入门的第一道坎也是往后几年打交道最多的外设。不管是调试打印、和传感器模块通信还是板间数据传输USART基本都绕不开。我见过不少朋友在GPIO点上跑马灯玩得很溜一到USART收发数据就翻车要么乱码要么收不到要么进中断就卡死。这篇文章就是把我自己踩过的坑和验证过的方案整理一遍从外设概念讲到代码落地最后附一份排查清单适合刚把USART用起来的新手也适合经常在串口调试上浪费时间的同学当速查手册。1. 串口基础认知USART与UART、外设架构1.1 USART和UART到底差在哪很多初学者对USART和UART这两个缩写一头雾水实际上STM32芯片上几乎都集成了USART全称是Universal Synchronous/Asynchronous Receiver/Transmitter翻译过来就是通用同步/异步收发器。UARTUniversal 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默认在PA9USART1_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总线上典型频率72MHzUSART2/3挂在APB1上典型频率36MHz。F4系列USART1也在APB2上通常84MHzUSART2/3在APB1上通常42MHz。举个例子F103的USART1跑115200波特率USARTDIV 72MHz / (16 × 115200) 39.0625。BRR寄存器整数部分写390x27小数部分0.0625乘以16等于1最终BRR写入0x271。如果波特率除不尽怎么办实际工作中我们不会手算BRR但碰到乱码要能判断是不是误差太大。USART的容错能力大致在正负2%以内你算出的小数部分四舍五入到4位误差不超过半个量化单位一般都能正常工作。比如36MHz下跑115200USARTDIV19.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、NEUSART_SR寄存器里除了TXE/RXNE/TC还有ORE溢出错误、FE帧错误、NE噪声错误三个标志。ORE是最常见的元凶接收数据到达时DR里的数据还没被读走新数据一来就溢出置位硬件会停止接收如果中断里没清掉这个标志接收功能会一直处于异常状态。所以在接收中断里要养成习惯发生ORE就先读SR再读DR把错误状态清掉。FE帧错误通常是波特率不匹配或者信号质量差导致的检测到停止位电平不对接收数据仍然会放进DR但会被标记为错误。NE噪声错误同理主要来自电磁干扰或者地电平不稳定。如果现场环境恶劣我一般会在代码里定期检查这三个标志一旦出现就做串口重新初始化或者丢弃当前缓冲区数据防止错误数据进入协议解析层。很多人在开发板上跑串口从没遇过这些问题一上真实设备就各种诡异现象其实就是缺了这一步错误处理。5.3 TXE和TC的区别以及DMA发送的坑发送端也有一个高频困惑点到底该查TXE还是TCTXE表示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做自动化收发测试这样能模拟高频连续数据更容易暴露丢包和时序问题。工程上稳妥的串口程序标志位检查、错误恢复、超时机制一样都不能少代码看起来多但都是保命的。

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

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

免费获取报价 →
↑