• 正文
  • 相关推荐
申请入驻 产业图谱

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

10/16 15:08
105
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

公众号 | strongerHuang

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

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

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

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

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

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

方法一: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 后稍作延时、使用循环模式配合半满/全满中断。

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

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

相关推荐

作者黄工,从事嵌入式软件开发工作8年有余,高级嵌入式软件工程师,业余维护公众号『strongerHuang』,分享嵌入式软硬件、单片机、物联网等内容。