ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32G474 USART1中断收发避坑指南:从CubeMX配置到HAL库回调实战

STM32G474 USART1中断收发避坑指南:从CubeMX配置到HAL库回调实战 1. 为什么STM32G474的串口调试值得单独写一篇避坑指南STM32G474这颗芯片这两年在电机控制、数字电源、工业传感器采集这些领域用得越来越多Cortex-M4内核跑170MHz带FPU和CORDIC加速模拟外设也堆得足。但很多人拿到G474的第一件事不是跑电机算法而是先把串口调通——毕竟没有串口打印后面所有调试都是盲人摸象。问题就在这儿。G474的USART1和F103时代的USART1看起来都是“串口”但CubeMX里的配置项、HAL库的调用时序、中断优先级的处理细节坑点完全不一样。我见过太多人卡在“CubeMX配置看起来没问题但串口就是收不到数据”或者“中断只能进一次”这类问题上一卡就是半天。这篇内容面向的是已经会用CubeMX建工程、但对G474的USART1中断收发还不够熟的人。我会从CubeMX的时钟树配置开始一路讲到中断回调函数的写法、HAL库的锁机制、以及那些手册上不会写的实操细节。目标很简单你看完能直接抄作业把USART1的中断收发跑稳。2. 先搞清楚G474的USART1和F103到底差在哪2.1 时钟源变了配置逻辑跟着变F103时代USART1挂在APB2上时钟一般是72MHz配置波特率的时候HAL库自动算BRR寄存器基本不用操心。G474不一样它的时钟树复杂得多USART1可以挂在APB2上但APB2的时钟来源可以是SYSCLK、PLLQ、HSI16等等而且G474支持更高的主频APB2的预分频器设置直接影响USART1的实际时钟。这里有个很容易踩的坑CubeMX里Clock Configuration页面如果你只改了SYSCLK的数值没有检查APB2 PrescalerUSART1的时钟可能和你以为的不一样。比如你设SYSCLK170MHzAPB2 Prescaler设成1那USART1时钟就是170MHz但如果Prescaler设成2USART1时钟就是85MHz。波特率计算是基于这个时钟的时钟不对波特率就偏串口助手收到的就是乱码。注意CubeMX在USART1配置页面会显示“Baud Rate”输入框但它不会告诉你当前USART1的时钟是多少。你需要自己回到Clock Configuration页面找到APB2那条线看它标注的频率值。2.2 G474的USART1支持更高的波特率和更多功能G474的USART1支持最高到17Mbit/s的波特率在170MHz时钟下而且支持自动波特率检测、TX/RX引脚交换、单线半双工模式这些F103没有或者不好用的功能。但这也意味着CubeMX里的配置项更多选错一个模式串口就不工作。比如“Single Wire (Half-Duplex)”模式如果你不小心勾上了TX和RX就合并到一根线上了你按全双工的方式接线自然收不到数据。还有“Hardware Flow Control”如果勾了RTS/CTS但硬件上没接这两根线发送数据时HAL库会等CTS信号结果就是卡死在发送函数里。2.3 HAL库版本差异带来的行为变化G474用的是STM32CubeG4系列的HAL库和F1系列的HAL库虽然API名字一样但内部实现有差异。最典型的是HAL_UART_Receive_IT这个函数在G4的HAL库里它对接收缓冲区的管理更严格如果你在中断回调里没有及时重新调用HAL_UART_Receive_IT下一次接收就不会触发中断。还有一个差异是错误处理。G4的HAL库对OREOverrun Error的处理和F1不一样F1时代很多人习惯在中断里清ORE标志就完事但G4的HAL库如果检测到ORE会调用错误回调并且可能把UART状态机搞乱导致后续收发都失效。这个坑后面会详细讲怎么填。3. CubeMX配置USART1的完整流程和关键参数3.1 新建工程和时钟树配置打开CubeMX选STM32G474RET6或者你实际用的型号进入Pinout Configuration页面。第一步不是去点USART1而是先把RCC配好。在System Core里找到RCCHigh Speed Clock (HSE)选Crystal/Ceramic Resonator如果你的板子上有外部晶振的话。如果没有就选Disable用内部HSI16。然后去Clock Configuration页面。G474的时钟树长这样HSI16或HSE进PLLPLL输出给SYSCLKSYSCLK再分频给AHB、APB1、APB2。我的习惯是先把PLL配到170MHz然后APB1和APB2的Prescaler都设成1这样USART1的时钟就是170MHz波特率计算最准。具体操作在Clock Configuration页面找到PLL Source选HSE或HSI16然后PLLM、PLLN、PLLR这几个参数要算一下。比如HSE是8MHz想要170MHz的SYSCLK可以设PLLM2PLLN85PLLR2这样VCO输入是4MHzVCO输出是340MHzPLLR分频后是170MHz。CubeMX会自动帮你算你只需要在HSE输入框填8然后在PLLN那里输入85看SYSCLK是不是170。提示如果你用的是G474的官方Nucleo板板载晶振是24MHzPLL参数要重新算。24MHz进PLLM3PLLN85PLLR4得到170MHz。3.2 USART1的参数配置在Connectivity里找到USART1Mode选Asynchronous。Hardware Flow Control选Disable除非你确实接了RTS/CTS。Baud Rate填115200Word Length选8 BitsParity选NoneStop Bits选1。这些是标准配置和串口助手对应。重点看下面几个选项Over Sampling默认是16 Samples可以改成8 Samples。16 Samples抗噪能力更强8 Samples在高波特率下更准。115200这种低速用16就行。Single Wire不要勾除非你明确知道自己在做半双工。TX/RX Pin检查一下引脚是不是你板子上实际用的。G474的USART1默认是PA9/PA10但也可以重映射到PB6/PB7或者PC4/PC5。如果你板子上串口接的是PB6/PB7就要在Pinout视图里把PA9/PA10取消然后手动把PB6设成USART1_TXPB7设成USART1_RX。3.3 NVIC中断配置在USART1的配置页面找到NVIC Settings标签页勾上“USART1 global interrupt”的Enabled。然后去System Core里的NVIC找到USART1中断设置Preemption Priority和Sub Priority。这里有个经验如果你后面还要用其他中断比如定时器、DMAUSART1的中断优先级不要设太高也不要设太低。设太高会影响其他实时性要求高的中断设太低会导致串口数据丢失。我一般设Preemption Priority为5中等偏上Sub Priority为0。注意G474的NVIC有4位优先级所以Preemption Priority的范围是0-15。数字越小优先级越高。如果你用了FreeRTOS要注意FreeRTOS的SysTick和PendSV优先级通常设成最低15USART1的优先级要高于它们否则中断里调用FreeRTOS的API会出问题。3.4 生成代码时的注意事项在Project Manager页面Toolchain/IDE选MDK-ARM或STM32CubeIDE看你用什么。Code Generator页面勾上“Generate peripheral initialization as a pair of .c/.h files per peripheral”这样USART1的初始化代码会单独放在usart.c和usart.h里方便管理。还有一个选项“Set all free pins as analog (to optimize power consumption)”如果你做低功耗产品可以勾上如果只是调试不勾也行勾了之后未使用的引脚都设成模拟输入省电但可能会影响某些外部电路。点Generate CodeCubeMX会生成完整的工程。接下来就是写代码了。4. 中断收发的HAL库实现和那些手册不写的细节4.1 发送别直接用HAL_UART_Transmit很多人习惯用HAL_UART_Transmit(huart1, data, len, timeout)来发送数据这个函数是阻塞式的发完之前CPU一直等着。115200波特率下发100个字节大概要8.7ms这8.7ms里CPU什么都干不了。如果你在中断里调用它还会导致中断嵌套问题。更好的方式是使用中断发送HAL_UART_Transmit_IT或者用DMA发送。这里先讲中断发送。HAL_UART_Transmit_IT的工作流程是你把要发送的数据缓冲区和长度传给它它把第一个字节写入TX寄存器然后使能TXE中断之后每次TXE中断触发HAL库的中断处理函数会把下一个字节写入TX寄存器直到发完最后调用HAL_UART_TxCpltCallback回调。关键点你传给HAL_UART_Transmit_IT的缓冲区必须是全局变量或者静态变量不能是局部变量。因为函数返回后中断还在后台继续发送如果缓冲区是局部变量函数返回后栈空间被回收中断里读到的就是垃圾数据。/* 正确做法 */ uint8_t tx_buf[] Hello STM32G474\r\n; void send_data(void) { HAL_UART_Transmit_IT(huart1, tx_buf, sizeof(tx_buf) - 1); } /* 错误做法局部数组 */ void send_data_bad(void) { uint8_t tx_buf_local[] Hello\r\n; HAL_UART_Transmit_IT(huart1, tx_buf_local, sizeof(tx_buf_local) - 1); /* tx_buf_local在函数返回后失效中断发送会出错 */ }还有一个坑HAL_UART_Transmit_IT在发送过程中如果再次调用会返回HAL_BUSY。所以如果你要连续发送多包数据要么等上一包发完在TxCpltCallback里发下一包要么用DMA。4.2 接收HAL_UART_Receive_IT的锁机制接收比发送更容易出问题。HAL_UART_Receive_IT的用法是你给它一个缓冲区和一个长度它使能RXNE中断每收到一个字节就存入缓冲区收满指定长度后调用HAL_UART_RxCpltCallback。但这里有个关键细节收满之后HAL库会自动关闭RXNE中断。也就是说如果你不在回调函数里重新调用HAL_UART_Receive_IT下一次串口来数据就不会触发中断了。很多人调试时发现“只能收到第一包数据”就是这个原因。uint8_t rx_buf[10]; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { /* 处理接收到的数据 */ process_data(rx_buf, 10); /* 重新使能接收中断准备接收下一包 */ HAL_UART_Receive_IT(huart1, rx_buf, 10); } }另一个坑是HAL_UART_Receive_IT的锁机制。HAL库内部有一个__HAL_LOCK宏如果UART正在被其他操作占用比如正在发送调用HAL_UART_Receive_IT会返回HAL_BUSY。所以在初始化时要先调用HAL_UART_Receive_IT启动接收再启动发送避免锁冲突。4.3 中断回调函数的正确写法HAL库的UART中断处理函数HAL_UART_IRQHandler会根据中断标志位调用不同的回调。除了HAL_UART_RxCpltCallback和HAL_UART_TxCpltCallback还有HAL_UART_ErrorCallback。HAL_UART_ErrorCallback很重要但很多人不写。当发生ORE溢出错误、NE噪声错误、FE帧错误时HAL库会调用这个回调。如果你不处理UART状态机可能卡在错误状态后续收发都失效。void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { /* 清除错误标志 */ __HAL_UART_CLEAR_OREFLAG(huart); __HAL_UART_CLEAR_NEFLAG(huart); __HAL_UART_CLEAR_FEFLAG(huart); __HAL_UART_CLEAR_PEFLAG(huart); /* 重新启动接收 */ HAL_UART_Receive_IT(huart1, rx_buf, 10); } }注意__HAL_UART_CLEAR_OREFLAG这个宏在G4的HAL库里是存在的但它的实现是先读SR寄存器再读DR寄存器。如果你在中断里直接操作寄存器清标志要注意顺序否则清不掉。4.4 中断优先级的实际影响前面说了USART1中断优先级设5但具体到实际项目中这个优先级要和你的系统设计匹配。比如你用了FreeRTOSUSART1中断里调用了xQueueSendFromISR那USART1的优先级必须高于configMAX_SYSCALL_INTERRUPT_PRIORITY否则会触发断言。G474的NVIC优先级分组默认是4位抢占优先级、0位子优先级NVIC_PRIORITYGROUP_4。如果你用CubeMX生成的代码默认就是这个分组。在这个分组下Preemption Priority的范围是0-15Sub Priority没用。我一般把USART1的Preemption Priority设成5SysTick设成15PendSV设成15。这样USART1中断可以打断SysTick但不会影响其他更高优先级的中断比如电机控制的PWM中断设成0或1。5. 实操验证从零搭建一个中断收发工程5.1 硬件连接和串口助手设置先确认硬件。G474的USART1默认引脚是PA9TX和PA10RX。把PA9接到USB转串口模块的RXPA10接到模块的TXGND对接。注意不要接错TX对TX是收不到数据的。串口助手设置波特率115200数据位8停止位1校验位None流控None。打开串口如果一切正常你应该能看到MCU发来的数据。5.2 初始化代码的调用顺序CubeMX生成的main.c里MX_USART1_UART_Init()会在main()开头被调用。但HAL_UART_Receive_IT需要你手动调用一般放在MX_USART1_UART_Init()之后。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); /* 启动接收中断 */ HAL_UART_Receive_IT(huart1, rx_buf, RX_BUF_SIZE); /* 发送一个启动信息 */ HAL_UART_Transmit_IT(huart1, (uint8_t *)STM32G474 USART1 Ready\r\n, 25); while (1) { /* 主循环 */ } }注意HAL_UART_Transmit_IT的第三个参数是长度不要用sizeof去算字符串字面量的长度因为字符串字面量末尾有\0sizeof会把它算进去。用strlen或者手动数。5.3 回环测试收到什么就发什么最简单的验证方式是回环测试。在HAL_UART_RxCpltCallback里把收到的数据原样发回去。#define RX_BUF_SIZE 10 uint8_t rx_buf[RX_BUF_SIZE]; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { /* 回发收到的数据 */ HAL_UART_Transmit_IT(huart1, rx_buf, RX_BUF_SIZE); /* 重新启动接收 */ HAL_UART_Receive_IT(huart1, rx_buf, RX_BUF_SIZE); } }在串口助手里发送10个字节你应该能收到同样的10个字节。如果收不到检查接线、波特率、中断是否使能。5.4 用逻辑分析仪抓波形验证时序如果回环测试不通过用逻辑分析仪抓PA9和PA10的波形。正常情况PA10上应该能看到起始位低电平、8个数据位、停止位高电平。波特率115200对应每个位的时间是8.68微秒。如果位宽不对说明波特率配置有问题回去检查USART1的时钟。逻辑分析仪还能帮你确认中断响应时间。从PA10收到最后一个字节到PA9开始发送第一个字节这段时间就是中断响应加处理的时间。如果超过100微秒说明中断优先级太低或者中断里做了太多事情。6. 常见问题速查表和排查思路6.1 串口助手收到乱码乱码是最常见的问题原因通常有三个波特率不对、时钟配置不对、数据位/停止位/校验位不匹配。排查顺序先确认串口助手的设置和CubeMX里USART1的设置一致。然后检查Clock Configuration页面APB2的频率用公式算一下实际波特率。G474的USART1波特率计算公式是Baud fCK / (8 * (2 - OVER8) * USARTDIV)其中fCK是USART1的时钟OVER8是过采样模式0表示16倍1表示8倍USARTDIV是分频系数。如果算出来和115200差很多说明时钟配置有问题。比如你设了170MHz的SYSCLK但APB2 Prescaler是2那USART1时钟是85MHzHAL库按85MHz算出来的BRR值在170MHz下就是错的。6.2 只能收到第一包数据前面讲过HAL_UART_Receive_IT收满指定长度后会自动关闭RXNE中断。如果你不在回调里重新调用就只能收到第一包。解决办法就是在HAL_UART_RxCpltCallback里重新调用HAL_UART_Receive_IT。还有一种情况你在回调里调用了HAL_UART_Receive_IT但返回HAL_BUSY。这是因为UART的锁被其他操作占用了。检查一下是不是在发送数据时调用了接收函数或者中断优先级配置导致锁竞争。6.3 中断进不去中断进不去的原因比较多按以下顺序排查NVIC里USART1中断是否使能。CubeMX里勾了“USART1 global interrupt”之后生成的代码里会有HAL_NVIC_EnableIRQ(USART1_IRQn)。HAL_UART_Receive_IT是否被调用。这个函数内部会使能RXNE中断如果不调用RXNE中断不会触发。中断优先级是否被其他中断屏蔽。如果某个高优先级中断一直在执行USART1中断会被挂起。串口引脚是否配置正确。用万用表测一下PA10在空闲时是不是高电平如果是低电平说明引脚配置有问题。6.4 发送数据时卡死HAL_UART_Transmit是阻塞式的如果timeout设得太大或者设成HAL_MAX_DELAY而硬件流控又使能了但CTS没接就会卡死。解决办法要么把timeout设小一点比如100ms要么用HAL_UART_Transmit_IT要么检查硬件流控配置。还有一种卡死是发生在中断里的。如果你在中断回调里调用了HAL_UART_Transmit阻塞式而发送过程中又来了接收中断就可能死锁。中断里永远用非阻塞的API。6.5 ORE错误导致串口失效OREOverrun Error发生在接收移位寄存器里的数据还没被读走下一个字节又来了。G474的HAL库检测到ORE后会调用HAL_UART_ErrorCallback并且把UART状态设成错误状态。如果你不处理后续HAL_UART_Receive_IT会返回HAL_ERROR。处理方式在HAL_UART_ErrorCallback里清ORE标志然后重新调用HAL_UART_Receive_IT。清标志的宏是__HAL_UART_CLEAR_OREFLAG(huart)注意这个宏要先读SR再读DR顺序不能反。问题现象可能原因排查方法解决方案收到乱码波特率不匹配检查APB2时钟和BRR值重新配置时钟树只能收一包未重启用接收中断检查RxCpltCallback回调里重新调用Receive_IT中断进不去NVIC未使能检查NVIC配置使能USART1全局中断发送卡死阻塞发送流控检查Transmit调用改用Transmit_ITORE错误接收不及时检查ErrorCallback清标志并重启接收7. 几个让调试更顺手的实操技巧7.1 用printf重定向到USART1虽然中断收发是正路但调试阶段用printf打印变量还是很方便。重定向的方法是实现_write或fputc函数在里面调用HAL_UART_Transmit。#include stdio.h int __io_putchar(int ch) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 100); return ch; } int _write(int file, char *ptr, int len) { for (int i 0; i len; i) { __io_putchar(ptr[i]); } return len; }注意HAL_UART_Transmit是阻塞式的在中断里不要用printf。另外重定向之后要在CubeMX里勾上“Use MicroLIB”否则链接会报错。7.2 用DMA解放CPU如果串口数据量大比如每秒钟几百包中断收发会占用大量CPU时间。这时候用DMA更好。CubeMX里USART1的DMA Settings标签页添加USART1_TX和USART1_RX的DMA请求模式选Normal或Circular。DMA接收配成Circular模式配合HAL_UART_Receive_DMA可以实现不间断接收。但要注意Circular模式下DMA会一直往缓冲区写你需要用HAL_UART_GetRxCount或者直接读DMA的NDTR寄存器来知道当前收到哪里了。7.3 中断里不要做耗时操作中断回调里只做最必要的事情存数据、置标志、发信号。数据处理放到主循环里做。比如收到一包数据后在回调里把数据拷贝到一个环形缓冲区然后置一个标志主循环检测到标志后再处理。我见过有人在HAL_UART_RxCpltCallback里做浮点运算、查表、甚至调用HAL_Delay结果就是串口频繁丢包。中断里执行时间超过几十微秒就会影响下一次接收。7.4 用示波器看TX波形确认发送如果发送数据但串口助手收不到用示波器看PA9。正常发送时PA9在空闲是高电平发送时会有下拉脉冲。如果PA9一直是高电平说明发送函数没被调用或者UART没使能。如果PA9一直是低电平说明引脚配置成了开漏输出或者被其他外设占用了。7.5 注意G474的USART1引脚复用G474的USART1可以映射到多组引脚CubeMX里默认是PA9/PA10但也可以改成PB6/PB7或PC4/PC5。如果你板子上串口接的是PB6/PB7要在Pinout视图里先取消PA9/PA10的USART1功能然后把PB6设成USART1_TXPB7设成USART1_RX。CubeMX会自动处理复用寄存器的配置。提示改引脚之后记得检查生成的代码里HAL_UART_MspInit函数中的GPIO初始化是否正确。有时候CubeMX会保留旧的GPIO配置导致两个引脚都被初始化成USART1功能实际只有一个生效。8. 我个人在实际操作中的几点体会G474的USART1中断收发说到底就是三件事时钟配对、中断使能、回调重入。时钟配对是基础APB2的频率决定了波特率的准确性中断使能是前提NVIC和RXNE中断都要打开回调重入是关键收完一包必须重新启动接收否则就只能收一包。我踩过最深的坑是ORE错误。当时用G474做Modbus从站主机发数据比较快从站处理稍慢就触发ORE然后串口就死了。后来在HAL_UART_ErrorCallback里加了清标志和重启接收问题才解决。这个错误在F103上不太常见因为F103的HAL库对ORE的处理更宽松但G4的HAL库更严格。另一个体会是CubeMX生成的代码只是骨架中断回调函数需要自己写。很多人以为CubeMX配完就能收发了其实不是。HAL_UART_Receive_IT必须手动调用回调函数必须自己实现。这一点在CubeMX的界面里没有任何提示只能靠看HAL库的文档或者例程。最后分享一个小技巧如果你不确定USART1的时钟是多少可以在MX_USART1_UART_Init()之后加一句printf(USART1 clock: %d\r\n, HAL_RCC_GetPCLK2Freq())打印出来的就是APB2的频率也就是USART1的时钟。用这个频率去算波特率就不会错。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进