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传输基本流程

  1. 驱动配置阶段:CPU设置DMA控制器的源地址、目的地址、传输计数、传输模式等寄存器。
  2. 启动传输:CPU写控制寄存器使能DMA通道,DMA控制器开始工作。
  3. 总线仲裁:DMA控制器向总线仲裁器请求总线控制权,获得授权后成为总线主设备。
  4. 数据搬运:DMA控制器在源与目的之间逐字/逐字节搬运数据,同时递减传输计数寄存器。
  5. 传输完成:传输计数归零后,DMA控制器置位状态寄存器,并向CPU发出中断请求。
  6. 中断处理:CPU响应中断,读取状态寄存器确认传输结果,释放或重新配置DMA通道。
DMA控制器工作原理流程图 CPU配置DMA寄存器 CPU启动DMA通道 DMA请求总线控制权 DMA搬运数据(SAR→DAR) DMA发出传输完成中断 CPU与DMA并行工作 CPU配置完成后立即返回 执行其他任务 DMA在后台搬运数据 传输完成中断 CPU响应中断处理数据

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
DMA链表传输模式(Scatter-Gather)架构图 描述符 0 SRC: 0x1000_0000 DST: 0x2000_0000 LEN: 0x1000 NEXT: → Desc 1 描述符 1 SRC: 0x1000_1000 DST: 0x2000_1000 LEN: 0x0800 NEXT: → Desc 2 描述符 2 SRC: 0x1000_1800 DST: 0x2000_1800 LEN: 0x0C00 NEXT: NULL (结束) 源内存(不连续) 0x1000_0000 / 0x1000_1000 / 0x1000_1800 目的内存(不连续) 0x2000_0000 / 0x2000_1000 / 0x2000_1800 DMA控制器 依次解析描述符链表 DMA自动遍历链表,无需CPU逐段配置 → 极大降低中断频率

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 协同工作流程图

DMA与CPU协同工作流程图(双缓冲模式) CPU:准备缓冲区A CPU:启动DMA传输A CPU:处理缓冲区B数据 CPU:等待DMA完成中断 DMA:空闲等待 DMA:传输数据到缓冲区A DMA:传输数据到缓冲区B DMA:发出完成中断 并行执行区域 CPU处理缓冲区B的同时 DMA传输数据到缓冲区A 两者互不阻塞

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中完成。