☰
AXI PCIe桥核实战:BAR、地址翻译、DMA与带宽调优
2026/9/29 1:59:29 网站建设 项目流程

手上有块板卡,主机侧只想做一件事:像访问内存一样读写 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 的大小和类型是第一个不可逆的决定

BAR(Base Address Register)是主机眼里你的卡占用的那几段地址窗口。绝大多数项目只用到 BAR0,用一个 32 位或 64 位的内存型 BAR 就够了。几个必须想清楚的点:

第一,BAR 的大小。它决定了主机能直接看到多大的空间。如果你只是想暴露 4KB 的寄存器,配 4KB 就行;如果你想让主机直接 mmap 几 MB 的共享缓存,就必须开对应的大小。BAR 大小一旦确定,地址解码逻辑就要按这个宽度实现,改起来意味着地址空间重新规划。

第二,地址位宽选 64 位还是 32 位。现在的系统基本都是 64 位地址空间,选 64 位 BAR 更省心,代价是它占用两个 BAR 槽位。如果你的卡上有多个功能需要分开窗口,槽位就变紧张了。

第三,prefetchable 属性的选择。普通寄存器和有副作用的状态寄存器绝对不能标成 prefetchable,否则主机可能提前读、批量读,读出你没预料到的行为——你以为是读一次,实际是读了四次。

使用场景建议 BAR 配置需要注意的点
控制寄存器、状态寄存器BAR0,32 位或 64 位,内存型,非 prefetchable读操作可能有副作用,禁止预取
大块共享缓存 / 片上存储单独一段 BAR,大小按实际需要地址要和 AXI 侧窗口对齐到 BAR 粒度
DMA 描述符区可以合进上面的缓存 BAR描述符和数据的可见性顺序要处理
需要中断上报不影响 BAR,但 MSI 使能寄存器必须能配见 2.3

2.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 COMMAND=0406 # 映射 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 侧在某一刻可以连续吃进数据的速率是 R1,AXI 侧能持续吐出的速率是 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_AXI

PCIe 的物理层复位和协议层复位是两套东西,加上热复位、功能层复位,一共好几条复位路径。如果用的是高速收发器(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 有效带宽x4x8x16
Gen12.5 GT/s8b/10b250 MB/s1.0 GB/s2.0 GB/s4.0 GB/s
Gen25.0 GT/s8b/10b500 MB/s2.0 GB/s4.0 GB/s8.0 GB/s
Gen38.0 GT/s128b/130b984 MB/s3.9 GB/s7.9 GB/s15.8 GB/s
Gen416.0 GT/s128b/130b1.97 GB/s7.9 GB/s15.8 GB/s31.5 GB/s

这是编码之后的裸带宽,还没扣掉 TLP 的开销。TLP 的开销分两层:事务层头,写请求用 3DW(12 字节)或 4DW(16 字节);链路层还有序列号和 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、连互联、写软件。这张表能省掉后面大量的来回折腾。还有一个习惯是每次跑通第一笔交易之后,立刻把当时的配置、约束、测试程序完整备份一份,因为后面加功能时一旦出问题,这份"已知能工作"的现场就是最快的对照基线。

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

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

立即咨询