# 基于 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 等场景,希望对你有所帮助。