4、PCIe DMA引擎概述:DMA控制器结构、通道、描述符引擎

DMA 引擎,说白了就是让数据自己跑起来,别老去打扰 CPU。我刚开始接触 PCIe DMA 时,以为它就是个简单的内存拷贝器,后来吃过亏才明白——里面的门道多着呢。这一章,我们聊聊 DMA 控制器到底长啥样,通道和描述符是怎么配合的。

4.1 DMA 控制器整体架构

一个典型的 PCIe DMA 控制器,内部大致分这几个模块:

  • 总线接口:对接 PCIe 事务层,处理读写请求和完成包。
  • 通道仲裁器:多个通道抢着用总线时,谁先上。
  • 描述符取指单元:从内存拉取搬运指令,也就是描述符。
  • 数据搬运引擎:真正的搬砖工,控制数据从源搬到目的。
  • 中断控制器:搬完了通知 CPU(或直接触发 MSI-X)。

我记得有一次调试一款 FPGA 上的 DMA IP,数据老是丢包。翻 RTL 代码发现描述符取指单元和搬运引擎之间缺了一个足够深的 FIFO,导致取指速度跟不上搬运。后来在逻辑里加了一级缓冲,问题就解决了。所以你看,架构里的每个小模块都得仔细推敲。

核心要点:DMA 控制器是“指令驱动”的,它自己不产生指令,全靠宿主 CPU 在内存里摆好描述符。

4.2 多通道设计

大部分高性能 DMA 控制器有多个独立通道。每个通道都有自己的寄存器组、FIFO 和控制状态机。它们共享总线接口,但内部各自为政。

为什么要搞多通道?举个例子:你同时要处理网卡收包、SSD 写块、GPU 传纹理。如果只有一个通道,就得排队,延迟就上去了。多通道可以并行,而且每个通道可以单独配置优先级。

仲裁策略常见有这几种:

  • 固定优先级:通道 0 永远优先——简单但容易饿死低优先级通道。
  • 轮询:公平,但高带宽通道会被低速通道拖累。
  • 加权轮询:给高吞吐通道更多权重,兼顾公平。
我曾经踩过的坑:某项目用固定优先级,通道 1 做控制消息,通道 0 做大数据块。结果通道 0 一直占用总线,通道 1 的消息延迟飙到几十毫秒,差点导致系统超时。改成加权轮询后,控制通道优先级提高,问题消失。

4.3 描述符引擎

描述符是搬运任务的“小纸条”。CPU 写好描述符放在内存里,DMA 自己来取,然后按纸条干活。没有描述符引擎,DMA 就是个傻搬砖工。

描述符结构通常长这样(以常用格式为例):

struct dma_descriptor {
    uint64_t   src_addr;      // 源地址(PCIe 地址或系统物理地址)
    uint64_t   dst_addr;      // 目的地址
    uint32_t   length;        // 搬运字节数,通常不能超过 4KB/64KB
    uint16_t   control;       // 控制标志:中断使能、链式/循环
    uint16_t   status;        // 状态:完成/错误(搬运完成后由硬件写回)
    uint64_t   next_ptr;      // 下一个描述符指针(链式结构用)
} __attribute__((packed));

描述符引擎工作流程:

  1. 取指:通过 PCIe 读事务,把描述符从系统内存拉到内部的描述符 FIFO。
  2. 解析:解码 src_addr、dst_addr、length 等字段。
  3. 执行:数据搬运引擎按解析结果发起读写事务,完成一次搬运。
  4. 回写:搬运完成后,硬件把 status 字段写回内存,通知 CPU。
  5. 中断:如果 control 中使能了中断,则触发一个 MSI/MSI-X。
描述符关键字段说明
字段说明常见限制
src_addr/dst_addr源/目的物理地址,需 4 字节对齐(部分 IP 要求 64 字节)地址不能跨越 MaxPayloadSize 边界?
length搬运长度,单位为字节通常不超过 4KB 或 64KB,否则会分段处理
control低 8 位保留,高 8 位标志:bit0=链式使能,bit1=循环使能,bit2=中断使能视具体 IP 而定
next_ptr链式/循环时指向下一个描述符的地址,必须 16 字节对齐地址为 0 表示链式结束
我的习惯:描述符结构体建议用 __attribute__((packed)) 避免编译器填充;同时用 static_assert 检查大小是不是 32 字节的倍数——很多硬件硬性要求。

4.4 工作模式:单次、链式、循环

描述符引擎支持三种调度模式,你得根据场景选:

  • 单次模式:CPU 提交一个描述符,DMA 搬运完就停。简单但效率低,每次都要 CPU 参与。
  • 链式模式:描述符通过 next_ptr 串成链表。DMA 自动取下一个,直到 next_ptr = NULL。批量搬运时特别香。
  • 循环模式:描述符形成一个环形缓冲区,最后一个的 next_ptr 指向第一个,永不停止。适合网卡、视频流这类持续搬运。

我建议:能用链式就别用单次。链式模式配合门铃寄存器(Doorbell),CPU 一次写好一批描述符,然后写地址通知 DMA,DMA 自己跑完整个链表,中断一次即可。开销小多了。

避坑:链式模式千万注意描述符的内存驻留。有一次我用了链式,但描述符所在页被交换出去了,DMA 取指时读到垃圾数据,直接死机。后来把描述符内存锁在物理内存里(mlock 或大页),再没出过事。

4.5 关键技术参数

在实际项目中,这几个参数经常卡你脖子:

  • 最大传输长度:受 PCIe MaxPayloadSize 约束,通常 128/256/512 字节。描述符长度超过它,DMA 内部会自动分段,但最好手动对齐。
  • 对齐要求:源地址和长度必须自然对齐(比如 4 字节、64 字节)。我曾见过一个 SSD 控制器,要求源地址 64 字节对齐,否则硬件会静默回读错误的数据。
  • SMMU/IOMMU 映射:如果启用了 SMMU,描述符里的地址必须是 IOVA,而不是物理地址。映射错了,DMA 根本找不到目标。我在调 Xilinx QDMA 时就在这上面花了三天。

最后,让我们用一张图把 DMA 引擎内部结构串起来:

PCIe DMA引擎内部结构 PCIe 总线接口 通道仲裁器 通道 0 通道 1 通道 … 通道 N 描述符取指单元 数据搬运引擎 中断控制器 系统内存(描述符列表) 图例 蓝: 总线 紫: 仲裁 绿: 通道 红: 取指 橙: 搬运 青: 中断 箭头: 数据/ 控制流

这张图能帮你快速建立坐标系。实际项目中,你拿到芯片手册后,第一件事就是把它的 DMA 引擎框图对着这个图看,找到每个模块的位置和寄存器偏移,心里就有数了。