第4章 DMA中断与事件管理:传输完成中断、半传输中断、传输错误中断、中断优先级配置实战

DMA 中断,说白了就是告诉 CPU:「活干完了,你来收个尾吧」。很多初学者把 DMA 配置好就不管了,结果数据传完都不知道。我刚开始做项目时也犯过这毛病——数据丢了半天才发现,后来老老实实把中断全开了一遍。

这一章我们重点聊三个中断:传输完成中断、半传输中断、传输错误中断。再加上中断优先级的配置,基本覆盖了 DMA 事件管理的核心场景。

核心观点:DMA 中断不是摆设,它是低功耗设计和实时响应的关键桥梁。用好了,CPU 可以安心睡大觉;用不好,CPU 被频繁打断,功耗和性能两头吃亏。

4.1 三种中断的触发时机与典型用途

先看一张表,把三种中断的触发条件和典型场景说清楚。

中断类型 触发时机 典型用途
传输完成中断(TC) DMA 传输完设定的数据量(NDTR 减到 0) 通知 CPU 数据已就绪,可以处理或进入休眠
半传输中断(HT) DMA 传输完一半数据量(NDTR 减到初始值的一半) 双缓冲切换、流水线处理、实时数据分帧
传输错误中断(TE) 总线错误、FIFO 溢出/下溢、配置冲突 异常捕获、系统保护、调试定位

嗯,这里要注意:半传输中断只对固定长度的块传输有意义。如果你用的是循环模式(Circular Mode),半传输中断配合完成中断,可以实现完美的双缓冲乒乓操作。我在一个音频采集项目里就用过这个技巧——ADC 采完前半段数据,CPU 处理前半段,DMA 继续采后半段,效率直接翻倍。

4.2 中断使能与标志位操作

STM32 的 DMA 中断使能,是通过 DMA_CCR 寄存器里的三个位控制的:

  • TCIE(Transfer Complete Interrupt Enable)
  • HTIE(Half Transfer Interrupt Enable)
  • TEIE(Transfer Error Interrupt Enable)

用 HAL 库的话,配置起来很简单。我习惯在初始化结构体里直接设置:

DMA_HandleTypeDef hdma_usart1_tx;

hdma_usart1_tx.Instance = DMA1_Channel4;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_HIGH;

// 使能传输完成中断和传输错误中断
hdma_usart1_tx.Init.TCIE = ENABLE;   // 传输完成中断
hdma_usart1_tx.Init.HTIE = DISABLE;  // 半传输中断,这里先关掉
hdma_usart1_tx.Init.TEIE = ENABLE;   // 传输错误中断

HAL_DMA_Init(&hdma_usart1_tx);

中断使能后,DMA 传输完成或出错时,硬件会自动置位对应的标志位。标志位在 DMA_ISR 寄存器里:

  • TCIF(传输完成标志)
  • HTIF(半传输标志)
  • TEIF(传输错误标志)

中断服务函数里,记得手动清除标志位。HAL 库的弱函数已经帮我们做了这件事,但如果你自己写中断处理,千万别漏了这一步。我曾经见过一个同事,中断标志位没清,结果 CPU 一直在中断里打转,系统直接卡死。

4.3 中断服务函数编写实战

以 USART1 发送为例,我们写一个完整的中断处理流程。先注册中断回调:

// 传输完成回调
void HAL_DMA_TC_Callback(DMA_HandleTypeDef *hdma)
{
    if (hdma->Instance == DMA1_Channel4)
    {
        // 数据发送完成,关闭 USART 发送使能,节省功耗
        __HAL_UART_DISABLE(&huart1);
        
        // 通知应用层:数据已发送完毕
        uart_tx_complete_flag = 1;
    }
}

// 传输错误回调
void HAL_DMA_Error_Callback(DMA_HandleTypeDef *hdma)
{
    if (hdma->Instance == DMA1_Channel4)
    {
        // 记录错误信息
        dma_error_code = HAL_DMA_GetError(hdma);
        
        // 复位 DMA 通道
        HAL_DMA_Abort(hdma);
        
        // 重新初始化
        HAL_DMA_Init(hdma);
        
        // 打印或记录日志(调试阶段用)
        printf("DMA Error: 0x%X\r\n", dma_error_code);
    }
}
小技巧:在低功耗项目中,我习惯在传输完成回调里关闭外设时钟或进入休眠模式。比如 USART 发送完成后,立刻关闭 USART 时钟,能省下不少微安级的电流。别小看这点功耗,积少成多。

4.4 半传输中断与双缓冲应用

半传输中断最经典的应用就是双缓冲乒乓操作。原理很简单:

  1. DMA 开始往 Buffer A 写数据
  2. 写到一半时触发半传输中断,CPU 开始处理 Buffer A 的前半段
  3. DMA 继续写 Buffer A 的后半段
  4. 写完后触发传输完成中断,CPU 处理后半段

但更高效的做法是:用两个独立的 Buffer,半传输中断时切换目标地址。不过 STM32 的标准 DMA 不支持传输中动态改地址(除非你用双缓冲模式的 DMA,比如 STM32F4 的 DMA2 流)。

我个人的经验是:半传输中断更适合做数据分帧。比如一个 1024 字节的音频帧,半传输中断时正好是 512 字节,你可以在这时启动下一个 DMA 传输,或者更新状态机。这样流水线就建立起来了。

void HAL_DMA_HT_Callback(DMA_HandleTypeDef *hdma)
{
    if (hdma->Instance == DMA2_Stream0)
    {
        // 半传输完成,处理前半段数据
        process_audio_frame(audio_buffer, 0, 512);
        
        // 更新状态
        half_transfer_done = 1;
    }
}

void HAL_DMA_TC_Callback(DMA_HandleTypeDef *hdma)
{
    if (hdma->Instance == DMA2_Stream0)
    {
        // 传输完成,处理后半段数据
        process_audio_frame(audio_buffer, 512, 512);
        
        // 重新启动 DMA(循环模式不需要)
        HAL_DMA_Start_IT(hdma, (uint32_t)&adc_value, (uint32_t)audio_buffer, 1024);
    }
}

4.5 中断优先级配置——别让 DMA 中断堵车

中断优先级配置,是很多工程师容易忽略的地方。DMA 中断的优先级,直接影响系统的实时性。

STM32 的 NVIC 支持抢占优先级和子优先级。DMA 中断一般建议配置为中等偏上的优先级。为什么?

  • 优先级太高:会打断关键任务(比如系统滴答定时器、高优先级外设中断)
  • 优先级太低:DMA 传输完成通知被延迟,数据可能被覆盖

我一般这样配:

HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 2, 0);  // 抢占优先级2,子优先级0
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);

这里有个坑:多个 DMA 通道共用同一个中断向量。比如 STM32F103 的 DMA1 通道 1~7 共用 DMA1_Channel1_IRQn 到 DMA1_Channel7_IRQn,但每个通道有自己的中断向量。而有些系列(如 STM32F4)的 DMA 流是独立中断向量。一定要查数据手册,别配错了。

避坑指南:我曾经在一个项目里,把 DMA 中断优先级配成了 0(最高优先级),结果 DMA 中断把 SysTick 中断给抢了。HAL_Delay() 直接失效,系统调度乱套。排查了一整天才发现是优先级的问题。所以,DMA 中断优先级别超过 1,给系统留点余地。

4.6 中断事件管理的完整流程图

下面这张图,把 DMA 中断从触发到处理的完整流程画清楚了。你可以对照着理解。

DMA 中断事件处理流程图 DMA 传输启动 传输完成? TC 中断触发 半传输完成? HT 中断触发 传输错误? TE 中断触发 说明:三种中断互斥触发,优先级由 NVIC 配置决定。TC 和 HT 可同时使能,TE 建议始终使能。

4.7 中断使能与关闭的功耗考量

低功耗设计里,中断不是越多越好。每多一个中断,CPU 被唤醒的概率就大一分。我总结了几条原则:

  • 非必要不使能:如果不需要半传输通知,就把 HTIE 关掉。少一个中断源,就少一次唤醒。
  • 传输完成后及时关闭中断:在 TC 回调里,如果不再需要 DMA,可以调用 HAL_DMA_Abort() 或直接关闭中断使能位。
  • 错误中断必须开:TE 中断建议始终使能。DMA 出错时如果不处理,数据可能静默丢失,排查起来非常痛苦。
// 传输完成后关闭中断,降低功耗
void HAL_DMA_TC_Callback(DMA_HandleTypeDef *hdma)
{
    // 关闭 DMA 中断
    __HAL_DMA_DISABLE_IT(hdma, DMA_IT_TC | DMA_IT_TE);
    
    // 关闭 DMA 通道
    __HAL_DMA_DISABLE(hdma);
    
    // 通知应用层
    dma_done = 1;
}
经验之谈:在电池供电的产品里,我习惯在进入休眠前检查所有 DMA 通道是否都处于空闲状态。如果有 DMA 还在传输,先等它完成再休眠。否则数据传一半断电,重启后状态就乱了。

4.8 常见问题与调试方法

调试 DMA 中断时,我遇到过几个典型问题,列出来供你参考:

问题现象 可能原因 解决方法
中断不触发 中断使能位未设置;NVIC 未使能;DMA 未启动 检查 CCR 寄存器;检查 NVIC_ISER;确认 DMA 已使能
中断频繁触发 标志位未清除;循环模式未正确处理 在 ISR 中清除标志位;循环模式下用回调处理
中断响应延迟大 优先级配置过低;其他高优先级中断阻塞 提高 DMA 中断优先级;检查中断嵌套情况
传输错误中断触发 总线冲突;FIFO 溢出;地址对齐问题 检查 DMA_ISR 寄存器;降低传输速率;检查地址对齐

调试时,我习惯用逻辑分析仪抓 DMA 的请求和应答信号。如果没有逻辑分析仪,可以在中断服务函数里翻转一个 GPIO,用示波器看波形。这个方法虽然土,但很管用。

避坑指南:我曾经在调试一个 SPI DMA 传输时,发现传输完成中断偶尔不触发。查了半天,发现是 SPI 的 NSS 信号没拉低,导致 SPI 从机没响应,DMA 一直在等数据。所以,DMA 中断不触发时,先检查外设本身是否正常工作,别光盯着 DMA 寄存器看。

好了,这一章的内容就到这里。DMA 中断管理是嵌入式开发里的基本功,但也是容易出细节问题的地方。多动手、多调试,慢慢就能找到感觉。