公众号 | 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++;//接收超时定时:10msDGUSRcv_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 时用方案二:超时时间可编程且无需额外硬件,适合协议帧间隔灵活的场景。
方案三作为兜底:任何芯片都能跑,但高波特率下慎用,适合低速、低数据量场景。
105