ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F103串口DMA接收FE/NE错误全解析与实战解决方案

STM32F103串口DMA接收FE/NE错误全解析与实战解决方案 1. 串口DMA接收翻车现场为什么FE和NE总是阴魂不散搞过STM32F103的人十个里有八个在串口DMA接收上栽过跟头。你满心欢喜地用CubeMX配好USART1的DMA接收代码一跑发现数据偶尔丢几个字节或者干脆整帧错位。打开调试器一看SR寄存器里FEFraming Error和NENoise Error两个标志位赫然在目。更气人的是你明明清了标志下次接收照样报错。这不是你代码写得烂而是HAL库在串口DMA接收这件事上确实埋了几个不太显眼的坑。STM32F103的USART外设本身并不复杂但HAL库为了兼顾各种使用场景在错误处理和DMA交互上做了一些“通用化”设计这些设计在DMA接收模式下反而容易引发问题。这篇文章面向的是已经能用HAL库跑通串口收发、但在DMA接收稳定性上遇到瓶颈的开发者。我会从FE和NE的成因讲起拆解HAL库在DMA接收时的错误处理逻辑然后给出几套经过实际项目验证的解决方案。不管你是用CubeMX生成的代码还是手动移植的HAL库这里的内容都能直接参考。先明确一个基本认知FE和NE不是“错误”而是USART外设对物理层信号质量的状态标记。FE表示在期望的停止位位置没有检测到有效的停止位NE表示在接收线上检测到了噪声。这两个标志在轮询和中断接收模式下通常不会造成大问题但在DMA接收模式下它们会触发HAL库的错误回调进而影响DMA的接收状态机。我见过太多项目在这上面浪费时间最后用“降低波特率”或者“加硬件滤波”草草了事。其实只要理解了HAL库的处理流程软件层面就能解决绝大部分问题。2. FE与NE的底层原理从物理层到HAL库的状态传递2.1 USART接收状态机与错误标志的产生条件STM32F103的USART接收单元在每个比特位采样时会进行多次采样通常是3次来判断电平。当检测到起始位下降沿后接收移位寄存器开始工作。在停止位的位置如果采样到的电平不是高电平硬件就会置位FE标志。NE标志则是在采样过程中发现多次采样结果不一致时置位说明信号上有毛刺或噪声。这两个标志的置位并不影响当前字节的接收——数据仍然会被移入接收数据寄存器。但问题在于HAL库在DMA接收模式下会把这些标志当作“错误事件”来处理。具体来说当FE或NE置位时如果开启了错误中断EIE就会触发USART的中断服务程序。HAL库的USART_IRQHandler会检查这些标志并调用HAL_UART_ErrorCallback。关键点来了在DMA接收模式下HAL库的错误处理函数会执行一系列操作包括中止DMA接收、重置接收状态、清除标志等。如果这些操作没有正确完成下一次接收就会处于异常状态。2.2 HAL库DMA接收的状态机与错误处理流程HAL库的UART DMA接收使用了一个状态变量huart-RxState来跟踪当前接收阶段。正常流程是READY - BUSY_RX - READY。当发生错误时HAL库会尝试把状态恢复到READY并调用错误回调。但在实际代码中HAL_UART_ErrorCallback的默认实现是空的而HAL库内部的错误处理逻辑存在几个问题第一当FE或NE置位时HAL库会调用UART_EndRxTransfer这个函数会关闭DMA接收通道、禁用USART的接收中断和错误中断。如果此时DMA正在传输数据突然关闭会导致部分数据丢失。第二错误处理完成后HAL库不会自动重新启动DMA接收。你需要手动调用HAL_UART_Receive_DMA重新开启接收。很多开发者不知道这一点以为错误处理后接收会自动恢复。第三如果错误标志没有完全清除下一次接收会立即再次触发错误中断形成死循环。2.3 为什么轮询模式下没问题DMA模式下就翻车轮询模式下你调用HAL_UART_ReceiveHAL库会阻塞等待数据每次接收一个字节后检查标志。如果FE或NE置位HAL库会返回错误码你可以选择忽略或处理。由于是同步操作状态机不会混乱。中断模式下每个字节触发一次中断HAL库在中断里处理错误标志虽然效率低但状态机是逐步推进的不容易出现状态错乱。DMA模式下DMA控制器在后台搬运数据CPU不参与每个字节的接收。当错误发生时DMA可能已经搬运了多个字节而HAL库的错误处理会突然打断这个过程。更麻烦的是DMA接收完成中断和USART错误中断可能同时挂起中断优先级处理不当就会导致状态机卡死。我实测过一个案例波特率115200发送方连续发送1000字节接收方用DMA接收。如果不处理FE和NE大约每接收200-300字节就会触发一次错误然后DMA接收停止后续数据全部丢失。用逻辑分析仪抓波形发现发送方的信号质量其实很好FE和NE的置位主要是由于发送方在上电初始化时产生的电平抖动。3. 彻底解决FE和NE的三种实战方案3.1 方案一禁用错误中断在DMA完成回调中统一清标志这是最简单粗暴但非常有效的方案。核心思路是不让FE和NE触发中断而是在DMA接收完成中断里统一清除所有错误标志。具体操作是在USART初始化完成后调用__HAL_UART_DISABLE_IT(huart1, UART_IT_ERR)。这样FE和NE置位时不会触发错误中断HAL库不会执行错误处理流程DMA接收可以继续进行。然后在DMA接收完成回调函数HAL_UART_RxCpltCallback中读取SR寄存器检查FE和NE标志如果置位则通过读取DR寄存器来清除注意清除FE和NE需要先读SR再读DR。最后重新启动DMA接收。这种方案的优点是代码改动小不影响DMA的连续接收。缺点是如果FE和NE频繁置位说明物理层确实有问题长期来看还是需要检查硬件。我在一个工业采集项目里用过这个方案设备运行在电机旁边电磁干扰比较大。禁用错误中断后DMA接收再也没停过数据完整性通过应用层CRC校验来保证。实测连续运行72小时没有出现接收卡死。3.2 方案二重写错误回调在错误中断中恢复DMA接收如果你需要保留错误中断来监控通信质量那就必须重写HAL_UART_ErrorCallback。在这个回调里你需要做三件事第一读取SR和DR寄存器清除FE和NE标志。注意顺序先读SR再读DR。第二检查DMA接收状态如果DMA已经被HAL库中止需要重新配置DMA接收缓冲区。这里有个细节HAL库在错误处理中会调用UART_EndRxTransfer这个函数会把huart-RxState设为READY但不会重新配置DMA。你需要手动调用HAL_UART_Receive_DMA重新启动接收。第三如果错误频繁发生建议在回调里加一个计数器超过阈值后上报应用层提示检查硬件。这个方案的关键在于理解HAL库的错误处理流程。我建议在错误回调里加一句__HAL_UART_CLEAR_OREFLAG(huart1)来清除溢出错误因为FE和NE有时会伴随ORE一起出现。3.3 方案三使用LL库直接操作寄存器绕过HAL库的错误处理如果你对HAL库的错误处理实在不放心可以直接用LL库或者寄存器操作来配置USART和DMA。LL库的USART配置更直接没有HAL库那套状态机。具体做法是用LL_USART_Init配置USART参数用LL_DMA_Init配置DMA通道然后使能USART的DMA接收请求。在DMA传输完成中断里直接读取USART的SR和DR寄存器清标志然后重新设置DMA的传输长度。这种方案的控制粒度最细但代码量最大适合对稳定性要求极高的场景。我在一个医疗设备项目里用过LL库方案因为那个项目要求通信误码率低于10^-9HAL库的错误处理会引入不确定性。三种方案的对比方案代码改动量稳定性适用场景禁用错误中断小高干扰可控应用层有校验重写错误回调中中需要监控通信质量LL库直接操作大最高高可靠性要求4. 完整实操从CubeMX配置到代码实现4.1 CubeMX中的关键配置项在CubeMX里配置USART1时有几个选项直接影响FE和NE的处理DMA接收配置在DMA Settings里添加USART1_RX模式选Normal不是Circular数据宽度选Byte。为什么选Normal因为Circular模式下DMA会自动重装但HAL库的错误处理会打乱这个流程Normal模式配合手动重启更可控。USART中断配置在NVIC Settings里USART1全局中断必须使能。DMA通道中断也要使能。错误中断Error Interrupt建议先不使能用方案一的话就不需要。USART参数波特率、字长、停止位、校验位按实际需求配。这里有个经验如果通信距离超过1米建议把停止位设为2位可以显著降低FE的发生率。生成代码后你会得到MX_USART1_UART_Init和MX_DMA_Init两个函数。注意DMA初始化必须在USART初始化之前调用否则DMA通道配置可能不生效。4.2 代码实现禁用错误中断手动清标志在main.c的初始化部分MX_USART1_UART_Init之后添加__HAL_UART_DISABLE_IT(huart1, UART_IT_ERR);然后定义接收缓冲区和启动DMA接收#define RX_BUFFER_SIZE 256 uint8_t rxBuffer[RX_BUFFER_SIZE]; HAL_UART_Receive_DMA(huart1, rxBuffer, RX_BUFFER_SIZE);在DMA接收完成回调里处理数据并重启接收void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // 清除FE和NE标志 if (__HAL_UART_GET_FLAG(huart, UART_FLAG_FE) || __HAL_UART_GET_FLAG(huart, UART_FLAG_NE)) { volatile uint32_t tmp; tmp huart-Instance-SR; tmp huart-Instance-DR; (void)tmp; } // 处理接收到的数据 ProcessData(rxBuffer, RX_BUFFER_SIZE); // 重新启动DMA接收 HAL_UART_Receive_DMA(huart1, rxBuffer, RX_BUFFER_SIZE); } }注意那个volatile变量tmp它的作用是防止编译器优化掉读SR和DR的操作。有些编译器会把“读了但没用”的语句优化掉导致标志清除失败。4.3 代码实现重写错误回调的完整版本如果你选择方案二错误回调需要这样写void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // 清除所有错误标志 __HAL_UART_CLEAR_PEFLAG(huart); // 清除PE __HAL_UART_CLEAR_FEFLAG(huart); // 清除FE __HAL_UART_CLEAR_NEFLAG(huart); // 清除NE __HAL_UART_CLEAR_OREFLAG(huart); // 清除ORE // 重新启动DMA接收 HAL_UART_Receive_DMA(huart1, rxBuffer, RX_BUFFER_SIZE); } }这里有个坑__HAL_UART_CLEAR_FEFLAG和__HAL_UART_CLEAR_NEFLAG这两个宏在有些HAL库版本里不存在你需要手动实现#define CLEAR_FE_NE_FLAG(__HANDLE__) do { \ volatile uint32_t tmp; \ tmp (__HANDLE__)-Instance-SR; \ tmp (__HANDLE__)-Instance-DR; \ (void)tmp; \ } while(0)另外错误回调里重新启动DMA接收之前最好先调用HAL_UART_DMAStop确保DMA通道已经关闭避免重复配置。4.4 参数计算DMA缓冲区大小与波特率的匹配DMA缓冲区大小不是随便设的。如果缓冲区太小DMA完成中断太频繁CPU开销大如果太大单次接收延迟高而且错误发生时丢失的数据更多。一个实用的计算方法是假设波特率115200每个字节10位1起始8数据1停止那么每秒最多传输11520字节。如果你希望每10ms处理一次数据缓冲区大小设为128字节左右比较合适。对于STM32F103DMA通道有传输长度限制最大65535所以缓冲区不要超过这个值。另外F103的RAM只有20KB缓冲区太大会挤占其他变量的空间。我一般用256字节的缓冲区配合DMA半传输中断HT和传输完成中断TC可以实现双缓冲效果进一步降低数据丢失风险。5. 常见问题排查与避坑经验5.1 问题速查表现象可能原因排查方法解决方案DMA接收一次后停止错误中断触发后未重启DMA在错误回调里加打印重写错误回调重启DMA数据偶尔错位FE/NE导致DMA指针偏移用逻辑分析仪抓波形禁用错误中断应用层校验接收卡死中断不触发ORE溢出导致中断锁死检查SR寄存器ORE位清除ORE重新初始化USART波特率越高越容易出错信号完整性差示波器看信号质量降低波特率或加终端电阻上电后第一次接收必出错发送方上电抖动延迟启动接收上电后延时100ms再开DMA5.2 独家避坑技巧第一个技巧在USART初始化之后、启动DMA接收之前先手动清除一次所有错误标志。因为上电时USART引脚上的电平跳变可能已经置位了FE或NE如果不先清除第一次接收就会触发错误。// 在HAL_UART_Receive_DMA之前调用 volatile uint32_t tmp; tmp huart1.Instance-SR; tmp huart1.Instance-DR; (void)tmp;第二个技巧如果通信环境干扰大可以在USART的RX引脚上加一个RC低通滤波。具体参数串联100欧姆电阻对地接100pF电容截止频率约16MHz对115200波特率的数据没有影响但能滤掉高频噪声。这个硬件改动成本极低效果立竿见影。第三个技巧用DMA的循环模式配合IDLE中断。IDLE中断在接收线空闲时触发可以判断一帧数据结束。这样就不需要精确知道每帧的长度配合DMA循环模式接收永远不会停止。具体做法是DMA设为Circular模式使能USART的IDLE中断在IDLE中断里计算接收到的数据长度并处理。void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); uint16_t len RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); ProcessData(rxBuffer, len); } HAL_UART_IRQHandler(huart1); }这个方案的好处是DMA永远在运行不需要在回调里重启FE和NE的影响也被降到最低。我在多个量产项目里用的都是这个方案稳定性非常好。5.3 调试工具与手段调试串口DMA接收问题光看代码是不够的。我常用的工具组合是逻辑分析仪抓RX线的波形看数据位和停止位是否完整有没有毛刺。Saleae的逻辑分析仪配合UART解码器能直接看到每个字节的值和错误标志。示波器看信号质量特别是上升沿和下降沿是否陡峭有没有过冲或振铃。如果信号质量差先解决硬件问题。串口调试助手发送固定模式的数据比如0x55或0xAA方便观察错位情况。Keil的Event Recorder实时监控HAL库的状态变量和错误回调调用次数不用打断点就能看到运行时行为。我一般先用逻辑分析仪确认物理层没问题然后再用Event Recorder看HAL库的状态机。如果物理层没问题但HAL库状态机混乱那就是软件问题按上面的方案改代码就行。6. 从根上理解HAL库串口DMA接收的设计取舍HAL库的串口DMA接收实现本质上是在通用性和效率之间做妥协。它要支持轮询、中断、DMA三种模式还要支持各种STM32系列所以错误处理逻辑写得比较保守——一有错误就中止接收把控制权交还给应用层。这种设计在低速、短帧、干扰小的场景下没问题。但在高速、长帧、干扰大的场景下频繁的错误中断会严重拖累性能。所以ST后来在HAL库的更新版本里增加了UART_Start_Receive_DMA这样的函数允许更灵活的DMA配置。对于STM32F103这种经典款我的建议是不要完全依赖HAL库的默认错误处理。要么禁用错误中断自己管要么用LL库直接操作寄存器。HAL库适合快速原型开发但量产项目里关键通信路径上的代码最好自己掌控。另外STM32F103的USART外设本身有个特点FE和NE标志的清除需要先读SR再读DR而且读DR会清空接收数据寄存器。如果你在清除标志的时候不小心把有效数据读走了就会丢数据。所以清除标志的代码要放在数据处理之后或者用DMA把数据搬走之后再清。还有一个容易被忽略的点STM32F103的DMA通道和USART的对应关系是固定的。USART1_RX只能用DMA1_Channel5USART1_TX只能用DMA1_Channel4。CubeMX会自动分配但如果你手动改代码搞错了通道就完全收不到数据。最后说一个实际项目里的教训。我曾经在一个项目里用HAL库的DMA接收波特率921600结果FE和NE频繁触发DMA接收断断续续。后来发现是PCB走线太长RX线没有做阻抗匹配。把走线缩短、加串阻之后问题消失。所以软件方案再完美也救不了硬件设计缺陷。遇到FE和NE先查硬件再改软件这个顺序不能反。
返回列表