STM32 串口进阶:空闲中断 + DMA,把不定长帧一次性收齐

发布时间:2026/8/16 22:31:18
STM32 串口进阶:空闲中断 + DMA,把不定长帧一次性收齐 串口 DMA 适合定长数据DMA 帮你搬、不占 CPU。但真到了现场收 GPS 的 NMEA、模组的 AT 回复、或者自定义的变长协议帧长根本不固定。我一开始用最笨的办法每收到一个字节进中断拼到缓冲里看到0x0D 0x0A就认为一帧结束。波特率一上 115200帧密一点中断被打爆、字节丢得亲妈都不认识后来改主循环轮询超时判断断帧CPU 又被占满干不了别的。直到用上空闲中断IDLE DMA才真正把不定长帧这件事干漂亮硬件把字节默默搬进缓冲线路一空闲啪告诉你一整帧到齐了。不定长帧为什么难串口就是个纯字节流本身没有帧的概念。你收A1 A2 LEN ... CRC到底哪到哪是一帧协议自己定。常见的两种土办法都有硬伤靠结束符0x0D 0x0A断帧万一数据里本身就含 0x0D 0x0A帧被提前截断而且逐字节进中断高速下丢字节。靠超时断帧主循环盯着超过 N 毫秒没新字节就算一帧结束。CPU 被轮询绑死别的活干不了。空闲中断 DMA 怎么干活思路DMA 一直开着把 RX 引脚来的字节自动搬进一块内存缓冲完全不进 CPU当线路上连续一个字节时间没有新数据这就是空闲硬件触发IDLE 中断此时缓冲里躺着的刚好是一整帧。你只需要在 IDLE 回调里把这一帧拷走处理然后让 DMA 继续。关键点DMA 搬数据不进中断只在一帧结束那个瞬间进一次 IDLE 中断——中断次数从每字节一次降到每帧一次CPU 彻底解放。CubeMX 怎么配USART1 → ModeAsynchronous参数设好波特率如 115200。然后DMA Settings加USART1_RXDirectionPeripheral To MemoryMode 选Circular环形最稳不会收满停流或NormalData WidthByte。NVIC Settings勾USART1 global interruptIDLE 中断走这个。不用开 TX DMA发用普通HAL_UART_Transmit就行。代码HAL 的新 API 最省事老 HAL 要自己开 IDLE 中断、手动清标志坑一堆。新 HALCubeMX 近几年生成的直接有HAL_UARTEx_ReceiveToIdle_DMA收满指定长度或遇到空闲都会进HAL_UARTEx_RxEventCallback。#defineUART_BUF_LEN256uint8_tuart_buf[UART_BUF_LEN];volatileuint8_tframe_ready0;uint16_tframe_len0;voiduart_idle_start(void){// 启动收满 UART_BUF_LEN 或 线路空闲 都触发回调HAL_UARTEx_ReceiveToIdle_DMA(huart1,uart_buf,UART_BUF_LEN);__HAL_DMA_DISABLE_IT(hdma_usart1_rx,DMA_IT_HT);// 关半传输中断只要整帧}// 回调Size 本次已收到的字节数voidHAL_UARTEx_RxEventCallback(UART_HandleTypeDef*huart,uint16_tSize){if(huart-InstanceUSART1){frame_lenSize;frame_ready1;// 只置标志处理留给主循环}}intmain(void){HAL_Init();SystemClock_Config();MX_USART1_UART_Init();MX_DMA_Init();uart_idle_start();uint8_twork[UART_BUF_LEN];while(1){if(frame_ready){frame_ready0;memcpy(work,uart_buf,frame_len);// 先拷走process_frame(work,frame_len);// 再慢慢解析uart_idle_start();// 重新启动继续收下一帧}// 主循环现在能干别的事了不用盯着串口}}注意uart_idle_start()在每次处理完都要重新调用——DMA 这一轮收完就停了不复启下一帧收不到。如果你用的是老版本 HAL没有ReceiveToIdle_DMA就得手动开 IDLE 并清标志__HAL_UART_ENABLE_IT(huart1,UART_IT_IDLE);// 启动前开空闲中断// 在 USART1_IRQHandler 里if(__HAL_UART_GET_FLAG(huart1,UART_FLAG_IDLE)){__HAL_UART_CLEAR_IDLEFLAG(huart1);// 先读 SR 再读 DR 清标志// 算收到长度 BUF_LEN - __HAL_DMA_GET_COUNTER(hdma_usart1_rx)}10 个真实翻车点1. 用普通 Receive_DMA 又自己等 IDLE标志清不干净。★ 老方法最容易栽IDLE 标志必须先读 SR、再读 DR才能清顺序反了清不掉结果反复进中断、只触发一次就再也不来。能用HAL_UARTEx_ReceiveToIdle_DMA就别手搓。2. DMA 缓冲开太小被覆盖。帧比uart_buf还长DMA 接着往后写把前面的帧头覆盖了你拷出来是半新半旧的垃圾。缓冲按最大可能帧长 × 安全余量开必要时用 Circular 及时取走。3. 回调里忘了重启 DMA。IDLE 收完一轮DMA 流停了。你若只在回调里处理、没重新ReceiveToIdle_DMA后续帧石沉大海。记住每帧处理完必重启。4. 在中断回调里干重活。RxEventCallback是中断上下文里面printf、浮点运算、长循环都会拖垮系统、还可能丢下一帧。回调里只置标志、拷数据解析处理一律丢回主循环。5. 两帧之间太近IDLE 不触发。IDLE 靠一个字节时间没新数据判断。若两帧间隔小于一个字节时间硬件认为还是同一帧两帧被拼成一个。协议要么带帧头/长度字段自己切要么保证帧间有空闲间隔。6. NDTR 算长度踩版本差异。老方法靠BUF_LEN - __HAL_DMA_GET_COUNTER(...)算已收字节新ReceiveToIdle的RxEventCallback直接给你Size已收字节数。混着用会算错。统一用Size参数最稳。7. 波特率误差超 2% 丢字节。高波特率921600、2M下时钟分频误差超过约 2%采样点漂移偶发错字节。检查USARTDIV和过采样8 倍 / 16 倍必要时降波特率或换更高主频。8. 收发互相拖累。你用阻塞HAL_UART_Transmit发一长串期间 RX DMA 照常收但发送占着 CPU 时间片主循环来不及处理帧、缓冲被覆盖。高速双向通信建议发送也走 DMA 或 IT。9. 半传输中断来捣乱。Circular 模式下 DMA 跑到一半会触发 HT半传输中断如果你没关它又没处理可能误判成一帧到齐。用ReceiveToIdle时顺手__HAL_DMA_DISABLE_IT(..., DMA_IT_HT)关掉。10. 帧解析想当然。DMA 只管搬字节不解析协议。收到的一整帧还是原始字节流长度字段、CRC、转义都得你自己解。别以为DMA 收齐了就一定是合法帧校验一步不能省。收尾适合GPS NMEA、模组 AT 指令回复、自定义变长协议、任何帧长不固定、还得高速收的场景。不适合严格定长、且对实时性要求极致的定长 DMA 更简单直接用就行。铁律三条用HAL_UARTEx_ReceiveToIdle_DMA别手搓 IDLE 标志、每帧处理完必重启 DMA、回调里只置标志不干重活。把这三条焊死串口收帧从CPU 噩梦变成睡醒收快递。空闲中断 DMA 把收这一侧彻底自动化了。但串口只有一对收发、距离短、抗干扰差工业现场几十个节点要隔着几十米互相喊话还得自带仲裁和校验。那得换总线。进阶玩法CAN 怎么在嘈杂环境里把一堆节点稳住。