2. DMA基础原理:DMA控制器工作原理、DMA传输模式与描述符链

直接存储器访问(Direct Memory Access, DMA)是USB Gadget高性能驱动的核心硬件基础。在USB高速(480 Mbps)或超高速(5 Gbps)传输场景下,若每个数据包都由CPU执行memcpy或ioread,CPU将完全被数据搬运占据,无法处理协议栈与业务逻辑。DMA控制器接管数据通路,使CPU从字节搬运中解放,专注控制面决策。

性能基线: 在ARM Cortex-A72 @1.5GHz上,单次memcpy 4KB耗时约1.2μs;而DMA搬运同样4KB仅需约0.3μs,且CPU可并行处理其他任务。USB Gadget批量传输场景下,DMA可将吞吐量提升3~5倍。

2.1 DMA控制器工作原理

DMA控制器是系统总线上的独立总线主控设备。它通过以下四个核心组件完成数据搬运:

  • 源地址寄存器(SAR): 存放数据读取的起始地址(内存或外设FIFO地址)。
  • 目的地址寄存器(DAR): 存放数据写入的起始地址。
  • 传输计数寄存器(CTL/BCR): 记录待传输的字节数或事务数。
  • 控制寄存器(CR): 配置传输方向、地址增减模式、中断使能、链模式等。

一次典型DMA传输流程如下:

  1. CPU初始化: 设置SAR、DAR、BCR、CR,启动DMA通道。
  2. 总线仲裁: DMA控制器向总线仲裁器发出总线请求(HREQ)。
  3. 总线授权: CPU完成当前总线周期后释放总线,仲裁器授权DMA(HACK)。
  4. 数据搬运: DMA在源与目的之间直接读写,每次传输后更新地址与计数。
  5. 传输完成: BCR减至0时,DMA置位完成标志,可选触发中断通知CPU。
关键指标: DMA传输延迟 = 总线仲裁延迟 + 单次传输周期 × 传输次数。在USB Gadget场景中,单次DMA传输应覆盖整个USB数据包(通常512B~1024B),以减少总线仲裁开销。
DMA控制器内部架构与传输流程 DMA控制器内部架构与传输流程 CPU DMA控制器 SAR 源地址寄存器 DAR 目的地址寄存器 BCR 传输计数 CR 控制寄存器 总线主控接口 系统内存 USB外设FIFO ①初始化 ②总线请求→授权→搬运

2.2 DMA传输模式

根据数据源与目的的不同组合,DMA支持三种基本传输模式。USB Gadget驱动中最常用的是外设到内存(接收)和内存到外设(发送)。

传输模式 源地址 目的地址 USB Gadget典型场景 地址增减策略
内存→内存 内存缓冲区A 内存缓冲区B 数据重组、协议头剥离(较少直接使用) 源递增,目的递增
外设→内存 USB FIFO(固定地址) 内存缓冲区 USB OUT端点接收数据 源固定,目的递增
内存→外设 内存缓冲区 USB FIFO(固定地址) USB IN端点发送数据 源递增,目的固定
地址对齐陷阱: 多数DMA控制器要求源地址和目的地址按总线宽度对齐(如32位总线要求4字节对齐)。若USB缓冲区未对齐,DMA可能触发总线错误或静默截断数据。在分配DMA缓冲区时,必须使用 dma_alloc_coherent() 或 kmalloc() 配合 GFP_DMA 标志。

三种模式在Linux DMA引擎驱动中的配置差异:

/* 内存到内存:两个地址都递增 */
struct dma_async_tx_descriptor *tx;
tx = dmaengine_prep_dma_memcpy(chan, dst_phys, src_phys, len, flags);

/* 外设到内存:源地址固定(外设FIFO),目的递增 */
tx = dmaengine_prep_slave_sg(chan, sgl, nr_sg, DMA_DEV_TO_MEM, flags);

/* 内存到外设:源递增,目的地址固定 */
tx = dmaengine_prep_slave_sg(chan, sgl, nr_sg, DMA_MEM_TO_DEV, flags);
三种DMA传输模式对比 三种DMA传输模式对比 内存 → 内存 源递增 → 目的递增 数据重组 / 协议处理 USB Gadget中较少直接使用 外设 → 内存 源固定 → 目的递增 USB OUT端点接收 从USB FIFO搬运到系统内存 内存 → 外设 源递增 → 目的固定 USB IN端点发送 从系统内存搬运到USB FIFO USB Gadget核心模式:外设↔内存 外设到内存用于接收(OUT端点),内存到外设用于发送(IN端点)

2.3 DMA描述符链

在高速USB Gadget驱动中,单个DMA传输无法满足持续数据流需求。每次DMA完成都触发中断并重新配置寄存器,会引入大量中断开销。DMA描述符链(Descriptor Chain / Scatter-Gather)允许将多个传输任务链接在一起,DMA控制器自动按序执行,仅在整链完成后才通知CPU。

描述符链的核心数据结构:

struct dma_descriptor {
    __le32 src_addr;      /* 源物理地址 */
    __le32 dst_addr;      /* 目的物理地址 */
    __le16 transfer_size; /* 本次传输字节数 */
    __le16 control;       /* 控制位:链使能、中断使能、方向等 */
    __le32 next_desc;     /* 下一个描述符的物理地址(0表示链尾) */
    /* 可选:状态反馈字段 */
    __le32 status;        /* 传输完成后硬件写入状态 */
} __attribute__((packed));

描述符链的工作流程:

  1. 链构建: CPU在内存中分配描述符数组,填充每个描述符的地址、大小、控制位,并将next_desc指向下一个描述符。
  2. 首描述符加载: CPU将第一个描述符的物理地址写入DMA控制器的描述符基址寄存器(Descriptor Base Address Register)。
  3. 自动执行: DMA控制器加载第一个描述符,执行传输;完成后自动读取next_desc,加载下一个描述符继续传输。
  4. 链完成: 当next_desc为0或NULL时,DMA停止并触发完成中断。
  5. 循环链(可选): 将最后一个描述符的next_desc指回第一个描述符,形成环形缓冲区,实现不间断流传输。
性能收益: 在USB 3.0 Gadget批量传输中,使用描述符链可将中断频率从每包1次降低到每批32~64包1次。以每包1024B计算,中断减少约97%,CPU利用率从85%降至15%以下。
DMA描述符链结构 DMA描述符链结构 描述符 0 src: 0x1000_0000 dst: 0x2000_0000 size: 1024 next → 描述符1 描述符 1 src: 0x1000_0400 dst: 0x2000_0400 size: 1024 next → 描述符2 描述符 2 src: 0x1000_0800 dst: 0x2000_0800 size: 1024 next → NULL (链尾) DMA控制器 基址寄存器 → 描述符0 执行顺序:描述符0 → 描述符1 → 描述符2 → 完成中断 CPU只需配置一次基址,DMA自动遍历整链

2.4 描述符链在USB Gadget中的典型应用

在USB Gadget驱动中,描述符链通常与端点请求队列(Endpoint Request Queue)配合使用。每个USB请求(URB/struct usb_request)映射为一个或多个DMA描述符:

  • 批量OUT端点: 预分配8~16个描述符,每个指向一个4KB缓冲区。USB主机连续发送数据包时,DMA自动将数据分散写入不同缓冲区,CPU仅在整链完成后统一处理。
  • 批量IN端点: 将多个待发送的数据缓冲区链接成描述符链,DMA自动从各缓冲区搬运数据到USB FIFO,实现零拷贝发送。
  • 等时端点: 使用循环描述符链(环形缓冲区),配合硬件时间戳,实现精确的周期性数据传输。
缓存一致性: 描述符链本身存储在内存中,DMA控制器通过总线直接读取。在带缓存(Cache)的CPU系统中,必须确保描述符链所在内存区域被标记为非缓存(Non-cacheable)或使用缓存一致性协议(如ARM的DMA API中的dma_alloc_coherent)。否则,CPU修改描述符后,DMA可能读到过时的缓存数据,导致传输错误。

Linux内核中描述符链的典型初始化代码:

struct dma_async_tx_descriptor *tx;
struct scatterlist sg[8];
int i, ret;

/* 构建8个分散缓冲区 */
sg_init_table(sg, 8);
for (i = 0; i < 8; i++) {
    sg_set_buf(&sg[i], buffer_pool[i], BUFFER_SIZE);
}

/* 将scatterlist映射为DMA描述符链 */
tx = dmaengine_prep_slave_sg(chan, sg, 8, DMA_DEV_TO_MEM,
                             DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
if (!tx) {
    pr_err("Failed to prepare DMA descriptor chain\n");
    return -ENOMEM;
}

/* 注册完成回调 */
tx->callback = usb_gadget_dma_complete;
tx->callback_param = ep;

/* 提交描述符链到DMA引擎 */
dmaengine_submit(tx);

/* 启动DMA通道 */
dma_async_issue_pending(chan);

通过描述符链,USB Gadget驱动实现了批量传输的流水线化:CPU在DMA搬运当前批次数据的同时,可以准备下一批描述符,形成多级流水,最大化总线利用率。