4、DMA与中断的协同:DMA传输完成中断、错误中断、中断服务程序编写要点

DMA 这家伙,说白了就是个勤快的搬运工。它自己在那吭哧吭哧搬数据,CPU 就能腾出手干别的。但问题来了——你怎么知道它搬完了?搬的过程中有没有摔跤?这时候,就得靠中断来搭把手了。

我刚开始做 DMA 驱动时,总觉得中断就是个“通知到位”的事。后来踩了坑才明白,DMA 和中断的协同,是决定系统实时性和稳定性的关键一环。今天咱们就聊聊这个。

4.1 DMA 传输完成中断

传输完成中断,是最常见的一种。DMA 控制器把指定长度的数据搬完后,会拉一根中断信号给 CPU。CPU 收到后,就知道“哦,数据准备好了”。

我个人习惯,在初始化 DMA 时就把完成中断使能打开。代码里大概是这样的:

// 使能传输完成中断
dma_channel->IER |= DMA_IER_TCIE;
// 清除可能残留的中断标志
dma_channel->IFCR |= DMA_IFCR_CTCIF;

这里有个细节:一定要先清除中断标志,再使能中断。为什么?因为有些芯片在上电或复位后,中断标志位可能是脏的。不清掉的话,一使能中断,CPU 立马就跳进 ISR,结果啥事没有,白忙活一场。我在项目里遇到过这种“假中断”,排查了半天才发现是初始化顺序的问题。

4.2 错误中断

错误中断,很多人容易忽略。觉得 DMA 这么可靠的机制,怎么会出错?其实不然。

常见的 DMA 错误包括:

  • 总线错误:访问了不存在的地址,或者外设没准备好就强行读写
  • 传输长度错误:配置的传输长度超过了硬件支持的范围
  • FIFO 溢出/下溢:尤其在高速传输时,FIFO 没来得及处理

我曾经在一个音频项目中,DMA 从 I2S 外设搬运数据到内存。板子跑起来一切正常,但一上机箱,偶尔会出现“噗噗”的杂音。查了半天,发现是 DMA 的 FIFO 下溢中断触发了。原因是机箱内的电磁干扰导致 I2S 时钟抖动,DMA 读取速度跟不上。后来我在 ISR 里加了错误恢复逻辑,问题才解决。

所以,错误中断一定要处理。哪怕只是打印一条日志,也比让它静默发生要好。

警告: 不要忽略错误中断!很多驱动“看起来”能跑,但偶尔卡死或数据错乱,往往就是 DMA 错误没处理导致的。

4.3 中断服务程序编写要点

写 ISR 和写普通函数不一样。ISR 是在中断上下文里跑的,讲究一个“快”字。我总结了几个要点:

4.3.1 快速判断中断源

一个 DMA 控制器通常有多个通道。ISR 进来后,第一件事就是判断是哪个通道、哪种中断触发了。别上来就一通操作。

void DMA_IRQHandler(void)
{
    uint32_t status = dma->ISR;
    
    // 检查通道1传输完成
    if (status & DMA_ISR_TCIF1) {
        dma->IFCR |= DMA_IFCR_CTCIF1;
        // 处理完成事件
        handle_dma_done(1);
    }
    
    // 检查通道1错误
    if (status & DMA_ISR_TEIF1) {
        dma->IFCR |= DMA_IFCR_CTEIF1;
        // 处理错误事件
        handle_dma_error(1);
    }
    
    // 其他通道同理...
}

你看,先读状态寄存器,再逐个判断。处理完一个就清一个标志。这样逻辑清晰,也不会漏掉。

4.3.2 尽量减少 ISR 里的工作量

ISR 里只做最必要的事:清标志、读状态、设置一个标志位或唤醒一个任务。真正的数据处理,放到任务级去干。

举个例子:

// ISR 里只做这些
g_dma_done_flag = 1;  // 设置全局标志
// 或者
osSemaphoreRelease(dma_sem);  // 释放信号量,唤醒任务

千万别在 ISR 里做 printf、malloc、或者复杂的计算。你想想看,ISR 占用的时间越长,系统响应其他中断的能力就越差。严重时会导致中断嵌套溢出,系统直接崩掉。

提示: 我习惯在 ISR 里只放一个函数调用,比如 dma_isr_handler(ch)。这样 ISR 本身很干净,具体的处理逻辑封装在函数里,也方便单元测试。

4.3.3 注意中断优先级

DMA 中断的优先级设置,直接影响系统的实时性。一般来说:

  • 传输完成中断:优先级可以设得中等。因为数据已经搬完了,晚一点处理也没关系
  • 错误中断:优先级要设得高一些。错误需要尽快处理,否则可能引发连锁反应

我曾经在一个多路 DMA 并发的系统里,把两个通道的中断优先级设成了相同。结果高频率的通道频繁抢占,低频率的通道一直得不到响应。后来把低频通道的优先级调高了一级,问题就解决了。嗯,优先级这东西,不是越高越好,而是要合理分配。

4.3.4 处理中断嵌套

如果你的系统支持中断嵌套,那就要小心了。DMA 的 ISR 在执行时,如果来了一个更高优先级的中断,当前 ISR 会被打断。这时候,如果共享了某些全局变量,就可能出现竞态条件。

我的做法是:在 ISR 里访问的全局变量,都加上 volatile 修饰。如果涉及多字节变量,还要考虑原子操作。实在不行,就在进入临界区时关中断,处理完再开。

// 进入临界区
__disable_irq();
// 操作共享变量
g_shared_count++;
// 退出临界区
__enable_irq();

当然,关中断的时间要尽可能短。别在临界区里做耗时操作。

4.4 知识体系图

下面这张图,把 DMA 与中断协同的核心逻辑串起来了。你可以对照着看,心里会更清楚。

DMA与中断协同核心逻辑 DMA 控制器 搬运数据 中断产生 完成 / 错误 中断控制器 (NVIC) 优先级管理 / 分发 CPU 执行 ISR 快速处理 / 清标志 ISR 处理要点 • 快速判断中断源 • 减少 ISR 工作量 • 注意中断优先级 • 处理中断嵌套 任务级处理 数据搬移 / 协议解析 错误处理 恢复 / 重传 / 上报

从图上你能看到,DMA 产生中断后,经过中断控制器分发到 CPU。CPU 在 ISR 里快速处理,然后唤醒任务去做真正的数据搬运或错误恢复。这个流程,说白了就是“快进快出”。

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 中断标志没清干净:有些芯片的清标志操作需要“写1清0”,有些是“写0清1”。一定要看数据手册,别想当然。
  • ISR 里调用了阻塞函数:比如在 ISR 里等信号量、等互斥锁。这会导致系统死锁。ISR 里只能用“从中断中唤醒”的 API。
  • 忽略了中断丢失:如果 DMA 传输非常频繁,ISR 处理不过来,就可能丢中断。这时候要考虑用“中断合并”或“轮询+中断”混合模式。

嗯,DMA 和中断的协同,说难不难,说简单也不简单。关键是把每个细节都想到位。你按上面这些要点去写,基本不会出大问题。

核心总结: DMA 中断处理的核心是“快”。快速判断、快速清标志、快速退出。把复杂逻辑留给任务级。错误中断一定要处理,别让它静默发生。

可可精品资料库