2. DMA基础原理
DMA(Direct Memory Access,直接存储器访问)的本质,是让外设与内存之间的大块数据搬运脱离 CPU 的逐字节参与。CPU 只负责“下命令”和“收完成通知”,真正的数据流由 DMA 控制器(DMAC)在总线上自主完成。理解 DMA,必须同时看清五件事:控制器内部由哪些通道与引擎组成、数据往哪个方向流、地址如何映射到物理内存、总线带宽如何被占用、以及突发传输和描述符如何把一次大传输拆解成可调度的任务。
一、DMA控制器架构:通道、引擎与寄存器
一个典型的 DMAC 由总线主接口、通道逻辑、传输引擎和寄存器组构成。总线主接口让 DMAC 能主动发起读写事务,这是它区别于普通外设的关键;通道逻辑负责仲裁多个外设请求;传输引擎按配置完成源地址递增、目的地址递增、传输宽度和突发长度控制;寄存器组则保存源地址、目的地址、传输长度、控制位和状态位。
在 Linux 中,DMAC 被抽象为 dma_device 与 dma_chan。驱动通过 dma_request_chan() 申请通道,用 dma_slave_config() 配置方向、地址和突发参数,再用 dma_prep_* 准备传输。下面是一个典型的从设备配置片段:
struct dma_slave_config cfg = {
.direction = DMA_DEV_TO_MEM,
.src_addr = (dma_addr_t)dev_phys_addr,
.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
.src_maxburst = 16,
.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
.dst_maxburst = 16,
};
dmaengine_slave_config(chan, &cfg);
二、传输方向与地址映射
DMA 传输方向通常分为三类:内存到外设(MEM_TO_DEV)、外设到内存(DEV_TO_MEM)和内存到内存(MEM_TO_MEM)。方向决定了源地址与目的地址谁是“设备侧”、谁是“内存侧”,也决定了地址是否需要固定。例如 UART 接收时,外设地址固定、内存地址递增;而内存拷贝时,两端地址都递增。
地址映射是 DMA 最容易出错的地方。CPU 看到的是虚拟地址,DMAC 看到的是总线物理地址。驱动必须用 dma_map_single() 或 dma_map_sg() 把内存缓冲区映射为 DMA 可访问地址,并在传输结束后用 dma_unmap_single() 解除映射。若使用流式映射,还要注意 cache 一致性:CPU 写内存后需 dma_sync_single_for_device(),DMAC 写回后需 dma_sync_single_for_cpu()。
kmalloc() 返回的虚拟地址写入 DMA 寄存器。DMAC 无法识别虚拟地址,轻则传输失败,重则踩坏物理内存。必须使用 dma_alloc_coherent() 或流式映射得到的 DMA 地址。三、总线带宽、突发传输与描述符
总线带宽决定 DMA 的理论上限。若总线位宽 64 bit、频率 200 MHz,则峰值带宽为 64/8 × 200M = 1.6 GB/s。但实际有效带宽受仲裁、握手、读写切换和 cache 一致性开销影响。突发传输(burst)通过一次总线事务搬运多个数据单元,减少地址阶段和仲裁开销,从而逼近峰值带宽。突发长度过小,事务开销占比高;过大则可能挤占其他主设备带宽,增加延迟。
描述符(descriptor)是 DMA 从“单次搬运”走向“可调度传输”的关键。一个描述符通常包含源地址、目的地址、长度、控制/状态字段和下一个描述符指针。多个描述符可组成链表或环形队列,DMAC 按链自动执行,完成后再触发中断。这样 CPU 只需准备描述符链,而不必逐次干预。
四、案例场景:UART 接收 4 KB 数据的 DMA 路径
假设 UART 以 115200 波特率接收 4 KB 数据,驱动采用 DEV_TO_MEM 方向。流程如下:
- 驱动用
dma_alloc_coherent()分配 4 KB DMA 缓冲区,得到虚拟地址和 DMA 地址。 - 调用
dmaengine_slave_config()设置源地址为 UART FIFO 物理地址、目的地址为 DMA 缓冲区地址、突发长度为 16。 - 用
dma_prep_dma_cyclic()或dma_prep_slave_sg()准备描述符,提交给通道。 - DMAC 收到 UART 请求后,按突发长度从 FIFO 搬运到内存,地址递增,长度递减。
- 传输完成触发中断,驱动调用
dma_sync_single_for_cpu()后读取数据,再重新提交描述符。
这个案例中,描述符链让驱动可以一次性提交多个缓冲区,DMAC 自动切换,CPU 只在整链完成或半满时被唤醒,显著降低中断频率。
五、参数关系与常见误区复盘
| 参数/概念 | 作用 | 典型取值/注意点 |
|---|---|---|
| 传输方向 | 决定源/目的谁是设备侧 | DEV_TO_MEM 时源地址固定 |
| 突发长度 | 减少总线事务开销 | 常取 4/8/16,需匹配 FIFO 深度 |
| 传输宽度 | 单次搬运字节数 | 1/2/4/8 字节,需与外设对齐 |
| DMA 地址 | DMAC 实际访问的物理地址 | 必须由 dma_map 或 dma_alloc_coherent 获得 |
| 描述符 | 描述一次搬运任务 | 含 src/dst/len/next,可链式或环形 |
| cache 一致性 | 保证 CPU 与 DMAC 看到相同数据 | 流式映射需 sync,一致性映射免 sync |
总结来看,DMA 控制器通过总线主接口和传输引擎把数据搬运从 CPU 卸载;传输方向决定地址行为;地址映射保证 DMAC 能正确访问内存;总线带宽与突发长度共同决定吞吐;描述符则把大传输拆成可链式调度的任务单元。掌握这五者的关系,才能在高带宽场景下设计出稳定、低 CPU 占用的 DMA 驱动。