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) | 用于双缓冲切换,延迟影响小 |
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 中断优先级太低,被其他外设中断打断了。
4.4 延迟优化实战策略
针对延迟敏感型传输,我总结了几个实用策略:
- 使用 DMA 的循环模式 + 双缓冲:避免频繁中断,CPU 只在缓冲区切换时处理
- 中断优先级分组优化:把延迟敏感的中断放在独立的优先级组里
- 关闭不必要的 Cache 或使用 MPU 隔离:避免 Cache 导致的数据不一致
- DMA 数据流使用专用 FIFO:减少总线争抢
下面这张图展示了 DMA 与 CPU 协同工作的完整流程:
4.5 实际案例:音频 I2S 传输优化
最后分享一个实际案例。一个音频项目,I2S 通过 DMA 传输 16 位立体声音频,采样率 48kHz。最初配置如下:
- DMA 中断优先级:3(默认)
- 中断处理:在 ISR 里直接拷贝数据
- CPU 主频:168MHz
结果:偶尔爆音,尤其是在系统负载高的时候。
优化过程:
- 把 DMA 中断优先级提升到 1
- ISR 里只设置标志,不拷贝数据
- 使用双缓冲 + 信号量唤醒数据处理任务
- 数据处理任务优先级设为高
优化后爆音消失,CPU 占用率反而降低了 15%。
核心结论:DMA 与 CPU 的协同设计,本质是中断延迟和处理效率的平衡。优先级不是越高越好,而是要匹配数据的实时性要求。任务唤醒用信号量或事件标志组,比轮询高效得多。