串口的应用非常广泛,几乎绝大多数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 时用方案二:超时时间可编程且无需额外硬件,适合协议帧间隔灵活的场景。
方案三作为兜底:任何芯片都能跑,但高波特率下慎用,适合低速、低数据量场景。