2. DMA基础概念

在嵌入式与服务器 Linux 驱动开发中,CPU 频繁搬运数据会带来两个直接后果:一是占用大量指令周期,二是打断缓存与流水线的局部性。DMA(Direct Memory Access,直接存储器访问)正是为解决这一矛盾而设计的硬件机制:它允许外设与内存之间、内存与内存之间在不经过 CPU 逐字节搬运的前提下完成数据块传输,CPU 只需配置好源地址、目的地址、传输长度和触发方式,随后即可去处理其他任务,传输完成后再由中断通知。

DMA 的本质不是“更快的内存”,而是“把搬运工作从 CPU 卸载到独立的传输引擎”,从而提升系统吞吐并降低 CPU 占用率。

一、DMA 的工作原理

一次典型的 DMA 传输可以拆成五个阶段:请求、仲裁、配置、搬运、完成通知。外设(如 UART、SPI、ADC、网卡)在需要发送或接收数据时,通过请求线向 DMA 控制器发出 DMA Request;DMA 控制器根据通道优先级进行仲裁,获得总线使用权后,从内存或外设 FIFO 读取数据并写入目标地址;每完成一个数据单元(beat)或一个块(burst),地址寄存器按配置递增或保持不变;当传输计数减到零时,DMA 控制器产生完成中断,驱动在中断服务程序中回收缓冲区、唤醒等待队列或提交网络协议栈。

从总线视角看,DMA 控制器是总线上的一个主设备(Master),它与 CPU 竞争总线带宽。因此在高带宽场景下,必须关注总线矩阵、QoS 和带宽分配,否则 DMA 可能反过来拖慢 CPU 对内存的访问。

DMA 传输数据流与请求/中断协同 CPU / 驱动 配置描述符 启动传输 DMA 控制器 通道仲裁 / 地址生成 传输计数 / 中断产生 外设 UART / SPI / ADC 网卡 / 存储控制器 系统内存 源缓冲区 / 目的缓冲区 DMA 一致性内存 配置寄存器 DMA 请求线 读写内存 完成中断 中断控制器 GIC / NVIC 通知驱动处理 ISR 关键参数 源地址 / 目的地址 / 长度 传输宽度 / burst / 优先级

二、传输类型与通道、请求线

按源和目的的组合,DMA 传输通常分为三类:内存到外设(如音频播放、UART 发送)、外设到内存(如 ADC 采样、网卡收包)、内存到内存(如大块数据搬移、图像处理)。前两类是驱动中最常见的,第三类在部分控制器上支持,但通常效率低于 CPU 的 SIMD 拷贝,需谨慎使用。

DMA 控制器内部由多个通道(Channel)组成,每个通道可独立配置源、目的、长度和触发源。通道与请求线(Request Line)之间通过多路选择器连接:外设的请求信号进入 DMAC 后,由通道映射表决定哪个请求触发哪个通道。Linux 中这一映射通常由设备树描述,例如:

/* 设备树片段:将 UART2 的发送请求映射到 DMA 通道 3 */
uart2: serial@40004000 {
    compatible = "vendor,uart";
    dmas = <&dma0 3 0>;
    dma-names = "tx";
};

dma0: dma-controller@40020000 {
    compatible = "vendor,dma";
    #dma-cells = <2>;
    dma-channels = <8>;
    dma-requests = <16>;
};

驱动侧通过 DMA Engine 框架申请通道,核心 API 如下:

struct dma_chan *chan;
struct dma_slave_config cfg = {
    .direction = DMA_MEM_TO_DEV,
    .dst_addr = uart_base + UART_TDR,
    .dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE,
    .dst_maxburst = 16,
};

chan = dma_request_chan(dev, "tx");
dmaengine_slave_config(chan, &cfg);

struct dma_async_tx_descriptor *tx;
tx = dmaengine_prep_slave_sg(chan, sgl, sg_len,
                             DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
tx->callback = uart_tx_done;
tx->callback_param = uart;
dmaengine_submit(tx);
dma_async_issue_pending(chan);

三、Linux 驱动中的典型应用场景

场景 方向 典型外设 驱动关注点
串口高速收发 内存 ↔ 外设 UART / SPI 环形缓冲区、半满中断、流控
音频播放与录制 内存 ↔ 外设 I2S / SAI 周期传输、双缓冲、指针同步
网络数据包收发 外设 ↔ 内存 以太网 MAC 描述符环、NAPI、缓存一致性
ADC 连续采样 外设 → 内存 ADC / DMA 循环模式、采样率、溢出处理
存储块传输 内存 ↔ 外设 SDIO / NAND 大块 SG 列表、超时与错误恢复

四、常见误区与复盘要点

误区一:认为 DMA 传输不需要缓存维护。对于非一致性架构,CPU 写内存后启动 DMA 发送,必须先 clean 缓存;DMA 接收完成后,CPU 读取前必须先 invalidate 缓存,否则可能读到旧数据或覆盖新数据。

误区二:在中断中做大量拷贝。DMA 的价值在于卸载搬运,若 ISR 中又用 CPU 逐字节拷贝,等于把卸载的工作重新捡回来,应尽量使用零拷贝或 SG 列表。

误区三:忽略通道资源竞争。多个外设共享 DMA 控制器时,若通道分配不当或优先级配置不合理,会出现请求丢失、传输延迟抖动,甚至死锁。

调试建议:先确认 dma_request_chan() 是否成功,再检查 dmaengine_slave_config() 的地址宽度与 burst 是否与外设 FIFO 匹配;用 cat /proc/interrupts 观察 DMA 中断计数是否增长;用 dmesg 查看 DMA 映射失败或超时日志。若传输完成但数据异常,优先排查缓存一致性与地址对齐问题。

理解 DMA 的定义、工作原理、传输类型、通道与请求线,以及它在 Linux 驱动中的典型落点,是后续学习“中断协同”和“缓存一致性调试”的前提。只有先建立“谁在搬、搬什么、搬完怎么通知、搬的过程中缓存是否一致”这条完整链路,才能在真实项目中快速定位 DMA 相关问题。