MCU串口接收不定长数据的三种方案,哪种最稳?

strongerHuang 2026-10-16 08:10

关注+星标公众号,不错过精彩内容

作者 | strongerHuang

公众号 | strongerHuang


串口的应用非常广泛,几乎绝大多数MCU项目都会用到串口(最基础的功能就是调试),所以,本期重点讲述一下串口接收数据的方法。


MCU串口接收数据常见的方法有:


当然,还有一种不推荐使用的方法:轮询(查询)法.


方法一:DMA + 串口空闲中断(IDLE)

原理:利用串口硬件自带的空闲线路检测功能。当总线上超过一个字节传输时间没有新数据时,硬件置位 IDLE 标志。

配合 DMA 自动搬运数据,只需在 IDLE 中断中计算已接收长度即可。

比如:

#define RX_BUFFER_SIZE  256uint8_t rx_dma_buffer[RX_BUFFER_SIZE];volatile uint16_t rx_data_len = 0;volatile uint8_t  rx_frame_ready = 0;
/* 1. 初始化:开启 IDLE 中断,启动 DMA 接收 */void UART_DMA_Init(void){    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);    HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);}
/* 2. 串口中断服务函数中处理 IDLE 事件 */void USART1_IRQHandler(void){    /* 先让 HAL 处理常规中断 */    HAL_UART_IRQHandler(&huart1);
    /* 手动检测 IDLE 标志 */    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET) {        __HAL_UART_CLEAR_IDLEFLAG(&huart1);       // 清除标志        HAL_UART_DMAStop(&huart1);                 // 停止 DMA
        /* 计算接收长度:总大小 - DMA 剩余计数器 */        rx_data_len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);        rx_frame_ready = 1;                        // 通知主循环
        /* 重新开启 DMA 接收,等待下一帧 */        HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);    }}
/* 3. 主循环处理 */void main_loop(void){    if (rx_frame_ready) {        // 处理 rx_dma_buffer 中 rx_data_len 长度的数据        process_frame(rx_dma_buffer, rx_data_len);        rx_frame_ready = 0;    }}

这种方法的优点:硬件自动判定帧尾,响应快,CPU 占用极低。

注意:要求发送端帧与帧之间有明确空闲间隔。

存在的陷阱:IDLE 中断触发时,最后一个字节可能还在移位寄存器中未被 DMA 搬走,导致帧尾偶发丢字节 。规避方法包括在停止 DMA 后稍作延时、使用循环模式配合半满/全满中断。

MCU串口接收不定长数据的三种方案,哪种最稳?图1


方法二:DMA + 接收超时中断(RTO)

原理:部分较新的 STM32 系列(如 G系列、U系列、H7等)的串口外设支持“接收超时”硬件功能。


可配置一个超时阈值(单位为一个位时间),当总线空闲超过该阈值时,触发超时中断,原理类似 IDLE,但超时时间可编程,且不依赖外部定时器。


比如:

#define RX_BUFFER_SIZE  256uint8_t rx_dma_buffer[RX_BUFFER_SIZE];volatile uint16_t rx_data_len = 0;
/* 1. 初始化:配置接收超时中断并启用 */void UART_RTO_Init(void){    /* 设置超时时间为约 10ms(计算逻辑与波特率相关) */    HAL_UART_ReceiverTimeout_Config(&huart1, huart1.Init.BaudRate / 100);    HAL_UART_EnableReceiverTimeout(&huart1);
    /* 使能超时中断 */    __HAL_UART_ENABLE_IT(&huart1, UART_IT_RTO);
    HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);}
/* 2. 在错误回调中处理超时事件(RTO 触发走 ErrorCallback) */void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart){    if (huart == &huart1) {        /* 检查是否为接收超时(RTO 中断) */        if (__HAL_UART_GET_FLAG(huart, UART_FLAG_RTOF)) {            __HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_RTOF);
            HAL_UART_DMAStop(huart);            rx_data_len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);            rx_frame_ready = 1;
            HAL_UART_Receive_DMA(&huart1, rx_dma_buffer, RX_BUFFER_SIZE);        }    }}

这种方法的优点:超时时间灵活可调,无需额外定时器资源,适合对帧间隔有严格要求的场景 。


注意:RTO 中断在 HAL 库中通常归入错误回调处理,具体清除标志位和检测逻辑需参考对应芯片的 HAL 实现 。


方法三:接收中断 + 软件超时判断

原理:不使用 DMA 和空闲中断,而是逐字节进入接收中断(HAL_UART_RxCpltCallback)。


早期,硬件不支持以上方案,这种方法在以前项目用得比较多,即每收到一个字节数据就中断一次,然后通过协议解析数据,判断接收完一帧数据。


比如:

void USART1_IRQHandler(void){  static uint8_t sDgus_RxNum = 0;                //数量  static uint8_t  sDGUS_RxCnt = 0;  static uint8_t sDgus_RxBuf[DGUS_PACKAGE_LEN];  static portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
  if((USART1->SR & USART_FLAG_RXNE) == USART_FLAG_RXNE)  {    sDgus_RxBuf[sDGUS_RxCnt] = ((uint8_t)USART_ReceiveData(USART1));    sDGUS_RxCnt++;
    //接收超时定时:10ms    DGUSRcv_Timing10ms(1);
    /* 判断帧头 */    if(sDgus_RxBuf[0] != DGUS_FRAME_HEAD1)         //接收到帧头1    {      sDGUS_RxCnt = 0;      return;    }    if((2 == sDGUS_RxCnt) && (sDgus_RxBuf[1] != DGUS_FRAME_HEAD2))    {      sDGUS_RxCnt = 0;      return;    }
    /* 确定一帧数据长度 */    if(sDGUS_RxCnt == 3)    {      sDgus_RxNum = sDgus_RxBuf[2] + 3;    }
    /* 接收完一帧数据 */    if((6 <= sDGUS_RxCnt) && (sDgus_RxNum <= sDGUS_RxCnt))    {      sDGUS_RxCnt = 0;
      if(xQueueDGUSRcv != NULL)      {                                            //发送队列数据        xQueueSendFromISR(xQueueDGUSRcv, &sDgus_RxBuf[0], &xHigherPriorityTaskWoken);                                                   //唤醒更高优先级任务,旧API:portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);      }    }  }}

这种方法优点:实现最简单,不依赖芯片是否支持 IDLE 或 RTO,任何 STM32 都能用。缺点:每个字节都触发中断,高波特率下 CPU 负担较重 。

该方案的核心在于超时阈值的选取:太短会截断长帧,太长则帧间延迟增大。通常设为 3~5 个字节的传输时间。


最后建议

就目前优先选择方案一:绝大多数 STM32 都支持 IDLE,实现成熟,CPU 占用低。注意帧尾丢字节问题,可通过在 HAL_UART_DMAStop 后加短延时或使用循环 DMA + 半满/全满中断规避 。


芯片支持 RTO 时用方案二:超时时间可编程且无需额外硬件,适合协议帧间隔灵活的场景。


方案三作为兜底:任何芯片都能跑,但高波特率下慎用,适合低速、低数据量场景。

------------ END ------------

关于科技区角:国内科技展会垂直内容策划服务商,提供从论坛内容全案策划、会展市场化IP打造到精准专业观众一站式邀约服务,以产业内容吸引高质量B端人群,打通展会从议题设计、演讲嘉宾邀约、宣传预热、精准邀观到供需对接全链路。
声明:内容取材于网络,仅代表作者观点,如有内容违规问题,请联系处理。
MCU
more
MCU和PMIC,迎来涨价潮
奕斯伟计算 RISC-V车规级AI MCU,助力尊界智能座椅全新升级
从MCU到舱驾一体,国产芯片的突围路径在哪?蓝皮书邀您联合编撰
瑞萨电子亮相2026中国国际工业博览会,以创新MCU/MPU解决方案赋能智能工业未来
32位MCU,都在争最小
Microchip收购VORAGO,给全球MCU企业指了条活路
Wi-Fi 7,杀入MCU领域
笙泉科技推出MG32F04A064大电流直驱MCU,强化高性价比M0+产品布局
盛群MCU,全面涨价
机器人带火EtherCAT,国产MCU抱团布局
Copyright © 2025-成都区角科技有限公司
蜀ICP备2025143415号-1
  
川公网安备51015602001305号