基于 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 与环形缓冲区
// 环形缓冲区定义
#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. 处理函数示例
void process_data(uint8_t *data, uint16_t len)
{
// 将数据拷贝到应用缓冲区或直接解析
// 注意:此时 DMA 正在写另一块区域,不会冲突
for (uint16_t i = 0; i < len; i++) {
// 处理 data[i]
}
}
4. 若使用中断方式读取数据(非回调)
// 在中断服务函数中手动判断标志
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 接收)
// 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 等场景,希望对你有所帮助。