尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

STM32串口丢数据排查:7大原因与DMA+空闲中断接收方案

发布时间:2026/9/28 16:46:53

资讯中心
01
ARTICLE

STM32串口丢数据排查:7大原因与DMA+空闲中断接收方案

STM32串口丢数据排查:7大原因与DMA+空闲中断接收方案
有一次我调试一块STM32F103C8T6和激光雷达的串口通信115200波特率雷达每50ms主动上报一帧约200字节的数据。刚开始用中断方式接收串口助手看数据偶尔会缺一行重新同步后又能跑一阵。当时第一反应是“雷达坏了”后来把波特率降到9600现象消失这才意识到问题出在MCU这一侧的串口接收上。排查下来发现串口接收数据丢失几乎没有单一原因。波特率偏差、中断函数执行时间、缓冲区大小、DMA配置、代码里标志位处理顺序任何一环出问题都会表现为“丢数据”。我把这7种最常见的诱因按排查经验整理出来再给出目前最稳的DMA空闲中断组合方案并附上基于STM32CubeMX的完整配置和代码。正在用标准库或HAL库调串口、遇到丢帧乱码问题的朋友可以直接对照排查。1. 丢数据的7个元凶现象、原理与排查1.1 波特率误差不是所有标称115200都真的115200很多人丢数据第一反应是代码问题其实线路物理层面的波特率误差最容易被忽略。STM32内部串口时钟来自APB总线时钟如果系统主频本身不准或者分频系数计算方式不对实际波特率会和标称值产生偏差。UART每帧按起始位、数据位、停止位的节奏采样接收端在每一位的中点附近采样。只要采样点还落在数据位有效窗口内通信就能成立一旦偏差超过2%~3%尤其在连续传输多个字节时误差会逐步累积最终某一位的采样点漂出了数据窗口就出现偶发乱码或丢字节。我遇到过最典型的场景板子上外部晶振标称8MHz实际只有7.6MHz明明代码里配置的是115200示波器实测TX脚只有大约109000的波特率和USB转串口芯片对不上接收端自然随机丢数据。排查建议先别急着查代码用逻辑分析仪或者示波器抓一下TX引脚的波形看一帧0x5501010101的实际脉宽是多少。如果实测波特率和配置值偏差超过2%请先检查晶振、系统时钟树配置和CLK_SetSysTick这类时钟源设置。1.2 中断服务函数耗时过长最常见的自作自受这是中断接收方案里最普遍的坑。USART在接收到一个字节后置RXNE标志并触发中断如果CPU不能在下个字节到达前把数据从数据寄存器DR中取走新数据就会覆盖旧数据产生溢出。算一下时间115200波特率每字节在线路上的时间大概是10位除以波特率约86.8微秒。如果你的中断函数里做了如下任何一件事大概率会超时在中断里调用printf重定向输出调试信息串口输出本身还在排队一个字节卡几十上百微秒很常见在中断里做协议解析、字符串拼接、CRC计算在中断里调用HAL_Delay延时在中断里写Flash或者操作其他慢速外设。正确做法是中断里只做“把DR寄存器数据搬进缓冲区清标志置一个标志位”解析和业务逻辑全部放到主循环或任务里。这个原则无论用标准库还是HAL库都适用。1.3 接收缓冲区过小或处理不及时即使中断服务函数足够快如果自定义接收缓冲区太小也会丢数据。比如一帧数据协议长度是256字节你只给了64字节的缓冲区那超出的数据要么覆盖写越界要么直接丢弃。缓冲区设计要考虑两个维度大小和消费速度。大小至少应该大于最大协议帧长建议留2~3倍余量消费速度指的是主循环要经常去检查缓冲区里有没有数据而不是等缓冲区满了才去处理。我见过一种错误写法主循环里用阻塞方式等待一个“缓冲区满”标志满了才一次性处理。一旦协议数据量不稳定缓冲区满的瞬间就是丢数据的开始。更好的做法是使用环形缓冲区写指针和读指针独立推进主循环每次检查读指针和写指针的差值有数据就处理缓冲区始终在流动。1.4 标志位操作顺序错误一次读错后续全乱串口外设的标志位清除有很多细节最经典的是RXNE和ORE标志的清除。在STM32上很多错误标志是通过“先读状态寄存器SR再读数据寄存器DR”来清除的。如果代码里只读了DR没读SR或者操作顺序反了标志位可能一直保持置位导致后续接收异常。HAL库用户容易遇到的问题更隐蔽HAL的HAL_UART_Receive函数一次接收指定长度内部会处理很多状态判断如果在中断中或者多线程环境中使用不当会把UART句柄状态字段搞乱。我自己就遇到过因为清标志顺序不对导致执行完一次接收后串口再也不进中断回调的情况。解决办法只有一个严格按照参考手册的顺序操作。尤其在F1系列上清除RXNE、ORE、IDLE这些标志时“先读SR再读DR”是通用规则。调试时可以打开寄存器窗口观察SR各位的变化确认标志确实被清掉了。1.5 中断优先级配置不当被“插队”的丢失USART中断优先级设置不合理也会造成接收丢失。如果系统里有一个高优先级的中断频繁触发并且在中断里执行较长时间USART中断会被持续打断字节在RDR里等着取走下一个字节一到就会溢出。常见的错误思维是“把串口中断优先级调到最高就万事大吉”。实际上如果多个外设中断都设成最高优先级NVIC内部会按硬件编号继续排优先级序这时候串口未必被优先响应。更合理的做法是给串口一个足够高的优先级同时保证它能够及时抢占其他低优先级中断但不要在中断回调里做耗时操作。如果用了FreeRTOS还要注意在中断回调里尽量不要调用会引起任务切换的API避免中断上下文被拖长。1.6 ORE溢出错误未清除一次错误导致长时间接收异常OREOverrun Error是溢出错误当RXNE标志还是1的时候又有新数据到达硬件就会置ORE。更麻烦的是在很多应用里这一步出错后如果没被及时清除后续的接收行为会变得不可预测表现为“某次丢字节之后串口彻底安静了”。HAL库对ORE的处理在所有版本里不算统一。有些库版本在HAL_UART_IRQHandler内部会尝试清除有些版本不会。如果你在调试中发现接收中断偶发停止大概率就是ORE没有清干净。稳妥做法是在串口中断处理函数里主动判断ORE标志一旦置位就通过读SR再读DR的方式把它清掉再决定要不要重新启动接收。DMA模式下ORE同样可能出现处理逻辑类似。1.7 DMA配置不当用了DMA但没用好有一部分人确实用了DMA丢数据问题却依然存在基本可以归为三类第一DMA模式选错。在CubeMX配置里如果Mode选成了NormalDMA搬运完指定长度后会自动停止后续数据根本不会进入缓冲区表现出来的就是“第一批数据正常后面全部丢失”必须勾选Circular循环模式。第二地址或不自增。DMA接收外设到内存时Memory地址必须开启Increment否则每个字节都会写到同一个地址后一个字节覆盖前一个字节数据只剩最后一个字节。第三缺少帧边界判断。DMA只是把数据搬运到缓冲区它自己并不知道一帧什么时候结束。如果不配合空闲中断或其他超时机制协议解析时就无法准确判断“这一批数据是完整的一帧”要么把半包当全包解析要么前后帧混在一起解析结果自然就是乱码。这7个原因相互叠加的情况也很常见比如波特率有点偏差的同时中断函数又慢了一点就会在某个特定数据长度时集中爆发丢帧。2. 普通中断接收为什么顶不住一笔时间账如果只是低速串口比如9600波特率偶尔发几个字节中断接收完全够用。但数据量上来之后问题就会暴露。以115200波特率为例每个字节在线路上的传输时间是10位/115200大约86.8微秒。如果接收的是持续流式数据就意味着每86.8微秒就要触发一次接收中断。累计起来每秒大约会发生11520次中断也就是每秒11.5k次。STM32F1主频72MHz一次简单的中断服务函数包括压栈、跳转、读DR、存数组、清除标志、出栈再怎么优化也需要2~5微秒。按平均3微秒算每秒中断占用CPU的时间是34.5微秒约占总处理能力的3%~4%。如果只是纯搬运听起来还能接受但只要在中断里加上任何协议判断代码单次中断时间很容易突破10微秒占用率一下子就到10%以上。更致命的是中断延迟抖动。系统里哪怕只有一个高优先级中断偶尔执行50微秒就会让串口中断被延迟而这段时间里新的串口字节已经到达。只要“中断处理时间被抢占延迟时间”超过86.8微秒丢字节就是必然的。所以中断接收其实是在和时间打赌赌每次都能在下一个字节到来之前处理完上一个字节。低速时这个赌局容易赢高速大流量时基本必输。反过来看DMA方案CPU根本不参与逐字节搬运自然就没有这个赌局。3. DMA空闲中断的解题思路硬件搬运CPU只处理帧边界DMA全称是Direct Memory Access作用是让外设和内存之间的数据搬运不经过CPU。串口收到一个字节硬件自动把它从DR搬到内存缓冲区里全程不用指令干预。缓冲区里的数据满了或者收到指定长度后DMA才通过中断通知CPU一次。但DMA解决了“怎么搬”没解决“什么时候算一帧结束”。很多协议帧是可变长的比如GPS的NMEA语句不同语句长度不同或者设备主动上报的数据帧每帧长度可能因为内容不同而变化。这时就需要空闲中断来补充。USART的空闲中断IDLE触发条件是RX线上已经被接收过数据之后线路保持空闲超过一个字节时间。换句话说数据流送到一半停了硬件就知道这一帧大概率结束了于是产生IDLE事件。DMA空闲中断的组合模型是DMA默默地把每一个到达的字节存入环形缓冲区只要线路空闲超过一个字节时间IDLE中断触发CPU在中断里读一次DMA计数器计算“这一帧实际收到了多少字节”然后提交给上层解析。整个过程中CPU只在帧边界出现时工作一次不再逐字节处理。这种设计同时解决了很多问题中断频率从每秒上万次降到了一帧一次CPU不再需要在微秒级响应缓冲区的字节搬运交给专用硬件不占指令周期配合DMA循环模式缓冲区还能自动回卷。只要CPU能在下一帧到来之前处理完当前帧就不会丢数据。4. 基于CubeMX的完整实现与关键代码下面以STM32F103C8T6 HAL库 CubeMX为例给出完整的配置和代码。其他F系列或G系列思路完全一致只是外设地址和库函数名略有差异。4.1 CubeMX配置步骤打开CubeMX选择芯片型号后按以下步骤配置RCC里把HSE设为Crystal/Ceramic Resonator也就是外部晶振SYS里Debug选Serial Wire方便后续用ST-LINK调试Clock Configuration里确认系统主频是72MHz确保USART时钟源正确USART1选择Asynchronous异步模式波特率1152008位数据无校验1位停止位并在NVIC Settings里勾选Enabled打开串口中断DMA Settings点击Add选择USART1_RXDirection选Peripheral To MemoryMode一定要选CircularPeripheral和Memory的数据宽度都选ByteMemory的Increment必须打开Priority可设为High同样在DMA Settings里也可以加一条USART1_TX的Normal模式发送DMA如果项目里发送也走DMA一并配置NVIC里确保DMA通道的全局中断也勾上因为HAL库管理DMA传输状态依赖DMA中断如果不使能一些异常回调无法触发生成代码。这里最关键的是两点DMA模式必须是Circular循环模式对应底层自动使能了连续请求数据宽度必须是Byte。如果选了WordDMA一次搬运4个字节串口一个字节一个字节地到达搬出来的数据会完全错位。4.2 缓冲区定义和全局变量#define RX_BUF_SIZE 512 uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_last_index 0; volatile uint16_t rx_frame_len 0; volatile uint8_t rx_frame_ready 0;rx_last_index保存上一次处理时DMA已经搬运到的位置rx_frame_len是本次空闲中断计算出来的帧长度rx_frame_ready是通知主循环“有数据可以解析了”的标志。缓冲区大小建议取2的整数次幂比如256、512、1024这样后续取模可以用位运算也方便对齐DMA传输。4.3 启动DMA接收在CubeMX生成的MX_USART1_UART_Init之后调用一次HAL_UART_Receive_DMA(huart1, rx_buf, RX_BUF_SIZE);这一步是把DMA接收通道启动起来告诉DMA“数据就往这个缓冲区里搬”。注意一定要在串口和DMA都初始化完成之后再调用。调试时经常有人把这句话放到了初始化之前导致DMA配置无效。4.4 空闲中断处理函数标准的HAL库在不同版本里对IDLE中断的回调支持不一样。有的版本检测到IDLE后会调用HAL_UART_IdleCpltCallback但很多旧版本根本不处理IDLE最后还是需要在串口中断服务函数里自己判断标志位。建议采用下面这种更直接的方式不依赖库版本void USER_UART1_IDLE_Handler(UART_HandleTypeDef *huart) { uint16_t current_index; if (huart-Instance ! USART1) return; if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE) RESET) return; // 先读SR再读DR清除IDLE标志 __HAL_UART_CLEAR_IDLEFLAG(huart); // ORE错误顺手清掉防止堆积 if (__HAL_UART_GET_FLAG(huart, UART_FLAG_ORE) ! RESET) { __HAL_UART_CLEAR_OREFLAG(huart); } // 计算DMA已经搬运的字节数 current_index RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); // 和上一次的索引做差得到这一帧的长度 if (current_index ! rx_last_index) { if (current_index rx_last_index) { rx_frame_len current_index - rx_last_index; } else { // 缓冲区回卷的情况 rx_frame_len RX_BUF_SIZE - rx_last_index current_index; } rx_last_index current_index; rx_frame_ready 1; } }注意__HAL_UART_CLEAR_IDLEFLAG这个宏在不同HAL库版本里不一定都有如果编译报错直接手动读SR再读DR即可(void)huart-Instance-SR; (void)huart-Instance-DR;ORE标志的清除同理。4.5 串口中断服务函数在stm32f1xx_it.c中找到USART1_IRQHandler改成先处理空闲中断再交给HAL库处理其他UART中断void USART1_IRQHandler(void) { USER_UART1_IDLE_Handler(huart1); HAL_UART_IRQHandler(huart1); }顺序上我习惯先处理自己的IDLE判断是因为HAL_UART_IRQHandler在有些库版本内部会清一些标志放到前面更可控。4.6 主循环里的取帧逻辑while (1) { if (rx_frame_ready) { rx_frame_ready 0; HandleFrame(rx_buf, rx_last_index, rx_frame_len); } }还有一个需要处理的细节由于DMA缓冲区是环形回卷的一帧数据可能被拆成两段存放比如前半段在缓冲区中间后半段因为回卷跑到了缓冲区开头。直接拿rx_buf的头指针去解析会出错。需要做一个跨区拷贝或访问函数uint16_t ReadRingData(uint8_t *out, uint16_t start, uint16_t len) { uint16_t first_part; if (start len RX_BUF_SIZE) { first_part RX_BUF_SIZE - start; memcpy(out, rx_buf[start], first_part); memcpy(out first_part, rx_buf, len - first_part); } else { memcpy(out, rx_buf[start], len); } return len; }解析时先调用这个函数把数据拷贝到临时缓冲区再按协议解析。虽然多了次拷贝但逻辑清晰出错概率低。如果对性能有极致要求也可以在解析函数里直接按两段地址分别处理但代码会复杂不少。5. 装完还要调试用回环和压测验证不再丢数据方案实现完不能直接上线至少要做一轮针对性验证。5.1 回环测试最基础的是回环测试。把STM32的TX和RX通过杜邦线或者板载跳线短接程序里收到什么就在TX口发出去。用串口调试助手发送一帧固定数据看返回是否完全一致。如果逐字节回环都丢说明链路或者配置本身就有问题先别谈后续协议解析。稍微进阶一点回环测试时发送的内容不要用纯ASCII文本最好用带长度字段和校验字段的二进制定长帧比如一帧40字节包含帧头、帧序号、数据区、CRC16校验。连续发1000帧统计收到的帧序号是否有跳号就能算出丢帧率。5.2 压测数据流串口调试助手一般有定时发送功能把发送间隔设成10ms、5ms甚至1ms每次发送几十字节。观察STM32这边接收是否连续无丢失。还有一种测试方式是设备端主动以最大速率上报数据比如用循环调用HAL_UART_Transmit往TX发一段长数据自收自发看本地接收缓冲区是否稳定。实际跑下来DMA空闲中断在115200波特率下接收连续数据流CPU占用比中断接收低很多而且几乎不会丢字节。如果数据流速率超过缓冲区承受能力优先考虑加大RX_BUF_SIZE并优化主循环的解析速度。5.3 调试辅助工具开发阶段建议用逻辑分析仪抓RX引脚的实际波形用来确认波特率是否准确、数据帧间隔分布情况。比如一帧数据的字节之间有没有超过一个字节时间的空隙这直接影响IDLE中断的分包效果。如果能看到帧中间有明显空隙说明发送端设备本身停顿了IDLE会把一帧拆成两半这不是MCU接收的问题而是上层协议需要做帧拼接。常用的USB转串口工具像CH340方案或者CP2102方案在高波特率下质量参差不齐。如果发现模拟测试没问题接到实际设备上就丢数据换一个USB转串口模块再测能排除电脑端工具造成的影响。5.4 万一还是丢数据怎么办走了DMA空闲中断后依旧丢数据从这几个方向排查第一主循环解析太慢。把接收缓冲区填满的速度和解析速度做对比如果解析一帧的耗时接近甚至超过数据帧间隔就会把rx_frame_ready处理不过来。这时要做的是优化解析逻辑或者把解析任务放到DMA中断之外的低优先级处理核心原则是不要让缓冲区长期处于满状态。第二DMA中断关闭了。如果CubeMX里没开DMA通道的全局中断HAL库收不到DMA传输完成事件某些异常情况下不会自动恢复DMA状态也会表现为偶发丢失。第三ORE错误再次出现。把ORE检测加进IDLE处理函数里一旦置位就清除并复位DMA接收状态。可以在处理函数里加一个统计变量专门记录ORE发生次数连续出现就说明系统级的响应时间出了问题。6. 边界条件与进阶玩法这个方案不是银弹DMA空闲中断适合处理突发性、一帧连续传输的串口数据流但它也有自身局限。如果两帧数据之间的间隔非常短短到小于一个字节时间IDLE就不会产生两帧会被DMA当成一帧收进来。这时候需要协议层做二次分包比如根据帧头字段查找帧起始位置再按长度字段切分。如果帧与帧之间完全无缝只能靠解析状态机去切。反过来如果一帧数据内部因为发送端设备调度问题停顿超过了一个字节时间IDLE会提前触发把完整的一帧拆成两个半包。处理办法是在解析层做粘包处理缓存半包等待下一段数据到来后再拼接。用帧头判断、长度字段判断都比单纯依赖IDLE更可靠。更极端的高速大流量场景比如460800波特率以上或者一帧数据超过2KB可以考虑这样几招DMA半传输中断配合传输完成中断把缓冲区切成两个半区DMA在写前半区时CPU处理后半区形成流水线使用DMA双缓冲模式内存地址在缓冲区1和缓冲区2之间自动切换两个区域独立操作吞吐率更高用定时器超时判断替代IDLE判断通过捕获定时器计数值来精确计算帧间间隔适合Modbus RTU这类要求帧间隔的协议。在FreeRTOS工程中空闲中断里尽量只做“计算长度、置标志、发信号量”这几个动作解析任务放在线程里等信号量触发。中断里调复杂解析虽然偶尔能跑通但会在高优先级中断上下文里占用大量时间破坏实时性。我个人还有一个习惯拿到新的串口设备先用逻辑分析仪看一眼波形再写驱动先确认波特率、帧间隔、数据极性这些链路层参数再写解析代码。很多“丢数据”表面上是程序bug本质上是发送端行为没摸清楚。DMA空闲中断给了一个很稳的接收底座但协议层怎么切帧、怎么容错还得靠具体场景来设计。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。