资讯动态

STM32CubeMX USART配置与实战:从轮询到DMA空闲中断

发布时间:2026/10/15 21:38:34 来源:尧图企业网站定制
1. 为什么USART值得单独拿出来聊嵌入式开发里串口通信几乎是绕不开的一道坎。不管是打印调试信息、跟传感器模块对话、还是跟上位机做数据交互USART都是最基础也最常用的外设之一。STM32CubeMX把USART的初始化配置做成了图形化操作点几下就能生成初始化代码看起来门槛很低但实际用起来坑一点都不少。我自己在多个项目里用过USART从最简单的printf重定向到中断收发、DMA搬运、空闲中断做不定长接收每一步都踩过坑。这篇文章就把这些经验系统地整理出来从CubeMX的配置逻辑讲起一直讲到实际项目里怎么稳定收发数据。这篇文章适合谁看如果你刚开始接触STM32用CubeMX配好了USART但不知道怎么收发数据那这篇能帮你打通从配置到实际通信的完整链路。如果你已经用过USART但总是在中断、DMA、不定长接收这些地方出问题那这篇里的排查思路和参数计算过程应该能帮你省不少时间。我会尽量把每个配置项背后的原因讲清楚而不是只告诉你“勾上这个就行”。先明确一个概念USART是通用同步异步收发器UART是异步版本。STM32里的USART既能做异步也能做同步但我们日常说的“串口”基本都是指异步模式。CubeMX里配置USART的时候Mode选项里选Asynchronous就是最常用的异步串口模式。这个模式下只需要TX和RX两根线就能通信不需要时钟线接线简单这也是它应用最广的原因。2. CubeMX里USART配置的完整拆解2.1 参数配置背后的计算逻辑打开CubeMX在Connectivity里找到USARTMode选Asynchronous然后下面会出现一堆参数。很多人配到这里就开始懵了波特率、字长、校验位、停止位、方向、过采样这些到底怎么选我一个个说。波特率是最关键的参数。CubeMX里填的波特率是目标值实际能不能精确达到取决于你的时钟配置。这里有个计算公式需要理解USARTDIV fCK / (8 × (2 - OVER8) × Baud)其中fCK是挂载在USART上的时钟频率OVER8是过采样倍数0表示16倍过采样1表示8倍过采样。算出来的USARTDIV要拆成整数部分和小数部分分别写入BRR寄存器。CubeMX会自动帮你算但你要知道它算得准不准。举个例子假设USART挂在APB2上时钟是72MHz目标波特率115200过采样16倍。那么USARTDIV 72000000 / (16 × 115200) 39.0625。整数部分是39小数部分0.0625乘以16等于1所以BRR寄存器应该写入0x271。实际波特率 72000000 / (16 × 39.0625) 115200误差为0。但如果你的时钟是72MHz目标波特率是9600USARTDIV 72000000 / (16 × 9600) 468.75整数468小数0.75×1612BRR 0x1D4C实际波特率 72000000 / (16 × 468.75) 9600也是精确的。问题出在时钟频率不能整除的时候。比如时钟是25MHz目标波特率115200USARTDIV 25000000 / (16 × 115200) 13.563。整数13小数0.563×169.008取9实际USARTDIV 13 9/16 13.5625实际波特率 25000000 / (16 × 13.5625) 115207。误差大约0.006%完全可以接受。但如果误差超过2%到3%通信就会不稳定甚至失败。所以配完时钟后一定要在CubeMX的Clock Configuration里确认USART的时钟源频率然后自己验算一下波特率误差。注意CubeMX在参数配置界面会直接显示实际波特率和误差百分比配完一定要看一眼这个数值。如果误差超过2%要么调整时钟树要么换一个能整除的波特率。字长一般选8位这是最通用的。校验位通常选None因为大多数协议自己会做校验硬件校验位反而增加开销。停止位选1这也是最常用的。方向选Receive and Transmit除非你只需要单向通信。过采样默认16倍8倍过采样可以在高波特率下减少误差但抗噪能力会下降一般不用改。2.2 中断和DMA配置的取舍NVIC Settings里可以开USART的全局中断。开了中断之后接收、发送、错误事件都能触发中断。但这里有个关键选择你是用中断收发还是用DMA收发还是轮询轮询最简单但效率最低。发送的时候CPU要一直等着TXE标志位置位接收的时候要一直等着RXNE标志位置位。在115200波特率下发一个字节大概87微秒如果发100个字节CPU要空转8.7毫秒这在大多数项目里是不可接受的。中断方式好一些CPU不用空转但每个字节都要进一次中断。115200波特率下每秒最多11520个字节也就是每秒最多11520次中断。如果系统还有其他中断这个频率会明显增加CPU负担。而且中断方式下如果处理不及时还可能丢数据。DMA方式是最省CPU的。配置好DMA通道设置好接收缓冲区和长度USART收到数据后DMA自动搬运到内存搬够了指定长度再触发中断通知CPU。发送也一样CPU把数据放进缓冲区DMA自动搬到USART的DR寄存器搬完了再通知CPU。整个过程CPU只需要在开始和结束的时候介入。我的建议是调试打印用轮询或者中断都行数据量小实际通信数据量大的时候接收用DMA加空闲中断发送用DMA。这样CPU占用率最低也最不容易丢数据。2.3 GPIO和引脚配置的细节CubeMX会自动分配TX和RX的引脚一般是PA9和PA10或者PB6和PB7具体看你的芯片型号。这里要注意的是TX要配置成复用推挽输出RX配置成浮空输入或者上拉输入。CubeMX默认会配好但如果你手动改了GPIO模式可能会出问题。还有一个容易忽略的点如果你用的是USB转串口模块跟电脑通信模块的TX要接STM32的RX模块的RX要接STM32的TX交叉连接。这个看起来是常识但我见过不止一个新手在这里卡了半天。另外如果通信距离比较远或者环境干扰大建议在TX和RX上加上拉电阻典型值4.7k到10k。STM32的USART引脚内部有弱上拉但驱动能力有限外部加上拉能提高抗干扰能力。3. 从零实现USART收发轮询、中断、DMA三种方式3.1 轮询方式最简单但最不实用CubeMX生成代码后轮询发送的代码大概长这样void USART_SendByte(USART_TypeDef* USARTx, uint8_t data) { while(!(USARTx-SR USART_SR_TXE)); USARTx-DR data; } void USART_SendString(USART_TypeDef* USARTx, uint8_t* str, uint16_t len) { for(uint16_t i 0; i len; i) { USART_SendByte(USARTx, str[i]); } while(!(USARTx-SR USART_SR_TC)); }接收的轮询代码uint8_t USART_ReceiveByte(USART_TypeDef* USARTx) { while(!(USARTx-SR USART_SR_RXNE)); return USARTx-DR; }这段代码能跑但问题很明显发送的时候CPU完全被占用接收的时候如果数据没来CPU就一直卡在while循环里。在实际项目里这种写法只能用在初始化阶段的调试打印正式通信绝对不能用。我早期做的一个项目用轮询方式接收上位机指令结果主循环里其他任务全部被阻塞按键响应延迟超过500毫秒用户体验极差。后来改成中断方式才解决。3.2 中断方式平衡效率和复杂度中断方式需要先在CubeMX里使能USART全局中断。生成代码后HAL库会提供HAL_UART_Receive_IT和HAL_UART_Transmit_IT两个函数。但HAL库的中断收发有个特点它是按指定长度收发的收够了指定字节数才会触发回调。如果你要收不定长数据需要自己处理。先看中断发送uint8_t txBuffer[] Hello USART\r\n; HAL_UART_Transmit_IT(huart1, txBuffer, sizeof(txBuffer)-1);发送完成后会触发HAL_UART_TxCpltCallback回调你可以在这里做下一步操作。中断接收uint8_t rxBuffer[10]; HAL_UART_Receive_IT(huart1, rxBuffer, 10);收够10个字节后触发HAL_UART_RxCpltCallback回调。在回调里处理数据然后重新调用HAL_UART_Receive_IT开启下一次接收。这里有个关键点HAL库的中断接收是一次性的收完指定长度后就不会再自动接收了必须在回调里重新开启。我见过很多人忘了这一步结果只能收到第一包数据后面的全丢了。还有一个坑如果在回调里处理数据的时间太长可能会影响下一次接收。所以回调里尽量只做数据搬运复杂处理放到主循环里。3.3 DMA方式大数据量收发的首选DMA方式需要先在CubeMX里添加DMA通道。USART1_TX对应DMA1 Channel4USART1_RX对应DMA1 Channel5具体通道号看芯片参考手册。DMA模式选Normal还是Circular接收一般用Circular这样DMA会一直循环搬运不会因为搬够一次就停止。发送用Normal发完就停。DMA发送代码uint8_t txBuffer[] DMA Test\r\n; HAL_UART_Transmit_DMA(huart1, txBuffer, sizeof(txBuffer)-1);DMA接收代码uint8_t rxBuffer[256]; HAL_UART_Receive_DMA(huart1, rxBuffer, 256);但DMA接收有个问题如果数据不是正好256字节DMA不会触发中断你就不知道数据来了。解决办法是配合空闲中断IDLE Interrupt。空闲中断在总线空闲一个字节时间后触发可以用来判断一帧数据接收完毕。配置空闲中断需要在CubeMX里使能USART全局中断然后在代码里手动开启IDLE中断__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);然后在USART中断服务函数里判断IDLE标志void USART1_IRQHandler(void) { if(__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 计算接收到的数据长度 uint16_t rxLen 256 - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); // 处理数据 ProcessData(rxBuffer, rxLen); // 重新设置DMA接收 HAL_UART_Receive_DMA(huart1, rxBuffer, 256); } HAL_UART_IRQHandler(huart1); }这套组合拳是我在实际项目里用得最多的方案。DMA负责搬运数据空闲中断负责通知CPUCPU只在数据收完后处理一次效率极高。在115200波特率下即使连续接收几百字节的数据CPU占用率也不到1%。注意__HAL_DMA_GET_COUNTER返回的是DMA剩余未搬运的字节数用总长度减去它才是已接收的字节数。这个计算要在清除IDLE标志之后、重新设置DMA之前做顺序错了会算错长度。4. 实际项目中的典型问题与排查实录4.1 数据丢失和乱码的排查思路数据丢失和乱码是USART最常遇到的问题原因可能有很多。我整理了一个排查表按可能性从高到低排列现象可能原因排查方法解决方案完全收不到数据TX/RX接反交换TX和RX接线交叉连接收到乱码波特率不匹配检查两端波特率设置统一波特率收到乱码时钟配置错误检查CubeMX时钟树重新配置时钟偶尔丢数据中断优先级冲突检查NVIC优先级调整优先级偶尔丢数据处理时间过长在回调里加计时缩短回调处理时间大量数据时丢包缓冲区溢出检查DMA缓冲区大小增大缓冲区通信一段时间后死机中断标志未清除检查中断服务函数清除所有相关标志波特率不匹配是最常见的原因。我遇到过两次一次是两端都设了115200但一端时钟配置错了实际波特率是9600结果全是乱码另一次是USB转串口模块的晶振精度不够在高波特率下误差累积导致丢包。所以配完波特率一定要用示波器或者逻辑分析仪量一下实际波形确认位宽对不对。中断优先级冲突也很隐蔽。如果USART中断优先级低于其他中断而其他中断处理时间又长USART中断就会被延迟响应导致数据丢失。我的经验是把USART的接收中断优先级设得高一些但不要设成最高留一点余地给系统关键中断。4.2 HAL库回调函数的正确使用姿势HAL库的回调机制很方便但有几个坑必须注意。第一个坑HAL_UART_RxCpltCallback是在中断上下文里执行的里面不能做耗时操作不能调用HAL_Delay不能使用可能阻塞的函数。我见过有人在回调里直接调用HAL_UART_Transmit发送响应数据结果因为发送是阻塞的整个中断被卡住后续数据全丢了。正确做法是在回调里设置一个标志位主循环检测到标志位后再处理。第二个坑HAL库的接收函数在收够指定长度后会自动关闭接收中断。如果你用的是HAL_UART_Receive_IT必须在回调里重新调用它开启下一次接收。忘了这一步就只能收到第一包数据。第三个坑错误回调HAL_UART_ErrorCallback一定要实现。过载错误ORE、帧错误FE、噪声错误NE都会触发这个回调。如果不处理错误标志会一直置位导致后续通信全部失败。在错误回调里要清除错误标志重新初始化接收。void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if(__HAL_UART_GET_FLAG(huart, UART_FLAG_ORE)) { __HAL_UART_CLEAR_OREFLAG(huart); } // 重新开启接收 HAL_UART_Receive_IT(huart, rxBuffer, RX_BUFFER_SIZE); }4.3 printf重定向的几种方案对比调试的时候用printf打印信息非常方便但STM32上默认的printf是输出到调试器的要输出到串口需要重定向。常见的有两种方案。方案一重写fputc函数。在代码里加上int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t*)ch, 1, 0xFFFF); return ch; }然后在Keil或IAR里勾选“Use MicroLIB”。这个方案最简单但HAL_UART_Transmit是阻塞的打印大量数据时会明显拖慢程序。方案二用DMA发送。自己实现一个printf函数把格式化后的字符串通过DMA发出去。这样不阻塞CPU但需要自己管理发送缓冲区防止DMA还在发送时缓冲区被覆盖。void MyPrintf(const char* format, ...) { static uint8_t txBuf[128]; va_list args; va_start(args, format); vsnprintf((char*)txBuf, sizeof(txBuf), format, args); va_end(args); HAL_UART_Transmit_DMA(huart1, txBuf, strlen((char*)txBuf)); }这个方案要注意DMA发送期间不能修改txBuf否则数据会错乱。如果打印频率很高建议用双缓冲区或者队列来管理。我个人的习惯是调试阶段用方案一简单直接正式版本用方案二或者干脆把调试打印全部关掉只保留必要的错误日志。4.4 不定长数据接收的三种实现方式对比不定长数据接收是USART应用里的一个经典难题。因为USART本身不知道一帧数据有多长需要额外的机制来判断帧结束。常见的有三种方式第一种固定帧头帧尾。协议里规定数据以特定字节开头和结尾接收的时候逐字节判断。这种方式简单可靠但需要协议支持而且如果数据内容里恰好出现了帧尾字节会误判。第二种超时判断。收到一个字节后启动定时器如果超过一定时间没有新字节到来就认为一帧结束。这种方式不需要协议支持但超时时间不好确定太短会截断长帧太长会延迟响应。第三种空闲中断。利用USART的IDLE中断总线空闲一个字节时间后自动触发。这种方式最优雅不需要额外定时器判断也准确。缺点是需要DMA配合而且IDLE中断的触发时间固定为一个字节时间在低波特率下可能不够灵活。我实际项目里用得最多的是第三种配合DMA和环形缓冲区能稳定处理各种长度的数据帧。如果芯片不支持IDLE中断或者DMA通道不够用就用第二种用一个定时器做超时判断。5. 进阶技巧与性能优化5.1 环形缓冲区在串口接收中的应用DMA加空闲中断的方案虽然好但如果数据来得太快CPU还没来得及处理上一帧下一帧就来了还是会丢数据。解决办法是加一个环形缓冲区。环形缓冲区的基本思路是DMA把数据搬到缓冲区A空闲中断触发后把缓冲区A的数据拷贝到环形缓冲区然后重新设置DMA。主循环从环形缓冲区里取数据处理。这样即使CPU处理慢一点只要环形缓冲区够大就不会丢数据。#define RING_BUF_SIZE 1024 typedef struct { uint8_t buffer[RING_BUF_SIZE]; volatile uint16_t head; volatile uint16_t tail; } RingBuffer; RingBuffer rxRingBuf; void RingBuffer_Write(RingBuffer* rb, uint8_t* data, uint16_t len) { for(uint16_t i 0; i len; i) { uint16_t next (rb-head 1) % RING_BUF_SIZE; if(next ! rb-tail) { rb-buffer[rb-head] data[i]; rb-head next; } } } uint16_t RingBuffer_Read(RingBuffer* rb, uint8_t* data, uint16_t maxLen) { uint16_t count 0; while(rb-tail ! rb-head count maxLen) { data[count] rb-buffer[rb-tail]; rb-tail (rb-tail 1) % RING_BUF_SIZE; } return count; }这个环形缓冲区实现里head和tail都用volatile修饰因为它们在中断和主循环里都会被访问。写入的时候判断next是否等于tail等于说明缓冲区满了丢弃数据。读取的时候从tail读到head。注意如果缓冲区满了是丢弃新数据还是覆盖旧数据取决于你的应用场景。控制指令不能丢那就丢弃新数据传感器数据可以丢旧的那就覆盖。我的习惯是丢弃新数据因为旧数据往往更重要。5.2 波特率误差对通信稳定性的影响前面提到了波特率误差的计算这里展开说一下误差对通信的影响。USART是异步通信收发双方没有共享时钟全靠各自的波特率来采样。如果双方波特率有误差采样点会逐渐偏移偏移超过半个位宽就会采错。一个字节是10位1起始位8数据位1停止位如果波特率误差是2%那么到第10位的时候采样点偏移了10×2%20%的位宽。半个位宽是50%所以2%的误差在10位内是安全的。但如果误差达到5%第10位就偏移了50%正好在边缘很容易出错。实际经验是波特率误差控制在2%以内通信基本没问题2%到3%之间短帧可以长帧可能出错超过3%通信不可靠。所以配时钟的时候尽量让USART的时钟频率能被波特率整除或者至少误差小于1%。如果实在无法避免误差可以降低波特率。波特率越低每个位的时间越长同样的绝对误差对应的相对误差就越小。比如9600波特率下即使时钟误差大一点通信也比115200稳定。5.3 多串口同时工作的资源分配很多项目里不止一个串口。比如一个串口接调试终端一个串口接无线模块一个串口接传感器。多个串口同时工作的时候资源分配就很重要。首先是DMA通道。STM32的DMA通道数量有限每个USART的TX和RX各占一个通道。如果串口数量多DMA通道可能不够用。这时候可以只给数据量大的串口配DMA数据量小的用中断。其次是中断优先级。多个串口同时收数据的时候如果优先级配置不当低优先级的串口可能会丢数据。我的做法是数据量大的串口优先级高数据量小的优先级低接收中断优先级高于发送中断。最后是缓冲区。每个串口都要有自己的接收缓冲区和环形缓冲区不能共用。缓冲区大小根据数据量和处理速度来定一般256字节到1KB就够了。5.4 低功耗场景下的USART处理如果项目对功耗有要求USART的使用方式需要调整。USART在空闲时也会消耗电流虽然不大但在电池供电的设备里也需要考虑。一个常用的技巧是不需要通信的时候把USART关掉需要的时候再打开。HAL库提供了HAL_UART_DeInit和HAL_UART_Init函数可以动态开关USART。但要注意重新初始化后之前的配置比如DMA、中断需要重新设置。另一个技巧是用USART的唤醒功能。STM32的USART支持在停止模式下通过RX引脚上的起始位唤醒CPU。配置好之后CPU可以进入停止模式USART检测到数据后自动唤醒CPU处理。这个功能在低功耗传感器节点里很有用。注意停止模式下USART的时钟源可能会被关闭需要确保USART的时钟在停止模式下仍然可用。具体配置参考芯片的参考手册不同系列的支持情况不一样。6. 我在实际项目里踩过的那些坑说几个具体的案例都是真实踩过的坑希望能帮你省点时间。第一个坑DMA发送时修改缓冲区。有一次我用DMA发送一串数据发送还没完成主循环就修改了缓冲区的内容结果发出去的数据一半是新的、一半是旧的。后来改成发送前先拷贝到独立的发送缓冲区或者用双缓冲区交替才解决。第二个坑中断优先级配置错误。有个项目里USART接收中断优先级设成了最低结果系统里一个定时器中断处理时间太长USART中断被延迟数据全丢了。后来把USART接收中断优先级调高问题解决。第三个坑忘记清除错误标志。有一次通信突然中断查了半天发现是过载错误标志没清除USART一直处于错误状态。后来在错误回调里加了清除所有错误标志的代码再也没出现过。第四个坑波特率配置错误。CubeMX里改了时钟树但忘了重新生成代码结果USART的时钟频率变了波特率也跟着变了通信全乱。后来养成习惯每次改时钟树都重新生成代码并验算波特率。第五个坑DMA缓冲区对齐问题。有些STM32系列的DMA对缓冲区地址有对齐要求如果缓冲区没有对齐DMA搬运会出错。解决办法是用__attribute__((aligned(4)))修饰缓冲区确保4字节对齐。这些坑看起来都不复杂但实际遇到的时候往往要花不少时间排查。我的建议是每次配置完USART先做最简单的回环测试确认基本收发正常再逐步加上中断、DMA、空闲中断这些功能。每加一个功能就测试一次这样出问题的时候容易定位。最后分享一个小技巧如果手头没有USB转串口模块可以用另一块STM32板子做回环测试。一块板子发另一块板子收两块板子交叉连接TX和RX共地。这样不需要额外的硬件就能验证通信是否正常。

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

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

免费获取报价 →
↑