☰
XDMA描述符链表与寄存器控制面:PCIe DMA传输机制解析
2026/11/3 3:09:00 网站建设 项目流程

简介:面向从事FPGA与PCIe接口开发的工程师及学习者,这份中文读书笔记围绕Xilinx DMA/Bridge Subsystem for PCI Express v4.1(XDMA)展开,参考PG195、官方驱动仓库与支持文档,对IP核配置、DMA传输和驱动开发中的常见问题做系统梳理。压缩包仅含1个PDF文件,大小约174KB,篇幅精炼,适合作为查阅型资料随用随看。内容覆盖XDMA构成与描述符机制、BAR与AXI4-Lite/AXI-MM接口配置、Legacy/MSI/MSI-X中断、H2C与C2H传输流程、端口定义、寄存器空间、时钟复位注意事项,以及设计指南、Example设计、Test Bench和Linux驱动应用开发等模块,基本对应文档全貌。已有4221人学习,便于读者按目录快速定位知识点,理解XDMA的软硬件协同关系,并为IP配置、仿真验证和驱动调试提供中文参考。

1. 控制面与数据面分离:XDMA 的性能到底来自哪里

第一次翻 XDMA 的资料,注意力很容易被 512bit 数据位宽和 4 条 H2C 通道吸走,觉得带宽就是这么堆出来的。真正把描述符和寄存器两章读完,结论会反过来:主机侧驱动全程只写了不到十个 32 位寄存器,剩下的搬运全是 IP 核自己回主机内存取描述符完成的。XDMA 把控制面和数据面切得很干净——控制面是一条 32 位固定宽度的 AXI4-Lite 与寄存器通路,数据面是 PCIe TLP 与 AXI 通道之间的直连,中间没有 CPU 参与。这条分界线一旦立住,后面所有看着琐碎的规矩,包括描述符 Magic、地址对齐、长度粒度、中断握手,约束的其实都是控制面,不是数据面。这份中文笔记的价值也在这里:它把手册里散在十几章的寄存器和时序,按「主机写什么、硬件做了什么、你怎么知道做完了」重新串了一遍。

2. 描述符链表:主机只写三个寄存器怎么搬完几个 GB

XDMA 的所有 DMA 传输都由描述符驱动,这和很多「写个长度寄存器就开搬」的 DMA 控制器完全不是一回事。主机侧驱动真正做的事只有三件:在主机内存里排好一条描述符链、把链首地址和相邻条数写进两个寄存器、写一个 doorbell 让引擎开始工作。剩下的取描述符、发读请求、收数据、写 AXI、发中断,全部在 FPGA 内部完成,主机 CPU 一次都不用参与。一次传输的 PCIe 事务数因此从「按数据长度线性增长」变成「按描述符条数增长」,而描述符条数是可以由软件自由决定的——这才是 XDMA 能把大块搬运做得便宜的根本原因。

2.1 32 字节的固定槽位:Magic 与 Nxt_adj 各自省掉了什么

描述符在主机内存里是定长 32 字节、8 个 32 位槽位的结构,不是变长编码。这一点决定了硬件可以不写任何解析器,只按固定偏移取数,取完就能直接送进发送路径。

偏移字段说明
0x00Magic[15:0] / Rsv[1:0] / Nxt_adj[5:0] / Control[7:0]Magic 固定为 0xad4b
0x04Len[27:0]传输字节数,高位保留
0x08–0x0cSrc_adr[63:0]源地址
0x10–0x14Dst_adr[63:0]目的地址
0x18–0x1cNxt_adr[63:0]下一条描述符的地址
/* 描述符定长 32 字节,必须 32 字节对齐,否则硬件按槽位取数会错位 */ struct xdma_desc { uint32_t ctrl; /* 0x00: Magic[31:16] | Rsv[15:14] | Nxt_adj[13:8] | Control[7:0] */ uint32_t len; /* 0x04: {4'h0, Len[27:0]},单位字节 */ uint32_t src_lo; /* 0x08 */ uint32_t src_hi; /* 0x0c */ uint32_t dst_lo; /* 0x10 */ uint32_t dst_hi; /* 0x14 */ uint32_t nxt_lo; /* 0x18: 链表指针,指向下一条 */ uint32_t nxt_hi; /* 0x1c */ } __attribute__((aligned(32))); #define XDMA_DESC_MAGIC 0xad4bu #define XDMA_CTRL_STOP (1u << 0) /* 停止继续取描述符 */ #define XDMA_CTRL_IE_DESC (1u << 1) /* 本条完成时产生中断 */ #define XDMA_CTRL_EOP (1u << 4) /* 包结束,AXI-Stream 下映射为 tlast */ static inline uint32_t xdma_desc_ctrl(uint8_t nxt_adj, uint8_t control) { /* nxt_adj 只有 6bit,最大 63;control 8bit 全部透传 */ return ((uint32_t)XDMA_DESC_MAGIC << 16) | ((uint32_t)(nxt_adj & 0x3f) << 8) | (uint32_t)control; }

Magic 被放在第一个 32 位槽的高 16 位,这个位置选得很有讲究:硬件取回第一条描述符的第一拍就能立刻判断「这是不是一个有效链表」,不需要等整个描述符到齐,也不需要额外的握手信号。代价是每 32 字节浪费 2 字节做校验——但相对于一次 4KB 传输,这点开销可以忽略,而省下来的是一次错误链表导致的整条通道挂死。

Nxt_adj 是 6bit,最多表示 63 条。它和 Nxt_adr 不是一回事:Nxt_adr 是链表指针,决定下一条去哪;Nxt_adj 是「首条地址后面还紧挨着几条」的预取提示,让引擎知道这一块连续内存里有多少条可以直接抓。我一般这么理解:Nxt_adr 负责正确性,Nxt_adj 负责性能。

Control 里三个位要记住。bit0 是 Stop,链表末条置 1,硬件取到就停,不再管 Nxt_adr 指向哪。bit1 是完成中断使能,按条生效而不是按通道生效,这是做精细流控的关键。bit4 是 EOP,一个数据包可以跨多条描述符,EOP 标在最后一条上,在 AXI-Stream 模式下直接映射为 tlast。

2.2 从 doorbell 到状态读清:一次 H2C 传输的完整时序

H2C(Host to Card)是主机内存搬到卡上。整个流程手册里列了十几步,但按「谁在做什么」拆开只有三段:驱动准备、硬件干活、驱动收尾。

  1. 应用层给出 buffer 地址和长度,通常是页对齐的一大块;
  2. 驱动按 4KB 或更小的粒度把大块切成多条描述符,串成链;
  3. 驱动写 0x4080/0x4084,即首条描述符的 64 位地址(属于 H2C SGDMA 的 target 0x4);
  4. 驱动写 0x4088,告诉引擎首条后面还有多少条相邻描述符;
  5. 驱动写 0x0004 置 1,踩下 H2C 通道 0 的 doorbell;
  6. 硬件发起 Descriptor fetch,通过 PCIe 读主机内存;
  7. 硬件拿到描述符,按 Src_adr 发读请求;
  8. 主机返回数据,硬件经 AXI-MM Master 或 AXI-Stream 写出;
  9. 按 Control[1] 决定是否向主机发中断;
  10. 驱动读 0x2044 判断是哪个通道,写 0x2018 关掉该通道中断;
  11. 驱动读 0x0044 清状态,再读 0x0048 拿已完成描述符计数与提交条数比对;
  12. 驱动写 0x0004 置 0 停引擎,写 0x2014 重新使能中断,返回传输字节数。

第 4 步是整条流程里最容易被忽略、但对性能影响最大的一步。0x4088 写进去的值会进描述符的 Nxt_adj 字段,引擎知道首条地址后面紧挨着 N 条,就可以把这 N 条一次抓回来,而不是一条一个 Mem Read TLP。PCIe 上一次读往返是几百纳秒量级,如果按 4KB 一条描述符搬 1MB,就是 256 次 fetch;把相邻条数填满,fetch 次数能压到个位数。血泪经验是:很多人只填 0x4080/0x4084,0x4088 留默认值,结果小包传输时带宽只有理论值的三分之一,还查不出原因——因为从功能上看它完全正常,只是每搬 4KB 就要多等一次几百纳秒的往返。

第 10 到 12 步的顺序不能乱。0x2044 是「通道中断请求」的镜像,谁置位说明谁完成了;0x0044 是读清语义,读一次就把状态清掉,这是硬件给软件的一个天然 ack,省掉一次单独的写事务。而 0x2018 与 0x2014 是同一组 mask 寄存器的不同位域,一个用来在 ISR 里关、一个用来处理完重新开,别把它们当成两个独立开关,位定义以手册为准。

C2H 方向把源和目的对调,流程结构完全一样:首条描述符地址写 0x5080/0x5084,相邻条数写 0x5088,doorbell 是 0x1004,状态读 0x1044,完成计数读 0x1048。这种「只有 target 半字节不同」的对称性不是巧合,第 6 章会解释它从哪来。

2.3 C2H 的写回记录:AXI-Stream 模式下谁来告诉主机收了多少

AXI-MM 模式下 C2H 很好理解:描述符里的目的地址就是主机 buffer,硬件往那儿写就完了。切到 AXI-Stream 模式麻烦就来了——数据从 s_axis_c2h 进来,描述符里的源地址没有意义(数据不是从某个地址读的,是从流里来的),主机驱动也就没法通过「我提交了多长」来推断「实际收到了多长」。

XDMA 的解法是让硬件往主机内存写一条 8 字节的完成记录:

/* C2H 流的写回记录,由硬件写入驱动预先指定的地址,格式固定 8 字节 */ struct xdma_c2h_wb { uint32_t magic_status; /* 0x0: WB Magic[31:16]=0x52b4 | Rsv[15:1] | Status[0](EOP 标志) */ uint32_t length; /* 0x4: 本条描述符实际传输的字节数 */ };

驱动拿到这条记录,先比 Magic 是不是 0x52b4,确认写回有效,再看 Status 里的 EOP 位判断这一包是不是收全了,最后用 length 更新接收队列。注意这里有两个不同的 Magic:描述符里是 0xad4b,写回记录里是 0x52b4,方向相反、值也相反,翻车最多的就是把它俩写反——现象是驱动永远认为写回无效,数据其实已经在内存里了。

还有一条硬约束:C2H 流的长度必须是 64 字节的整数倍。64 字节是 cache line 尺寸,也是这条通路做写合并的最小单位;长度不对齐时,最后一拍会踩到相邻 buffer,而且不会报错,只会在压力测试里偶发数据错乱。

2.4 描述符 FIFO 的深度与 Stop/EOP 的边界

硬件内部有一个按 AXI 位宽组织的描述符 FIFO,容量是位宽 × 512。AXI 位宽 256bit 时正好 16KB,而一条描述符 32 字节,所以能缓存 512 条;位宽窄的配置下,一条描述符要占更多拍,能缓存的条数按比例减少。这个数字决定了引擎的预取深度:链表再长,硬件也只会提前抓 FIFO 装得下的那么多,剩下的等腾出空间再抓。

Stop 位和 EOP 位经常被混为一谈,其实一个管链表、一个管数据包。Stop 置在末条上,硬件取到就不再顺着 Nxt_adr 走,通道进入 descriptor stop 状态;EOP 置在包的末条上,只影响 AXI-Stream 的 tlast,不影响取描述符。一个包跨多条描述符时,中间那些条的 EOP 是 0,最后一条才是 1。MM 模式下 EOP 基本没有实际作用,除非你要按包做统计。

如果连中断都不想用,还有 Poll 模式:把 ie_descriptor_completed 和 Pollmode_wb_enable 打开,用 Pollmode_hi_wb_addr / Pollmode_lo_wb_addr 指定一个主机内存地址,硬件每完成一条描述符就把累计完成条数写回去,驱动轮询这块内存就行。中断从触发到进 ISR 通常是几微秒,轮询可以压到亚微秒,代价是占一个核。高吞吐低延迟的场景里这个取舍很划算。

3. BAR 与三条主机访问路径:控制面为什么只有 32 位

XDMA 对主机暴露的不止一条路。数据面走 DMA 引擎,控制面走 AXI4-Lite,还有一条绕过引擎的 Bypass 通路。这三条路的宽度、用途、延迟差了一个数量级,搞混了就会出现「设备在、寄存器能读、数据搬不动」这种典型故障。

3.1 BAR 的默认分配:32 位和 64 位是两套组合

BAR 的分配在 IP 生成时就固定了,驱动里硬编码的偏移必须和它一致。默认组合是这样的:

BAR 宽度配置项BAR0BAR1 / BAR2BAR4
32bit使能 AXI4-Lite MasterPCIe to AXI4-Lite MasterBAR1 = DMABAR2 = DMA Bypass
32bit使能 DMA BypassDMABAR1 = DMA Bypass—
64bit使能 AXI4-Lite MasterPCIe to AXI4-Lite MasterBAR2 = DMABAR4 = DMA Bypass
64bit使能 DMA BypassDMABAR2 = DMA Bypass—

64 位 BAR 会吃掉相邻的 BAR 号,所以编号是 0/2/4 而不是 0/1/2。这个细节导致的典型翻车是:换了 BAR 宽度配置但没改驱动,驱动还在按 32 位的 BAR1 去映射 DMA 寄存器,读到全 0xffffffff,于是判断「设备没起来」。实际设备好好的,只是地址映射错了位置。

3.2 AXI4-Lite 主从:控制面钉死 32 位的理由

AXI4-Lite Master 是主机访问用户逻辑的唯一低速通道,固定 32 位、单拍、无 burst。这个「窄」是故意的:它服务的对象是控制寄存器、状态寄存器、配置参数,这类访问一年也发生不了几万次,把位宽堆上去只会让用户逻辑的地址译码变复杂,收益为零。主机一次 MMIO 写就是一次 PCIe Mem Write TLP,转成一次 AXI4-Lite 写,中间没有任何打包,延迟可预测,这才是控制面真正需要的性质。

反方向是 AXI4-Lite Slave,让用户逻辑去读写 XDMA 内部寄存器。这条路的实战价值被严重低估:调试阶段可以通过它在 FPGA 侧直接读通道状态(h2c_sts / c2h_sts 的 8 个位分别对应 busy、descriptor completed、descriptor stop、descriptor done、packet done、irq event pending、control register run),不必等主机侧驱动打印日志。板子上没有串口、主机又卡在枚举阶段时,这是唯一能看到内部状态的口子。

3.3 AXI-MM 与 AXI-ST:共享一个 Master 还是每通道独立

AXI-MM 模式下,最多 8 条独立 DMA 引擎(4 路 H2C、4 路 C2H)共享同一个 AXI-MM Master,仲裁在 IP 内部做。好处是对外只暴露一套地址接口,接一个 DDR 控制器就够;坏处是通道之间会抢带宽,四路同时跑的时候单路有效带宽大概是理论值的四分之一,而且这个退化是非线性的,跟包大小强相关。

AXI-ST 模式给每个通道独立端口,直接连 RTL 数据通路。没有地址概念,天然流式,适合 AD 采样、图像流水线这类本来就没有「地址」的东西。代价是描述符里有一半字段失去意义:H2C 流的源地址有效、目的地址不用(数据进的是卡上的 FIFO);C2H 流反过来,源地址不用、目的地址有效。写驱动时如果两边都填,不会报错,只是那一半字段被静默忽略,出问题时你会盯着一个根本没人读的字段查半天。

3.4 Bypass:绕开 DMA 引擎的两条近路

Host DMA Bypass 走的是 M_AXI_BYPASS 接口,主机写 BAR 里的 Bypass 区间,请求直接透传到用户逻辑的 memory,不经过 DMA 引擎、不经过描述符。典型用法是 peer-to-peer:两块卡之间直接搬数据,不落主机内存,省掉一次写一次读。

Descriptor Bypass 更彻底:每个通道有独立的 c2h_dsc_byp / h2c_dsc_byp 总线,源地址、目的地址、长度由 FPGA 逻辑直接给,主机驱动完全不参与。延迟从「建链表 + doorbell」的几微秒降到几十纳秒,适合固定模式、小包、高频的传输。代价是主机侧失去控制权——流控、边界检查、buffer 归属全得自己在逻辑里做,做不好就是把主机内存写穿,而且是那种跑一晚上才崩一次的写穿。

4. 中断:从电平语义到消息语义的三级跳

XDMA 支持 Legacy、MSI、MSI-X 三种中断,很多人以为只是「新旧的差别」,实际是三种完全不同的语义模型。握手时序的差异不是兼容性包袱,而是电平触发和边沿触发这两种物理事实在接口上的投影。

4.1 Legacy 中断:req 拉低后为什么必须等第二次 ack

Legacy 中断是电平语义。usr_irq_req 拉高,产生中断到 PCIe 侧,主机进 ISR 处理。用户逻辑需要知道主机什么时候处理完了,通常靠自己实现一个主机可读写清零的寄存器来传递这个信息,处理完再把 req 拉低。

关键约束在后面:req 拉低之后不能立刻重新拉高,必须等到 usr_irq_ack 第二次有效。原因是电平触发的中断线在主机侧是「一直有效直到 EOI」的,如果 req 刚拉低又立刻拉高,第二次中断会和还没退出的第一次合并,或者干脆被中断控制器丢掉。第二次 ack 有效代表主机已经从 ISR 里退出来了,此时重新拉 req 才是安全的。

这条约束的实际后果是:legacy 模式下不能靠「拉低再拉高」快速连发中断,每发一次都要等一个完整的 ISR 往返。高包率场景下这是硬瓶颈,也是为什么带 MSI-X 的设计基本不会退回 legacy。

4.2 MSI 与 MSI-X:向量号寄存器与 PBA

MSI/MSI-X 是消息语义,中断变成一次 Mem Write TLP 写到特定地址。边沿触发,没有「线一直有效」的问题,所以 usr_irq_req 拉低之后可以立即重新拉高,不需要等第二次 ack。这一条差异,让 MSI-X 在高频中断场景下的有效中断率能高出接近一个数量级。

MSI-X 多了向量表(target 0x8)和 PBA,每个向量有独立的地址和数据,中断亲和性、多核分发都靠它。用户中断要占向量号:每一个用户中断都要在 IRQ Block User Vector Number 寄存器组里编程一个不同的编号,硬件按编号去向量表里取地址和数据。

注意:多个用户中断源共用的 ack 侧只有一份信号,不是每个向量一条。用多个 user vector 时,你的逻辑必须能判断「当前被确认的是哪一路」,不能简单把 ack 当成 req 的按位镜像。常见的实现思路是给每个中断源配一个 pending 位,ack 有效时按优先级清掉最高优先的那一位。

通道中断(H2C/C2H 每通道一路)走的是另一组寄存器:IRQ Block Channel Interrupt Request(0x2044)用于查询,Channel Interrupt Enable Mask(0x2010 / 0x2014 / 0x2018)用于开关,Channel Vector Number(0xA0 / 0xA4)用于指向量。AXI Bridge 和 AXI DMA 共享这套中断寄存器,所以同一个 target 下两类功能的中断状态是混在一起的,读的时候要按位区分。

4.3 ISR 里的三步读清与 mask 顺序

中断服务程序的骨架看着简单,但顺序错了就是偶发丢中断。

static irqreturn_t xdma_isr(int irq, void *dev_id) { struct xdma_dev *d = dev_id; /* 第一步:查是谁。0x2044 = IRQ Block(t=0x2) channel 0 offset 0x44 */ uint32_t req = ioread32(d->bar + 0x2044); if (!req) return IRQ_NONE; /* 不是本设备,别占着中断线 */ for (int ch = 0; ch < 4; ch++) { if (!(req & BIT(ch))) continue; /* 第二步:先关本 <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>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询