1. DMA 基础概念:什么是 DMA?为什么需要它?

大家好,我是你们这门课的老朋友。做了十几年嵌入式驱动,DMA 可以说是我打交道最多的外设之一。今天咱们就从最基础的概念聊起,把 DMA 到底是什么、它和 CPU 搬数据有啥区别、以及什么时候该用它,一次性讲透。

1.1 什么是 DMA?

DMA,全称 Direct Memory Access,直接存储器访问。说白了,就是一块硬件,专门负责在内存和外设之间、或者内存和内存之间搬运数据。

你想想看,如果没有 DMA,CPU 得亲自下场搬数据。比如从网卡收一个包,CPU 要先把数据从网卡 FIFO 读到寄存器,再写到内存。一次两次还行,但网络数据是源源不断的,CPU 大部分时间都花在搬砖上了,哪有空干正事?

DMA 就是来解决这个问题的。它是个独立的总线主控,能自己发起读写操作。CPU 只需要告诉它:从哪搬、搬到哪、搬多少,然后就可以去忙别的了。搬完数据,DMA 会发个中断通知 CPU。

核心要点:DMA 是一个硬件数据搬运工,CPU 是它的老板。老板只下指令,不亲自干活。

1.2 DMA 与 CPU 数据搬运的区别

我刚开始做驱动时,总觉得 DMA 可有可无。后来在一个高速数据采集项目里,CPU 被中断和搬数据压得喘不过气,我才真正体会到两者的差距。

咱们直接看对比表:

对比维度 CPU 直接搬运 DMA 搬运
执行主体 CPU 核心 DMA 控制器(独立硬件)
占用 CPU 100% 占用,无法做其他事 仅初始化时占用,搬运过程 CPU 可并行处理
搬运速度 受限于 CPU 主频和指令执行周期 接近总线带宽,通常比 CPU 快 2-10 倍
适合数据量 小数据(几十字节以内) 大数据(几百字节到几兆字节)
中断频率 每次传输都可能触发中断 一次传输完成才触发中断,频率低
实现复杂度 简单,直接读写寄存器 需要配置描述符、处理缓存一致性

嗯,这里要注意:不是说 DMA 一定比 CPU 快。对于几个字节的传输,DMA 的初始化开销反而更大。我见过有人对 4 字节的 SPI 数据也用 DMA,结果性能反而下降了。

1.3 DMA 传输的优势

总结下来,DMA 有三大核心优势:

  1. 解放 CPU:CPU 从繁重的数据搬运中解脱出来,可以专心处理协议栈、算法、用户交互等核心任务。
  2. 提高吞吐量:DMA 能利用总线突发传输,一次搬一大块数据,效率远高于 CPU 逐字搬运。
  3. 降低延迟:数据从外设到内存的路径更短,不需要经过 CPU 寄存器中转。

我的经验:在一个 4G 基站的加速器项目中,我们用 DMA 把数据搬运延迟从 12μs 降到了 1.8μs。CPU 利用率从 85% 降到了 15%。这个提升是质变的。

1.4 DMA 的适用场景

不是所有场景都适合用 DMA。我给大家画个图,一看就明白:

DMA 适用场景决策图 数据搬运需求 小数据(< 64 字节) 大数据(> 256 字节) CPU 直接操作 PIO 模式 DMA 传输 DMA + 双缓冲 音频/视频流 高速网络(以太网) 存储(SD/MMC/NAND) 决策原则:数据量大 + 周期性 + 对延迟敏感 → 优先考虑 DMA 数据量小 + 随机访问 + 对实时性要求极高 → 考虑 CPU 直接操作

具体来说,以下场景强烈建议使用 DMA:

  • 高速外设数据流:以太网、USB、SDIO、高速 SPI/I2S
  • 多媒体处理:摄像头数据采集、音频播放/录制、显示刷新
  • 大块内存拷贝:比如从用户态到内核态的数据传递
  • 周期性数据搬移:ADC 采样、定时器触发的数据采集

避坑指南:我曾经在一个低功耗 IoT 项目里,对每 100ms 一次的传感器数据用了 DMA。结果发现 DMA 的初始化功耗和总线仲裁开销,比 CPU 直接读寄存器还大。后来我改成了 PIO 模式,功耗降了 30%。记住:DMA 不是万能的,小数据量别用。

1.5 DMA 传输的基本流程

咱们用代码来感受一下 DMA 的典型工作流程。以 STM32 的 DMA 为例:

// 1. 配置 DMA 通道
DMA_InitTypeDef dma_config;
dma_config.PeriphBaseAddr  = (uint32_t)&USART1->DR;   // 外设地址
dma_config.MemoryBaseAddr  = (uint32_t)rx_buffer;     // 内存地址
dma_config.Direction       = DMA_PERIPH_TO_MEMORY;    // 方向:外设→内存
dma_config.BufferSize      = 256;                     // 传输大小
dma_config.PeriphInc       = DMA_PERIPHINC_DISABLE;   // 外设地址不递增
dma_config.MemInc          = DMA_MEMINC_ENABLE;       // 内存地址递增

// 2. 使能 DMA
DMA_Cmd(DMA1_Channel5, ENABLE);

// 3. CPU 去干别的事...
// 4. DMA 传输完成,触发中断
void DMA1_Channel5_IRQHandler(void) {
    if(DMA_GetITStatus(DMA1_IT_TC5)) {
        // 数据已经安安稳稳在 rx_buffer 里了
        process_data(rx_buffer, 256);
        DMA_ClearITPendingBit(DMA1_IT_TC5);
    }
}

你看,CPU 只需要做两件事:配置和收尾。中间的数据搬运,DMA 全包了。

1.6 小结

DMA 的本质,就是把 CPU 从重复的数据搬运中解放出来。它适合大数据量、周期性、对延迟敏感的场景。但小数据量时,CPU 直接操作反而更高效。

嗯,这一章就到这里。记住一句话:让 CPU 做它擅长的事(运算、控制),让 DMA 做它擅长的事(搬数据)。后面我们会深入 DMA 的硬件架构、描述符链、缓存一致性等实战内容。