4、PCIe DMA引擎概述:DMA控制器结构、通道、描述符引擎
DMA 引擎,说白了就是让数据自己跑起来,别老去打扰 CPU。我刚开始接触 PCIe DMA 时,以为它就是个简单的内存拷贝器,后来吃过亏才明白——里面的门道多着呢。这一章,我们聊聊 DMA 控制器到底长啥样,通道和描述符是怎么配合的。
4.1 DMA 控制器整体架构
一个典型的 PCIe DMA 控制器,内部大致分这几个模块:
- 总线接口:对接 PCIe 事务层,处理读写请求和完成包。
- 通道仲裁器:多个通道抢着用总线时,谁先上。
- 描述符取指单元:从内存拉取搬运指令,也就是描述符。
- 数据搬运引擎:真正的搬砖工,控制数据从源搬到目的。
- 中断控制器:搬完了通知 CPU(或直接触发 MSI-X)。
我记得有一次调试一款 FPGA 上的 DMA IP,数据老是丢包。翻 RTL 代码发现描述符取指单元和搬运引擎之间缺了一个足够深的 FIFO,导致取指速度跟不上搬运。后来在逻辑里加了一级缓冲,问题就解决了。所以你看,架构里的每个小模块都得仔细推敲。
4.2 多通道设计
大部分高性能 DMA 控制器有多个独立通道。每个通道都有自己的寄存器组、FIFO 和控制状态机。它们共享总线接口,但内部各自为政。
为什么要搞多通道?举个例子:你同时要处理网卡收包、SSD 写块、GPU 传纹理。如果只有一个通道,就得排队,延迟就上去了。多通道可以并行,而且每个通道可以单独配置优先级。
仲裁策略常见有这几种:
- 固定优先级:通道 0 永远优先——简单但容易饿死低优先级通道。
- 轮询:公平,但高带宽通道会被低速通道拖累。
- 加权轮询:给高吞吐通道更多权重,兼顾公平。
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));
描述符引擎工作流程:
- 取指:通过 PCIe 读事务,把描述符从系统内存拉到内部的描述符 FIFO。
- 解析:解码 src_addr、dst_addr、length 等字段。
- 执行:数据搬运引擎按解析结果发起读写事务,完成一次搬运。
- 回写:搬运完成后,硬件把 status 字段写回内存,通知 CPU。
- 中断:如果 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 自己跑完整个链表,中断一次即可。开销小多了。
4.5 关键技术参数
在实际项目中,这几个参数经常卡你脖子:
- 最大传输长度:受 PCIe
MaxPayloadSize约束,通常 128/256/512 字节。描述符长度超过它,DMA 内部会自动分段,但最好手动对齐。 - 对齐要求:源地址和长度必须自然对齐(比如 4 字节、64 字节)。我曾见过一个 SSD 控制器,要求源地址 64 字节对齐,否则硬件会静默回读错误的数据。
- SMMU/IOMMU 映射:如果启用了 SMMU,描述符里的地址必须是 IOVA,而不是物理地址。映射错了,DMA 根本找不到目标。我在调 Xilinx QDMA 时就在这上面花了三天。
最后,让我们用一张图把 DMA 引擎内部结构串起来:
这张图能帮你快速建立坐标系。实际项目中,你拿到芯片手册后,第一件事就是把它的 DMA 引擎框图对着这个图看,找到每个模块的位置和寄存器偏移,心里就有数了。