简介:面向FPGA与PCIe驱动开发的XDMA中文学习笔记,围绕DMA/Bridge Subsystem for PCI Express v4.1(PG195)逐章整理,适合想打通主机与FPGA高速数据通路的工程师查阅。压缩包仅含1个PDF文件,体积约174KB,篇幅紧凑却覆盖完整,便于随查随用。内容从IP核构成、描述符机制与H2C/C2H通道讲起,延伸至BARs配置、AXI4-Lite主从接口、Legacy与MSI/MSI-X中断、AXI-MM接口初始化与双向传输流程,并详解端口定义、寄存器空间、时钟复位注意事项、Vivado配置与Example Design、Test Bench仿真,最后落到Linux驱动下载、中断处理与收发示例。已有4221人学习,可帮助读者对照官方文档搭建XDMA知识框架、理清调试思路。
1. 板卡插上主机,2 MB 数据怎么才能稳定写入用户态缓冲区
板卡插上主机,FPGA 里攒出来的数据怎么稳定写入用户态缓冲区?如果从 PCIe 硬核开始自己搭一套 DMA,时间基本都耗在四件事上:描述符怎么组织、中断怎么握手、地址怎么对齐、BAR 怎么切分。XDMA(PCIe DMA/Bridge Subsystem v4.1)把这套固定动作封装成 IP,前端提供 AXI-MM 或 AXI-ST 接口,主机侧配一个字符驱动。
这份中文读书笔记把整条路径都过了一遍:通道构成、描述符格式、BAR 配置、三类中断、H2C/C2H 传输时序、全部端口定义、寄存器空间、时钟复位约束,以及 Example Design 和驱动侧调用顺序。适合刚接手 PCIe 数据通路、需要按寄存器手册把链路跑通的人。
2. 通道、描述符与三条旁路:XDMA 的数据从哪里进出
2.1 八条通道、两条出口:AXI-MM 和 AXI-ST 怎么选
XDMA 内部可以被配置成最多 8 路独立的物理 DMA 引擎,其中 4 路 H2C(主机到卡)、4 路 C2H(卡到主机)。这 8 个引擎可以映射成各自独立的 AXI4-ST 接口,也可以共享同一条 AXI4-MM 接口。这个选择一旦定下来,后面寄存器、描述符、驱动全都要跟着变,所以第一件事就是把它想清楚。
AXI-MM 是单一 Master,四条 H2C 通道共用一套写地址/写数据/写响应通道,四条 C2H 通道共用一套读地址/读数据通道。好处是用户逻辑只需要实现一块地址空间,DDR 控制器、BRAM 环回、寄存器阵列都能直接挂;代价是四条通道抢同一份带宽,仲裁在 IP 内部完成,你无法控制比例。AXI-ST 反过来,每个通道单独一组 tdata/tvalid/tready/tlast,数据可以直接送入 RTL 流水线,通道之间互不阻塞,但用户侧要自己处理流控和包边界。
我一般这么判断:用户侧数据最后要落到一块线性地址空间,选 AXI-MM,省事;用户侧是流式的、多路独立、后面接的是自研打包解包逻辑,选 AXI-ST,省掉一层地址译码。
ST 模式下有两个细节容易出问题。H2C 流传输时描述符里的目标地址是不用的,数据被塞进 FPGA 内部的 FIFO;C2H 流传输时描述符里的源地址不用,数据从 FIFO 出来打到主机目标地址。一个数据包可以跨越多个描述符,包尾由 control 里的 EOP 位标记,EOP 会让 ST 接口的 tlast 拉高一拍。第二点是 C2H 流传输完成后有写回动作:通道把 WB Magic(16'h52b4)、Status 和实际传输长度写回到驱动指定的 writeback 地址,0x0 放 {WB Magic[15:0], Rsv[14:0], Status},0x4 放 Length[31:0],Status 里带 EOP 标志。驱动的完成判断基本靠这个回写,回写地址没设或者指到没映射的内存,表现就是中断来了却拿不到长度。还有一条必须遵守的规定:C2H 流传输的数据长度必须始终是 64 字节的整数倍。
| 维度 | AXI4-MM | AXI4-ST |
|---|---|---|
| 通道映射 | 8 通道共享 1 组 MM 接口 | 每通道独立一组 ST 接口 |
| 适用场景 | 用户侧是统一地址空间 | 用户侧是流式 RTL 数据通路 |
| 描述符地址字段 | 源和目标都用 | H2C 不用目标,C2H 不用源 |
| 包边界 | 接口上没有 EOP 语义 | EOP 驱动 tlast |
| 长度约束 | 受起始地址对齐约束 | 对齐约束 + C2H 的 64 字节整数倍 |
2.2 描述符:8 个 DWORD 里哪几个字段容易出错
描述符是驱动在主机内存里建好的链表,XDMA 靠它在主机内存和板卡之间搬数据。每条固定 32 字节,八个 DWORD:
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0x00 | Magic[15:0] / Rsv[1:0] / Nxt_adj[5:0] / control[7:0] | Magic 固定 0xad4b |
| 0x04 | Len[27:0] | 本次传输字节数,上限 2^28-1 |
| 0x08 | Src_adr[31:0] | 源地址低 32 位 |
| 0x0c | Src_adr[63:32] | 源地址高 32 位 |
| 0x10 | Dst_adr[31:0] | 目标地址低 32 位 |
| 0x14 | Dst_adr[63:32] | 目标地址高 32 位 |
| 0x18 | Nxt_adr[31:0] | 下一条描述符地址低 32 位 |
| 0x1c | Nxt_adr[63:32] | 下一条描述符地址高 32 位 |
Magic 是 IP 用来校验描述符合法性的,写错引擎会直接停住,而且不一定报错。control 里 bit0 是 Stop(取到这条就停止预取),bit1 是完成中断使能,bit4 是 EOP。Src/Dst 是设备视角的 DMA 地址,开 IOMMU 时是 IOVA,不是用户态虚拟地址,这一条搞错的表现是数据写到了物理内存的随机位置。Nxt_adj 用来在描述符地址不连续时调整下一条地址的高位。
DMA 内部还有一个深度为Bit_width * 512的 FIFO 用来缓存取回来的描述符。AXI 位宽 256 bit 时就是 256 bit × 512 = 16 KB,按每条 32 字节算能压 512 条。这个数字决定了描述符链规模和带宽上限的关系:链太长、FIFO 满了,IP 会反压预取,但不会报错,表现出来就是带宽上不去,查半天查不到原因。
拼描述符的活一般交给驱动,但用脚本先验证格式很快:
import struct def pack_desc(src, dst, length, nxt_adr=0, nxt_adj=0, stop=0, irq=1, eop=0): """按 XDMA 描述符格式打包 32 字节,地址均为设备视角 DMA 地址""" ctrl = ((stop & 1) << 0) | ((irq & 1) << 1) | ((eop & 1) << 4) d0 = (0xad4b << 16) | ((nxt_adj & 0x3f) << 8) | (ctrl & 0xff) return struct.pack('<IIIIIIII', d0, length & 0x0fffffff, src & 0xffffffff, (src >> 32) & 0xffffffff, dst & 0xffffffff, (dst >> 32) & 0xffffffff, nxt_adr & 0xffffffff, (nxt_adr >> 32) & 0xffffffff) # 单条 H2C 描述符:主机地址 0x1_0000_0000 -> 卡上 0x0,搬 4096 字节 blob = pack_desc(0x100000000, 0x0, 4096, irq=1) assert len(blob) == 32 and blob[0:2] == b'\x4b\xad'pack_desc里每个参数都对应一个硬件语义:length超过 0x0fffffff 会被截断,所以大块传输要在驱动里自己拆链;irq=1只是允许这条描述符产生中断,真正能不能上报还取决于通道中断使能掩码和 IRQ Block 总闸;nxt_adr填 0 表示链到此结束。脚本拼出来的字节串可以先用调试器写进一块 DMA 内存,验证完格式再搬到驱动里,比直接改内核模块快得多。
2.3 三条不经过 DMA 引擎的路径
DMA 之外,XDMA 还留了三条直通路径,很多人只用了其中一条就以为够了。
| 路径 | 接口 | 位宽 | 典型用途 |
|---|---|---|---|
| Cfg Master | m_axil_*(AXI4-Lite Master) | 32 bit 固定 | 主机访问用户逻辑的控制/状态寄存器 |
| PCIe to DMA Bypass | m_axi_bypass(AXI4-MM Master) | 跟随通道位宽 | 高带宽用户 memory、低延迟小包 |
| AXI4-Lite Slave | s_axil_* | 32 bit 固定 | 用户逻辑反向读写 XDMA 内部寄存器 |
| Descriptor Bypass | *_dsc_byp | 按通道 | 描述符由 FPGA 逻辑直接给,不走主机内存 |
Cfg Master 速率不高但胜在简单,主机写一个 BAR 偏移就能改用户逻辑里的寄存器,不需要碰 DMA 流程。Bypass 走的是完整位宽,适合 peer-to-peer 这种不经过主机内存的传输,延时比 DMA 低一截。Descriptor Bypass 每通道独立,开了以后描述符信息从 c2h_dsc_byp / h2c_dsc_byp 总线进来,主机驱动不用再建链,代价是主机侧失去了对传输队列的可见性。
BAR 的默认映射要记牢,改配置就得同步改驱动。32 位 BAR 下:只使能 DMA 时 BAR0 给 DMA;同时使能 PCIe to AXI4-Lite Master 时 BAR0 给 Lite Master、BAR1 给 DMA;再使能 Bypass 时 BAR2 给 Bypass。64 位 BAR 下编号变成 BAR0 Lite / BAR2 DMA / BAR4 Bypass。驱动里 mmap 的偏移是按这套默认布局算的,换了配置没改驱动,拿到手的地址就指向空处。
还有一条硬性限制写在笔记里:AXI4-MM 从端只支持 INCR burst,其他类型会触发 Slave Illegal Burst 中断;另外不是所有配置都能生成 Example Design。
3. 寄存器空间与初始化:从加载驱动到第一次传输完成
3.1 地址是三段拼出来的,别死记绝对地址
XDMA 的寄存器不在一条连续总线上,而是拿 PCIe 写地址的高位当路由:[31:16]保留,[15:12]是 Target,[11:8]是 Channel,[7:0]是 Byte Offset。
| Target | 区块 |
|---|---|
| 0x0 | H2C Channel |
| 0x1 | C2H Channel |
| 0x2 | IRQ Block |
| 0x3 | Config Block |
| 0x4 | H2C SGDMA |
| 0x5 | C2H SGDMA |
| 0x6 | SGDMA Common |
| 0x8 | MSI-X Vector Table / PBA |
所以笔记里那一堆 0x0004、0x0090、0x1090、0x2010、0x4080、0x5080 全是拼出来的:0x0004 = Target 0、Channel 0、offset 0x04,H2C 通道 0 控制寄存器;0x1090 = Target 1、Channel 0、offset 0x90,C2H 通道 0 中断使能掩码;0x4080 = Target 4、Channel 0、offset 0x80,H2C SGDMA 描述符低地址。通道 1 就是把[11:8]加一。记住这个拼法,手册翻到任何一页都能自己算出地址。
/* target / channel / offset 三段拼接,高 16 位保留 */ #define XDMA_REG(target, ch, off) \ (((( <p> <a href="https://download.csdn.net/download/weixin_43354598/84369369" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>