资讯动态

STM32 USART串口通信实战:从原理到代码,带你避开所有坑

发布时间:2026/10/3 7:59:19 来源:尧图企业网站定制
聊USART这个话题对玩STM32的人来说几乎是绕不开的第一道关卡。不管是串口打印调试信息、跟传感器模块通信还是跟PC上位机对接USART都是生态最成熟、也最容易上手的外设。很多人第一个外设是点亮LED第二个就是它。这篇东西我不想写成Datasheet翻译而是想以实际跑通为主把“为什么这么配”“为什么这个值要这样算”讲清楚再给出一套可以直接抄的收发程序顺便把串口调试中最容易踩的坑都列出来。无论你是刚建好工程、准备点灯的新手还是已经会点灯、想进一步搞懂通信原理的进阶玩家这篇都适合你先收藏再看。USART这块内容看似简单实际上坑一点都不少。比如波特率误差、GPIO复用配置、中断标志位、半主机模式、DMA和中断的搭配任何一个环节没处理好现象都是同一个——串口没反应或者乱码。但反过来说USART也是投入产出比最高的外设你只要把底层逻辑吃透一次后面学I2C、SPI、CAN这类通信协议都会轻松很多。下面我按自己的理解从原理、配置、代码到排障一步步拆给你看。1. USART整体设计与思路拆解先搞清楚它在系统里的位置1.1 从UART到USART搞懂底层逻辑比抄代码重要USART的全称是Universal Synchronous/Asynchronous Receiver/Transmitter通用同步/异步收发器。STM32家族里绝大多数芯片集成的叫USART少部分叫UART。两者的差别就一个字母同步能力。USART支持同步模式多一根时钟线SCLK可以在发送数据的同时输出时钟信号用来驱动从设备UART没有这根时钟线纯异步通信。实际项目里90%的场景走的都是异步模式所以我们平时嘴里的“串口”默认识别成UART就行。异步串行通信的核心是通信双方约定同一个波特率然后一根TX发、一根RX收再共用同一个地。数据是按位bit一个个排队送的不是一次传一个字节。就好比两个人面对面聊天一句话里的字要按顺序说出来对方按同样的语速听才能完整接住。这里“语速”就是波特率一句话的起止就是起始位和停止位。从STM32系统架构的角度看USART外设挂在APB总线上。F1系列里USART1挂APB2最高72MHzUSART2和USART3挂APB1最高36MHz这一点在评估波特率误差的时候特别重要后面我会专门算给你看。外设内部有三个核心寄存器数据寄存器DR、控制寄存器CR1/CR2/CR3、状态寄存器SR。所谓“用USART收发数据”本质就是往DR写数据、从DR读数据再通过SR判断数据是否发完、是否收到。理解了这一层你再看后面的代码就会觉得所有操作都顺理成章。1.2 USART数据包结构串口传的每一帧到底长什么样很多教程上来就让你调库函数从不讲通信协议本身导致一遇到数据对不上就懵。我建议把帧结构先吃透。USART异步传输的最小单位是“帧”一帧由这些部分组成帧字段电平状态作用空闲态高电平线路空闲时保持高相当于“没有说话”起始位低电平宽度1位告诉接收方“我开始发了”从高拉低这个跳变很关键数据位8位或9位低位在前真正的数据内容一个字节按最低位先发校验位可选奇校验或偶校验用来粗略检错不是必须的停止位高电平宽度1位或2位告诉接收方“这帧结束了”这里有几个新手容易绕晕的点第一线路空闲是高电平起始位是拉低接收方正是靠这个“高到低”的跳变来对齐数据位的第二数据位是低位先发也就是说0x01实际在线上是10000000的顺序发出去的从bit0到bit7第三校验位只做校验不算在数据里如果开启了校验实际有效数据位会少一位。这些细节决定了你解析数据包的逻辑尤其是做Modbus这类协议的时候帧边界判断全靠起止位和空闲电平。1.3 为什么串口能和I2C、SPI“平起平坐”这里把热词里老问的“USART、UART、I2C、SPI区别”一起讲掉。很多人以为它们是同一种东西其实只是都属于串行通信大类而已。简单对比一下协议时钟线数据线速率典型场景UART/USART异步无时钟USART同步可带SCLKTX/RX两根一般115200bps以内常用高可到几Mbps调试打印、GPS、蓝牙、RS485I2CSCL时钟SDA数据半双工100k/400k/1M传感器、EEPROMSPISCK时钟MOSI/MISO全双工可达几十MbpsFlash、屏幕、高速ADC一句话总结USART胜在简单、成熟、线少、不需要时钟线缺点是速率和实时性一般I2C胜在连线最少但协议繁琐SPI胜在快但占用引脚多。项目里选哪个看的不是谁高级而是场景匹配。早期学习阶段USART是理解串行通信最好的切入点。2. USART核心参数与配置细节引脚、时钟、波特率三板斧2.1 引脚复用与时钟使能新手第一道坎USART要工作第一步不是写通信代码而是把引脚和时钟配好。很多新手在这里就翻车了因为串口没有任何反应检查半天代码结果是GPIO复用没配。以最常见的STM32F103C8T6为例USART1的TX在PA9RX在PA10。你要做的有三件事打开GPIOA的时钟同时打开USART1的时钟把PA9配置为复用推挽输出GPIO_Mode_AF_PP这是TX由外设控制引脚输出把PA10配置为浮空输入或上拉输入GPIO_Mode_IN_FLOATING这是RX接收外部电平。这里有个细节TX模式为什么是复用推挽而不是普通推挽输出因为普通推挽是让GPIO模块控制引脚而复用推挽是把这个引脚的输出控制权交给USART外设。如果你配置错了代码写再多也没用。RX为什么可以是浮空输入因为USART接收时会自己采样引脚电平外部设备空闲时是高电平浮空输入能直接读到状态。有些板子外部加了上拉电阻那用浮空输入没问题如果外部没有上拉建议用上拉输入更稳。另外不同STM32系列GPIO配置方式差异很大。F1用标准库GPIO_Mode_AF_PPF4/H7就要用GPIO_Mode_AF并调用GPIO_PinAFConfig指定复用功能。所以网上搜代码的时候一定要先看清楚芯片型号和库版本F1的代码直接拷到F4上跑编译能过但实际工作不正常因为引脚复用机制变了。2.2 波特率计算说人话就是“聊天语速”波特率就是每秒传多少个符号位。通信双方必须用同一个波特率否则你在这边说“你吃饭了吗”对方那边听起来就是“你吃#%”。USART的波特率不是随便设置的它由外设时钟经过分频得到。F1的公式是波特率 外设时钟 / (16 × USARTDIV)这里的USARTDIV是一个16位寄存器值整数部分12位、小数部分4位。反过来算就是USARTDIV 外设时钟 / (16 × 目标波特率)举个例子USART1挂在APB2上F103默认系统时钟72MHz那USART1外设时钟就是72MHz。如果要配置115200波特率算一下72000000 / (16 × 115200) 39.0625所以USARTDIV的整数部分是39小数部分是0.0625 × 16 1。换算成BRR寄存器的值就是0x0271其中高12位是39低4位是1。如果配置9600波特率72000000 / (16 × 9600) 468.75整数部分是4680x1D4小数部分是0.75 × 16 120xCBRR 0x1D4C。如果你用标准库或HAL库这些计算库函数内部都帮你做了你只要传入波特率数值就行。但你必须理解这个机制因为后面遇到“115200乱码但9600正常”这种诡异问题就是波特率误差在作怪。用72MHz主频跑USART19600和115200实际误差都很小几乎可以忽略但如果换到USART2/3挂在36MHz的APB1上某些波特率的误差就会变大极端情况下就会乱码。碰到乱码时第一反应应该是拿计算器算一遍误差而不是急着换芯片。2.3 三种收发方式怎么选轮询、中断、DMAUSART收发数据有三种模式很多教程会一股脑教但初学者很容易搞混。我用最直白的话区分一下轮询发送CPU死等发送寄存器空闲。代码简单逻辑清晰适合打印日志这种不频繁、不紧迫的场景。轮询接收同样死等接收标志但缺点很致命——在等待的过程中CPU什么都干不了一旦没有数据到程序就卡在循环里。所以除了极简单的Demo我基本不推荐主循环里死等接收。中断收发收到一个字节或发送寄存器空了触发中断CPU在中断里处理完马上回去干别的事。这是实际项目中最常用的方式尤其是接收。DMA收发数据搬运不经过CPU由DMA控制器直接在外设和内存之间搬。适合大数据量、高速率场景比如用串口刷固件、传输音频等。我个人建议的学习路径是先老老实实用轮询把收发跑通理解了标志位和流程然后改成中断接收因为这是实际项目使用频率最高的最后有余力再上DMA。上来就抄DMA代码遇到问题你根本不知道是DMA配置错了还是USART配置错了排查难度翻倍。3. 从零跑通USART发送与接收一份带注释的实测代码3.1 工程准备标准库、HAL库和CubeMX到底怎么选现在STM32开发环境比较多有人用标准库有人用HAL库加CubeMX图形化配置还有人直接操作寄存器。我的看法是F1系列、学习阶段标准库非常合适因为它的函数名直接对应硬件寄存器代码里能看清楚“现在操作的是哪个寄存器、触发的是什么逻辑”HAL库封装层次更高CubeMX点点点就能生成初始化代码但隐藏了很多细节一旦出问题不好排查。如果你用CubeMX生成代码的时候要注意选择正确的芯片型号热词里也有人问“cube程序更改单片机型号”。CubeMX实际上支持在工程后期通过Device Selector切换同系列的不同型号芯片但切换后要重新生成代码并检查引脚配置因为不同型号的引脚数和外设数量不一样直接替换往往会报错。标准库的话F1系列需要手动新建工程模板把启动文件、内核文件、标准外设库加进去。Keil里安装对应的芯片包是前提这里就不展开讲了网上教程很多。为了让你看清数据通路下面代码我用标准库手写以STM32F103的USART1为例亲测可跑。3.2 发送实现查询发送和printf重定向发送的核心流程是往数据寄存器DR写一个字节然后等待发送完成标志。标准库代码如下直接跑通的USART1初始化函数void USART1_Init(uint32_t baud) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 1. 打开时钟GPIOA和USART1都在APB2上 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); // 2. 配置PA9为TX复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置PA10为RX浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // 4. 配置USART参数8数据位、1停止位、无校验、无硬件流控 USART_InitStructure.USART_BaudRate baud; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); // 5. 使能USART USART_Cmd(USART1, ENABLE); }然后写一个发送单字节的函数。发送最关键的是等待TXE标志即发送数据寄存器为空。注意TXE标志不是“发送完成”而是“数据寄存器已空可以写入下一个字节”如果写太快前一个字节可能还没移出到移位寄存器数据会覆盖。void USART1_SendChar(uint8_t ch) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, ch); }再把printf重定向到串口这是串口调试最常用的手段。在Keil中你需要包含stdio.h然后重写fputc函数int fputc(int ch, FILE *f) { USART1_SendChar((uint8_t)ch); return ch; }这里有个大坑如果你在Keil里使用标准库的printf而没有勾选微库Use MicroLIBfputc里的底层实现会启动半主机模式运行时会卡死或者程序进HardFault。解决办法有两个要么在Options for Target的C/C页面勾选Use MicroLIB要么在代码里屏蔽半主机模式实现_sys_exit之类的函数。新手用微库最省事。3.3 接收实现查询、中断、空闲中断接收比发送复杂因为“对方什么时候发数据”是不可预测的。查询接收的代码长这样uint8_t USART1_ReceiveChar(void) { while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) RESET); return (uint8_t)USART_ReceiveData(USART1); }读USART_ReceiveData会自动清除RXNE标志。但这个函数放在主循环里一旦没有数据CPU就死死卡在这个等待里什么活都干不了。所以我强烈建议用中断接收。初始化里加上这行USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure);然后在中断服务函数里收数据uint8_t rx_buffer[64]; volatile uint8_t rx_index 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t ch USART_ReceiveData(USART1); if (rx_index 64) { rx_buffer[rx_index] ch; } } }注意中断服务函数里不要做复杂的事情比如解析协议、调用printf、操作大型数组。中断里应该只做“把数据快速存下来”这一步解析和处理放到主循环里。为什么因为中断服务函数执行时间过长会导致串口新到的数据来不及处理触发ORE溢出错误。我见过有人直接在中断里调用printf打印收到的数据结果数据一多就死机折腾了一整天。更进一步如果你想接收一包不定长的数据比如一个完整的AT指令或Modbus帧那就要用到空闲中断IDLE。所谓空闲中断是指接收完一帧数据后、线路空闲下来时触发的中断。利用它你可以判断“这一包数据已经收完了”。配置方式是在刚才的基础上多开一个中断USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);中断函数里这样处理void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { // 取出数据存到缓冲区 } if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { // 这里表示一帧数据收完了可以置一个标志位 frame_ready 1; // 注意IDLE标志的清除方式是先读SR再读DR USART_ReceiveData(USART1); } }这里有个最隐蔽的坑IDLE中断标志不能像RXNE那样通过读DR自动清除你需要先读SR寄存器再读DR寄存器才能把IDLE位清掉。很多人在中断里死循环、反复进入IDLE中断就是因为没清标志。搞定这一块你就能优雅地实现“不定长一包数据接收”这也是很多实际项目的雏形。3.4 实测记录USB转TTL、回环测试、逻辑分析仪看波形程序写完后最蠢的验证方法也要讲一下。你需要一个USB转TTL模块常见的CH340或者CP2102都可以。接线方式是STM32的PA9接到模块的RXPA10接到模块的TX然后把两个设备的GND接到一起。别小看共地这一点很多人串口收不到数据就是忘了共地两边参考地不一样电平判断全是乱的。注意STM32的引脚是3.3V电平USB转TTL模块的跳线帽如果选择5V供电输出那你给模块供电没问题但通信引脚的逻辑电平取决于模块设计大部分模块的串口逻辑电平是3.3V但一些老式模块是5V电平接到STM32的RX上会有风险。保险起见用支持3.3V逻辑电平的模块。接好线后打开串口调试助手波特率选115200数据位8、停止位1、无校验先发送一个字节看STM32能否收到再用STM32主动发送“Hello STM32”看PC能否收到。我建议额外做一步回环测试如果软件里把收到的数据原样发回去PC发送什么串口助手就收到什么基本可以确定USART硬件链路是通的。这里有个小技巧用十六进制显示模式来观察不要只盯ASCII因为很多数据包不是可打印字符十六进制显示才能看出真实内容。如果你手头没有真实硬件用Proteus仿真也可以完整仿真STM32的串口收发对初学者来说是低成本验证的好方法。但仿真终究是仿真时序和真实硬件有差异我建议有条件还是上板子实测。逻辑分析仪是排查串口问题的利器采样率至少选1MHz以上把TX或RX引脚夹住发送一段数据你就能在波形上清楚看到起始位、数据位、停止位还能算实际波特率是否符合预期。没有逻辑分析仪的话用示波器也够用只是触发抓取稍麻烦。4. 常见问题与排障经验合集从坑里总结的干货4.1 高频翻车现场乱码、收不到、一收就死机这里把我在各个技术社区、群里看到的以及自己踩过的问题整理成一张速查表建议直接存下来遇到问题照着排查效率至少快一倍现象最常见原因排查顺序串口完全没输出引脚复用没配、时钟没开、TX/RX接反、没共地先查硬件接线再查GPIO配置最后查时钟是否使能输出乱码波特率不一致、主频和Boot引脚配置不匹配、外部晶振频率不对核对统一波特率检查SystemInit时钟配置用逻辑分析仪量实际波形发送死循环卡住TXE标志没正确清或GPIO配置错误导致外设不工作单步调试看是否卡在while循环检查GPIO模式是否为AF_PP中断收不到数据NVIC优先级没配置、RXNE中断没使能、引脚配置成普通输入检查中断优先级分组检查USART_ITConfig是否调用一收数据就HardFault数组越界、缓冲区溢出、栈溢出检查中断里写入数组的下标是否越界降低缓冲区写速度收几帧后不再进中断ORE溢出错误没处理的话在某些库版本会产生连锁问题在中断里检查ORE标志不要积压未读数据网上有个高频问题“stm32 cfsr 为0x00008200是什么错误”这个值是SCB-CFSR寄存器的一部分0x00008200通常指向总线错误或用法错误常见诱因就是中断里数组越界写了非法地址、栈溢出或者DMA配置的地址不合法。串口中断接收时最容易触发这类问题因为你在中断里操作缓冲区一旦下标处理不严谨越界写坏栈顶程序就跑飞了。解决办法就是检查所有中断里对数组的操作给下标加边界保护。另外一个高频问题“stm32延时函数delay卡死”。这个和串口关系也很大如果delay用的是SysTick中断方式而你的串口中断优先级比SysTick低串口数据持续进入时SysTick中断得不到响应delay就永远等不到中断标志表现为“程序卡死”。有些新手误以为是串口冲突其实只是中断优先级没配好。合理的做法是把SysTick或者其他时间基准的优先级设得比串口中断低或者不要在串口中断服务函数里调用任何延时函数。4.2 串口调试三件套printf逐点打印、环形缓冲区、逻辑分析仪排障效率高的人不是运气好而是工具用得好。我强烈建议新手掌握这三招第一招printf逐点打印。在代码关键路径上打日志程序跑到哪里、变量值是多少一目了然。要特别注意打印语句本身也会消耗时间如果放在高频中断里可能干扰时序尽量把日志放到主循环或低频代码路径。第二招简单的环形缓冲区。中断收数据、主循环解析数据两个模块之间靠环形缓冲区解耦。这个结构非常简单核心是一个数组加两个下标写指针和读指针写入时只有中断修改写指针读取时只有主循环修改读指针天然避免并发冲突。环形缓冲区能有效解决我上面提到的“中断里处理太复杂导致数据丢失”的问题建议每个做串口项目的人都封装一个。第三招逻辑分析仪抓波形。遇到莫名其妙的乱码、丢字节与其盲猜不如直接看波形。逻辑分析仪能直观显示时序你一眼就能看出波特率是否偏了、停止位是否正常。有条件的话我还喜欢用两个串口做交叉验证一个专门发测试数据一个打印内部状态缩小问题范围。4.3 延伸方向Bootloader升级、Modbus总线、低功耗唤醒USART学会了别停在这里它往深处走有很多实用方向。比如在线升级IAP就是利用USART作为下载通道通过Bootloader程序接收升级固件然后写入Flash并跳转。热词里有人提到“stm32 flash loader demonstrator”和“bootloader驱动下载”这都是USART IAP的典型应用。实现思路是芯片上电先跑Bootloader串口收数据收完校验、写Flash完成后跳转到App。注意Flash访问要处理中断向量表偏移、跳转前关闭外设中断这些细节。再比如RS485总线。USART本身是TTL电平距离短、抗干扰差但通过外接MAX485等芯片就能转成差分信号传输距离可达上千米。热词里“stm32和变频器通讯”“stm32控制伺服电机485”这些场景底层基本都是USART加RS485的用法。Modbus RTU是这类应用里最常见的协议帧结构就是基于USART这个“一帧收发”模型定出来的。理解了USART的数据包结构再去看Modbus RTU的报文格式你会觉得前面那套原理没有白学。还有低功耗场景。MCU进入休眠模式后USART仍可以作为唤醒源利用RXNE或WAKE中断把芯片从睡眠中唤醒。但要注意唤醒后需要重新检查接收数据是否有效因为在低功耗模式下接收时钟可能被关闭前几个字节可能已经丢失。这些都是实打实的项目细节。4.4 从“跑通”到“稳定”代码健壮性的一些个人建议最后这段算是个人心得。很多新手写串口程序跑通一次就完事了但实际项目里你会发现串口程序“好写”但“写稳”很难。我后来总结出几条经验第一所有缓冲区操作都必须有边界检查。不管是用数组、指针还是DMA越界写是最隐蔽的杀手。第二串口中断里只做“保存现场”级别的最小操作复杂处理一律丢给主循环。第三对外部数据的校验不能省帧头、帧尾、长度、累加和至少做一种因为串口在工业现场被干扰时什么离谱数据都可能出现。第四初始化顺序也有讲究先配置GPIO再配置USART参数最后开中断。顺序反了可能在初始化过程中就触发一次中断处理到半初始化的状态程序直接跑飞。我记得自己第一次调USART和传感器通信时数据总是隔几秒就错一个字节排查了一整天都没头绪后来用逻辑分析仪才发现是波特率有微小误差长时间累计就丢位了。从那之后我再也不敢忽略波特率误差的问题。USART这个东西入门简单精通难但只要你肯把原理吃透、把工具用好它能陪你走很远的路。希望这篇实战内容能帮你少踩几个我踩过的坑后面的路就顺了。

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

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

免费获取报价 →
↑