简介:面向FPGA与PCIe开发者的XDMA IP核中文学习笔记,围绕DMA/Bridge Subsystem for PCI Express v4.1展开,适合已接触Vivado与AXI总线、希望系统掌握XDMA配置与调试的中级工程师查漏补缺。笔记覆盖文档全部章节:从IP构成、描述符机制、MSI/MSI-X与Legacy中断,到BARs配置、AXI4-Lite主从接口、AXI-MM与AXI-ST两种通道的H2C/C2H传输流程,并逐一梳理全局端口、各接口信号与寄存器空间(H2C/C2H、IRQ、Config、SGDMA、MSI-X向量表)的含义。时钟复位设计注意事项、Vivado配置生成、约束与仿真、Example Design及Test Bench流程、Linux驱动与H2C/C2H示例代码亦有涉及,可配合PG195与官方驱动仓库对照阅读。资源为单个PDF,约174KB,已有4215人学习。
1. XDMA 知识地图:一条从 PCIe 到 AXI 的搬运通道
XDMA 全称 DMA/Bridge Subsystem for PCI Express,本质职责只有一句话:在主机内存和 AXI 地址空间之间搬运数据,同时把配置访问和中断这两件事也管了。它有两种角色:DMA 引擎,以及 PCIe 到 AXI 存储空间的桥(桥模式只在 UltraScale+ 器件上成立)。在 DMA 角色下,最多 4 条 H2C 加 4 条 C2H,共 8 条独立通道;接口可以配成一条共享的 AXI-MM,也可以每通道独立配成 AXI-ST 直连用户 RTL。
反直觉的地方在于,XDMA 的难点不在 RTL 本身,而在三张表上:BAR 把哪段地址映射给谁,target/channel 拼出来的那 16 位地址怎么拆,以及描述符里那 8 个双字怎么填、每个字段放在哪。这三张表对上了,H2C/C2H 的流程基本就是照时序走一遍;对不上,表现就是 link up 了但数据搬不动,或者中断来了却定位不到是谁发的。很多人一开始就去看端口清单,其实先把“地址怎么拼”想明白,后面查寄存器时会轻松一半。
2. 体系与配置:H2C/C2H 通道、BAR 布局与三种主机访问路径
2.1 内部构成与两条数据流
XDMA 内部不是一团黑盒,拆开后是几组明确的方向性通路。接收方向:PCIe RX 进来,经 PCIe IP 转成 RQ/RC(Requester Request / Requester Completion),进入 H2C 通道,最后从 AXI Write 接口(MM 或 ST)送到用户逻辑。发送方向反过来:用户逻辑从 AXI Read 接口出,进入 C2H 通道,经 RQ/RC 回到 PCIe IP,再从 PCIe TX 发出去。这两条是数据面。
另外三条是控制面。中断方向:用户逻辑 → IRQ Module → PCIe IP,用户逻辑只负责把usr_irq_req拉起来,剩下的 MSI/MSI-X 封装由 IP 完成。配置方向:Cfg Master 的 AXI4-Lite Master/Slave 经 Bridge 接到 CQ/CC(Completer Request / Completer Completion),主机因此能读写用户逻辑里的控制状态寄存器。旁路方向:Host DMA Bypass 是一条 AXI-MM Master,主机可以直接把写请求投到用户逻辑,不经过 DMA 引擎。
把这些通路记住,调试时就有方向感了。数据搬不动,先分清楚是 H2C 还是 C2H、是 MM 还是 ST、有没有走描述符;中断不来,先看是usr_irq_req没拉、还是 IRQ Block 的使能掩码没开、还是主机侧 MSI-X 向量号没对上。三个层次一层层排,比漫无目的地翻寄存器快得多。
2.2 BAR 布局:32 位与 64 位的默认映射
BAR 决定了主机侧看到的是什么窗口,是整份笔记里最先要背下来的一块。32 位 BAR 和 64 位 BAR 的默认分配不一样,使能“PCIe to AXI4-Lite Master”和使能“PCIe to DMA Bypass”也不一样。
| BAR 位宽 | 配置组合 | BAR0 | BAR1 | BAR2 | BAR4 |
|---|---|---|---|---|---|
| 32 位 | 使能 AXI4-Lite Master | PCIe to AXI4-Lite Master | DMA | PCIe to DMA Bypass | — |
| 32 位 | 使能 DMA Bypass | DMA | PCIe to DMA Bypass | — | — |
| 64 位 | 使能 AXI4-Lite Master | PCIe to AXI4-Lite Master | — | DMA | PCIe to DMA Bypass |
| 64 位 | 使能 DMA Bypass | DMA | — | PCIe to DMA Bypass | — |
表格里最能坑人的是 64 位那一行:BAR 号不是连续的,BAR1 是空的,DMA 落在 BAR2,Bypass 落在 BAR4。写驱动或者手写测试程序时,如果按“下一个 BAR 就是下一个功能”的直觉去推,一定会读到错误窗口。正确做法是先把这张表在驱动里显式写死,或者先读一遍配置空间确认实际分配,不要靠猜。
提示:64 位 BAR 会占用两个 BAR 槽位,所以序号会跳。看到 BAR1 不存在,属于正常现象,不是配置出错。
2.3 三种主机访问路径:DMA、Cfg Master、Bypass
主机访问用户逻辑有三条路,选错路不会报错,但性能或语义会不对。第一条是 DMA 通道,走描述符,适合大批量数据搬运,是 H2C/C2H 的主战场。第二条是 AXI4-Lite Master,固定 32 位位宽,主机对用户逻辑的读写请求从这条口出去,适合访问速率要求不高的配置寄存器和状态寄存器——比如某图像处理 Demo 里,主机每隔一段时间读一次帧计数、写一次使能位,走这条最省事。
第三条是 AXI MM Master CQ Bypass,数据位宽与 DMA 通道一致,用于高带宽的用户 memory 访问,典型场景是 peer-to-peer 传输。它的价值在于绕开了描述符这一层,没有取描述符的往返延迟。相对地,用户逻辑要自己保证地址和突发类型合法,没有驱动帮你兜底。
另外还有一条反向路径:AXI4-Lite Slave。主机读用户逻辑用的是 Master,而用户逻辑读 XDMA 内部寄存器用的是 Slave。这一对接口经常被记混——m_axil_*是 XDMA 发出去的,s_axil_*是 XDMA 收进来的,看前缀的 m/s 就能分清方向。
2.4 端口分组、时钟复位与两条硬约束
端口清单很长,按功能分组记会轻松很多:全局端口(sys_clk、sys_rst_n、axi_aclk、axi_aresetn、dma_bridge_resetn、user_lnk_up、msi_enable、msi_vector_width、msix_enable)、PCIe 串行接口(pci_exp_txp/txn/rxp/rxn,宽度由PL_LINK_CAP_MAX_LINK_WIDTH决定)、H2C 的 AXI4-ST 接口(m_axis_h2c_*)、C2H 的 AXI4-ST 接口(s_axis_c2h_*)、AXI4-MM 的五组通道(m_axi_ar*、m_axi_r*、m_axi_aw*、m_axi_w*、m_axi_b*)、Bypass 的 AXI-MM Master、两组 AXI4-Lite(m_axil_*与s_axil_*)、中断接口(usr_irq_req/usr_irq_ack)、通道状态(h2c_sts[7:0]/c2h_sts[7:0])、外部配置接口(cfg_ext_*)、配置管理接口(cfg_mgmt_*),以及 Descriptor Bypass 模式下才出现的h2c_dsc_byp/c2h_dsc_byp。
AXI4-MM 里有几个“看起来多余但必须接对”的信号:m_axi_arprot固定 3'h0,m_axi_arlock固定 1'b0,m_axi_arburst指示读突发类型。这些固定值不是建议,是 IP 的实际行为,用户逻辑里如果拿它们做分支判断,会白写一堆永远走不到的逻辑。ST 接口的tuser(m_axis_h2c_tuser、s_axis_c2h_tuser)只在 Propagate Parity 模式下才使能,宽度是 DATA_WIDTH/8;MM 接口对应的奇偶校验信息则走ruser/wuser,两者是同一套机制的两个出口。
时钟和复位这块,梳理下来有三个点值得单独记住。sys_clk是 PCIe 参考时钟,axi_aclk是 PCIe 对 AXI 接口的驱动时钟,两者是不同的域,跨域的地方要靠 IP 内部处理,但用户侧的 AXI 逻辑必须挂在axi_aclk上。axi_aresetn是 AXI 接口复位,必须与axi_aclk同步释放,异步释放会让某些通道状态机停在半路。dma_bridge_resetn只有把SOFT_RESET_EN参数设为 TRUE(即启用软件复位功能)时才需要接,其他配置下这个引脚不用管,接错了反而引入额外复位源。
两条硬约束必须在这儿点明。其一,XDMA 只支持 INCR burst 类型(地址递增),其他 burst 类型会直接触发 Slave Illegal Burst 中断。这条不是“最好遵守”,而是“违反了会断”。其二,并非所有配置组合都能顺利用 Example Design 重新生成,遇到工程打不开的情况,先回头核对 IP 配置项,再怀疑工具链版本。这两条放在架构章末尾,是因为它们属于选型阶段就要确认的事,等到调时序时才发现,返工量很大。
3. 描述符与 DMA 操作:H2C/C2H 的寄存器级时序
3.1 Descriptor 的八个双字逐字段拆解
XDMA 靠描述符链接列表来指定一次传输的源、目的和长度。链表在主机驱动里创建,放在主机内存中;驱动只负责初始化少数几个通道控制寄存器,剩下的取描述符、解析、发读写请求,都是 IP 自己完成的。这是理解整个流程的关键:CPU 写寄存器只是“点火”,真正的搬运是硬件按链表走。
描述符固定 8 个双字,共 32 字节,字段排布如下:
/* XDMA 描述符内存布局,每项 4 字节,共 32 字节 */ struct xdma_desc { uint32_t dw0; /* 0x00: Magic[15:0]=0xad4b | Rsv[1:0] | Nxt_adj[5:0] | control[7:0] */ uint32_t dw1; /* 0x04: {4'h0, Len[27:0]},注意高 4 位保留 */ uint32_t src_lo; /* 0x08: 源地址低 32 位 */ uint32_t src_hi; /* 0x0c: 源地址高 32 位 */ uint32_t dst_lo; /* 0x10: 目的地址低 32 位 */ uint32_t dst_hi; /* 0x14: 目的地址高 32 位 */ uint32_t nxt_lo; /* 0x18: 下一个描述符地址低 32 位 */ uint32_t nxt_hi; /* 0x1c: 下一个描述符地址高 32 位 */ };Magic固定为 16'hAD4B,作用是让硬件验证驱动生成的描述符是否可用——填错这个字段,DMA 会直接不启动,而且不一定给你一个直观的错误码。Len只有 28 位,高 4 位必须清零。control[7:0]里三个位最常用:bit0 是 Stop,用来停止继续取描述符;bit1 是完成中断标识,置位表示这个描述符完成时产生中断;bit4 是 EOP(End of Packet),标记一个数据包的结束,在 AXI-ST 模式下会导致tlast拉高。
描述符 FIFO 的深度是位宽 × 512。以 AXI 位宽 256 bit 为例,FIFO 深度是 256 bit × 512 = 16 KB。这个数字决定了预取能力的上限,也解释了为什么描述符链要尽量连续分配——链表跳得太散,预取会退化。
3.2 H2C 传输的完整时序
H2C 是主机到卡。整条链路上,驱动和 FPGA 各做一半,责任边界很清楚。驱动侧要先Load driver,然后写三个使能:H2C 通道中断使能掩码寄存器0x0090(产生对应 bit 上的中断)、C2H 通道中断使能掩码寄存器0x1090、IRQ Block Channel Interrupt Enable Mask寄存器0x2010(使能全部 H2C/C2H 通道产生中断)。这三步少任何一步,最后都表现为“数据搬完了但没中断”。
流程展开是这样:应用程序初始化 H2C 传输,给定长度和 buffer 位置;驱动创建描述符;驱动把首个描述符基地址写到0x4080和0x4084,如果是多次传输,把后续描述符数量写到0x4088;驱动写 H2C 引擎控制寄存器0x0004,启动传输。到这里 CPU 的工作就结束了。
FPGA 侧接力:先发起描述符取指请求,收到一个描述符,按描述符里的源地址发读请求,从主机收数据,再通过 AXI-MM Master 接口把数据写进用户逻辑,最后发中断到主机。驱动进中断后,读IRQ Block Channel Interrupt Request寄存器0x2044判断是哪条通道,把对应通道的中断掩码写0x2018;然后读Status寄存器0x0044——这一步会顺带清状态,是个容易翻车的设计;读已完成描述符计数0x0048,与预期数量比对;写控制寄存器0x0004停止 DMA,写0x2014为下一次传输重新使能中断;最后把实际传输的 size 返回给应用层。
地址0x4080、0x5080、0x2010、0x2044看起来毫无规律,其实全都遵循同一套拼接规则,第 4 章会把这条规则拆开讲。现在先记住结论:这些值不是魔法数字。
3.3 C2H 传输与 writeback 格式
C2H 是卡到主机,整体结构对称,但地址段不同。驱动侧同样先建描述符,把首个描述符基地址写到0x5080和0x5084,后续数量写0x5088,然后写 C2H 引擎控制寄存器0x1004启动。FPGA 取描述符、按描述符给的源地址(FPGA 板卡侧地址)读数据、经 PCIe 把数据送到主机目标地址、发中断。驱动的收尾和 H2C 一样,只是状态寄存器换成0x1044,完成计数换成0x1048,停止写0x1004。
这里有个文档笔误值得单独标一下:C2H 流程里写的是“写 C2H 引擎控制寄存器地址0x1004,启动 H2C 传输”。从上下文和地址段看,启动的显然是 C2H。照着字面理解会以为 C2H 里还要再启动一次 H2C,白折腾半天。
AXI-ST 模式下的 C2H 还有一个特有机制:一个描述符完成后,C2H 通道会把 writeback 数据写到指定地址,格式是两个双字:
/* C2H 流模式下的写回结构,共 8 字节 */ struct c2h_wb { uint32_t dw0; /* {WB Magic[15:0]=0x52b4, Rsv[14:0], Status} */ uint32_t dw1; /* {Length[31:0]} */ };WB Magic固定 16'h52B4,用来验证写回信息是否有效;Status里带 EOP 标识位;Length是本次传输长度。注意它和 H2C 描述符里的 Magic 16'hAD4B 是两个不同的常量,一个在描述符里,一个在写回里,方向相反,记混了会一直等不到有效写回。
注意:C2H 数据流的长度必须始终保证是 64 字节的整数倍。这条在 AXI-ST 模式下尤其容易漏,因为 MM 模式下偶尔不对齐也能跑过去,ST 模式下会直接卡死。
3.4 位宽、地址对齐与 Length Granularity
这两个概念经常被合并记忆,其实是两回事。地址对齐约束的是源地址和目的地址的低位,Length Granularity 约束的是传输长度参数的低位。位宽越大,要求清零的位数越多。
| DATA_WIDTH | 源/目的地址要求 | Length 要求 |
|---|---|---|
| 64 bit | Src_addr[2:0] == Dst_addr[2:0] == 3'h0 | Length[2:0] == 3'h0 |
| 128 bit | Src_addr[3:0] == Dst_addr[3:0] == 4'h0 | Length[3:0] == 4'h0 |
| 256 bit | Src_addr[4:0] == Dst_addr[4:0] == 5'h0 | Length[4:0] == 5'h0 |
| 512 bit | Src_addr[5:0] == Dst_addr[5:0] == 6'h0 | Length[5:0] == 6'h0 |
原文里 128/256/512 三档的 Length 条件写的是Length[2:0],从位宽对齐的一般规律看,应当是随位宽递增的,上表按规律给出,实际使用以 IP 生成的参数为准。真正要记的是那条规律:位宽每翻一倍,要求清零的位数加一。写驱动时把对齐检查做成一个按位宽参数化的函数,比给每档写死一个常量更不容易出错。
3.5 Poll 模式与 Descriptor Bypass
中断不是唯一选择。每个 DMA 引擎都能把已完成描述符数量写回主机内存,驱动改用轮询就能替代中断。使能需要置位ie_descriptor_completed和Pollmode_wb_enable,写回地址由Pollmode_hi_wb_addr和Pollmode_lo_wb_addr两个寄存器指定。这里的“已完成数量”是从 DMA 初始化之后累计的,不是本轮的增量,比对时要拿累计值减基线,直接用会和预期对不上。
Descriptor Bypass 是另一条路:每条通道都有独立的 bypass 功能,使能后描述符不再由驱动提供,而是从c2h_dsc_byp或h2c_dsc_byp总线直接给源地址、目的地址、长度这些信息。适合描述符内容高度可预测、不想付主机侧取描述符延迟的场景。代价是灵活性没了,用户逻辑得自己维护这套信息。
4. 寄存器空间与中断:target/channel 寻址模型
4.1 PCIe to DMA Address Format 的拆解
前面那些“看起来没规律”的地址,全部来自同一张分段表:
| 位段 | 字段 | 含义 |
|---|---|---|
| 31:16 | Reserved | 保留 |
| 15:12 | Target | 选哪个寄存器块 |
| 11:8 | Channel | 选块内哪条通道 |
| 7:0 | Byte Offset | 块内字节偏移 |
Target 的编码是固定的:4'h0 对应 H2C Channels,4'h1 对应 C2H Channels,4'h2 对应 IRQ Block,4'h3 对应 Config,4'h4 对应 H2C SGDMA,4'h5 对应 C2H SGDMA,4'h6 对应 SGDMA Common,4'h8 对应 MSI-X Vector Table 和 PBA。
有了这张表,前面所有地址都能心算出来:
def dma_addr(target, channel, offset): """按 target/channel/offset 拼出 PCIe 侧访问地址""" return (target << 12) | (channel << 8) | offset print(hex(dma_addr(0x0, 0, 0x04))) # 0x4 H2C 通道 0 控制寄存器 print(hex(dma_addr(0x1, 0, 0x04))) # 0x1004 C2H 通道 0 控制寄存器 print(hex(dma_addr(0x4, 0, 0x80))) # 0x4080 H2C SGDMA 描述符低地址 print(hex(dma_addr(0x5, 0, 0x80))) # 0x5080 C2H SGDMA 描述符低地址 print(hex(dma_addr(0x2, 0, 0x10))) # 0x2010 IRQ Block 通道中断使能掩码 print(hex(dma_addr(0x2, 0, 0x44))) # 0x2044 IRQ Block 通道中断请求 print(hex(dma_addr(0x2, 0, 0x18))) # 0x2018 IRQ Block 通道中断使能掩码(高位双字) print(hex(dma_addr(0x0, 0, 0x44))) # 0x44 H2C 通道状态(读清) print(hex(dma_addr(0x1, 0, 0x44))) # 0x1044 C2H 通道状态(读清)七个地址一次对上,说明这套模型是对的。以后看到任何0xXYZW形式的 XDMA 寄存器地址,先把它拆成 target = (addr >> 12) & 0xF、channel = (addr >> 8) & 0xF、offset = addr & 0xFF,再对照对应块的偏移表查,比翻文档快得多。这也是为什么0x0090和0x1090只差最高位的 nibble——它们是同一个偏移0x90在 H2C 和 C2H 两个 target 下的实例。
4.2 六个寄存器块的分工
H2C Channel(target 0x0)和 C2H Channel(target 0x1)结构几乎完全对称,偏移一致:0x00是通道标识,0x04/0x08/0x0C是通道控制,0x40/0x44是通道状态,0x48是已完成描述符计数,0x4C是通道对齐参数,0x88/0x8C是 Poll 模式写回地址的低/高位,0x90/0x94/0x98是中断使能掩码的三个双字,0xC0到0xD0是性能监控(控制、周期计数、数据计数)。对称带来的好处是驱动可以写一份代码走两条通道,只换 target。
IRQ Block(target 0x2)是中断中枢:0x04/0x08/0x0C用户中断使能掩码,0x10/0x14/0x18通道中断使能掩码,0x40用户中断请求,0x44通道中断请求,0x48用户中断挂起,0x4C通道中断挂起,0x80到0x8C用户中断向量号,0xA0/0xA4通道中断向量号。
Config Block(0x3)管的是链路层参数:0x04是 BusDev,0x08是 PCIe Max Payload Size,0x0C是 PCIe Max Read Request Size,0x10是 System ID,0x14是 MSI Enable,0x18是 PCIe Data Width,0x1C是 PCIE Control,0x40/0x44是 AXI 用户侧 Max Payload Size 和 Max Read Request Size,0x60是写刷新超时。0x60这个写刷新超时容易被忽略,但它在写路径拥塞时决定了行为。
SGDMA 的三个块(0x4、0x5、0x6)里,0x80/0x84是描述符低/高地址,0x88是描述符相邻地址,0x8C是描述符 credits。AXI Bridge 和 AXI DMA 共享中断处理的那组寄存器,也就是说同一套 IRQ Block 同时服务两种角色,配置时要意识到这个共享关系。
4.3 Legacy 中断的握手约束
Legacy 中断的协议比 MSI 严格,严格在“什么时候可以重新拉起来”。使能 legacy 中断后,usr_irq_req上任一 bit 拉高就产生中断,而这个信号一旦拉高必须保持到usr_irq_ack拉高为止,表示中断已经送到 PCIe Block,主机需要进中断服务程序并清中断。
关键在后半段:usr_irq_req撤销后不能立刻重新拉高,必须等到usr_irq_ack第二次拉高才能再次拉起。原因是主机有固定的中断响应时间,第二次 ack 才代表主机从上一个中断处理中退出,可以接受新的中断了。用户逻辑通常还需要一个可被主机读写清零的寄存器,让主机能告诉逻辑“我处理完了”,否则逻辑不知道什么时候该退。
这条约束在仿真里不容易暴露,因为仿真里主机响应几乎是瞬时的;上板之后一旦中断密集,就会出现丢中断,而且现象很玄学——单次触发正常,连续触发丢一半。查的时候直接看usr_irq_req和usr_irq_ack的时序关系,比猜别的原因快。
4.4 MSI/MSI-X 与用户中断向量
使能 MSI 或 MSI-X 后,usr_irq_req拉高就产生对应中断,而且撤销后不需要等第二次 ack,可以立刻重新拉起。这是它和 legacy 最实质的差别,也是高吞吐场景优先选 MSI-X 的原因之一。
MSI-X 模式下用户中断有个额外步骤:需要为每一个用户中断在IRQ Block User Vector Number寄存器(0x80到0x8C)里编程不同的矢量号。多个矢量号共用一个 ack 信号,也就是有且只有一个usr_irq_ack。这意味着 ack 回来时,逻辑必须自己判断是哪个矢量被响应了,不能指望 ack 信号本身带信息。msi_vector_width决定 MSI 字段大小,msix_enable和msi_enable分别对应两种模式,配置时三者要保持一致。
5. 易错点:XDMA 调试里最容易翻车的五个地方
5.1 把地址对齐和 Length Granularity 当成一件事
易漏点:只检查源地址和目的地址的低位是否清零,忘了长度参数也有低位要求。
为什么漏:两者在文档里分属不同小节,但描述句式高度相似,都是“位宽 X 对应低位清零”,扫读时很容易合并成一个概念。MM 模式下长度不对齐有时能歪打正着跑过去,进一步掩盖了问题。
怎么补:在驱动里把两者做成两个独立的断言函数,输入是位宽和地址/长度,任何一次描述符构造都过一遍。单元测试里专门造一个“地址对齐但长度不对齐”的用例,确保它能被拦住。
5.2 读 Status 寄存器会顺带清状态
易漏点:在中断处理里先读状态、再读完成计数,中间插了别的调试打印,结果状态被清掉,第二次读全是零。
为什么漏:0x0044(H2C)和0x1044(C2H)的读清语义不在寄存器名里体现,名字看起来就是个普通状态寄存器。调试阶段加打印是本能反应,而这个寄存器最怕的就是多读。
怎么补:把状态读取做成“读一次就存到局部变量”的模式,后续所有判断都用局部变量,禁止在中断处理里二次访问。代码评审时把这条列成检查项。
5.3 AXI-ST 模式下描述符的源/目的地址不参与
易漏点:配成 AXI-ST 之后,仍然认真填描述符里的源地址和目的地址,甚至为填错而排查半天。
为什么漏:描述符格式是一样的,字段都在,看起来都该填。实际上 H2C 流模式下目标地址不被使用,因为目标是 FPGA 板卡里的一个 FIFO;C2H 流模式下源地址不被使用,数据直接读到主机指定地址。
怎么补:在建描述符的函数里按接口模式分支,ST 模式下把不用的字段显式置零并加注释说明“本模式下该字段被忽略”,避免后来人误以为漏填。
5.4 Legacy 中断的二次 ack 被简化掉
易漏点:按 MSI 的思维写用户逻辑,usr_irq_req撤销后立刻重新拉起,仿真通过,上板丢中断。
为什么漏:仿真环境里主机响应过快,二次 ack 的约束几乎体现不出来;而这段逻辑通常写在用户 RTL 里,不在 IP 内部,不会被 IP 的示例代码覆盖到。
怎么补:给 legacy 模式单独写一个中断请求状态机,明确三个状态——请求中、等待二次 ack、可再次请求,把“必须等到第二次 ack”写进状态转移条件,而不是靠计数器凑。
5.5 C2H 流长度不是 64 字节整数倍
易漏点:应用层传下来一个任意长度,驱动直接拿去做描述符长度,MM 模式跑得好好的,切到 ST 模式就卡住。
为什么漏:这条约束写在 C2H 流的说明里,不在通用的 Length Granularity 表里,而那张表更显眼,容易被当成“长度约束已经看过了”。
怎么补:在驱动的 C2H 提交入口做一次向上取整到 64 字节倍数,并把实际补齐的长度回传给应用层,让应用层知道缓冲区尾部可能有填充数据。这条向上取整逻辑要写单元测试,覆盖 1、63、64、65 四个边界值。
6. 复习清单:把 XDMA 这张图默写一遍的自查路线
复习到最后,与其反复翻文档,不如做一次“默写式自查”。拿一张白纸,按下面四层往下写,写不出来的地方就是漏点。
| 层次 | 默写内容 | 判定标准 |
|---|---|---|
| 地址层 | target 编码表 + 位段划分 | 能由0x4080、0x2044、0x1090反推出 target/channel/offset |
| 描述符层 | 8 个双字的字段位置 | 能说出 Magic 两个常量、Len 的位宽、control 的三个关键位 |
| 流程层 | H2C 与 C2H 各自的启动地址和收尾地址 | 两边地址不串,知道哪个是0x4080哪个是0x5080 |
| 中断层 | legacy 与 MSI/MSI-X 的三点差异 | 保持条件、二次 ack、矢量号编程 |
地址层最容易过,因为规律性强;描述符层次之,0xad4b和0x52b4两个常量记住就不太会错;流程层最容易串,尤其是 H2C 的0x4080/0x0004和 C2H 的0x5080/0x1004,建议成对背;中断层的差异点少但都是行为级差异,光背文字不够,最好在波形上确认一遍。
再补三个自查动作。第一,把四个位宽档的对齐要求和 Length 要求列成一张两列表,遮住一列默写另一列,错一次就重来。第二,把h2c_sts和c2h_sts那 8 个 bit 的含义从 bit0 到 bit6 顺序默写一遍,这两个寄存器结构完全一样,能默写一个就等于记住两个,调试时看到哪一位亮就知道卡在描述符取指、传输、还是中断挂起。第三,把usr_irq_req在 legacy 和 MSI 两种模式下的时序图各画一遍,重点画撤销之后到下一次拉高之间隔了什么。
如果时间只够做一件事,就做地址层的默写。我自己的血泪经验是:早期调一个 H2C 不通的板子,怀疑过时钟、怀疑过复位、怀疑过 BAR,最后发现是通道控制寄存器写到了错误的 target 段,写进 C2H 的控制寄存器里去了,而 MM 模式下两边偏移一模一样,读回来毫无异常。从那以后我每次碰 XDMA,都先把 target/channel/offset 那三行位段在纸上写一遍,再开始动其他东西,这个习惯已经帮我省掉好几次通宵。希望帮到你。
本文还有配套的精品资源,点击获取