STM32 串口接收不定长数据:空闲中断 + DMA 方案详解
👁 2 阅读 · 2026-08-14 · STM32 串口
在嵌入式开发中,串口接收不定长数据是常见需求,如接收AT指令、传感器数据等。传统中断逐字节接收方式CPU负担重,且难以处理帧边界。本文基于STM32 HAL库,介绍利用空闲中断(IDLE)结合DMA实现高效、可靠的不定长数据接收方案,包括原理剖析、CubeMX配置步骤、完整代码示例及关键注意事项,帮助你彻底掌握这一实用技巧。
## 一、为什么需要空闲中断 + DMA?
串口接收数据时,我们往往不知道一帧数据有多长。常见做法有:
- **固定长度接收**:简单但灵活性差,无法应对变长帧。
- **逐字节中断**:每收一个字节就进一次中断,高频数据下CPU占用高,且容易丢失数据。
**空闲中断(IDLE)** 是STM32 USART的一个硬件事件:当接收完一个字节后,总线保持空闲(电平持续为高)超过一个字节时间,就会触发中断。利用它,可以精准判断一帧数据结束。
**DMA(直接内存访问)** 负责把串口接收数据自动搬运到内存缓冲区,不占用CPU。两者结合,可实现:**DMA持续接收,空闲中断标记帧结束**,CPU只在帧完整时处理一次数据,效率极高。
## 二、工作原理
1. 开启USART的DMA接收请求,设置DMA为循环模式(Circular Mode)。
2. 数据到达时,DMA自动将字节存入缓冲区,并更新DMA计数器。
3. 当一帧数据结束后,串口检测到线空闲,触发IDLE中断。
4. 在IDLE中断中,读取DMA当前剩余计数(NDTR),即可算出本次接收到的数据长度:`len = buffer_size - NDTR`。
5. 主程序或回调函数中处理这帧数据,然后DMA继续循环接收下一帧。
关键点:DMA是循环模式,意味着它可以不间断地写入缓冲区,不会在接收完一帧后停止;IDLE中断只是告知“有完整帧可处理”。
## 三、CubeMX 配置步骤
假设使用STM32F103系列,USART1,DMA通道。
### 1. 配置串口参数
- 打开USART1,模式选择`Asynchronous`,波特率115200,8位数据,无校验,1停止位。
### 2. 配置DMA
- 在DMA Settings中添加`USART1_RX`,方向`PeripheralToMemory`,模式`Circular`,数据宽度均为`Byte`,优先级`High`。
- 注意:不需要配置TX DMA(若需要发送也可加)。
### 3. 开启中断
- 在NVIC Settings中使能`USART1 global interrupt`(这同时会覆盖IDLE中断和错误中断)。
- 不使能DMA中断(循环模式无需DMA传输完成中断)。
### 4. 生成代码
- 生成初始化代码,后续只需在`main.c`中补充逻辑。
## 四、完整代码示例
### 1. 定义缓冲区与接收标志
```c
#define RX_BUF_SIZE 256
uint8_t rx_buffer[RX_BUF_SIZE];
volatile uint8_t rx_frame_complete = 0; // 帧完成标志
volatile uint16_t rx_frame_len = 0; // 帧长度
```
### 2. 启动DMA接收(放在main初始化后)
```c
// 启动DMA循环接收,数据会持续填充rx_buffer
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUF_SIZE);
// 开启USART空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
```
### 3. 空闲中断处理(在中断回调里)
```c
void USART1_IRQHandler(void)
{
uint32_t isr = READ_REG(huart1.Instance->SR);
// 检查空闲中断标志
if ((isr & UART_FLAG_IDLE) != 0)
{
// 清除IDLE标志(先读SR再读DR)
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 停止DMA传输,读取当前剩余计数
HAL_UART_DMAStop(&huart1);
uint16_t remain = __HAL_DMA_GET_COUNTER(huart1.hdmarx);
// 计算已接收字节数
rx_frame_len = RX_BUF_SIZE - remain;
// 标记帧完成
rx_frame_complete = 1;
// 重新启动DMA接收,准备下一帧
HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUF_SIZE);
}
HAL_UART_IRQHandler(&huart1);
}
```
> **注意**:上面的代码中,`HAL_UART_DMAStop`会停止DMA,但也会关闭接收。重新调用`HAL_UART_Receive_DMA`可以再次开启。简单场景下也可不调用DMAStop,直接读取计数器再重启,但为了严谨,建议按上述方式。
### 4. 主循环处理帧数据
```c
while (1)
{
if (rx_frame_complete)
{
rx_frame_complete = 0;
// 现在rx_buffer中有效数据为前rx_frame_len个字节
// 例如打印十六进制数据
for (uint16_t i = 0; i < rx_frame_len; i++)
{
printf("%02X ", rx_buffer[i]);
}
printf("\r\n");
// 业务处理...
}
}
```
这段代码的精髓在于:**DMA循环写入缓冲区,帧边界由IDLE事件标定**,CPU仅在帧结束时参与一次计算和业务处理。
## 五、注意事项与常见坑
- **缓冲区大小**:必须大于最大可能帧长度,否则数据会覆盖。若帧可能超过缓冲区,可加大缓冲区或分段处理。
- **读取计数的时序**:在IDLE中断中,务必先清标志再读DMA计数器,避免再次触发中断导致计数错误。
- **DMAStop的副作用**:调用`HAL_UART_DMAStop`会同时清空DMA配置,因此必须重新调用`HAL_UART_Receive_DMA`。注意检查返回值,确保启动成功。
- **多字节接收竞态**:如果主循环读取缓冲区时,新数据又开始写入,可能导致数据交错。建议在读取前先关闭DMA(或使用双缓冲),本项目为演示简单场景,主循环处理及时则问题不大。
- **IDLE中断标志清除**:在STM32F1系列上,清除IDLE标志是通过“先读SR,再读DR”实现的。HAL库提供了`__HAL_UART_CLEAR_IDLEFLAG`宏,使用它更安全。
- **其他串口事件**:防止溢出错误(ORE)中断干扰,可在开启IDLE前使能`UART_IT_ERR`并用HAL_UART_ErrorCallback处理,或直接置位`huart1.RxState`恢复接收。
## 六、扩展:双缓冲与多帧处理
若需要连续处理大量数据,可使用两个缓冲区交替使用(乒乓缓冲)。当IDLE中断时,切换DMA目标到另一缓冲区,同时将当前缓冲区数据交付处理,这样接收和处理完全并行,避免丢帧。实现方式为:在IDLE中断里,用`HAL_UART_DMAStop` + 修改`huart1.hdmarx->Instance->CMAR`指向另一缓冲区,再启动。有兴趣的读者可自行实现。
## 结语
空闲中断+DMA方案是STM32串口接收不定长数据的经典高效解法。理解其原理后,你可以轻松应用于各种串口协议解析。记住核心要点:**DMA负责搬数据,IDLE负责划帧界**。配合CubeMX快速配置,代码清晰可维护,非常适合工程落地。