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

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

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

  • 方法一:DMA + 串口空闲中断

  • 方法二:DMA + 接收超时中断

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

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

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

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

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

比如:

cpp 复制代码
#define RX_BUFFER_SIZE  256
uint8_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 后稍作延时、使用循环模式配合半满/全满中断。

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

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

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

比如:​​​​​​​

cpp 复制代码
#define RX_BUFFER_SIZE  256
uint8_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)。

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

比如:​​​​​​​

cpp 复制代码
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 时用方案二:超时时间可编程且无需额外硬件,适合协议帧间隔灵活的场景。

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

相关推荐
inrichtt1 小时前
第七课时:STM32时钟系统揭秘:从零理解时钟树
c语言·数据结构·stm32·单片机·嵌入式硬件
硅基手札2 小时前
【Linux 内核专栏 17】进程间通信 IPC(上)— 管道·信号·System V IPC·mmap·文件锁
linux·运维·服务器·stm32·单片机·mcu
Zw-awa2 小时前
PID 用顺了,然后呢:增强手术与前馈
stm32
国科安芯2 小时前
无方向脚如何既快又稳:ASC0101S 自动方向感应机理与边沿加速应用研究
网络·嵌入式硬件·低轨卫星·商业航天·抗辐射
额额额对了2 小时前
Linux 驱动开发:基于 platform 总线 + misc 设备的 DS18B20 单总线温度驱动
linux·c语言·开发语言·驱动开发·嵌入式硬件
zcmodeltech3 小时前
沙盘模型控制系统STM32和PLC方案对比:选型逻辑与代码实现
stm32·嵌入式硬件·自动化
HQB20263 小时前
EG11731|屹晶微电子 100V 内置功率管大电流同步降压 DC‑DC
嵌入式硬件·硬件工程
2501_922639583 小时前
率能半导体 SS6209L半桥MOSFET驱动器:28V/4A集成方案,覆盖电子烟、无线充电、半桥应用三大场景
单片机·嵌入式硬件·硬件工程
HQB20263 小时前
EG1192S|屹晶微电子 100V 内置 MOS 异步降压 DC‑DC(零功耗使能)
嵌入式硬件·硬件工程