基于 STM32 的 DMA 半传输中断在环形缓冲区中的高效应用技巧
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,DMA 与环形缓冲区的结合是处理高速连续数据流的经典方案。然而,传统双缓冲或全传输中断存在延迟或丢数风险。本文深入剖析 STM32 DMA 的半传输中断(Half-Transfer Interrupt)机制,讲解如何利用它实现无缝的“乒乓”式数据采集,并给出完整的配置代码与工程注意事项,帮助开发者提升系统实时性与 CPU 效率。
# 基于 STM32 的 DMA 半传输中断在环形缓冲区中的高效应用技巧
## 一、为什么需要半传输中断?
在 ADC 连续采样、UART 接收长帧或 SPI 从机接收数据时,DMA 能大幅减轻 CPU 负担。但若仅使用 DMA 传输完成中断,则必须等待整个缓冲区填满才能处理数据,导致延迟高;若使用双缓冲(如 STM32F4 的 DMA 双缓冲模式),则需额外配置内存地址切换,且部分低端型号不支持。
**半传输中断(Half Transfer Interrupt)** 在 DMA 传输达到缓冲区一半时触发,配合环形缓冲区,可将缓冲区视为两个独立的“半区”,实现类似双缓冲的效果:CPU 处理前半区数据时,DMA 正在填充后半区,反之亦然。这种“乒乓”操作极大降低了数据丢失风险,且无需硬件双缓冲支持。
## 二、原理剖析:DMA 半传输与环形缓冲区
### 1. 环形缓冲区设计
环形缓冲区(Ring Buffer)通常包含:
- 数据存储数组 `buf[]`
- 读指针 `read_index`
- 写指针 `write_index`(由 DMA 硬件更新,软件只读)
- 容量 `size`(必须为 2 的幂,便于取模)
### 2. 半传输中断的触发时机
以 STM32F103 为例,DMA 通道的 `CNDTR` 寄存器记录剩余传输次数。当 `CNDTR` 从 `size/2` 变为 `size/2 - 1` 时,硬件置位 `HTIF` 标志,若使能中断则进入 ISR。此时 DMA 已填充了缓冲区的前半部分,而后半部分仍在填充中。
### 3. 工作流程
- 初始化:DMA 循环模式(Circular Mode),缓冲区大小 `N`(偶数)。
- 第一次半传输中断:处理 `buf[0]` 到 `buf[N/2-1]` 的数据。
- 第一次全传输中断:处理 `buf[N/2]` 到 `buf[N-1]` 的数据。
- 之后重复:半传输中断处理前半区,全传输中断处理后半区。
注意:在循环模式下,全传输中断后 DMA 自动重载 `CNDTR` 为 `N`,继续下一轮传输,因此中断会持续交替触发。
## 三、STM32 配置步骤(以 HAL 库为例)
### 1. 硬件环境
- MCU: STM32F103C8T6(或任意系列)
- 外设:ADC1 连续采样,或 USART1 接收
- 开发工具:STM32CubeIDE + HAL 库
### 2. 配置 DMA 与环形缓冲区
```c
// 环形缓冲区定义
#define BUFFER_SIZE 256 // 必须为偶数
uint8_t rx_buffer[BUFFER_SIZE];
volatile uint16_t read_index = 0;
// 使能 DMA 半传输中断和全传输中断
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 处理后半区数据(索引 N/2 到 N-1)
process_data(&rx_buffer[BUFFER_SIZE/2], BUFFER_SIZE/2);
}
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
// 处理前半区数据(索引 0 到 N/2-1)
process_data(&rx_buffer[0], BUFFER_SIZE/2);
}
// 启动 DMA 接收(循环模式)
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
```
### 3. 处理函数示例
```c
void process_data(uint8_t *data, uint16_t len)
{
// 将数据拷贝到应用缓冲区或直接解析
// 注意:此时 DMA 正在写另一块区域,不会冲突
for (uint16_t i = 0; i < len; i++) {
// 处理 data[i]
}
}
```
### 4. 若使用中断方式读取数据(非回调)
```c
// 在中断服务函数中手动判断标志
void DMA1_Channel5_IRQHandler(void)
{
if (DMA_GetITStatus(DMA1_IT_HT5)) {
DMA_ClearITPendingBit(DMA1_IT_HT5);
// 处理前半区
}
if (DMA_GetITStatus(DMA1_IT_TC5)) {
DMA_ClearITPendingBit(DMA1_IT_TC5);
// 处理后半区
}
}
```
## 四、完整代码示例(基于 HAL 库,UART 接收)
```c
// main.c 片段
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
#define RX_BUF_SIZE 256
uint8_t rx_buf[RX_BUF_SIZE];
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 处理后半区
process_data(&rx_buf[RX_BUF_SIZE/2], RX_BUF_SIZE/2);
}
}
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
// 处理前半区
process_data(&rx_buf[0], RX_BUF_SIZE/2);
}
}
void process_data(uint8_t *data, uint16_t len)
{
// 示例:将数据通过另一个 UART 发送出去
HAL_UART_Transmit(&huart2, data, len, 100);
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
// 启动 DMA 接收,循环模式
HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE);
while (1)
{
// 主循环可处理其他任务
}
}
```
## 五、注意事项与优化技巧
- **缓冲区大小必须为偶数**,否则半传输中断的位置会偏移。
- **确保 DMA 配置为循环模式(Circular)**,否则传输完成后停止,无法连续工作。
- **中断优先级设置**:半传输和全传输中断应设为较高优先级,避免数据覆盖。
- **处理数据时避免访问正在被 DMA 写入的区域**:在回调中,只处理对应半区,不要修改缓冲区内容,除非你确定 DMA 已离开该区域。
- **对于 ADC 采样**:若使用扫描模式,半传输中断可用来处理前一半采样值,适合实时性要求高的控制。
- **内存对齐**:建议将缓冲区按 4 字节对齐,提高访问效率。
- **若使用 STM32F4/H7 等带双缓冲 DMA 的型号**,可考虑硬件双缓冲,但半传输中断依然适用于简单场景。
## 六、总结
DMA 半传输中断是嵌入式开发中一个实用但常被忽视的技巧。通过合理利用它,开发者可以在不增加硬件成本的情况下,实现高效的数据流处理,降低 CPU 占用并减少数据丢失风险。本文提供的原理与代码可直接应用于 UART、SPI、ADC 等场景,希望对你有所帮助。