ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32G474串口USART从配置到DMA+IDLE接收踩坑指南

2026/8/29 20:47:32 拓冰建站 浏览量
STM32G474串口USART从配置到DMA+IDLE接收踩坑指南 手里的项目一直用STM32F030做RS485通信最近换了STM32G474RET6想跑更复杂的控制算法原以为USART这类老外设不就是那几个寄存器改改时钟就能跑通结果一上来就翻车——波特率怎么调都不对加上FIFO之后中断标志位跟F0完全不是一个脾气折腾了两天半才把G474这颗料上的USART彻底摸透。这篇文章就是把这段时间的实测和踩坑梳理一遍从外设差异、CubeMX配置、寄存器代码到DMAIDLE中断接收不定长数据给正在从F0/F1迁到G4、或者第一次用G474串口的同学做个参考。1. 从F030换到G474USART的第一个认知冲击点1.1 它不是多了几个串口而是换了一套串口架构STM32G474RET6总共给了5个独立的串口外设USART1、USART2、USART3再加上UART4和UART5另外还有一个LPUART1低功耗串口。从数量上看这比F030通常只有1~2个串口充裕得多但真正让人意外的不是数量而是功能密度。G474的USART支持同步模式、IrDA、智能卡、LIN、Modbus辅助功能、硬件RS-485方向控制DE信号、自动波特率检测、超时中断、以及硬件FIFO。其中任何一个功能放到F030上都是不可想象的。特别是FIFO它有4级发送FIFO和4级接收FIFO这就让传统的数据寄存器空就填下一个的思路瞬间过时了。最直观的变化是用传统方式写发送中断你会发现自己进中断的频率降低了但如果不小心处理数据又容易悄悄丢掉。很多人在F030上写的USART驱动是纯寄存器或基于HAL库基础函数拿到G474上第一反应是照搬结果连波特率都对不上。这里的根因不是寄存器地址变了而是G474的串口时钟源和F030完全不同。F030的USART时钟只能接PCLK而G474的每个USART可以选择PCLK、系统时钟、HSI16、LSE这四种时钟源之一。一旦你用CubeMX默认配置或者沿用F0的初始化逻辑很容易选错时钟源导致波特率偏差。1.2 时钟树变了波特率计算不能想当然F030的USART波特率计算方式很直接BRR fCK / (16 × baudrate)其中fCK是串口所挂APB总线时钟。比如F030跑48MHzAPB1也是48MHz115200波特率时BRR算出来是26于是BRR写26误差在0.15%左右完全可用。G474的公式在原理上是一样的但它有Over8和Over16两种过采样模式。如果你在CubeMX或者代码里选了8倍过采样波特率寄存器的分数部分只有3位计算方式会变成USARTDIV fCK / (8 × baudrate)BRR的mantissa部分和fraction部分写法也变了。更关键的是G474的APB1和APB2最大都能跑到170MHz而你的系统主频通常就是170MHz。如果APB分频器设置了1那PCLK就是170MHz如果系统主频通过PLL产生后不是整数倍分频那串口波特率的微小误差就会放大到乱码级别。举例用内部HSI16作为串口时钟时HSI16的精度本身只有±1%在115200波特率下误差虽然还在容忍范围内但到了1Mbps以上就可能出问题。而如果你在CubeMX里选了System Clock作为USART时钟源实际是跟主频走主频PLL一旦有微小抖动高速串口就会间歇性丢字节。所以G474上的串口配置第一件事就是确认时钟源到底选的哪一个。1.3 FIFO模式带来的标志位语义变化在F030上TXE标志代表发送数据寄存器已空你往DR寄存器写一个字节等TXE置位再写下一个。G474开启FIFO之后TXE的含义变成了发送FIFO未满也就是说你在一次中断里可以连续往数据寄存器里塞好几个字节直到FIFO填满。如果不开启FIFOG474也可以完全模拟传统模式那样TXE语义与F0一致但这显然浪费了硬件资源。接收侧也一样RXNE在FIFO模式下表示接收FIFO中至少有一个字节同时还有一个RXNE标志是FIFO非空的意思具体看参考手册中的RXNE和RXFNE两个标志。由于G4的寄存器里新增了ISR、ICR、RDR、TDR等更清晰的寄存器结构很多老手还在用STM32F1时代的SR/DR寄存器写法就很容易混淆。实际上G474虽然保留了部分兼容位但不建议再用老方式操作。我的建议是如果项目不是特别抠资源直接上HAL或LL库同时配合寄存器理解否则调试时会非常难受。2. 一套能直接跑起来的初始化CubeMX配置 寄存器级代码2.1 CubeMX里的关键配置项打开STM32CubeMX选择STM32G474RET6这颗芯片后先找到要用的串口比如USART1。在Mode栏选择Asynchronous。配置115200、8位数据、无校验、1位停止位。这些是常规项真正需要特别留神的是下面几个Clock Source有PCLK1、PCLK2、SYSCLK、HSI16、LSE可选默认是PCLK但裸机配置时很容易忽略。需要根据你的总线设计选比如USART1挂在APB2上它的PCLK2是170MHzUSART2/3挂在APB1上PCLK1也是170MHz。Over Sampling16或8。默认16尽量保持16只有在需要更高波特率时才切换到8。FIFO Enable使能Enable。如果你希望使用FIFO这里必须开。Auto Baud Rate日常通信不需要如果要做特殊主机同步可以开。RS485 Direction Control如果做RS485选DE模式并配置DE引脚。TX/RX引脚在Pinout视图中手动分配注意有些引脚默认是JTAG或其它复用功能要认真核对。CubeMX生成代码后HAL_UART_Init函数会配置基本参数MspInit里会打开GPIO时钟和USART时钟。很多新手直接在主循环里调用HAL_UART_Transmit发现发不出来原因多半是MspInit里没有正确配置GPIO复用。生成代码后一定要检查MX_GPIO_Init和HAL_UART_MspInit是否正确。2.2 寄存器级初始化参考代码我习惯把CubeMX生成的代码当作骨架但为了效率和控制力收发核心部分用寄存器直接操作。下面是一段在G474上初始化USART1并开启FIFO的简单示例时钟源以PCLK2为例波特率115200不开启中断void USART1_Init(void) { // 1. 使能GPIO和USART时钟以GPIOA.9 TX、GPIOA.10 RX为例 RCC-AHB2ENR | RCC_AHB2ENR_GPIOAEN; RCC-APB2ENR | RCC_APB2ENR_USART1EN; // 2. 配置GPIO复用为AF7USART1 GPIOA-MODER ~(GPIO_MODER_MODE9_Msk | GPIO_MODER_MODE10_Msk); GPIOA-MODER | (GPIO_MODER_MODE9_1 | GPIO_MODER_MODE10_1); // 复用模式 GPIOA-AFR[1] | (7UL GPIO_AFRH_AFSEL9_Pos) | (7UL GPIO_AFRH_AFSEL10_Pos); // 3. 复位并配置USART RCC-APB2RSTR | RCC_APB2RSTR_USART1RST; RCC-APB2RSTR ~RCC_APB2RSTR_USART1RST; // 4. 禁用USART后配置参数 USART1-CR1 ~USART_CR1_UE; // 设置波特率假定PCLK2 170MHz, Over16, 115200 // USARTDIV 170000000 / (16 * 115200) 92.2309 // BRR mantissa4 | fraction 924 | round(0.2309*16)1476 USART1-BRR (92 4) | 4; // 1476 // 8位数据无校验使能发送和接收使能FIFO USART1-CR1 USART_CR1_TE | USART_CR1_RE | USART_CR1_FIFOEN | USART_CR1_UE; // 1个停止位默认不开启硬件流控 USART1-CR2 0; // 启用发送FIFO空阈值等可选项这里保持默认 USART1-CR3 0; }这里的关键是BRR计算。当PCLK2是170MHz、目标波特率115200时USARTDIV为92.2309内部直接近似为92.25实际波特率是115176误差0.02%完全没问题。由于G474的BRR寄存器本质上是一个16位寄存器高12位存整数部分低4位存小数部分所以直接写(92 4) | 4就能得到最接近的配置。如果你用的是Over8模式fraction位只有3位需要把小数部分乘以8再四舍五入计算复杂度稍高。2.3 中断收发函数示例在实际应用中串口几乎不会用阻塞发送来处理大量数据一般都用中断。在G474上最简单的接收中断写法如下volatile uint8_t rx_buf[256]; volatile uint16_t rx_index 0; void USART1_IRQHandler(void) { uint8_t data; if (USART1-ISR USART_ISR_RXNE) { data (uint8_t)USART1-RDR; // 读取自动清RXNE rx_buf[rx_index] data; rx_index 0xFF; // 环形缓冲按实际需要调整 } }注意G474的寄存器名和旧系列不同。RDR是接收数据寄存器TDR是发送数据寄存器。读取RDR会自动清除RXNE如果FIFO还有数据RXNE会继续置位这比旧版读SR再读DR的流程更简洁。发送中断的写法稍复杂因为你要在TXE置位后写TDR。如果开启了FIFO你可以连续写多个字节。例如// 假设有一个待发送数组tx_data[]tx_len为长度tx_pos为当前发送位置 void USART1_Init_TX_IT(void) { USART1-CR1 | USART_CR1_TXEIE; // 使能发送FIFO空中断 tx_pos 0; } void USART1_IRQHandler(void) { if (USART1-ISR USART_ISR_TXE) { if (tx_pos tx_len) { USART1-TDR tx_data[tx_pos]; } else { USART1-CR1 ~USART_CR1_TXEIE; // 发完关闭中断 // 如果要等彻底发完再等待TC while (!(USART1-ISR USART_ISR_TC)); } } if (USART1-ISR USART_ISR_RXNE) { // 接收处理 } }这个例子看起来简单但藏着一个大坑如果TXE中断一直开着当你把所有数据都填进TDR之后TXE仍然会周期性置位因为FIFO空了如果不在置位时关闭中断就会不断进入中断浪费CPU。而且如果你在最后一字节写入TDR后立刻关闭TXEIE这最后一字节很可能还在FIFO里没有被移位寄存器取走此时如果接着切RS485方向或者关串口就会丢数据。正确做法是发完最后一字节后等TC标志置位再关中断或切方向。TC表示移位寄存器完全空闲这时才代表所有字节已经真正从引脚上发出去了。3. 调试G474 USART时的几个真坑3.1 波特率差一点日志全是乱码我最初把USART2接到外部设备波特率设置成9600但收到的全是0x00和0x7F之类的乱码。第一反应是接线问题但检查过没问题于是用逻辑分析仪看TX引脚的波形发现一个bit的实际时间比目标9600短了约3.4%。计算波特率偏差发现原因是我在CubeMX里给USART2选了HSI16作为时钟源而HSI16的出厂标称精度是±1%但当时的环境温度让偏差达到了将近2%再加上BRR取整误差最终导致9600下的累计误差超过3%达到了乱码的临界点。后来我把USART2的时钟源改成PCLK1170MHz重新计算BRR问题立刻消失。所以在G474上除非你要在低功耗停机模式下保持串口工作否则不建议用LSE或HSI16作为UART时钟源。PCLK来自PLL锁相环稳定度高得多。这个坑最隐蔽的地方是在CubeMX里默认的时钟方案可能已经是PCLK但如果你之前用F030的工程改芯片CubeMX可能自动把时钟源保留成HSI16导致一个看似无解的乱码故障。排查手法上除了逻辑分析仪还有一个非常实用的方法同时翻转一个IO比如在波特率配置完成后每隔一个固定时间翻转一次引脚用示波器测实际频率可以间接判断系统时钟是否正常。但更直接的做法是在板子上写一个回环测试把TX和RX短接自发自收如果收到的数据完全一致说明USART基本配置正确不一致则优先查时钟源和BRR。3.2 FIFO模式下TXE和TC不分最后一个字节总是丢做RS485时发送完一帧后要切换DE方向为接收。我在F030上是通过串口发送完成中断里执行延时2us再拉低DE来保证最后一字节从总线上走完。但同样的逻辑搬到G474上偶尔会丢最后一个字节。排查后发现因为G474开启了FIFO发送完成中断触发时FIFO里可能还有数据没完全移到移位寄存器TXE置位不代表发送结束。我当时看的标志是TXE但它只代表FIFO未满不是发送移位寄存器空闲。FIFO中的最后一个字节还要等待移位寄存器一位一位地移出去这个时间取决于波特率。在115200下一字节10bit大约需要87us如果执行到拉低DE时最后一字节还没移完总线上就只剩下半个字节甚至完全消失。解决方法是发送完最后一字节后等待TC标志置位再拉低DE。或者使用G474硬件DE功能配置好DEATdead time自动控制方向切换完全不需要软件干预。硬件DE功能具体在4.3节展开。3.3 接收溢出ORE和DMA中断的优先级冲突G474的USART接收FIFO只有4级如果短时间内一帧数据超过4字节而你的中断处理不够快就会触发OREOverrun Error。在F030时代只要在中断里读DR就能自然清掉但G474的FIFO模式下清ORE要更细心必须先读ISR再读RDR把FIFO里的残留数据读完否则溢出标志会一直锁存后续数据全部进不来。我的实际案例是从机以1ms间隔收到主机发来的12字节报文我在接收中断里做的事有解析、存数组、置位事件标志。原本在F030上跑得好好的换到G474后偶尔卡死。后来发现是中断优先级不够CPU在处理更高优先级中断时USART的接收FIFO满了触发ORE。解决思路有两个一是把USART中断优先级调高二是用DMA接收让硬件自动把数据从FIFO搬到内存彻底避免ORE。另外要注意在DMA接收模式下如果设置了循环模式每传输一个字节后DMA都会把数据写到缓冲区不会产生RXNE中断这时ORE几乎不会发生但前提是DMA的优先级要合理且不能和USART的DMA请求被总线仲裁长期阻塞。4. 进阶玩法DMA IDLE中断完整接收不定长数据4.1 为什么推荐这个方案串口通信中最常见的是不定长帧比如Modbus-RTU帧、自定义协议帧。最常见实现是每收到一个字节就进一次接收中断然后判断帧头和帧尾。这在低速、短帧场景下够用但在G474这种高性能MCU上浪费CPU周期不说还容易在繁忙时丢失数据。更好的办法是DMA自动搬运接收数据再利用USART的IDLE空闲线中断判断一帧结束。一帧结束后CPU只打断一次把所有数据一次性取走。G474的USART自带IDLE检测总线在收到最后一个字节后如果持续一个字节时间内没有新的起始位IDLE标志就会置位。这个特性天然适合做帧结束判断。与定时器超时判断相比IDLE无需额外定时器资源也不占用CPU轮询。配合DMA接收缓冲区用环形模式几乎可以实现零拷贝接收。4.2 完整配置步骤和代码先看CubeMX需要配置什么。在USART配置中DMA Settings里添加USART_RX设置为循环模式Circular数据宽度Byte优先级High。同时开启USART的全局中断但暂时不开启RXNE中断。然后在NVIC中使能DMA中断和USART中断。初始化后启动DMA接收的函数大致如下#define USART_RX_BUF_SIZE 256 volatile uint8_t usart_rx_buf[USART_RX_BUF_SIZE]; volatile uint16_t usart_rx_len 0; volatile uint16_t last_dma_pos 0; void USART1_Start_DMA_Receive(void) { // 设置DMA循环接收 hdma_usart1_rx.Init.Mode DMA_CIRCULAR; HAL_DMA_Init(hdma_usart1_rx); __HAL_LINKDMA(huart1, hdmarx, hdma_usart1_rx); // 开启IDLE中断不开启RXNE __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 启动DMA HAL_UART_Receive_DMA(huart1, (uint8_t *)usart_rx_buf, USART_RX_BUF_SIZE); }然后在USART1中断处理函数中往HAL库默认中断里追加IDLE处理void USART1_IRQHandler(void) { // 处理IDLE标志 if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); uint16_t current_dma_pos USART_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); if (current_dma_pos last_dma_pos) { usart_rx_len current_dma_pos - last_dma_pos; } else { usart_rx_len USART_RX_BUF_SIZE - last_dma_pos current_dma_pos; } last_dma_pos current_dma_pos; // 这里把usart_rx_len对应的数据取出并解析注意环形缓冲跨越结尾时需要拼接 // 实际应用中往往把usart_rx_buf和长度交给协议解析函数处理后再更新last_dma_pos } HAL_UART_IRQHandler(huart1); }这里最核心的是通过DMA当前计数器的值反推本次接收了多少字节。DMA计数器在循环模式下会随着传输递减从初始值减到0后又回到初始值。通过两次IDLE中断之间的计数差值就能得到新收到的一帧长度。需要注意如果环形缓冲区长度是2的幂可以更方便地用位运算处理回绕比如缓冲区长度设为256那么current_dma_pos - last_dma_pos 0xFF 就能得到长度不需要分支判断。这个技巧在G474上很实用因为DMA计数器是16位但一般环形缓冲都小于256字节。4.3 顺便解决RS-485自动方向控制G474的USART有一个传统F0/F1都没有的硬件功能Driver Enable mode俗称DE模式。它可以把RS485收发器的方向控制引脚直接映射到串口的某个复用引脚由USART硬件在发送开始前自动拉高DE发送结束后自动拉低DE中间还支持可配置的死区时间DEAT和延迟时间DET彻底解放CPU和定时器。配置方式很简单。CubeMX里在USART的RS485 Direction Control选择DE using GPIO然后指定一个引脚作为DE。底层配置是USART1-CR3 | USART_CR3_DEM; // 使能DE模式 USART1-CR3 ~USART_CR3_DEP; // DE极性高电平使能低电平禁用 USART1-CR3 | (0x05 USART_CR3_DEAT_Pos); // 设置DEAT死区时间为5个bit如果你在上面的发送代码里发出了最后一字节DE引脚并不会立即拉低而是会等待发送移位寄存器变空再经过DEAT的时间后拉低。这个硬件行为解决了之前丢最后一个字节的问题比在软件里延时稳定得多。5. 让G474串口更稳定的一些经验总结5.1 时钟源和波特率选择建议我在G474上做过多路串口同时跑的项目最后的经验是日常通信波特率不超过1Mbps时全部使用PCLK作为USART时钟源不开Over8。只有需要和特殊外设做高速通信时才开启Over8配合8倍过采样但必须仔细算BRR误差并且用示波器验证。如果在低功耗模式下需要保持串口唤醒可以选用LSE作为USART时钟但LSE只有32.768kHz波特率不能太高一般只适合低频唤醒帧。多个串口之间的时钟源尽量独立配置避免一个外设的时钟异常影响其他串口。5.2 中断优先级和临界区调优G474是M4内核支持可配置的16级抢占优先级。USART接收中断原则上要设置成比普通任务更高的优先级但不要高于SysTick。我常用的配置是USART_IDLE中断抢占优先级2DMA传输完成中断抢占优先级3普通外设中断优先级4。如果既开IDLE又开DMA注意不要让DMA传输完成中断阻塞USART中断太久。在临界区比如关中断保护共享变量时千万不要在临界区里调用HAL_UART_Transmit之类的阻塞发送函数。G474主频虽然高但发送一个字节仍要等待移位寄存器走完如果正好赶上串口波特率低临界区时间会被拉长到几十微秒直接影响实时性。我的做法是先填充发送FIFO然后在临界区外开启中断发送。5.3 调试工具与回环测试强烈建议在工程里保留一个回声测试功能初始化串口后把收到的字节原样发回去。这个功能看起来简单却能最快定位硬件问题。在G474上回环测试也能验证FIFO和中断标志是否正确。如果你用逻辑分析仪至少要有2个通道一个接TX一个接RX直接看总线波形的波特率。分析仪采样率最好在25MHz以上否则低速还好高速波特率下波形抖动会掩盖真实错误。我还会在发送函数中临时插入一个GPIO翻转来观测发送耗时。因为开FIFO后HAL库的阻塞发送和中断发送实际行为不同单纯靠代码逻辑很难判断数据是在FIFO里还是在总线上。GPIO翻转点可以直观看到从调用发送函数到TC置位的时间差这样你就能知道DE切换时机是否合理。5.4 F030代码迁移到G474的对照清单如果你也是从F030迁移过来的下面这张表基本覆盖了最常见的差异点项目STM32F030STM32G474RET6注意事项CPU核心Cortex-M0Cortex-M4F中断向量、FPU寄存器需要适配系统主频48MHz170MHz总线时钟和PLL配置差异大USART数量1~2个5个LPUART资源分配更灵活但时钟源更复杂时钟源仅PCLKPCLK/SYSCLK/HSI16/LSE选错时钟源是乱码第一原因过采样支持Over8/16支持Over8/16BRR小数位宽不同发送/接收FIFO无4级TXE/TC语义变化寄存器布局SR/DR为主ISR/ICR/RDR/TDR为主旧代码必须改自动波特率无支持可用于特殊同步场景RS485硬件DE无支持省掉软件延时和定时器DMA常规常规循环配合IDLE好用这张表不是让你逐条生搬硬套而是提醒你G474的USART已经是一个现代串口了用F0时代的思维去套很容易踩坑。最稳妥的过渡方式是以HAL/LL库为基础但要确保能看懂每个寄存器位作用这样遇到异常时不会无路可走。最后再分享一个小技巧在调试G474的USART时如果遇到非常奇怪的异常行为先做一次外设复位。G474的每个串口都有独立复位位比如RCC_APB2RSTR中的USART1RST。很多标志位和FIFO状态在软件复位后才会彻底归零比逐个清标志位简单得多。我现在的习惯是每次在初始化串口前都执行一次置位复位位-清复位位操作这个小小的动作规避了大量莫名其妙的启动问题。