4. DMA与CPU的协同设计:中断优先级配置、任务唤醒机制、延迟敏感型数据传输

DMA 不是独立工作的。它和 CPU 共享总线、共享内存、共享外设。如果协同没做好,DMA 反而会成为系统的瓶颈。我见过不少项目,DMA 配得挺漂亮,但一跑起来 CPU 就被中断淹没了,或者数据到了 CPU 却还在睡大觉。

这一章,我们就来聊聊怎么让 DMA 和 CPU 好好配合。

4.1 DMA 中断优先级配置

DMA 传输完成、半传输、传输错误都会触发中断。这些中断的优先级怎么设?设高了,CPU 频繁响应,影响主循环;设低了,数据可能被覆盖。

我的原则:按数据丢失的风险来排优先级。

中断类型 推荐优先级 理由
DMA 传输错误中断 最高(0 或 1) 错误必须立即处理,否则数据全乱
延迟敏感型传输完成中断 高(1~2) 音频、控制环路等实时性要求高
普通数据流传输完成中断 中等(2~3) 可以容忍少量延迟
半传输中断 低(3~4) 用于双缓冲切换,延迟影响小
小技巧:STM32 的 NVIC 支持抢占优先级和子优先级。我习惯把 DMA 错误中断设为抢占优先级 0,子优先级 0——谁都不能打断它。普通传输完成中断设抢占优先级 1,子优先级随意。

4.2 任务唤醒机制

DMA 传输完成后,怎么通知 CPU 来取数据?

最直接的方式:在 DMA 中断服务函数里设置一个标志位,主循环轮询这个标志。但轮询效率低,CPU 空转浪费电。

更好的做法:使用 RTOS 的事件标志组或信号量。

// 以 FreeRTOS 为例
void DMA2_Stream0_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    
    if (LL_DMA_IsActiveFlag_TC(DMA2, LL_DMA_STREAM_0))
    {
        LL_DMA_ClearFlag_TC(DMA2, LL_DMA_STREAM_0);
        
        // 释放信号量,唤醒等待数据的任务
        xSemaphoreGiveFromISR(xDmaSemaphore, &xHigherPriorityTaskWoken);
    }
    
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 任务中等待数据
void DataProcessTask(void *pvParameters)
{
    while (1)
    {
        // 阻塞等待,不消耗 CPU
        if (xSemaphoreTake(xDmaSemaphore, portMAX_DELAY) == pdTRUE)
        {
            // DMA 数据已就绪,开始处理
            ProcessDmaBuffer();
        }
    }
}

这样 CPU 在等待期间可以休眠或执行其他低优先级任务。只有 DMA 完成时才被唤醒。

关键点:ISR 里不能调用阻塞函数。用 xSemaphoreGiveFromISR 而不是 xSemaphoreGive,并且记得调用 portYIELD_FROM_ISR 来触发任务切换。

4.3 延迟敏感型数据传输

有些场景对延迟极其敏感。比如音频 I2S 传输、电机控制中的 ADC 采样、高速数据采集。DMA 延迟哪怕多一个微秒,系统表现就会出问题。

延迟来源分析:

  • 总线仲裁延迟:DMA 和 CPU 争抢 AHB/APB 总线
  • 中断响应延迟:从 DMA 请求到 CPU 执行 ISR 的时间
  • 缓存一致性延迟:如果用了 Cache,需要手动维护一致性

我曾在做一个音频播放器项目时遇到过这个问题。I2S 通过 DMA 传输音频数据,但偶尔会出现爆音。查了很久才发现是 DMA 中断优先级太低,被其他外设中断打断了。

避坑指南:我曾经把 DMA 中断优先级设得和 SysTick 一样高,结果系统 tick 被延迟,导致 RTOS 调度异常。记住:SysTick 的优先级通常是最高的,DMA 中断不要超过它。

4.4 延迟优化实战策略

针对延迟敏感型传输,我总结了几个实用策略:

  1. 使用 DMA 的循环模式 + 双缓冲:避免频繁中断,CPU 只在缓冲区切换时处理
  2. 中断优先级分组优化:把延迟敏感的中断放在独立的优先级组里
  3. 关闭不必要的 Cache 或使用 MPU 隔离:避免 Cache 导致的数据不一致
  4. DMA 数据流使用专用 FIFO:减少总线争抢

下面这张图展示了 DMA 与 CPU 协同工作的完整流程:

DMA与CPU协同工作流程图 外设 (ADC/UART/I2S/SPI) DMA 控制器 (数据搬运) 内存 (SRAM/Flash) DMA 请求 数据传输 CPU (任务处理) 中断信号 中断服务函数 设置标志/释放信号量 清除中断标志 数据处理任务 等待信号量 处理 DMA 数据 关键协同点: 1. 外设发出 DMA 请求,DMA 控制器自动搬运数据到内存 2. DMA 传输完成后触发中断,CPU 执行 ISR 释放信号量 3. 数据处理任务被唤醒,从内存中读取 DMA 搬运好的数据 4. 整个过程中 CPU 只在中断和任务切换时介入,效率最大化

4.5 实际案例:音频 I2S 传输优化

最后分享一个实际案例。一个音频项目,I2S 通过 DMA 传输 16 位立体声音频,采样率 48kHz。最初配置如下:

  • DMA 中断优先级:3(默认)
  • 中断处理:在 ISR 里直接拷贝数据
  • CPU 主频:168MHz

结果:偶尔爆音,尤其是在系统负载高的时候。

优化过程:

  1. 把 DMA 中断优先级提升到 1
  2. ISR 里只设置标志,不拷贝数据
  3. 使用双缓冲 + 信号量唤醒数据处理任务
  4. 数据处理任务优先级设为高

优化后爆音消失,CPU 占用率反而降低了 15%。

核心结论:DMA 与 CPU 的协同设计,本质是中断延迟和处理效率的平衡。优先级不是越高越好,而是要匹配数据的实时性要求。任务唤醒用信号量或事件标志组,比轮询高效得多。