ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AXI PCIe桥核实战:BAR、地址翻译、DMA与带宽调优

AXI PCIe桥核实战:BAR、地址翻译、DMA与带宽调优 手上有块板卡主机侧只想做一件事像访问内存一样读写 FPGA 里面那几块 BRAM 和寄存器或者反过来让 FPGA 主动把采集到的数据推进主机内存CPU 只负责收尾。需求落到工程上最后多半会指向同一个位置——AXI Memory Mapped to PCI Express 这颗 IP 核。它干的活其实很单一把主机通过 PCI Express 链路发下来的 TLP 事务翻译成 AXI4 总线上的地址读写反过来把 AXI 侧发起的读写打包成 TLP 送回主机。功能一句话说完了但真正上手过的人都清楚从例化到第一笔数据正确读回中间隔着一堆配置项、地址空间规划和复位时序。这篇内容适合三类人第一次在 FPGA 上做 PCIe 板卡、准备用这颗桥核快速跑通 BAR 读写的人做数字 IC 验证、需要搭 AXI PCIe 混合仿真环境的人以及已经把链路跑起来了、但带宽或者稳定性上不去、想回头看看哪里没调对的人。下面按我自己的实际项目顺序来讲先搞清楚这颗核替你扛了什么、边界在哪再钉死配置参数然后是跑通链路、搭仿真、收时序、测带宽最后是几类典型故障的排查链路。1. 这颗桥核到底替你扛了哪些脏活1.1 没有它你得自己写一遍 TLP 协议栈很多人对 PCIe IP 的认知停留在一个管脚接出去就能通其实链路层和事务层的工作量远超想象。如果不用这颗桥核纯靠自己搭逻辑接 PCIe 硬核的 AXI-Stream 接口你要自己实现的东西至少包括TLP 的组包与解包Header 的 Fmt/Type/TD/EP/Attr/TC/Length/Requester ID/Tag/Address 字段逐位解析、Tag 的分配与回收、Completion 与 Request 的匹配、接收端的 Credit 流控PH 与 PD 信用值、发送侧的重传缓冲、ACK/NAK 处理以及各种错误上报。这些工作里有相当一部分是写完了也很难验证对不对的类型因为出错往往是极端时序下的偶发丢包不是功能仿真能覆盖的。AXI Memory Mapped to PCI Express 这颗核把这些全部吃掉了对外暴露的是一组非常干净的接口。你面对的不再是 TLP而是 AXI4 的五个通道写地址 AW、写数据 W、写响应 B、读地址 AR、读数据 R。也就是说只要你会写 AXI4 从机或者主机就能把 PCIe 用起来。这是它最大的价值也是它在各种板卡项目里被反复使用的原因。1.2 两个方向的通路别搞反S_AXI 是入站M_AXI 是出站这是新手最容易搞混的一点我在早期项目里也栽过。记住一个判断标准站在FPGA 用户逻辑的视角看这颗核的接口谁是主动方。入站方向也就是主机发起的访问。主机读你的卡、写你的卡走的是这颗核的 AXI4 从机接口通常命名 S_AXI 或 AXI4_SLAVE。这颗核在 PCIe 侧是 Completer在 AXI 侧是 Master它把主机对某个 BAR 的读写变成对 S_AXI 的 AXI 读写。你的用户逻辑要在这条总线上做一个 Slave响应它的 AW/W/AR返回 R/B。这是最常用的一条路——主机通过 BAR 直接读写你的寄存器和缓存。出站方向也就是 FPGA 主动访问主机内存。这条走的是这颗核的 AXI4 主机接口通常命名 M_AXI 或 AXI4_MASTER。你在用户逻辑里发起 AXI 写核把它翻译成 Memory Write TLP 发到主机你发起 AXI 读核发 Memory Read TLP等主机的 Completion 回来后再把数据通过 R 通道还给你。DMA 就是这么做的。两条通路的地址翻译是分开的配置寄存器也是分开的。如果你把 DMA 引擎接到了 S_AXI 上现象会是能编译、能跑、但主机永远等不到数据而且不会有任何报错——因为它本质上是让主机去访问一个根本没人发起的地方。这个坑我在后面的章节还会展开。1.3 它明确不管的东西别指望它帮你兜把这颗核的能力边界划清楚能省下大量排查时间。它不管以下几件事时序约束。IP 例化出来只是逻辑PCIe 的参考时钟、GT 的物理位置约束、跨时钟域路径的时序例外全都要你在 XDC 里自己写。约束漏了现象是综合能过、实现能过、上板偶尔能枚举、跑一会儿挂死。地址空间规划。你的 BAR 开多大、AXI 侧地址怎么分配、互联怎么连这些都要你自己在 Block Design 里定。让工具自动分配固然省事但自动分配出来的地址常常和你后续软件里写的偏移对不上。主机侧驱动。核把 MEM/IO 空间映射出来之后主机得有人去使能 BAR、去 mmap、去做 DMA 缓冲区的物理地址转换。裸机环境、Linux 内核态、用户态驱动处理方式完全不同。所以真实的项目分工是核负责协议翻译你负责从主机软件到 AXI 总线这条完整链路上除了协议之外的每一环。下面讲的都是这一环。2. 上手之前必须钉死的三件事BAR、地址翻译、数据位宽2.1 BAR 的大小和类型是第一个不可逆的决定BARBase Address Register是主机眼里你的卡占用的那几段地址窗口。绝大多数项目只用到 BAR0用一个 32 位或 64 位的内存型 BAR 就够了。几个必须想清楚的点第一BAR 的大小。它决定了主机能直接看到多大的空间。如果你只是想暴露 4KB 的寄存器配 4KB 就行如果你想让主机直接 mmap 几 MB 的共享缓存就必须开对应的大小。BAR 大小一旦确定地址解码逻辑就要按这个宽度实现改起来意味着地址空间重新规划。第二地址位宽选 64 位还是 32 位。现在的系统基本都是 64 位地址空间选 64 位 BAR 更省心代价是它占用两个 BAR 槽位。如果你的卡上有多个功能需要分开窗口槽位就变紧张了。第三prefetchable 属性的选择。普通寄存器和有副作用的状态寄存器绝对不能标成 prefetchable否则主机可能提前读、批量读读出你没预料到的行为——你以为是读一次实际是读了四次。使用场景建议 BAR 配置需要注意的点控制寄存器、状态寄存器BAR032 位或 64 位内存型非 prefetchable读操作可能有副作用禁止预取大块共享缓存 / 片上存储单独一段 BAR大小按实际需要地址要和 AXI 侧窗口对齐到 BAR 粒度DMA 描述符区可以合进上面的缓存 BAR描述符和数据的可见性顺序要处理需要中断上报不影响 BAR但 MSI 使能寄存器必须能配见 2.32.2 双向地址翻译入站和出站各有一套这是配置时最容易出错的环节也是读回来全是 FF的最常见原因。核心概念是AXI 侧的地址空间和 PCIe 侧的地址空间是两个独立的世界中间必须做窗口映射。入站方向主机访问 BAR 时这个核需要知道这个 BAR 对应的 AXI 地址基址是多少。这个映射关系通常由 IP 配置界面上的翻译基址参数、或者后续写入的配置寄存器共同决定。地址的实际构成是AXI 访问地址 翻译基址 BAR 内偏移。举个例子主机读 BAR0 偏移 0x100而你配置的 AXIBAR 基址是 0x0000_0000那么你的用户逻辑会在 S_AXI 上看到地址 0x100 的读请求。如果你配置成 0x8000_0000就会看到 0x8000_0100。历史版本里这个映射是通过一组 PCIEBAR2AXIBAR 寄存器实现的名字里的 BAR 有 2 的幂次含义比如 PCIEBAR2AXIBAR_0 对应某个 BAR 的窗口你在例化之后、开启总线主控之前要先把这些寄存器写对。新一些的版本会把一部分翻译前移到 IP 配置界面一部分交给互联工具的地址分配。不同工具版本的差异挺大动手前我的习惯是先翻一遍手上版本对应的那份 IP 手册把翻译发生在哪里确认清楚别凭记忆配置。出站方向同理。用户逻辑在 M_AXI 上发起的读写用的是 AXI 侧地址核需要把它转换成 PCIe 系统地址主机才能正确响应。这套映射同样有对应的窗口配置寄存器。这里有个很实际的建议AXI 侧地址不要在互联工具里自动分配让它飘。手动把每一段窗口的基址和大小固定下来写进工程里的约束脚本这样软件侧、硬件侧、仿真环境里三处地址才不会打架。2.3 中断与 MSI 的配置顺序轮询是可以工作的但延迟和 CPU 占用都不好看正经项目都会用中断。中断这块的坑在于顺序MSI 或者 MSI-X 的地址和数据由主机在枚举阶段写进配置空间硬件侧必须等这一步完成之后才能发起中断否则你发出的中断描述符是无效的。常见的做法是主机驱动在初始化时读配置空间、写 MSI 使能、写 MSI 地址和数据寄存器然后通知 FPGA 侧可以开始了。硬件侧用一个寄存器标记来同步这件事。如果省略这一步现象就是中断一个都收不到或者偶发收到一次之后再也不来。另外用 AXI4-Stream 形式发起 MSI 的接口握手信号的处理和普通 AXI-Stream 不一样——它没有 ready 反馈给用户侧所以不能靠 backpressure 来限流得你自己保证不连续超发。3. 从 IP 例化到主机第一次读到正确数据3.1 配置界面里几个一改就翻车的选项打开 IP 配置界面选项很多但真正会把人卡住的就那么几个。第一是数据位宽。核的 PCIe 侧数据通路位宽64/128/256 位和 AXI 侧接口位宽需要匹配你的带宽需求与时钟规划这个在 2.2 节算过。改位宽不是点一下就行的事它会影响互联的位宽转换逻辑、地址对齐要求以及整个数据通路的时序收敛难度。第二是最大 Payload 和最大读请求大小的上限。这两个值受主机侧能力限制枚举时主机会读你的配置空间两边取小值。你在 IP 里配得比主机能力大是没用的反而可能造成行为不一致。第三是时钟模式。核的 AXI 侧时钟和 PCIe 数据通路时钟可以是同源也可以是异步的。同源简单但很多时候用户逻辑需要跑在一个独立的时钟域里比如你的数据采集逻辑被 ADC 时钟驱动那就必须走异步模式并且要正确设置跨时钟域 FIFO 的深度。这一点在第五章展开。第四是 BAR 的使能组合。你开了几个 BAR、每个 BAR 的大小和类型都会直接反映到配置空间的实现上。这里改了软件侧所有偏移量都要跟着改。3.2 地址分配与互联别让自动分配牵着走Block Design 里把这颗核和你的用户逻辑连起来时中间通常要挂一层 AXI 互联SmartConnect 或者 Interconnect。互联做的事情是位宽转换、时钟域转换、多主多从的仲裁和地址解码。我的习惯做法是关掉自动地址分配手工指定每一段的地址范围。原因很实际自动分配出来的地址经常是紧挨着的看起来没问题但当你后来想在中间插一段新窗口时所有下游地址都会平移软件里硬编码的偏移全部失效。手工分配好之后用 Tcl 脚本固化下来# 手工指定关键地址窗口避免重新生成 BD 后地址漂移 assign_bd_address -target_address_space /axi_interconnect_0/M00_AXI \ [get_bd_addr_segs {pcie_bridge/axi_slave/SEG_axi_slave}] \ -offset 0x00000000 -range 4K assign_bd_address -target_address_space /axi_interconnect_0/M01_AXI \ [get_bd_addr_segs {shared_buffer/axi_slave/SEG_axi_slave}] \ -offset 0x00100000 -range 1M # 重新生成后先校验一遍确认没有漂移 report_bd_address -quiet另外互联的仲裁模式要留意。默认是轮询或者固定优先级的组合如果你的系统里同时有 DMA 出站流量和主机入站流量仲裁配置不当会让其中一路被饿死。这个现象在带宽测试时特别明显单跑一路好好的两路一起跑就有一路的吞吐掉到接近零。3.3 主机侧枚举、BAR 使能、第一次读写硬件链路通了不等于软件能看到设备。主机启动后配置空间被扫描你的卡按照 VID/DID 被识别然后主机给 BAR 分配地址。注意一个关键点分配地址和使能 BAR 是两件事。主机分配了地址之后写入 Command 寄存器的 Memory Space Enable 位BAR 才真正开始响应读写。如果你的驱动或者测试程序没有做这一步读回来就是全 FF。在 Linux 下做第一次验证最省事的路径是看配置空间和映射# 找到设备确认 BAR 分配情况 lspci -d 10ee: -vvv | grep -A2 Region 0 # 确认 Memory Space Enable 位已经置起Command 寄存器 bit1 setpci -s 01:00.0 COMMAND # 如果输出是 0400 之类的说明内存空间访问没开 setpci -s 01:00.0 COMMAND0406 # 映射 BAR0 读一读 # 注意/dev/mem 方式需要 root且有内核配置限制 # 正式项目建议写一个最小的字符设备驱动裸机环境下更直接初始化 PCIe 枚举逻辑之后直接按 BAR 基址加偏移去访问就行但同样要记得使能 Memory Space。3.4 用 ILA 验证第一笔交易第一笔数据的正确性我从来不靠软件侧打印来判断。做法是在 S_AXI 上抓一个 ILA触发条件设成 AWVALID 或者 ARVALID 的上升沿抓一次完整交易看三个东西——地址对不对、突发长度对不对、数据对不对。这一步能一次性区分三类问题ILA 没抓到任何交易说明请求根本没到 AXI 侧问题在 PCIe 链路或 BAR 配置抓到了但地址不对问题在地址翻译地址对了但数据不对问题在用户逻辑的 Slave 实现上。这个三分法我用了很多年比盯着软件输出猜要快得多。4. 仿真环境搭建AXI VIP 与 PCIe 模型配合时最容易卡住的地方4.1 transaction 打印刷屏怎么收拾干净用 Synopsys 的 AXI VIP 搭环境几乎每个人都会经历一次仿真日志几百 MB的阶段。默认配置下VIP 会把每一笔 transaction 的每一个字段都打印出来如果环境里有多个 port、跑几万笔交易日志很快就爆炸仿真速度也掉得厉害。处理思路分三层从外到内第一层是全局的 UVM report 等级。把不关心模块的冗长信息压掉// 全局压低UPF_* 、VIP 的部分打印会一起被压掉 uvm_top.set_report_verbosity_level_hier(UVM_LOW); // 但对你真正在调的那个组件单独放开 uvm_config_db#(int)::set(null, uvm_test_top.env.axi_slave_agent*, recording_detail, UVM_FULL);注意这一层治不了 VIP 自己那套独立于 UVM report 机制的打印。VIP 的 transaction 打印通常由它自己的 configuration 对象控制比如 AXI 的 port configuration 里会有 verbosity 相关的设置项把它设到 NONE 或者 ERROR 级别同时 transaction recording 也要按需关闭只在你真的要看波形里的 transaction 时才打开并且优先用 FSDB 里的事务视图而不是日志打印。具体类名和枚举名在不同版本里改过动手时以你本地 VIP 安装目录下那组svt_axi_*头文件为准别照着旧帖子的类名硬写编译不过还容易怀疑人生。第二层是范围控制。我用得最多的招数是按地址过滤只在访问某几个关键地址窗口时才打开详细打印其他一律静音。AXI VIP 一般支持地址范围过滤能力配合 callback 在 transaction 开始的时候判断 target 地址命中才放开。这个比全局开关精准得多。第三层是加编译期开关。用宏包住详细的打印和 check默认关掉需要深挖时重新编译打开。这样日常回归跑得快出问题的时候也不用手忙脚乱改配置。关打印之前先想清楚一件事VIP 刷屏本身往往是个信号。背压场景下VIP 会反复打印 stall 相关的信息超时场景下会打印 timeout。如果你一上来就把所有打印关掉可能把一个真实的协议违规也一起关了。4.2 valid/ready 背压造成的仿真假死怎么定位AXI 的握手规则很朴素VALID 由发起方拉起拉起来之后必须保持直到 READY 到来READY 由接收方拉起可以提前于 VALID。规则简单但违反实现的代码五花八门尤其是在用户自己写的 Slave 上。仿真里最常见的假死是这样主机侧发起读请求ARVALID 拉起来你的 Slave 因为某个内部状态没准备好一直不拉 ARREADY同时你的 Slave 又在等一个永远不会来的信号比如某个握手完成标志。两边互相等波形上看就是所有信号静止仿真时间一路涨却没有事务完成。排查手法我一般按顺序来先看哪个通道的 VALID 拉起来了但 READY 一直不动锁定通道再顺着这个通道往上游看检查发起方是否违反了VALID 拉起后不能撤的规则——这条规则在仿真里经常被忽略因为撤掉了也可能碰巧能跑通上板就直接挂最后检查你自己的状态机有没有等一个握手之后再进下一步但握手条件依赖下一步这种循环依赖。一个非常有效的助推手段是在环境里挂一个看门狗每笔事务发起时打一个时间戳超过阈值还没收到响应就报错退出而不是让仿真无限跑下去。AXI VIP 本身通常有 transaction timeout 之类的配置把它打开比人工盯波形靠谱。4.3 仿真通过、上板不通过根因通常在这三处第一处是复位。仿真里你可以在任意时刻释放复位上板时复位序列有严格的先后关系尤其是 PCIe 链路训练相关的部分。仿真里没模拟完整的上电时序很多问题就藏起来了。第二处是时序约束导致的路径延迟。仿真没有布线延迟跨时钟域路径如果约束写错比如漏了 set_false_path 或者 set_clock_groups实现工具可能会在物理上把两个异步时钟域的路径当成同步路径去优化结果是实现后时序报告的路径和你想的不是一条。第三处是主机行为和仿真模型的差异。仿真里的 PCIe 模型是理想化的不会乱序回 Completion、不会突然降速、不会有各种奇怪的配置访问。真实主机在枚举阶段会做大量你以为不会发生的访问比如读一个你没实现的配置寄存器、访问一个刚好在你 BAR 边界外的地址。这些在仿真里都不会出现。5. 时钟、复位与约束把链路跑稳的工程细节5.1 跨时钟域边界在哪异步 FIFO 深度怎么估这颗核内部把时钟域分成了几块PCIe 数据通路的时钟、AXI 侧接口时钟、配置接口时钟。当你选择异步模式时数据通路上就会出现真实的跨时钟域边界核内部用异步 FIFO 把两侧隔开。FIFO 深度不够会怎样背压会从链路侧传到 AXI 侧然后再传回链路侧形成一个反复停起的循环吞吐直接掉一半。深度给太多也不行占资源、加延迟。估算方法是先算出两侧的瞬时速率差。假设 PCIe 侧在某一刻可以连续吃进数据的速率是 R1AXI 侧能持续吐出的速率是 R2两者差值乘以链路侧最坏情况下的连续突发时间就是需要的缓冲量。举个实际的数Gen3 x8 的链路理论峰值单向接近 7.9 GB/s实际有效载荷算下来 7 GB/s 左右你的 AXI 侧是 256 位宽、250 MHz峰值 8 GB/s但实际有效率大概八成也就是 6.4 GB/s。这个组合下 AXI 侧就是瓶颈异步 FIFO 的深度不再是关键真正要解决的是 AXI 侧的有效带宽。反过来如果你的 AXI 侧是 128 位 200 MHz峰值 3.2 GB/s去喂 Gen3 x8那 FIFO 深度和中间那块缓存的调度策略就变得极其重要。持久的经验先用第二章的算账方法确认瓶颈在哪一侧再决定要不要花时间调 FIFO 深度。绝大多数带宽上不去的案例瓶颈根本不在 FIFO。5.2 GT 复位与 PERST 序列link up 之前别发 M_AXIPCIe 的物理层复位和协议层复位是两套东西加上热复位、功能层复位一共好几条复位路径。如果用的是高速收发器GT收发器的复位序列有严格的先后要求参考时钟稳定之后才能释放 GT 的复位GT 复位完成之后链路训练才可能成功训练完成链路状态机进入 L0之后配置空间才可以被访问。我踩过的坑是用户逻辑在链路还没训练完的时候就去发 M_AXI 请求。这时核内部的数据通路还在复位状态请求要么被丢弃要么被卡在 FIFO 里永远不出来表现出来就是主机侧 DMA 缓冲区永远拿不到数据。正确做法是在用户逻辑里等一个明确的链路就绪 配置完成标志这个标志可以从配置接口读链路状态寄存器得到等它进入工作状态之后再放行用户逻辑。另外PERST 信号的处理也要注意。它在主机侧是异步的进 FPGA 之后必须做同步处理而且它的释放要满足最小宽度要求不能一抖就放。这块如果处理粗糙现象是冷启动能通热重启之后就再也通不了。5.3 约束文件里最常漏的几类约束这块漏一条可能就能让你调一整天。我总结最容易漏的是这几类参考时钟约束。PCIe 的参考时钟通常是 100 MHz 的差分时钟必须用 create_clock 正确声明并放到合适的时钟组里。漏了这条工具会给你一个默认的估算频率实现报出来的时序全是假的。跨时钟域约束。不同时钟域之间要么用 set_clock_groups -asynchronous要么用 set_max_delay -datapath_only 去约束异步路径的延迟。前者适用于两个完全独立的时钟后者适用于有相位关系但需要限制延迟的场景。两者选错要么时序过不去要么埋下亚稳态隐患。GT 的物理位置约束。收发器通道的位置是固定的必须在 XDC 里用 LOC 约束钉死否则工具可能把通道分配到与板卡走线不对应的位置上结果就是链路训练完全失败。输入输出延迟约束。如果你用的是外部时钟驱动用户逻辑接口输入输出延迟的约束直接决定第一级寄存器的时序余量这条漏了通常表现为功能正常但时序报告一片红。6. 带宽实测先算账再调参6.1 理论带宽怎么算先把账算清楚带宽这件事绝大多数人凭感觉调结果是在错的地方使劲。先把账算清楚。链路侧的理论峰值由编码方式和 lane 数决定链路速率单 lane 信号速率编码方式单 lane 有效带宽x4x8x16Gen12.5 GT/s8b/10b250 MB/s1.0 GB/s2.0 GB/s4.0 GB/sGen25.0 GT/s8b/10b500 MB/s2.0 GB/s4.0 GB/s8.0 GB/sGen38.0 GT/s128b/130b984 MB/s3.9 GB/s7.9 GB/s15.8 GB/sGen416.0 GT/s128b/130b1.97 GB/s7.9 GB/s15.8 GB/s31.5 GB/s这是编码之后的裸带宽还没扣掉 TLP 的开销。TLP 的开销分两层事务层头写请求用 3DW12 字节或 4DW16 字节链路层还有序列号和 LCRC一般 6 字节。以 Gen3 x8、最大 Payload 设成 256 字节、64 位地址为例每个写 TLP 的净载荷 256 字节开销是 16 加 6共 22 字节效率是 256 / 278约 92%。如果 Payload 只有 128 字节效率掉到 128 / 150约 85%。如果 Payload 只有 64 字节效率继续掉到 64 / 86约 74%。读方向的效率通常更差因为读响应会被拆成多个 Completion每个 Completion 都带一份头。在常见的以 64 字节为单位的响应拆分下效率会掉到八成以下。所以同样的链路写带宽通常能比读带宽跑得好看不少这不是你的逻辑写得不好是协议开销决定的。算完链路侧再算 AXI 侧位宽除以 8乘以时钟频率得到理论峰值再乘一个经验有效率考虑读写切换、地址相位占用、仲裁开销我一般按 75% 到 85% 估。两侧取小值就是你系统实际能到的大致上限。6.2 MPS、MRRS、outstanding 三个旋钮的调整顺序调参要按顺序来否则你改了半天不知道是哪个参数起作用。第一步先确认最大 Payload 大小两边一致。硬件侧配的上限和主机侧能力取小值如果两边不一致实际生效的是小值你可能一直在按大值估算带宽。确认之后把 Payload 尽量往大调这一步的收益最直接——从 128 到 256效率提升大概 7 个百分点从 64 到 256提升接近 20 个百分点。第二步看读请求的 outstanding 能力。读是有往返延迟的如果一次只发一个读请求等响应回来再发下一个那么实际带宽就等于单笔数据量除以往返延迟。这个数会低得让你怀疑人生。解决办法是并发发出多个未完成的读请求把延迟藏起来。核和互联通常支持多个未完成事务你要确认这个数量开够了同时用户逻辑能接收乱序返回的 Completion如果协议允许。第三步再回头看 FIFO 深度和仲裁策略。这一步的收益通常不如前两步明显只有当系统里有多路流量竞争时才变得关键。6.3 实测踩坑以为链路不行其实是 AXI 侧不行讲一个具体的。曾经有一版设计Gen3 x8 的链路手册上写理论有效载荷七点几 GB/s实测写带宽只有 2.3 GB/s。第一反应是链路训练降速了查链路状态发现协商在 Gen3 x8速率没问题又怀疑主机内存带宽不够换了几种缓冲区大小和 NUMA 节点没变化。后来在 M_AXI 上挂了 ILA 统计实际握手情况发现问题用户的 DMA 引擎每次只发长度为 16 的突发也就是 128 字节发完之后要等一个内部的写队列空标志才发下一笔而这个标志的产生逻辑里有一级多余的同步每次要等将近一百个时钟。也就是链路侧有大量时间在空转。改了之后突发长度提到 256去掉那个多余的等待带宽直接上到 6.8 GB/s。整个过程中链路侧一个参数都没动。这个故事的教训是带宽测试一定要在 AXI 侧也做统计看实际的握手效率。只看主机侧的数字你永远不知道瓶颈在哪一层。7. 调试实录三类典型故障的排查链路7.1 主机枚举不到设备这个现象说明问题还在链路层或者更下面。排查顺序我一般是这样的先确认物理层。参考时钟有没有、频率对不对、GT 通道的物理位置约束有没有写、收发器的电源和复位有没有满足时序要求。这些用示波器或者工具里的链路状态寄存器都能看出来。再看链路训练状态。核一般会暴露链路状态机的当前状态通过配置接口能读到。如果状态一直停在检测或者轮询阶段说明训练没开始或者开始后失败了。这时候重点查参考时钟和通道映射。如果链路已经到 L0但主机还是枚举不到那问题在配置空间的实现上。检查 VID/DID 是不是都设成了 0 或者 0xFFFF——0xFFFF 是主机读不到设备时的默认返回说明配置空间根本没响应。这时候排查配置接口的时钟和复位是否正常以及配置空间的实现是不是被优化掉了。7.2 枚举到了读回来全是 FF 或者全是 0区分这两种情况很重要它们的根因完全不同。全 FF 通常意味着地址没有落到任何有效空间上主机读到的是总线默认的悬空值。可能是 BAR 没使能、BAR 大小配置和实际解码逻辑不匹配、或者地址翻译配错了导致访问落到了 AXI 侧没人响应的区域。这时候先在 S_AXI 上抓 ILA有请求但没人响应问题在 AXI 侧的 Slave 实现连请求都没有问题在核内部的地址翻译或者 BAR 解码。全 0 的情况如果硬件里那块存储本来就是 0那就是正常的。但如果写进去之后读回来还是 0就是写通路的问题。重点查写响应的返回如果写响应一直不返回主机会认为写没完成如果写响应返回了但数据没落进去那问题在数据通路的位宽转换或者字节使能WSTRB的处理上。WSTRB 是个高频雷区。AXI 的写数据通道带字节使能位宽转换的时候这个信号必须跟着正确映射。我见过位宽从 64 转 32 时字节使能映射错位导致只写进去一半的数据现象就是寄存器有时对有时不对。7.3 跑几分钟挂死、偶发超时偶发问题最难查因为复现不稳定。我处理这类问题的套路是固定下来的第一步加计数器和看门狗。在关键通道上加超时计数一旦超过阈值就触发一个错误中断或者拉高一个标志让 ILA 抓现场。不要靠跑着看什么时候挂。第二步缩小复现条件。是只有在读的时候挂还是读写都有是只有大块传输才挂还是小事务也挂是长时间跑才挂还是上电几分钟内就挂把范围缩小到某一个维度往往答案就出来了。第三步怀疑背压和复位。我遇到的偶发挂死里占比最高的是背压路径上的死锁某个通道在特定组合下互相等仿真里因为时序不同没暴露。第二高的是复位处理不当比如某个状态机在功能复位时没被正确清零导致热复位之后状态错乱。第四步怀疑跨时钟域。亚稳态导致的问题极具随机性而且温度、电压、长时间运行都会影响。如果前面三步都没找到原因就回头检查每一个异步边界有没有被正确约束和同步眼里不要留任何这个应该没问题的路径。8. 关于 AXI 与 PCIe 的几个常见追问我一般怎么答面试或者技术交流里围绕这套东西的追问翻来覆去就那么几个。我把自己的回答思路记一下也算是给准备的人一个参考。第一类是关于地址空间。经常被问主机怎么看到 FPGA 里的存储器。答案链是这样的设备枚举时主机读配置空间的 BAR按 BAR 声明的空间大小分配一段系统地址写回 BAR 寄存器并置位 Command 的 Memory Space Enable之后主机对该地址段的访问被路由到 PCIe 链路上以 Memory Read/Write TLP 的形式送到设备设备侧由桥核解码 BAR、做地址翻译、转成 AXI 事务。这整条链路每一环都可能断所以调试才要分段排查。第二类是握手规则。VALID 和 READY 的依赖关系这个问题的关键不在顺序而在禁止组合回路VALID 不能依赖 READY 才能拉起否则两个模块可能互等。这是拿来区分会用 AXI和理解 AXI的经典问题。第三类是关于突发和边界。AXI 有 4KB 边界限制突发不能跨过 4KB 边界。这条规则在 PCIe 桥接场景里尤其重要因为 PCIe 的读请求和 Completion 也是按某种边界拆分的。如果用户逻辑在生成 AXI 突发时没做边界检查跨边界的突发会被下游拆开处理不好就丢数据或者返回错误响应。第四类是关于性能估算。这类问题看重的是你会不会先算账。我的回答习惯是先算链路侧理论带宽、扣掉编码和 TLP 开销再算 AXI 侧位宽乘频率乘有效率两侧取小然后指出瓶颈在哪一侧、需要先解决什么。能说清楚这个链条比背一个具体数字有说服力得多。最后说点我自己的体会。这颗核的功能很固定但把它用好的难点从来不在核本身而在它周围那一圈——地址怎么规划、复位怎么理顺、时序怎么约束、带宽瓶颈在哪一层。我做过几个项目之后养成一个习惯动手写代码之前先把地址空间画成一张表贴在旁边把每个 BAR 的大小、每个窗口的基址、AXI 侧的对应地址全部写清楚然后才去配 IP、连互联、写软件。这张表能省掉后面大量的来回折腾。还有一个习惯是每次跑通第一笔交易之后立刻把当时的配置、约束、测试程序完整备份一份因为后面加功能时一旦出问题这份已知能工作的现场就是最快的对照基线。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进