ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

XDMA原理与实战:描述符链表、中断机制与高速传输设计

XDMA原理与实战:描述符链表、中断机制与高速传输设计

简介:这份中文学习笔记围绕赛灵思的DMA桥接子系统与PCIe核,即常见的XDMA,进行系统讲解。资料基于官方参考手册、驱动源码和技术论坛中的常见问题整理,适合准备在FPGA设计中集成高效数据搬运通道的开发人员。笔记从整体架构出发,依次说明描述符环形队列怎么组织、主机到卡与卡到主机两条通道各自如何完成传输,并对比了存储器映射接口与流式接口的使用场景;同时针对传统中断、消息信号中断以及扩展消息信号中断分别解释配置方法,再总结各寄存器区域的偏移地址和关键位,以及时钟、复位设计时需要留意的细节。后半部分还给出了在Vivado里生成示例工程和执行仿真测试的步骤,并补充了Linux驱动程序的加载、中断处理以及应用程序发起DMA读写的常见代码思路。压缩包只有一个PDF文档,大小约174KB,内容浓缩,可以配合官方英文手册对照阅读。该资料已有4207人学习或下载,适合作为XDMA入门的导读和开发期间的速查参考。

1. XDMA 的第一性原理:DMA 引擎只是个“描述符解释器”,而真正的调度在主机内存里

大多数人对 DMA 的直觉是:FPGA 内部有一块很大的缓存,主机把数据搬进去,或者 FPGA 把数据搬出来。但 XDMA(DMA/Bridge Subsystem for PCI Express)的设计完全反过来——它把传输计划(源地址、目标地址、长度、下一个描述符在哪)全部放在主机内存里的描述符链表中,FPGA 侧只维护一个“取描述符 → 搬运数据 → 回写完成状态”的小状态机。这就是为什么这套机制能支撑最多 8 个独立 DMA 通道(4 路 H2C、4 路 C2H),却不需要在 FPGA 里为每个通道准备庞大的传输控制结构。理解 XDMA,关键不是看它的 AXI 接口怎么接,而是看描述符这条链表如何从主机内存一路“流”到 PCIe TLP、再到 AXI 总线。这篇文章会把 PCIe 事务层、描述符格式、中断握手、寄存器时序串成一条完整链路,读完你会明白为什么中断要分两次 ack、为什么 C2H 长度必须是 64 字节的整数倍、为什么只支持 INCR burst——每个“怪癖”背后都是一组明确的设计约束。

2. 数据路径与描述符:是谁在搬运数据,又是谁在指挥搬运

2.1 两条 DMA 路径和四条 PCIe 通道的职责分工

先看 XDMA 的整体构成。PCIe 侧收进来的 TLP,先经过 PCIE IP 核,进入 RQ/RC 模块——RQ 是 Requester Request(发起读写的请求方),RC 是 Requester Completion(接收对端返回的完成包)。H2C 方向(Host to Card)的路径是:PCIe RX → PCIE IP → RQ/RC → H2C channel → AXI 写接口(MM 或 ST)→ 用户逻辑。C2H 方向(Card to Host)反过来:用户逻辑 → AXI 读接口 → C2H channel → RQ/RC → PCIE IP → PCIe TX。

这里有个很容易被忽略的点:C2H 的数据不是由 FPGA 主动“推”给主机的,而是 C2H channel 作为 PCIe 事务的 Requester,向主机内存发起写请求。也就是说,在 PCIe 总线上,数据搬运请求的发起方始终是 FPGA 侧,主机是被动响应方。理解这一点,就能看懂后面描述符里的 Dst_adr(目标地址)字段为什么是主机物理地址——写请求的地址必须落在主机内存的地址空间里。

控制路径和 DMA 数据路径是分开的。Cfg Master 是 AXI4-Lite Master,固定 32 位位宽,用来读写信道寄存器,比如中断使能、状态清除、引擎启停。IRQ 模块专门接收用户逻辑的中断信号和 DMA 通道的完成事件,统一转换成 PCIe 中断消息。这样设计的好处是:中断处理和 DMA 搬运互不阻塞,主机驱动在读写状态寄存器时,不会打断正在进行的批量传输。

2.2 描述符链表:住址、长度、去向全在这张表里

描述符链表在主机驱动侧创建并保存在主机内存中。每个描述符固定 8 个 32-bit 字,布局如下:

// 每个描述符 32 字节,8 个 DW(Double Word) struct xdma_descriptor { uint32_t dw0; // [15:0] Magic = 0xadb4,[7:0] control uint32_t dw1; // [27:0] Len,传输字节数 uint32_t dw2; // Src_adr[31:0] uint32_t dw3; // Src_adr[63:32] uint32_t dw4; // Dst_adr[31:0] uint32_t dw5; // Dst_adr[63:32] uint32_t dw6; // Nxt_adr[31:0] uint32_t dw7; // Nxt_adr[63:32] };

dw0 的高 16 位是 Magic 字段,固定为 16’hadb4,只有这个值正确,XDMA 才认为这个描述符是驱动合法生成的,否则会直接判定为非法描述符而不启动传输。低 8 位 control 字段里,bit0 是 Stop 位,表示取完这个描述符就停止继续取链;bit1 是完成中断标志,置 1 时该描述符完成后产生中断;bit4 是 EOP(End of Packet),在 AXI-ST 模式下用来控制 tlast 信号的产生时机。

在 H2C 场景里,Src_adr 是主机端源地址,Dst_adr 在 AXI-MM 模式下指向 FPGA 侧地址;在 AXI-ST 模式下目标地址不再使用,因为数据直接流向板卡上的 FIFO。C2H 场景相反,Dst_adr 是主机目标地址,Src_adr 在 AXI-ST 模式下被忽略,数据从用户逻辑侧的 AXI-Stream 接口进来。Nxt_adr 指向下一个描述符的地址,驱动用这个字段把分散的物理地址片段串成链表——这正是“DMA 引擎只是解释器”这句话的落点:FPGA 侧不需要知道整张链表有多长,只需要跟着 Nxt_adr 一条一条跳。

字段偏移含义关键约束
Magic0x0描述符合法性校验固定 0xadb4
Control0x0bit0 Stop / bit1 完成中断 / bit4 EOP按需置位
Len0x4本次传输字节数低 3~6 位按 AXI 位宽强制为 0
Src_adr0x8~0xC源地址(64 位)按 AXI 数据位宽对齐
Dst_adr0x10~0x14目标地址(64 位)按 AXI 数据位宽对齐
Nxt_adr0x18~0x1C下一个描述符地址链表关键,NULL 或 Stop 结束

XDMA 内部用 FIFO 缓存描述符,深度是 Bit_width × 512。以 AXI 位宽 256 bit 为例,FIFO 容量就是 256 bit × 512 = 16 KB。这意味着 FPGA 侧最多能预取的描述符条数是有限的,驱动不能一次性塞成千上万条描述符让 DMA“异步跑完再通知”——描述符取完了,传输就断了,必须靠中断或轮询感知进度后补充。

2.3 AXI-MM 与 AXI-ST:同一个引擎,两种出口

AXI-MM 模式下,所有 DMA 通道共享一个单一的 AXI4 Master 接口,地址和数据都走标准 AXI 总线,适合连接带地址映射的存储器类逻辑。AXI-ST 模式下,每个通道都有独立的 AXI4-Stream 接口,可以直接接 RTL 逻辑模块,省掉了一层地址译码和 FIFO 适配。ST 模式的数据路径有个明显特征:数据包可以跨越多个描述符,描述符里 Len 控制每个片段的长度,EOP 标志表示整个数据包的最后一拍,这个标志会直接映射到 AXI-Stream 的 tlast 信号上。

关于 burst 类型需要特别注意:XDMA 只支持 INCR(地址递增)burst,其他类型会触发 Slave Illegal Burst 中断。这是 AXI 协议层面最容易踩的坑,后面避坑章节会单独展开。

3. 中断机制:为什么 Legacy 中断要等两次 ack,而 MSI-X 不用

3.1 用户中断的触发条件与握手信号

XDMA 的 IRQ 模块可以接收用户逻辑的中断线,每路 DMA 通道也对应一路中断。用户逻辑侧的中断接口只有一对信号:usr_irq_req 和 usr_irq_ack。在 Legacy 中断模式下,usr_irq_req 一旦拉高,必须保持到 usr_irq_ack 拉高为止。第一次 ack 表示 XDMA 已经把中断事件上报到 PCIe 块,主机的中断服务例程(ISR)准备介入处理。

但 Legacy 中断有个特殊约束:usr_irq_req 拉低之后,不能立刻重新拉高,必须等待 usr_irq_ack 第二次拉高,才能再次发起新的中断请求。原因在于主机对 Legacy 中断的响应是电平触发的,ISR 需要时间从中断处理流程中退出。第一次 ack 确认“中断已经触发”,第二次 ack 确认“主机已经处理完上一个中断,可以接受新的了”。如果用户逻辑不遵守这个时序,第二个中断请求会被直接吞掉,表现为中断丢失且无任何报错。

这个“二次 ack”机制是新手最容易困惑的地方。通常的做法是:在用户逻辑内部用一个可被主机读写清零的状态寄存器记录中断事件,主机 ISR 读该寄存器后写 0 清掉,用户逻辑检测到寄存器被清后,才允许重新拉高 req。这样既满足了 XDMA 的二次 ack 约束,也给了主机一个明确的中断处理完成信号。

3.2 MSI/MSI-X 模式下为什么放开了这个约束

如果使能了 MSI 或 MSI-X,usr_irq_req 拉低后不需要等第二次 ack 就能重新拉高。这是因为 MSI/MSI-X 是消息型中断,每个中断事件对应一个特定的内存写事务(MSI-X 还支持每个中断向量独立的地址和数据),不存在电平触发中断那样的“ISR 退出”等待问题。主机端在处理完 MSI 中断后,下一次中断消息到达时自然触发新一轮处理。

但 MSI-X 模式对软件侧的配置要求更严格。每个用户中断的矢量号必须由软件提前写入 IRQ Block User Vector Number 寄存器组(偏移 0x80~0x8C,每个寄存器对应一路用户中断),而且所有用户中断共享一个 acks 信号。也就是说,硬件侧只提供一个统一的 ack 通道,驱动必须通过矢量号来区分具体是哪一路用户中断触发的。通道中断的矢量号则写在偏移 0xA0~0xA4。矢量号的作用是把中断事件映射到 MSI-X Table 里的具体向量,从而在主机侧绑定到对应的中断处理函数。如果矢量号没配置或者配置重复,中断可能会落在错误的处理函数里,或者干脆触发不了。

3.3 中断使能、屏蔽、请求和完成读回的寄存器链路

XDMA 的中断控制寄存器分布很有规律,理解这条链路的顺序就能写出正确的中断服务代码:

  1. H2C 通道自己的中断使能 mask,写在 0x0090;C2H 通道对应 0x1090。这是第一层开关,控制的是“某个通道的哪些事件能产生中断”。
  2. IRQ Block 的 Channel Interrupt Enable Mask 写在 0x2010,这一层控制的是“哪些通道的中断能汇总到 IRQ 模块”。H2C 和 C2H 通道在这里共用一套屏蔽逻辑。
  3. 中断发生后,驱动读 IRQ Block Channel Interrupt Request(0x2044),判断是哪个通道产生的中断事件。0x2044 是请求状态,0x2048 是 Pending 状态,两者配合可以区分“发生过但还没被确认”与“正在等待上报”的事件。
  4. 驱动把对应通道的 mask 写入 0x2018,完成对该通道中断的屏蔽,防止中断处理过程中再次触发同一个中断导致嵌套。

这里有个隐藏细节:0x2010、0x2014、0x2018 这三个地址其实是同一组中断使能 mask 的不同操作视图。0x2010 是全局使能写入(enable all channels),0x2014 是重新使能(enable mask,为下一轮传输开启中断),0x2018 是中断屏蔽写入(mask off)。驱动在每一轮传输结束时写 0x2014,而不是重新写 0x2010,是为了避免把其他通道的中断状态也一并重置。这在多通道并发传输时特别关键:如果所有通道共用一个全局使能寄存器,某个通道的中断处理可能会误放其他通道的中断使能位。

4. DMA 传输流程:从建链到中断,寄存器时序的每一步都有讲究

4.1 H2C 的完整生命周期:初始化、搬运、确认、停止

以 AXI-MM 接口的 H2C 传输为例,整个流程可以拆成三个阶段。第一阶段是初始化:驱动加载后,设置 H2C 通道中断使能 mask 寄存器(0x0090),再设置 C2H 通道中断使能 mask(0x1090),最后设置 IRQ Block Channel Interrupt Enable Mask(0x2010)使能所有需要的通道。这里三处都必须写,少一处中断都不会到达主机。

第二阶段是发起传输。驱动的核心动作是:创建描述符链表,把第一个描述符的基地址写入 0x4080 和 0x4084(分别对应低 32 位和高 32 位),如果是多次传输的链表,再往 0x4088 写入后续描述符的数量。然后用 0x0004 引擎控制寄存器启动 H2C 引擎。这一步的顺序不能反:必须先写描述符地址,再启动引擎。因为引擎一旦启动,会在极短时间内发起描述符预取请求,如果地址寄存器还没准备好,预取会落到错误的地址上,轻则读到非法描述符,重则直接挂死。

第三阶段是完成与确认。FPGA 侧 DMA 发起描述符 fetch 请求,拿到描述符后向源地址发读请求,收完数据通过 AXI-MM Master 接口写出去,然后产生中断。驱动的中断处理流程很有章法:先读 0x2044 确定通道,把对应通道的 mask 写入 0x2018 屏蔽中断;然后读通道状态寄存器 0x0044——这个动作本身会清除 status 寄存器中的中断标志;再读 0x0048 的 Completed Descriptor Count,与驱动创建并提交的描述符数量做对比,确认没有丢数据;之后写 0x0004 停止 DMA 引擎,再写 0x2014 为下一轮传输使能中断。最后把实际传输长度返回给应用程序。

下面这段伪代码把这些操作串起来:

// H2C 传输启动与完成,驱动侧视角 void h2c_transfer(struct xdma_dev *dev, dma_addr_t buf, size_t len) { // 1. 构造描述符链表(此处在主机内存中完成) struct xdma_descriptor *desc = build_descriptor_chain(buf, len); // 2. 写首个描述符地址:低 32 位 + 高 32 位 writel(lower_32_bits(desc->dma_addr), dev->bar + 0x4080); writel(upper_32_bits(desc->dma_addr), dev->bar + 0x4084); // 3. 可选:写后续描述符数量 writel(chain_count - 1, dev->bar + 0x4088); // 4. 启动引擎 writel(RUN_BIT | IRQ_EN_BIT, dev->bar + 0x0004); // 5. 等待中断到达,进入 ISR // 6. ISR 里读 0x2044 确认通道,写 0x2018 屏蔽 // 7. 读 0x0044(清状态),读 0x0048 对比 completed count // 8. 写 0x0004 停止引擎,写 0x2014 重新使能中断 }

0x0048 的 Completed Descriptor Count 是累计值,不是本轮传输的完成数。它统计的是该通道自启动以来完成的总描述符数量,驱动必须自己记录上一轮的基准值,用差值来判断本轮是否全部完成。很多驱动 bug 就出在这里——拿完成数和总描述符数直接比较,多通道并发时就对不上了。

4.2 C2H 反向传输:寄存器镜像和写回的特殊规则

C2H 通道的寄存器布局和 H2C 几乎完全镜像:控制寄存器是 0x1004,状态寄存器是 0x1044,完成描述符计数是 0x1048,中断使能 mask 是 0x1090。描述符首地址写到 0x5080/0x5084,后续数量写到 0x5088。流程上的区别在于,C2H 的源数据来自 FPGA 侧,描述符里的 Src_adr 只在 AXI-MM 模式下有效,AXI-ST 模式下源地址直接被忽略,数据来自用户逻辑给出的 AXI-Stream 接口。

C2H 的 AXI-ST 模式还有个独有的机制:每当一个描述符完成,C2H 通道会把一段写回数据(Writeback)写到主机内存中的指定地址。写回数据的格式是:第一个 32 位字的 [31:16] 是固定 Magic 0x52b4,[15:1] 保留,bit0 是状态位;第二个 32 位字是实际传输长度。其中 Magic 0x52b4 的作用和描述符里的 0xadb4 一样,是给驱动做有效性校验的——如果写回位置的数据不是这个 Magic,驱动就应该认为这次写回无效。状态位表示 EOP,用于向驱动传递包结束信息。

这个写回机制是理解 C2H 流式传输的关键。它可以让驱动不依赖中断也能精确知道每次搬运的长度,只要轮询写回地址的 Magic 是否变成 0x52b4 即可。但代价是写回地址必须提前通过寄存器配置好,对应的内存区域必须保证 64 字节对齐,而且 C2H 数据流的长度必须始终是 64 字节的整数倍,否则最后一个不完整的 64 字节块可能无法触发写回,导致传输卡在等待状态。

注意:C2H 流的长度约束是 64 字节整数倍,不是 4 字节。很多第一次做流式采集的开发者在这里翻车,把 DMA 长度配成 1024 的奇数倍,结果最后一个描述符永远等不到完成中断。用户逻辑侧需要自己把长度凑整到 64 字节边界。

4.3 Descriptor Bypass 和 Poll 模式:绕过驱动、绕过中断的两条旁路

Descriptor Bypass 是 XDMA 给低延时场景留的一条捷径。正常情况下,描述符由驱动创建并通过 SGDMA 寄存器下发;Bypass 模式下,描述符不再由主机的驱动提供,而是由用户逻辑通过 c2h_dsc_byp(C2H 方向)或 h2c_dsc_byp(H2C 方向)总线直接提供描述符里最关键的信息——源地址、目标地址、传输长度。这样省掉了“驱动建链 → 写寄存器 → FPGA 预取描述符”的整条路径,数据搬运的发起完全由硬件逻辑控制,延时可以压到最低。

代价是失去了主机侧的调度能力。驱动无法预知 FPGA 何时会发起搬运、搬运到哪个地址,所以 Bypass 模式更适合 FPGA 主导的、固定数据流的场景,比如高速采集卡连续往主机预分配缓冲区写数据。它和普通模式的切换是按通道独立使能的,不是全局开关。

Poll 模式则是省中断的另一条路。每个 DMA 引擎可以把已完成的描述符数量直接写回主机内存的指定地址,驱动通过轮询这个地址来判断完成情况,替代中断通知。启用这个功能需要设置 ie_descriptor_completed 位和 Pollmode_wb_enable 位,写回地址由 Pollmode_hi_wb_addr 和 Pollmode_lo_wb_addr 两个寄存器指定(H2C 通道是 0x88/0x8C,C2H 通道是 0x1088/0x108C)。轮询的粒度是“完成的描述符个数”,驱动只需要读回这个数并与提交总数比较,就能判断整条链是否走完。

5. 踩坑排查:对齐、burst、中断握手与长度规则的实战复现

5.1 现象一:H2C 传输启动后一直 busy,但主机内存数据没变化

原因:源地址或目标地址没有按 AXI 数据位宽对齐。XDMA 的对齐要求是硬性的——64 位位宽要求源地址和目标地址的低 3 位为 0,128 位要求低 4 位为 0,256 位要求低 5 位为 0,512 位要求低 6 位为 0。不满足对齐时,DMA 不会自动填充或调整,而是直接卡在等待地址合法化的状态,status 寄存器显示 busy 但数据流一动也不动。

解决:检查驱动分配缓冲区的对齐属性,确保按 DMA 通道的 AXI 位宽做对齐。调试时可以读 H2C Channel Alignments 寄存器(0x4C),它里面保存了硬件要求的对齐信息,驱动可以根据这个值动态调整缓冲区的偏移。血泪经验是:缓冲区首地址对齐了,但长度不是对齐粒度的整数倍,传输也会在最后一个 beat 处卡住。

5.2 现象二:C2H 流式传输,最后一个包丢失或写回状态一直不更新

原因:C2H 数据流的长度不是 64 字节的整数倍。XDMA 的 C2H 通道按 64 字节块处理数据,长度不满足时,最后一个块的写回触发逻辑会等待永远不会到来的数据,表现为写回地址的 Magic 一直是旧值,或者永远等不到 EOP 状态。

解决:在用户逻辑侧对包长度做向上取整,凑到 64 字节边界。取整后多出来的字节在主机侧通过 Length 字段剔除——写回长度字段是实际传输长度,不包含填充字节。具体做法是维护一个字节计数器,在每个包结束时判断长度是否对齐,不对齐就补 0 并记录补齐量。

5.3 现象三:Legacy 中断第一次能触发,之后永远不再来

原因:用户逻辑在 usr_irq_req 拉低后立刻重新拉高,没有等待第二次 usr_irq_ack。前面第三章已经解释过,Legacy 中断需要第二次 ack 来确认主机 ISR 退出。很多状态机只处理了第一次 ack,把 req 当作普通的“拉高触发”信号处理,结果就是中断只来一次,后续请求全部被吞。

解决:在用户逻辑的状态机里加一个“等第二次 ack”的状态。第一次 ack 到达后,只有在主机 ISR 完成处理并清掉用户侧寄存器后,才允许再次拉高 req。如果用户侧逻辑拿不准主机的响应时序,建议直接用 MSI-X 模式,绕开二次 ack 的约束。

5.4 现象四:AXI 接口报 Slave Illegal Burst 中断

原因:XDMA 的 AXI 接口只支持 INCR(地址递增)burst,用户逻辑如果发出 FIXED 或 WRAP burst,会被当作非法请求,并触发中断上报。这个中断会一直保持 pending,如果驱动不处理,后续通道的传输也会被拖住。

解决:在用户逻辑的 AXI slave 侧强制把 burst 类型固定为 INCR。尤其需要注意使用 AXI 协议转换 IP 时,某些 IP 默认会生成 WRAP burst,必须在转换 IP 的参数里关闭 WRAP 支持。

5.5 现象五:使能奇偶校验后丢数据,但所有状态寄存器都正常

原因:奇偶校验功能包括 Check Parity 和 Propagate Parity 两个独立开关。如果只使能了 Check Parity 而没有使能 Propagate Parity,XDMA 内部对校验位做了检查,但没有把校验信息传递到用户 AXI 接口,用户逻辑收到的数据里校验位全部丢失。校验位的总数是 DATA_WIDTH/8,在 AXI-ST 模式下通过 *_tuser 端口传递,AXI-MM 模式下通过_ruser、_wuser 端口传递。

解决:要么同时使能 Check 和 Propagate,要么用户逻辑侧自己接上 ruser/wuser/tuser 作校验透传。如果用户逻辑完全不需要校验,直接关闭整个奇偶校验功能,不要只开一半——半开状态会引入额外的时序逻辑,某些路径上反而会插流水寄存器改变 AXI 时序。

6. 设计取舍:为什么描述符放在主机内存,而 XDMA 坚持做“哑”执行者

把传输计划放在主机内存而不是 FPGA 内部存储,这个选择值得反复琢磨。从硬件成本看,如果描述符存在 FPGA 的 BRAM 或寄存器堆里,容量注定有限,通道数一旦增加到 8 路,每路保存几条描述符就要吃掉大量存储资源。把描述符放在主机内存,FPGA 侧只需要一个 Bit_width × 512 的 FIFO 做缓存,硬件面积大幅缩小。从灵活性看,主机侧用软件构造链表,天然支持任意长度的链、任意复杂的地址布局,驱动只要把链表首地址写给 XDMA,后面的路径跟着 Nxt_adr 走就行。这是典型的“用可编程性换硬件复杂度”的思路。

但代价也明显:每一次数据传输都要经历“取描述符”这个过程,哪怕只有一个描述符,也有一次主机内存的读延迟。Descriptor Bypass 就是为了弥补这个短板——把描述符的生成权从驱动挪到硬件逻辑,换来的是最低延时,失去的是主机调度能力。两种模式并存,本质上是同一套引擎针对“显式传输”和“流式推送”两种场景做的妥协。

我的个人习惯是:凡是需要主机发起、地址动态变化的传输,一律走标准描述符链路;凡是 FPGA 固定的、周期性的数据流,直接上 Bypass。调试时先关掉中断,用 Poll 模式验证描述符回写计数,确认通路没问题再打开中断——这套流程帮我避开了一半以上的“中断不触发”问题。希望这篇从描述符第一性原理拆到寄存器时序的解读,能帮你在自己的板卡上少走几步弯路。

本文还有配套的精品资源,点击获取

返回列表