3. DMA基础概念
3.1 DMA控制器工作原理
直接存储器访问(DMA)是一种允许硬件子系统直接访问系统主内存的机制,无需CPU的干预。DMA控制器是这一机制的核心硬件单元,负责在源地址与目的地址之间搬运数据,并在传输完成后通知CPU。
核心价值:DMA将CPU从繁重的数据拷贝任务中解放出来,使CPU能够专注于计算与控制逻辑,从而显著提升系统吞吐量并降低延迟。
3.1.1 DMA控制器内部架构
一个典型的DMA控制器包含以下关键寄存器与逻辑单元:
| 组件 | 功能描述 | 驱动关注点 |
|---|---|---|
| 源地址寄存器(SAR) | 存放数据源的内存或外设地址 | 需保证物理地址连续或使用SG表 |
| 目的地址寄存器(DAR) | 存放数据目标的内存或外设地址 | 对齐要求(通常4/8/32字节) |
| 传输计数寄存器(TCR) | 记录剩余待传输的数据量(字节/字) | 递减至0时触发中断 |
| 控制寄存器(CR) | 配置传输方向、模式、宽度、增量等 | 驱动初始化时写入配置 |
| 状态寄存器(SR) | 指示传输完成、错误等状态位 | 中断服务程序中读取判断 |
| 通道仲裁逻辑 | 多通道DMA的优先级调度 | 高优先级通道抢占低优先级 |
3.1.2 DMA传输基本流程
- 驱动配置阶段:CPU设置DMA控制器的源地址、目的地址、传输计数、传输模式等寄存器。
- 启动传输:CPU写控制寄存器使能DMA通道,DMA控制器开始工作。
- 总线仲裁:DMA控制器向总线仲裁器请求总线控制权,获得授权后成为总线主设备。
- 数据搬运:DMA控制器在源与目的之间逐字/逐字节搬运数据,同时递减传输计数寄存器。
- 传输完成:传输计数归零后,DMA控制器置位状态寄存器,并向CPU发出中断请求。
- 中断处理:CPU响应中断,读取状态寄存器确认传输结果,释放或重新配置DMA通道。
3.2 DMA传输模式
根据应用场景的不同,DMA控制器支持多种传输模式。驱动开发者需要根据外设特性与系统需求选择合适的模式,以最大化传输效率并降低CPU干预频率。
3.2.1 单次传输模式(Single Transfer Mode)
单次传输模式是最基本的DMA工作方式。每次DMA请求只传输一个数据单元(字节、半字或字),然后释放总线控制权,等待下一次请求。
- 适用场景:低速外设(如UART、I2C)、数据量小且不连续的场景。
- 优点:实现简单,总线占用时间短,不影响其他总线主设备。
- 缺点:每次传输都需要重新仲裁总线,开销较大,吞吐量低。
- 驱动配置:设置控制寄存器中传输模式位为“单次”,配置源/目的地址增量方式。
3.2.2 循环传输模式(Circular/Burst Transfer Mode)
循环传输模式允许DMA控制器在一次总线授权后连续传输多个数据单元(突发传输),传输完成后自动重新加载初始地址和计数,形成循环缓冲区。
- 适用场景:音频流、视频采集、ADC连续采样等需要持续数据流的场景。
- 优点:无需CPU反复重新配置,传输效率高,适合实时数据流。
- 缺点:需要预先分配固定大小的循环缓冲区,内存利用率固定。
- 驱动配置:设置循环模式位,配置缓冲区基地址和大小,使能自动重载。
性能提示:循环模式下,驱动通常使用“半满中断”和“全满中断”交替处理数据,避免数据覆盖。例如,缓冲区大小为4KB时,传输2KB触发一次中断,CPU处理前半部分,DMA继续写入后半部分。
3.2.3 链表传输模式(Scatter-Gather / Linked-List Mode)
链表传输模式是最高级的DMA传输方式。DMA控制器通过读取内存中的描述符链表,自动获取多个不连续内存块的源地址、目的地址和传输长度,依次完成所有传输任务。
| 特性 | 单次模式 | 循环模式 | 链表模式 |
|---|---|---|---|
| 传输连续性 | 单次请求单数据 | 连续突发传输 | 多段不连续传输 |
| CPU干预频率 | 高(每次请求) | 低(仅中断时) | 极低(链表配置一次) |
| 内存连续性要求 | 连续 | 连续 | 不连续(SG表) |
| 适用数据量 | 小(<1KB) | 中等(流式) | 大(>1MB) |
| 典型外设 | UART、I2C | 音频、ADC | 网卡、存储、GPU |
3.3 DMA与CPU的协同
DMA与CPU的高效协同是高性能驱动设计的核心。两者通过中断、轮询、内存屏障等机制实现同步与数据一致性。
3.3.1 协同工作模式
| 协同模式 | 工作机制 | CPU负载 | 延迟 | 典型场景 |
|---|---|---|---|---|
| 中断驱动 | DMA完成传输后触发中断,CPU在ISR中处理数据 | 低(仅中断时) | 中等(中断响应) | 网卡、存储 |
| 轮询模式 | CPU循环检查DMA状态寄存器 | 高(持续占用) | 低(无中断开销) | 调试、低延迟控制 |
| 双缓冲(Ping-Pong) | 两个缓冲区交替使用,DMA写一个,CPU读另一个 | 低(无竞争) | 低(流水线) | 音频、视频 |
| 描述符环(Descriptor Ring) | CPU提交描述符到环,DMA消费,通过写指针同步 | 极低(批量提交) | 低(硬件调度) | 高性能网卡 |
3.3.2 缓存一致性(Cache Coherency)
关键陷阱:DMA直接访问物理内存,而CPU可能将数据缓存在Cache中。如果CPU修改了缓冲区但数据仍在Cache中未写回内存,DMA读取到的将是陈旧数据。反之,DMA写入内存后,CPU从Cache读取的也可能是旧数据。
解决缓存一致性的常用方法:
- 一致性映射(Coherent Mapping):使用dma_alloc_coherent()分配内存,该API保证CPU与DMA看到的数据一致(通常通过禁用Cache或使用硬件一致性协议实现)。
- 流式映射(Streaming Mapping):使用dma_map_single()/dma_unmap_single(),在DMA传输前执行Cache刷新(Clean/Invalidate),传输后执行Cache失效。
- 内存屏障(Memory Barrier):在CPU修改DMA描述符后使用wmb()确保写操作顺序,在读取DMA写入的数据前使用rmb()确保读操作顺序。
3.3.3 协同工作流程图
3.4 驱动开发中的DMA配置示例
以下是一个简化的DMA驱动初始化代码片段,展示如何配置单次传输模式:
/* DMA通道配置结构体 */
struct dma_channel_config {
u32 src_addr; /* 源物理地址 */
u32 dst_addr; /* 目的物理地址 */
u32 transfer_size; /* 传输字节数 */
u8 src_inc; /* 源地址增量:0-不变,1-递增 */
u8 dst_inc; /* 目的地址增量 */
u8 mode; /* 0-单次,1-循环,2-链表 */
};
/* 初始化DMA传输 */
int dma_transfer_init(struct dma_channel_config *cfg)
{
void __iomem *dma_base = ioremap(DMA_BASE_ADDR, 0x1000);
/* 1. 配置源地址寄存器 */
writel(cfg->src_addr, dma_base + DMA_SAR_OFFSET);
/* 2. 配置目的地址寄存器 */
writel(cfg->dst_addr, dma_base + DMA_DAR_OFFSET);
/* 3. 配置传输计数寄存器 */
writel(cfg->transfer_size, dma_base + DMA_TCR_OFFSET);
/* 4. 配置控制寄存器:模式、增量、中断使能 */
u32 ctrl = 0;
ctrl |= (cfg->mode & 0x3) << 0; /* 传输模式 */
ctrl |= (cfg->src_inc & 0x1) << 2; /* 源地址增量 */
ctrl |= (cfg->dst_inc & 0x1) << 3; /* 目的地址增量 */
ctrl |= (1 << 4); /* 传输完成中断使能 */
writel(ctrl, dma_base + DMA_CR_OFFSET);
/* 5. 启动DMA传输 */
writel(1, dma_base + DMA_ENABLE_OFFSET);
iounmap(dma_base);
return 0;
}
驱动调优要点:
- 优先使用dma_alloc_coherent()分配缓冲区,避免缓存一致性问题。
- 对于大块数据传输,使用链表模式(Scatter-Gather)减少中断次数。
- 合理设置突发传输长度(Burst Size),通常为Cache Line大小(32/64字节)的整数倍。
- 在中断服务程序中尽量只做最小处理(如更新指针、唤醒等待队列),将数据搬运到工作队列或tasklet中完成。