资讯动态

STM32串口DMA收发卡死:FE/NE错误恢复与HAL库实战指南

发布时间:2026/9/27 20:27:14 来源:尧图企业网站定制
刚拿到一块STM32F103的板子我习惯先把串口调通再上业务。这次用HAL库做串口DMA收发调了一下午踩到一个非常典型的坑板子上电后前几帧数据正常多收发几轮之后串口就再也不进数据了复位后又恢复跑一会儿又犯病。最后把目光锁定在UART_SR寄存器里的FE和NE标志位上FE是帧错误Framing ErrorNE是噪声错误Noise Error这两个标志在DMA模式下会引发一连串连锁反应导致接收直接“罢工”。这篇文章就把我这次的排查思路、代码恢复方案和调试经验一次性讲清楚。内容主要针对STM32F103 HAL库 串口DMA收发场景适合正在被串口卡死、数据乱码、不定长接收搞到头秃的朋友。不管你是刚入门还是已经写过不少项目只要遇到UART_FLAG_FE或NE这篇文章里的思路和代码都能直接拿来用。1. 为什么串口DMA会突然卡死FE和NE错误的底层机制1.1 帧错误FE和噪声错误NE到底是怎么产生的很多朋友看到FE和NE这两个缩写就发怵其实它们的原理并不复杂。UART是异步串行通信每一帧数据由起始位、数据位、可选的校验位和停止位组成起始位是低电平停止位是高电平。接收端通过检测起始位的下降沿来对齐时钟然后在一个位的中间区域采样数据。FE帧错误的意思是接收端在停止位的位置采到了低电平。正常情况下停止位必须是高电平如果那一位被拉低USART硬件就会立刻打上FE标志。这个场景最常见于波特率不匹配、通信双方时钟偏差过大、或者RX线因为电平不对被长时间拉低。换句话说发送端认为自己在发停止位但接收端看到的却是低电平那这一帧就废了。NE噪声错误的意思是在采样某个数据位时USART内部的多个采样点没有达成一致。STM32的USART外设会用波特率的16倍频率对RX引脚采样一个位里取多个采样点做多数表决如果这几个采样点里既有高电平又有低电平说明信号在采样窗口内发生了跳变硬件就认为线路噪声太严重于是打上NE标志同时把这位的采样结果也标记为不可靠。这里还有个容易混淆的点NE和FE经常成对出现。比如RX线上有一串毛刺毛刺刚好落在停止位的位置那这一帧既可能触发NE也可能触发FE所以你在SR寄存器里看到这两个标志同时为1不要惊讶这恰恰说明问题出在信号完整性上而不是某一个孤立寄存器位的问题。1.2 HAL库在DMA模式下的处理方式才是卡死元凶为什么普通中断接收很少遇到这种“卡死”一换DMA就特别容易撞上因为普通模式下每个字节都会进中断CPU会顺手把错误标志清掉。而DMA模式下数据由DMA控制器搬运到内存CPU完全不碰DR寄存器错误标志只能在UART中断里被批量发现。HAL库的HAL_UART_IRQHandler在处理DMA模式时检测到FE、NE、ORE溢出错误后不会帮你优雅地恢复它默认的路径是先把DMA通道停掉把huart的RxState和TxState置回READY状态然后调用HAL_UART_ErrorCallback。注意这个顺序它在调回调之前就已经把DMA停了。所以如果你只实现了数据接收回调没有实现HAL_UART_ErrorCallback一旦发生FE或NE接收DMA就被HAL库关掉之后再发多少数据都不会进中断表现就是复位后能收几帧多跑几次后串口彻底“没反应”。这不是芯片坏了也不是DMA配置错了而是HAL库的错误处理机制在DMA模式下本来就是这个逻辑。2. 先查硬件再查代码排除接线、电平和时钟的隐患2.1 接线、电平和共地问题最容易忽略我调试串口的第一条经验就是遇到FE/NE先别急着改代码99%的问题根因在硬件环境。最典型的是共地问题。两个板子之间做串口通信如果各自用独立电源供电又没有把GND连在一起接收端的参考电平就是飘的发送端发出来的高电平和低电平在接收端看来很可能就变成了错误的电压区间NE标志偶发甚至持续出现就是必然结果。还有电平匹配问题。STM32F103的RX引脚不要直接接5V TTL模块的输出很多老式USB转TTL模块默认输出5V逻辑电平和STM32的3.3V电平体系不匹配。轻则边界电平识别含糊、触发NE重则长期超压损坏引脚。我一般直接用3.3V逻辑电平的转接模块或者加电平转换芯片把两边信号变成同一套电平标准再对接。杜邦线长短和布局也不能忽视。飞线超过20厘米尤其从电机驱动、继电器、PWM输出这些高干扰源旁边路过NE基本跑不掉。如果是这种场景优先降低波特率到9600或者19200或者换用双绞线、屏蔽线甚至直接上RS485做差分传输比在代码里反复加错误恢复更治本。2.2 时钟配置和波特率精度别让CubeMX背锅串口波特率是从外设时钟分频来的。STM32F103的USART1挂在APB2上USART2和USART3挂在APB1上。CubeMX生成工程时会根据你填的外部晶振频率、PLL倍频系数、总线分频系数帮你把PCLK算好然后HAL库再用这个PCLK去计算USARTDIV和BRR寄存器值。这里最大的坑是外部晶振频率填错。很多STM32F103最小系统板用的是8MHz晶振但HAL库的HSE_VALUE宏默认值有的版本是25MHz有的版本是8MHz。如果你的板子是8M晶振工程里却按25M算时钟树那PCLK以及波特率都会严重偏离真实值。轻则偶发FE重则一上电就是满频报错。我当时的排查方法是打开调试器看SystemClock_Config里实际算出来的PCLK是多少再用示波器或逻辑分析仪量一下TX脚看实际波形频率离115200差多少。还有人会忽略CD和量化误差的问题。比如PCLK1是36MHz目标波特率115200BRR 36000000 / 115200 312.5取整后实际波特率和目标之间就可能引入0.16%左右的偏差。这个偏差本身不大但如果再把晶振本身的频偏叠加上去误差超过2%左右FE就会开始冒头。2.3 CubeMX里DMA配置的常见错误硬件没问题之后再回头看CubeMX里的DMA配置。串口DMA接收方向一定要选PeripheralToMemory意思是数据从外设数据寄存器搬到内存缓冲区。很多人把方向和发送搞反结果DMA一启动就开始往DR寄存器写数据接收自然不对。外设地址增量必须关闭。串口的DR寄存器只有一个固定地址DMA每次搬运都从同一个外设地址拿数据。如果这里开了增量DMA会跳到乱七八糟的地址去取数据接收缓冲区里的内容就是一堆不可预期的值。内存地址增量则要打开让数据连续写在rx_buf里。数据宽度统一选Byte因为串口DR寄存器是8位访问的如果你的内存宽度选了HalfWord或Word接收结果会错位。还有DMA通道映射。USART1_TX对应DMA1通道4USART1_RX对应DMA1通道5USART2_TX对应DMA1通道7USART2_RX对应DMA1通道6USART3_TX对应DMA1通道2USART3_RX对应DMA1通道3。这些映射是芯片固定的换串口不换DMA通道或者直接照抄别的工程配置DMA根本不会工作。3. 彻底解决DMA接收错误恢复与HAL库重写方案3.1 标准错误恢复流程清标志、停DMA、重新接收现在写代码层面的解决方案。核心思路是在HAL_UART_ErrorCallback里做完整的恢复动作一共四步顺序不要乱。第一步清除错误标志位。XTAL库在错误回调触发时SR寄存器里的FE、NE、ORE可能还挂着。不清掉的话下次接收还会带着旧错误状态可能又触发一次错误中断。用__HAL_UART_CLEAR_FLAG宏一次性清掉。第二步调用HAL_UART_DMAStop把DMA和串口状态机恢复到可以重新配置的状态。这里要注意如果此刻发送DMA也在占用中DMAStop会同时停掉发送通道。第三步清空接收缓冲区。错误发生时DMA可能已经搬了一部分数据不清空的话下次会和新数据混在一起。第四步重新调用HAL_UARTEx_ReceiveToIdle_DMA或HAL_UART_Receive_DMA开启下一次接收。代码参考void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // 清掉浮空、帧、真整和溢出错误标志 __HAL_UART_CLEAR_FLAG(huart, UART_FLAG_PE | UART_FLAG_FE | UART_FLAG_NE | UART_FLAG_ORE); // 停止DMA接收 HAL_UART_DMAStop(huart); // 清空接收缓冲区 memset(dma_rx_buf, 0, RX_BUF_SIZE); rx_len 0; // 重新启动DMA接收 HAL_UARTEx_ReceiveToIdle_DMA(huart, dma_rx_buf, RX_BUF_SIZE); } }这段代码就是我实际用的恢复函数。看起来很简单但很多项目挂在第二步因为大家以为清个标志就万事大吉了。清标志只是告诉硬件“错误我知道了”但HAL库的状态机还停在READY状态吗不如果你不清或者不重新初始化DMA下一次接收是不会启动的。HAL_UART_DMAStop在这里相当于做了一个软复位把所有状态都拉回来再启动新接收。3.2 不定长DMA接收的完整示例代码串口DMA接收最常见的需求是不定长数据接收比如接一个串口传感器、蓝牙模块或者上位机下发的指令每次数据长度不一定。新版HAL库可以用HAL_UARTEx_ReceiveToIdle_DMA它结合了USART空闲中断和DMA接收数据来了之后DMA一直往缓冲区塞直到线路空闲HAL库会调HAL_UARTEx_RxEventCallback把本次实际接收长度告诉你。主流程代码#define RX_BUF_SIZE 256 uint8_t dma_rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_len 0; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); // 初始化时先清一次错误防止上电瞬间线路不稳定带进错误状态 __HAL_UART_CLEAR_FLAG(huart1, UART_FLAG_FE | UART_FLAG_NE); HAL_UARTEx_ReceiveToIdle_DMA(huart1, dma_rx_buf, RX_BUF_SIZE); while (1) { if (rx_len) { // 在这里处理dma_rx_buf里的rx_len字节 // 处理完业务逻辑之后把rx_len清零 rx_len 0; // 如果不希望这次接收的半包数据被下一次错误恢复覆盖可以在这里重新启动接收 HAL_UARTEx_ReceiveToIdle_DMA(huart1, dma_rx_buf, RX_BUF_SIZE); } } }中断回调void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { // 记录本次实际接收到的字节数 rx_len Size; } }这个方案比我以前用的“DMA半满/全满中断 IDLE中断”要省心不少。老方案里接收长度需要自己去读DMA剩余计数器再做减法算出有效数据长度步骤繁琐很容易出错。新库帮你把这些封装好了你只要在回调里消费数据即可。如果你用的HAL库版本比较老没有HAL_UARTEx_ReceiveToIdle_DMA这个函数那就要自己在USART1_IRQHandler里判断IDLE标志然后停DMA、读剩余计数算长度再重新启动接收。原理一样但代码量会多一些。我建议直接升级到新版HAL库把时间省出来。3.3 中断优先级与回调处理的注意事项串口DMA收发涉及两个中断源USART全局中断和DMA通道中断。我在CubeMX里会给USART中断和DMA接收中断分别配置优先级比如USART1中断抢占优先级设成2DMA1_Channel5接收中断设成3这样USART错误处理能及时打断DMA中断里的收尾工作。回调函数里不要做大工作量的事情。有人喜欢在HAL_UARTEx_RxEventCallback里直接打印日志、刷新OLED、解析协议这些都是耗时操作。接收回调里只记录长度和标志位具体解析放主循环。否则你在中断里处理太久后续字节又到来了就会触发ORE溢出错误刚解决的问题又变成新问题。另外在多字节中断里使用HAL_Delay要特别小心。HAL_Delay依赖SysTick中断如果你的串口中断优先级高于SysTick那么SysTick中断被堵住HAL_Delay就不会按时返回程序会卡死在回调里。这个坑我踩过后来凡是中断回调里需要延时的逻辑全部改成非阻塞状态机或者直接去掉。4. 常见问题速查与高效调试技巧4.1 典型症状与排查方向对照表我整理了一份调试速查表把这次遇到的各种现象、可能原因和解决方向都列出来方便你直接对照。现象可能原因排查/解决方向复位后几帧正常多轮后卡死HAL库检测到FE/NE后停掉了DMA检查并实现HAL_UART_ErrorCallback做完整恢复上电一直报FE完全收不到数据RX线被拉低、模块未共地、电平不匹配检查GND连接、加上拉电阻、统一电平标准偶发NE和乱码线路干扰、杜邦线过长、电源纹波大降低波特率、缩短走线、加屏蔽/磁珠DMA发送第二次就失败上一次发送未完成或TC标志未清除在TxCplt回调后再启动下一次发送接收数据长度不对环形模式边界处理错误、缓冲区分片改用NormalIDLE模式或自己管理半满/全满中断接收卡死且SR寄存器ORE也置位溢出错误CPU来不及处理接收提高DMA/串口中断优先级精简回调逻辑4.2 调试工具和现场思路遇到FE/NE我建议按这个顺序查先看波形再看时钟最后才看代码。逻辑分析仪是成本最低的工具把STM32的RX引脚接上去抓一帧失败时的波形。如果停止位位置是低电平FE基本实锤如果波形上有明显毛刺、上升沿下降沿缓慢、电平没到标准高/低值NE实锤。降速法非常高效。把波特率从115200降到9600如果错误消失说明大概率是硬件限制或时钟偏差导致的高速率容差不足如果9600仍然报错就不用怀疑波特率了直接查接线和电平。这个排除法能快速缩小范围。还有一个很实用的调试技巧在主循环里周期性地读一下USART1-SR寄存器把错误标志位实时打印到调试串口。比如uint32_t sr USART1-SR; if (sr UART_FLAG_FE) { // 记录帧错误次数 }这样你可以先不管DMA那套逻辑直接观察硬件上报的错误情况。如果一上电就不断出现FE大概率是RX线被外部拉低或者模块没有共地如果只是偶发一次然后稳定再考虑是不是上电瞬间时序问题。此时在初始化里加一个100ms延时或者吞掉第一帧数据往往能解决。4.3 一套稳妥的初始化与运行建议经过这次折腾我现在做串口DMA项目的初始化固定按下面这套来第一初始化前先清一次错误标志。上电瞬间如果对端设备也在启动TX线处于不稳定状态可能产生虚假错误。第二使用HAL_UARTEx_ReceiveToIdle_DMA启动接收。第三错误回调里完整恢复。第四数据接收回调只做记录不在中断里做协议解析。第五发送DMA尽量不要连续多次调用HAL_UART_Transmit_DMA确认上一次发送完成再启动下一次否则要么返回HAL_BUSY要么覆盖前一次数据。如果你在调试中遇到DMA发送不能连续发送最常见的解决方法是在HAL_UART_TxCpltCallback里置一个发送完成标志主循环等到这个标志再发下一帧。或者使用标准库的DMA方式时注意在启动发送前清除DMA的TC中断标志否则第二次发送不会触发完成中断。这些原理在HAL库和标准库是一致的。另外不管项目大小我觉得都应该把错误恢复函数提前写好而不是等现场问题再补。FE/NE这些问题本来就不常有但一旦出现没有恢复函数就意味着整个通信链路瘫痪。很多朋友觉得HAL库“不好用”其实HAL库已经把错误路径暴露得很明显了只是我们常常只关心正常的收发回调忽略了那个ErrorCallback。把错误处理当成正常流程的一部分串口DMA就会稳定很多。回头再看这次的问题核心其实不是FE/NE本身而是我对HAL库DMA错误处理机制不够熟悉导致错误发生后没有主动接管恢复流程。现在遇到串口问题心态就很稳了先量波形再查时钟最后看代码这个顺序从来没让我失望过。希望这篇避坑指南也能让你少调一个下午。

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

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

免费获取报价 →
↑