简介:面向 Xilinx PCIe/XDMA 开发者的中文读书笔记,围绕 DMA/Bridge Subsystem for PCI Express v4.1 梳理 IP 核的关键机制与使用路径,适合需要理解 XDMA 架构、配置流程、仿真验证及 Linux 驱动联调的 FPGA 与驱动工程师查阅。压缩包内仅 1 个 PDF,大小约 174KB,内容按章节组织,便于快速检索与对照原文阅读。笔记覆盖 XDMA 构成与描述符、直接访问用户逻辑的非 DMA 方式、BARs 和 AXI4-Lite 主从接口、Legacy/MSI/MSI-X 中断及用户中断矢量配置、AXI-MM 与 AXI-ST 的 H2C/C2H 传输流程、Descriptor 详解、AXI-MM 初始化与 H2C/C2H 建立流程、位宽与地址对齐、Length Granularity 与奇偶校验、通道端口定义、寄存器空间以及时钟复位注意事项。笔记还延伸到设计指南、Example 设计、AXI-MM/AXI-ST 仿真与 Test Bench 任务,并整理 Linux 驱动下载、中断处理及 H2C/C2H 示例流程。已有 4221 人学习,可作为查阅 PG195 时的中文辅助材料,帮助定位 DMA 操作、旁路模式、SGDMA 与驱动联调中的关键概念,降低英文文档阅读和调试理解成本。
1. XDMA 读书笔记:先建立一张 PCIe 到 AXI 的地图
DMA/Bridge Subsystem for PCI Express(下文直接叫 XDMA)在 PCIe 加速卡、数据采集板、高速接口项目里几乎是绕不开的一个 IP。它的结构其实不吓人:两条方向相反的高速 DMA 通路,加几条低速旁路,外面套一层 BAR 和中断。真正让人栽跟头的是这四套机制——配置空间、描述符链、中断掩码、寄存器地址——叠在一起,任何一层错一位,现象都是同一句话:传不动。这份中文读书笔记按 PG195 手册的原始章节顺序重排,把 H2C/C2H 通路、描述符格式、Target/Channel 地址编排、端口时序到仿真流程串成一张可复习的地图。适合已经能跑通 Example Design、却说不出每个寄存器为什么这么写的人,也适合联调前想快速过一遍重点的人。
2. 通路与配置:三条数据路径,先分清谁走谁
XDMA 的名字里就藏着两个身份:DMA 和 Bridge。作为 DMA,它在主机内存和 FPGA 内部地址空间之间搬数据;作为 Bridge(只在 UltraScale+ 器件上支持,可作 EndPoint 也可作 Root),它把 PCIe 包转成 AXI 数据流。搞清楚这一点,后面所有的寄存器地址才不会变成死记硬背。
2.1 H2C 与 C2H:两条单向通道的骨架
整条链路可以拆成两条方向相反的流水线,背下这两行,端口方向就不会接反:
- H2C(Host to Card):PCIe RX → PCIe 硬核 → RQ/RC(Requester Request / Requester Completion)→ H2C channel → AXI Write 接口(MM 或 ST)→ 用户逻辑
- C2H(Card to Host):用户逻辑 → AXI Read 接口(MM 或 ST)→ C2H channel → RQ/RC → PCIe 硬核 → PCIe TX
注意这里 FPGA 扮演的是 Requester,H2C 的本质是"FPGA 向主机内存发起读请求,把读回来的数据落到本地",C2H 则是"FPGA 读本地数据,写到主机指定的目标地址"。所以描述符里的 Src/Dst 谁在前谁在后,和直觉是反的,这一点在第 3 章会反复出现。
XDMA 内部最多可以例化 8 个独立的物理 DMA 引擎:4 路 H2C 加 4 路 C2H。这些引擎有两种挂法:
| 接口模式 | 通道与接口的对应关系 | 典型用途 | 代价 |
|---|---|---|---|
| AXI4-MM | 所有通道共享单一 Master 接口 | 访问用户 memory、大块连续搬运 | 多通道争用同一组 aw/w/ar 通道 |
| AXI4-ST | 每个 channel 独立一组 AXI-Stream 接口 | 直连 RTL 逻辑模块、流式数据 | 端口数量随通道数膨胀 |
选 MM 还是 ST,本质是问"用户侧到底是地址空间还是一股数据流"。如果用户逻辑就是一个 AXI 从设备(比如一块 BRAM 或 DDR 控制器),选 MM;如果是视频流水线、ADC 采样流这种没有地址概念的模块,选 ST 更省事,因为 ST 模式下描述符的地址字段有一半是废的(见 3.3)。
2.2 非 DMA 直通:AXI4-Lite 与 Bypass
除了两条 DMA 主路,主机还有两种"不经过 DMA 引擎"的访问方式,这两条路经常被忽略,但在实际项目里用得极多:
第一种是 Cfg Master 端口,也就是 AXI4-Lite Master,固定 32 bit 位宽。主机通过它直接读写用户逻辑里的配置寄存器和状态寄存器。速率不高,但对延迟不敏感的控制面足够了,比如下发一个启动命令、读一个版本号。与之对称的是 AXI4-Lite Slave 接口,方向反过来:用户逻辑通过它读写 XDMA 内部寄存器,做自检或状态回读时很有用。
第二种是 Host DMA Bypass(AXI MM Master CQ Bypass),数据位宽与 DMA channel 保持一致。它绕开 DMA 引擎,主机写请求直接打到用户逻辑的存储器上,适合 peer-to-peer 这种高带宽、低延迟的场景。代价是没有描述符机制帮你做地址翻译和长度切分,主机侧要自己保证访问合法。
一句话对比:Lite 走控制面,Bypass 走数据面,DMA 走批量搬运。三者的配置在 IP 例化时勾选,一旦勾选,BAR 的分配就会跟着变。
2.3 BAR 映射:位宽决定你写哪个 BAR
这是最容易在软件侧翻车的地方。BAR 号和使能了哪些可选端口强相关,默认规则如下:
| BAR 位宽 | 配置状态 | BAR 分配 |
|---|---|---|
| 32 bit | 使能 PCIe to AXI4-Lite Master | BAR0 = AXI4-Lite Master,BAR1 = DMA,BAR2 = DMA Bypass |
| 32 bit | 使能 DMA Bypass | BAR0 = DMA,BAR1 = DMA Bypass |
| 64 bit | 使能 PCIe to AXI4-Lite Master | BAR0 = AXI4-Lite Master,BAR2 = DMA,BAR4 = DMA Bypass |
| 64 bit | 使能 DMA Bypass | BAR0 = DMA,BAR2 = DMA Bypass |
看出规律没有:每多使能一个可选端口,后面的 BAR 号整体往后挪一格。血泪经验是——驱动里把 BAR 号写死成 BAR0 的人,一旦在 IP 配置界面勾了 Lite Master,DMA 寄存器就全跑到 BAR1 去了,现象是"寄存器读出来全是 0xFFFFFFFF",然后开始怀疑硬件没上电。查表顺序永远是:先看 IP 配置里的勾选项,再定位 BAR,最后才怀疑链路。
2.4 端口速览:全局、AXI-MM、AXI-ST 与通道状态
端口定义一节内容很长,但按组记就清楚多了。全局侧有 sys_clk(PCIe 参考时钟)、axi_aclk(PCIe 驱动 AXI 接口的时钟)、axi_aresetn、user_lnk_up(与主机建立链接的标志)、msi_enable、msi_vector_width、msix_enable;dma_bridge_resetn 只在使能软件复位(SOFT_RESET_EN 参数为 TRUE)时才需要接。PCIe 侧就是 pci_exp_rxp/rxn/txp/txn 四组串行差分,位宽由链路宽度参数决定。
AXI-ST 模式下两组流接口方向相反,接反了仿真能过、上板没数据:
- H2C:m_axis_h2c_tdata/tvalid/tlast/tready/tuser,数据从 XDMA 流向用户逻辑
- C2H:s_axis_c2h_tdata/tvalid/tlast/tready/tuser,数据从用户逻辑流入 XDMA
其中 tuser 只在 Propagate Parity 模式下有效,宽度是 DATA_WIDTH/8,平时可以悬空。AXI-MM 模式则是一整套标准五通道:m_axi_ar*(读地址)、m_axi_r*(读数据)、m_axi_aw*(写地址)、m_axi_w*(写数据)、m_axi_b*(写响应),arprot 恒为 3'h0、arlock 恒为 1'b0,burst 类型只支持 INCR。
还有一组特别有用的调试信号:h2c_sts[7:0] 和 c2h_sts[7:0]。逐位含义是 bit0 busy、bit1 Descriptor completed、bit2 Descriptor stop、bit3 Descriptor Done、bit4 Packet Done、bit5 IRQ event pending、bit6 Control register run。抓波形时先看这 8 位,能立刻判断卡在"描述符没取到""描述符取到了但没搬完"还是"搬完了但中断没发",比盲猜寄存器高效得多。
3. 描述符与 DMA 流程:一次搬运要写哪几个寄存器
DMA 之所以叫 DMA,是因为 CPU 只负责"告诉硬件去哪儿搬",具体的搬运由引擎完成。这个"告诉"的载体就是描述符,而描述的提交靠一串有序的寄存器写操作。顺序错一步,引擎就不动。
3.1 Descriptor 的 8 个 DWord
描述符是 32 字节定长结构,在主机内存里由驱动创建,引擎通过链表把它们一个个取走。字段布局如下:
| 偏移 | 字段组成 | 说明 |
|---|---|---|
| 0x00 | {Magic[15:0], Rsv[1:0], Nxt_adj[5:0], control[7:0]} | Magic 固定 16'hAD4B,用于校验描述符有效 |
| 0x04 | {4'h0, Len[27:0]} | 本次搬运的字节数 |
| 0x08 / 0x0C | Src_adr[31:0] / [63:32] | 源地址,64 位 |
| 0x10 / 0x14 | Dst_adr[31:0] / [63:32] | 目标地址,64 位 |
| 0x18 / 0x1C | Nxt_adr[31:0] / [63:32] | 下一个描述符地址 |
control 字节里只有三位有意义:bit0 是 Stop,用来停止取描述符;bit1 是本描述符完成中断标识;bit4 是 EOP(End of Packet),在流模式下标记一个包的结束。写成 C 结构体更直观:
/* XDMA 描述符:32 字节,小端布局,字段按低位到高位排列 */ struct xdma_desc { uint32_t ctrl; /* [15:0] Magic=0xAD4B, [17:16] Rsv, [23:18] Nxt_adj, [31:24] control */ uint32_t len; /* [27:0] 有效字节数,高 4 位保留 */ uint32_t src_lo; /* 源地址低 32 位:H2C 时是主机地址 */ uint32_t src_hi; uint32_t dst_lo; /* 目标地址低 32 位:H2C 时是 FPGA 地址 */ uint32_t dst_hi; uint32_t nxt_lo; /* 下一个描述符地址,链式结构靠它串起来 */ uint32_t nxt_hi; };结构体里最容易写错的是 ctrl:Nxt_adj 在 bit[23:18],control 在最高字节,很多人按"Magic 在最低、control 紧随其后"的直觉去移位,结果 Magic 校验过不了,引擎直接罢工。另一个细节是引擎内部有一个 Bit_width × 512 深度的描述符 FIFO,以 256 bit 位宽为例就是 16 KB,能预取相当多描述符,所以链表长度不是性能瓶颈,真正限制吞吐的是每拍的数据位宽和 PCIe 链路本身。
3.2 初始化:三层中断掩码
在启动任何一次传输之前,驱动要先把中断链路打通,这里是三层,少一层都收不到中断:
- 写 H2C 通道中断使能 mask 寄存器 0x0090,打开对应 bit;
- 写 C2H 通道中断使能 mask 寄存器 0x1090,同样是按 bit 打开;
- 写 IRQ Block Channel Interrupt Enable Mask 寄存器 0x2010,把 H2C 和 C2H 各通道统一放行。
前两层是"通道级",第三层是"中断块级",逻辑上是与的关系。踩过坑的人都知道:只写了 0x0090、忘了 0x2010,波形上能看到描述符跑完、中断状态位也置起来了,但主机侧一个中断都收不到,然后开始怀疑 MSI-X 配置。顺序反过来查会快很多——先确认 0x2010,再确认 0x0090/0x1090,最后才动中断向量表。
3.3 H2C 全流程:驱动写五步,FPGA 走六步
H2C 是主机内存搬向 FPGA,完整时序按两侧拆开看最清楚。
应用层先初始化一次传输,确定长度和 buffer 位置。驱动层接着做三件事:创建描述符;把首个描述符基地址写进 0x4080(低 32 位)和 0x4084(高 32 位);如果是多次传输,把后续描述符的相邻信息写进 0x4088。最后写 H2C 引擎控制寄存器 0x0004 启动传输。
FPGA 侧的动作是完全被动的:引擎发起 descriptor fetch 请求,取回一个描述符,按描述符里的源地址向主机发读请求,收到数据后通过 AXI-MM Master(或 AXI-ST)接口把数据推给用户逻辑,全部搬完发中断。
驱动收到中断后还有五步收尾,这五步比启动更容易漏:
- 读 IRQ Block Channel Interrupt Request 寄存器 0x2044,判断是哪个通道产生的中断,再把对应通道的 mask 写进 0x2018 屏蔽掉;
- 读通道 Status 寄存器 0x0044,注意这次读操作本身就会清 status;
- 读 Completed Descriptor Count 0x0048,和提交的描述符数量比对,确认都完成了;
- 写通道控制寄存器 0x0004,停止 DMA;
- 写 Block Channel Interrupt Enable Mask 0x2014,为下一次传输重新使能中断。
第 2 步"读即清"是个暗坑,调试时在中断里多读一次 status,就会把状态清掉,后面再怎么读都是 0,看起来像"传输没完成"。第 5 步也常被忘,因为屏蔽是在第 1 步做的,不恢复的话下一次传输永远等不到中断,表现为"只成功一次,第二次卡死"。
3.4 C2H 全流程与 writeback
C2H 的流程是 H2C 的镜像,寄存器整体加 0x1000:首描述符地址写 0x5080/0x5084,后续描述符信息写 0x5088,启动写 0x1004,状态读 0x1044,完成计数读 0x1048,停止写 0x1004。中断处理与 H2C 共用同一套 IRQ Block 寄存器(0x2044、0x2014、0x2018),这也是为什么地址格式里 IRQ Block 单独作为一个 Target。
真正的差别在流模式。C2H 配成 AXI-ST 时,描述符里的源地址字段是无意义的(数据来自用户逻辑的 s_axis_c2h),引擎只认目标地址。一个数据包可以跨多个描述符,包结束由 EOP 标记。每个描述符完成后,C2H 通道会把一段写回信息存到驱动指定的 writeback 地址:
| 偏移 | 内容 |
|---|---|
| 0x0 | {WB Magic[15:0] = 16'h52B4, Rsv[14:0], Status} |
| 0x4 | Length[31:0],本次实际传输长度 |
Status 里带 EOP 标识位,WB Magic 用来验证这段写回信息是否有效——注意它和正向描述符的 16'hAD4B 是两个不同的魔数,记混了会一直判定写回无效。还有一条硬约束:C2H 流的数据长度必须始终保持 64 字节的整数倍,长度不对时引擎行为不可预期,这类问题在仿真里未必暴露,上板才炸。
3.5 Descriptor Bypass 与 Poll 模式
如果嫌"驱动建链 → 引擎取描述符"这条路过长,XDMA 给了两条降延迟的路。
Descriptor Bypass:每个通道都有独立的总线接口(c2h_dsc_byp / h2c_dsc_byp),描述符里的源地址、目标地址、长度这些信息直接由外部逻辑给定,不再经过主机驱动。适合 FPGA 内部有自主发起传输需求的场景,延迟可以做到很低。
Poll 模式:引擎把"已完成描述符数量"写回主机内存,驱动轮询这个值来代替中断。需要同时置位 ie_descriptor_completed 和 Pollmode_wb_enable,回写地址通过 Pollmode_hi_wb_addr 和 Pollmode_lo_wb_addr 两个寄存器指定。中断在某些实时性要求苛刻的场景下反而是负担(中断延迟抖动、CPU 亲和性问题),轮询能把这条不确定性抹掉,代价是占一个 CPU 核。
4. 中断与寄存器空间:地址是拼出来的
寄存器那一章看起来有几百个地址,实际上只有三个变量:Target、Channel、Byte Offset。把这三个搞明白,整张表就塌缩成几条规则。
4.1 三种中断机制的差别
IRQ 模块可以接收用户逻辑的中断线,每路 DMA 通道也各对应一路中断,支持 MSI-X、MSI 和传统中断三种模式。它们在时序上的差别,是理解驱动写法差异的根源:
| 机制 | 请求信号行为 | 关键约束 |
|---|---|---|
| Legacy Interrupt | usr_irq_req 拉高后必须保持到 usr_irq_ack 拉高 | req 撤销后不能立刻重新拉高,必须等到第二次 ack 才能再次触发 |
| MSI / MSI-X | req 撤销后可直接重新拉高 | 不需要等第二次 ack |
Legacy 那条"等第二次 ack"的规则是历史遗留:主机响应中断有延迟,第二次 ack 才表示主机已经从上一个中断处理程序里退出来,可以接受新中断了。用户逻辑里通常还要放一个主机可读写清零的寄存器,让软件能告诉硬件"中断处理完了"。MSI 模式下这条约束不存在,所以移植驱动时如果从 legacy 切到 MSI,那段"等第二次 ack"的状态机会一直等不到,看起来像中断丢了。
MSI-X 还有一处特殊:所有用户中断共用一个 ack 信号,但每个中断要在 IRQ Block User Vector Number 寄存器里编程不同的矢量号,靠向量号区分来源。也就是说硬件侧只有一根 ack,软件侧却要按向量号分发,这中间的对应关系必须提前规划好,否则多个中断同时到来时分不清是谁。
4.2 地址三段式:Target、Channel、Offset
PCIe 到 DMA 的地址格式是这样的:
31 16 15 12 11 8 7 0 +-----------+--------+--------+----------+ | Reserved | Target | Channel| Byte Off | +-----------+--------+--------+----------+Target 决定访问哪一类寄存器块:0x0 是 H2C 通道,0x1 是 C2H 通道,0x2 是 IRQ Block,0x3 是 Config Block,0x4 是 H2C SGDMA,0x5 是 C2H SGDMA,0x6 是 SGDMA Common,0x8 是 MSI-X 向量表和 PBA。Channel 字段的各个 bit 分别对应 H2C 通道、C2H 通道、H2C SGDMA、C2H SGDMA 这几类 Target。
于是所有地址都能算出来,不用背:
def xdma_reg(target, channel, offset): """按 Target/Channel/Offset 三段式拼出 XDMA 寄存器地址""" return (target << 12) | (channel << 8) | offset # 校验几个手册里高频出现的地址 print(hex(xdma_reg(0x0, 0, 0x04))) # 0x0004 H2C ch0 控制寄存器 print(hex(xdma_reg(0x1, 0, 0x04))) # 0x1004 C2H ch0 控制寄存器 print(hex(xdma_reg(0x2, 0, 0x10))) # 0x2010 IRQ Block 通道中断使能 print(hex(xdma_reg(0x4, 0, 0x80))) # 0x4080 H2C ch0 首描述符低地址 print(hex(xdma_reg(0x5, 0, 0x80))) # 0x5080 C2H ch0 首描述符低地址参数含义:target 取上表的值,channel 是通道号(0~3),offset 就是寄存器块内的字节偏移。跑一遍就能发现 0x0004 和 0x1004 差的那 0x1000 正是 Target 从 0 变成 1,0x4080 和 0x5080 差的那 0x1000 同理。理解了这一点,看到任何新地址都能反推出它属于哪个块的哪个通道,调试时不用来回翻手册。
4.3 关键寄存器速查
把联调中用得最多的地址集中在一张表里,按用途分组比按地址排序更好查:
| 地址 | 名称 | 用途 |
|---|---|---|
| 0x0004 | H2C Channel Control | 启动 / 停止 H2C 传输 |
| 0x0044 | H2C Channel Status | 读操作即清状态 |
| 0x0048 | H2C Completed Descriptor Count | 与提交数量比对 |
| 0x0090 | H2C Channel Interrupt Enable Mask | 通道级中断使能 |
| 0x1004 | C2H Channel Control | 启动 / 停止 C2H 传输 |
| 0x1044 | C2H Channel Status | 读操作即清状态 |
| 0x1048 | C2H Completed Descriptor Count | 与提交数量比对 |
| 0x1090 | C2H Channel Interrupt Enable Mask | 通道级中断使能 |
| 0x2010 | IRQ Block Channel Int Enable Mask | 中断块级总放行 |
| 0x2014 | IRQ Block Channel Int Enable Mask | 为下一次传输恢复使能 |
| 0x2018 | IRQ Block Channel Int Enable Mask | 中断处理中屏蔽 |
| 0x2044 | IRQ Block Channel Interrupt Request | 判断中断来自哪个通道 |
| 0x4080/0x4084 | H2C SGDMA Descriptor Low/High Address | 首描述符地址 |
| 0x4088 | H2C SGDMA Descriptor Adjacent | 后续描述符相邻信息 |
| 0x5080/0x5084 | C2H SGDMA Descriptor Low/High Address | 首描述符地址 |
| 0x5088 | C2H SGDMA Descriptor Adjacent | 后续描述符相邻信息 |
另外 Config Block(Target 0x3)里有几个值得记住的:0x3008 是 PCIe Max Payload Size,0x300C 是 Max Read Request Size,0x3040/0x3044 是 AXI 用户侧对应的两个参数,0x3060 是写刷新超时。这些参数直接决定单次 TLP 的大小,调吞吐时先看它们。还有一点要注意:AXI Bridge 和 AXI DMA 共用同一套中断处理寄存器,混用两种模式时不要以为中断是分开的。
4.4 对齐与 Length Granularity
位宽决定了地址和长度的低位必须为 0,这是硬件层面的要求,不是建议:
| 数据位宽 | 源/目标地址约束 | 流模式长度约束 |
|---|---|---|
| 64 bit | addr[2:0] == 3'h0 | Length[2:0] == 3'h0 |
| 128 bit | addr[3:0] == 4'h0 | Length[3:0] == 4'h0 |
| 256 bit | addr[4:0] == 5'h0 | Length[4:0] == 5'h0 |
| 512 bit | addr[5:0] == 6'h0 | Length[5:0] == 6'h0 |
读手册时这里有个容易看花眼的地方:Length 粒度那一节的 128 bit 行,位段编号和地址那一节不完全一致(地址写的是 [3:0],长度行写成了 [2:0] 与 4'h0 并列),按 4'h0 推断应为 Length[3:0]。这种原文笔误不影响结论——记住"低 N 位清零,N 等于 log2(位宽/8)"就不会错。
粒度参数本身只对流模式有意义,MM 模式下没有这个概念。另外奇偶校验是可选项:使能 Check Parity 和 Propagate Parity 后,校验信息会透传到用户 AXI 接口,由用户逻辑负责校验和生成,校验位总数为 DATA_WIDTH/8;ST 接口走 *_tuser,MM 接口走 *_ruser 和 *_wuser。不开这个功能时,对应端口可以完全不管。
5. 易漏点:五条最容易翻车的细节
前面几章是"应该长什么样",这一章是"实际会错成什么样"。这五条是梳理下来最容易反复踩的,每条按"易漏点 → 为什么漏 → 怎么补"给出。
5.1 描述符与数据通路的三处坑
其一,Magic 与 control 字节的位段位置。易漏点是把 ctrl 寄存器按直觉拼接,Magic 放到别的位置,或者把 Stop 位当成 bit7。为什么漏:字段定义写在一行 {Magic[15:0], Rsv[1:0], Nxt_adj[5:0], control[7:0]} 里,从低位往高位读,和人写代码的习惯方向一致,但从"control 是个字节"的直觉出发就会往低字节放。怎么补:写完结构体后打一个断点,把 ctrl 字段打印成二进制,确认低 16 位等于 0xAD4B、最高字节才是 control,两条都对再往下走。引擎对 Magic 的校验是硬性的,Magic 不对连 fetch 都不做,看波形像"启动寄存器写了但没反应"。
其二,C2H 流长度必须是 64 字节整数倍。易漏点是把最后一段不足 64 字节的数据也提交上去。为什么漏:MM 模式下长度是任意字节数,开发时先在 MM 模式调通,切到 ST 模式时直接复用同一套长度计算逻辑。怎么补:在提交描述符之前对长度做一次上取整,不足部分用无效数据填充到 64 字节边界,同时在驱动里加一条断言,长度对 64 取模不为 0 就直接报错,别留到硬件侧表现成"数据错位"。
其三,地址对齐与末拍边界。易漏点只检查了起始地址对齐,忘了传输长度也要落在位宽边界上。为什么漏:表格里地址约束和长度约束分开写,看的时候容易只记住一半。怎么补:把"低 N 位清零"抽成一个宏,在提交描述符前对地址和长度各做一次校验,N 由位宽决定(64 位宽 N=3、128 位宽 N=4、256 位宽 N=5、512 位宽 N=6)。另外流模式下 tlast 是由 EOP 驱动的,一个包可以跨多个描述符,别把"描述符结束"当成"包结束",否则 tlast 会在包中间冒出来。
5.2 中断与寄存器编排的两处坑
其四,三层中断掩码的顺序与恢复。易漏点有两个:只写了通道级 0x0090/0x1090 而漏掉块级 0x2010;中断处理里屏蔽了却没在退出前用 0x2014 恢复。为什么漏:手册里这三层分散在不同小节,0x2010 属于 IRQ Block,0x0090 属于 H2C Channel,很容易以为写一个就够。怎么补:把初始化写成固定的三段式函数,把收尾写成固定的恢复函数,"屏蔽—处理—恢复"永远成对出现;调不通时按 0x2010 → 0x0090/0x1090 → 0x2044 的顺序逐级回读,哪一级是 0 就补哪一级。
其五,Legacy 中断的第二次 ack 与通道状态位误读。易漏点是把 MSI 的时序套到 Legacy 上,req 撤销后立刻重新拉高;或者把 h2c_sts 的 bit1(Descriptor completed)和 bit3(Descriptor Done)当成一回事。为什么漏:两种中断在代码里长得一模一样,只差一个配置寄存器;状态位那 8 个 bit 名字又太像。怎么补:Legacy 模式下在状态机里显式加一个"等第二次 ack"的状态,不允许跳过;状态位按"bit0 busy 表示还在跑、bit2 stop 表示停了、bit4 Packet Done 表示整包完成、bit6 run 表示控制寄存器处于运行态"这四条来读,其余位只在对应场景看。抓波形时先看 bit0 和 bit6,能立刻区分"没启动"和"启动了没跑完"。
6. 复习清单:一页纸自查与复现路径
复习这个方向,光看是记不住的,得有一条可执行的复现路径。我自己习惯按下面的顺序过一遍,每步都有明确的验收标准,卡住时能立刻定位到是哪一层。
| 步骤 | 要做的事 | 验收标准 |
|---|---|---|
| 1 | 确认 IP 配置勾选项,列出实际 BAR 分配 | 能说出 Lite/Bypass/DMA 各在哪个 BAR |
| 2 | 确认通道数与接口模式(MM 还是 ST) | 通道数、位宽、是否开校验都能答上来 |
| 3 | 用三段式公式手算一遍所有要用的寄存器地址 | 0x0004/0x1004/0x2010/0x4080 全部能推出来 |
| 4 | 检查描述符结构的 Magic 与 control 位段 | 打印二进制,低 16 位为 0xAD4B |
| 5 | 走一遍三层中断掩码的初始化 | 0x2010、0x0090、0x1090 三级都非零 |
| 6 | 跑一次最小长度传输并抓 h2c_sts | bit0 拉高后归零,bit1/bit3 依次置起 |
| 7 | 做一次多次描述符链式传输 | Completed Descriptor Count 与提交数一致 |
| 8 | 换成 ST 模式,验证 tlast 与 EOP 对应 | 一个包只在末尾出现一次 tlast |
第 3 步值得单独强调:地址不是背下来的,是算出来的。把那个 python 函数抄进自己的脚本,每次遇到新地址先反推 Target 和 Channel,两三次之后整张寄存器表在脑子里就有结构了。第 6 步的验收标准也很关键——很多人只看中断有没有来,不看状态位,结果中断来了但数据是错的,白折腾半天。
仿真这一环别跳过。Example Design 有 AXI4-MM 和 AXI4-ST 两套默认设计,生成流程是自动的,先跑通默认设计再改成自己的逻辑,比从零搭 test bench 快得多。MM 模式的仿真重点看读地址、读数据、写地址、写数据、写响应这五组握手有没有卡在 valid 等 ready;ST 模式重点看 tlast 的位置和 EOP 是否一致。Test Bench 里的 test case 任务建议按"单描述符单包 → 多描述符单包 → 多描述符多包"三档递进,每档都断言一次 Completed Descriptor Count,这样出问题时能立刻判断是链表没串好还是包边界处理错了。
从那以后我每次启动一个新工程,都会先花二十分钟把这张自查表从上到下走一遍,尤其是 BAR 分配和三层中断掩码这两项——它们出问题的概率最高,排查成本也最高,提前确认一次,能省掉后面一整天对着波形猜的工夫。希望帮到你。
本文还有配套的精品资源,点击获取