2. DMA基础原理

DMA(Direct Memory Access,直接存储器访问)是一种让外设与内存之间、或内存与内存之间在不经过 CPU 逐字节搬运的前提下完成大批量数据转移的硬件机制。它的核心价值在于把 CPU 从“搬数据”的重复劳动中解放出来,让 CPU 专注于协议解析、业务处理与调度,而由 DMA 控制器(DMAC)承担数据通路的搬运工作。理解 DMA 的定义、工作模式、总线主控、地址映射、描述符与通道机制,是后续分析吞吐瓶颈、定位传输异常与做性能优化的基础。

一、DMA 的定义与基本数据通路

从系统结构看,DMA 传输涉及三个角色:源端(外设 FIFO 或内存缓冲区)、目的端(内存缓冲区或外设 FIFO)、以及DMA 控制器。CPU 只负责“配置”这次传输:告诉 DMAC 源地址、目的地址、传输长度、位宽、突发长度、传输方向以及完成后的中断方式。配置完成后,DMAC 向总线发起读写请求,数据在源与目的之间直接流动,CPU 在此期间可以继续执行其他任务。

一次典型的 DMA 传输生命周期可以概括为:CPU 写寄存器启动 → DMAC 申请总线 → 按描述符搬运数据 → 计数递减到 0 → 产生完成中断 → CPU 在中断中回收缓冲区。这个生命周期决定了后续所有性能分析的基本观察点:启动开销、总线占用、搬运效率与中断处理成本。

二、工作模式:从单次到循环与分散聚集

不同场景对 DMA 的连续性、实时性和缓冲区管理要求不同,因此 DMAC 通常支持多种工作模式:

  • 单次传输(Single/Block):一次配置搬运固定长度,完成后停止。适合一次性大块数据,如固件加载。
  • 循环传输(Circular/Cyclic):缓冲区首尾相接,搬完自动回到起点继续。适合音频、ADC 采样等连续流场景。
  • 分散聚集(Scatter-Gather):用一组描述符把多个不连续的内存块串成一次“逻辑连续”的传输,避免大块连续内存分配压力。
  • 双缓冲/乒乓(Double Buffer):两块缓冲区交替使用,一块在搬运时另一块可被 CPU 处理,降低等待与丢包风险。
选择工作模式的关键判断:数据是否连续到达、CPU 处理是否与搬运并行、内存是否允许大块连续分配。循环模式省去反复启动开销,但要求 CPU 在缓冲区回卷前完成处理,否则会覆盖未处理数据。

三、总线主控与地址映射

DMA 能否高效工作,取决于 DMAC 是否具备总线主控(Bus Master)能力。作为总线主控,DMAC 可以主动发起总线读写事务,与 CPU 竞争总线带宽。在多主总线系统中,仲裁器决定谁获得总线使用权,因此 DMA 吞吐不仅受 DMAC 自身限制,也受总线仲裁策略、优先级和并发访问影响。

地址映射是另一个高频出错点。CPU 视角的虚拟地址、物理地址,与外设视角的总线地址往往并不相同:

  • CPU 虚拟地址:驱动中 kmalloc/vmalloc 返回的地址,需经 MMU 映射。
  • 物理地址:virt_to_phys 或 page_to_phys 得到的地址,是内存真实位置。
  • 总线/DMA 地址:外设通过总线看到的地址,可能经过 IOMMU 或桥片转换。

因此驱动必须使用 dma_map_single()、dma_map_sg() 等 DMA API 建立映射,而不是直接把虚拟地址写入 DMA 寄存器。映射同时承担 cache 一致性维护职责:dma_sync_single_for_device() 在设备读取前把 cache 写回内存,dma_sync_single_for_cpu() 在 CPU 读取前使 cache 失效,避免读到旧数据。

/* 典型映射与同步流程 */
dma_addr_t dma_handle;
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

/* 流式映射:单向传输 */
dma_addr_t addr = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, addr)) { /* 处理失败 */ }

/* 设备读取前,确保数据已写入内存 */
dma_sync_single_for_device(dev, addr, size, DMA_TO_DEVICE);

/* 传输完成后解除映射 */
dma_unmap_single(dev, addr, size, DMA_TO_DEVICE);

四、描述符与通道机制

描述符(Descriptor)是 DMA 传输的“任务单”,通常包含源地址、目的地址、传输长度、控制位(中断使能、方向、突发类型)以及指向下一个描述符的指针。硬件按描述符链依次执行,形成链表式或环形队列式传输。描述符机制让驱动可以预先排好一批传输任务,DMAC 自动连续执行,显著降低 CPU 干预频率。

通道(Channel)是 DMAC 内部的独立传输引擎。一个 DMAC 往往有多个通道,每个通道可绑定不同外设请求线(如 UART TX、SPI RX、I2S TX)。通道之间可并行工作,但共享总线带宽,因此多通道并发时要注意带宽争用与优先级配置。通道与请求线的绑定关系、通道优先级、以及是否支持硬件握手(Hardware Handshake),直接决定外设能否以稳定速率触发 DMA 搬运。

DMA 传输核心结构:总线主控 · 地址映射 · 描述符 · 通道 CPU 配置 / 中断处理 内存 虚拟/物理地址 缓冲区 / 描述符链 外设 UART / SPI / I2S FIFO 请求线 DMA 控制器(DMAC) 总线主控 Bus Master 主动发起读写事务 · 参与仲裁 地址映射 虚拟 → 物理 → 总线地址 通道 Channel CH0 · CH1 · CH2 · CH3 独立引擎 · 共享总线带宽 绑定外设请求线 描述符 Descriptor 源地址 / 目的地址 长度 / 控制位 / next 指针 链表或环形队列 数据流:源 → DMAC 搬运 → 目的 CPU 仅配置与收尾,不逐字节参与搬运 配置寄存器 读写内存 请求触发 地址转换 取描述符

五、案例场景:UART 高速接收的 DMA 配置

假设需要以 3 Mbps 波特率连续接收 UART 数据,若采用中断逐字节读取,CPU 每秒要处理约 30 万次中断,几乎无法承受。改用 DMA 循环模式后,配置流程如下:

# 伪代码:UART RX 循环 DMA 配置
1. 分配环形缓冲区 rx_buf[64KB],dma_alloc_coherent 获取总线地址
2. 配置 DMAC 通道 CH2:
   src = UART_RDR 寄存器地址(固定)
   dst = rx_buf 总线地址(递增)
   len = 64KB,循环模式,半满/全满中断使能
3. 使能 UART 的 DMA 接收请求线
4. 启动通道,DMAC 等待 UART FIFO 触发阈值后自动搬运
5. 半满中断:处理前半缓冲区;全满中断:处理后半缓冲区

该场景中,描述符链把 64KB 拆成多个小块,循环模式让缓冲区自动回卷,半满/全满中断把 CPU 唤醒频率从“每字节”降到“每 32KB”,CPU 占用率大幅下降。若发现丢包,应优先检查:缓冲区是否在回卷前处理完、总线仲裁是否被高优先级主控长期占用、cache 同步是否遗漏。

六、常见误区与复盘清单

误区 现象 正确做法
直接把虚拟地址写入 DMA 寄存器 传输到错误内存或触发 IOMMU 异常 使用 dma_map_single / dma_alloc_coherent 获取总线地址
忽略 cache 一致性 CPU 读到旧数据,或设备读到未写回数据 传输前后调用 dma_sync_single_for_device / for_cpu
循环模式缓冲区过小 CPU 未处理完就被新数据覆盖,出现丢包 按最坏处理延迟估算缓冲区大小,配合半满中断
多通道无优先级规划 高带宽通道挤占低延迟通道,实时性变差 按业务优先级配置通道仲裁与突发长度
描述符链未对齐或越界 DMAC 取描述符失败,传输中途停止 描述符按硬件要求对齐,链尾置结束标志
复盘时建议按“配置—映射—搬运—完成”四段排查:配置段看寄存器与描述符,映射段看地址与 cache,搬运段看总线仲裁与通道优先级,完成段看中断与缓冲区回收。多数 DMA 异常并非 DMAC 本身故障,而是地址映射或同步语义使用不当。

掌握以上基础后,后续分析 DMA 吞吐时就能把问题定位到具体环节:是启动开销过大、总线带宽争用、描述符链断裂,还是 cache 同步引入额外延迟。DMA 的性能优化,本质上是对这些环节的逐一量化与取舍。