ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA PCIe板卡AXI Memory Mapped IP核配置与调优实战

FPGA PCIe板卡AXI Memory Mapped IP核配置与调优实战 板卡上跑数据采集或者算法加速的朋友多半都遇到过这种场面FPGA 里逻辑都仿真跑通了主机侧软件也写完了结果一插上机器主机读卡上的寄存器读回来全是 0xFFFFFFFF或者能读到数但只有几十 MB/s跟链路速率完全对不上。这类问题的根子十有八九不在你自己写的逻辑里而在 AXI Memory Mapped To PCI Express IP 核的配置、地址映射和握手约束上。我做过几块基于 FPGA 的采集卡和加速卡前前后后在这颗 IP 核上踩的坑比在其它任何一颗 IP 上都多。这篇文章就把这颗 IP 核从协议差异、参数选型、地址规划、工程搭建、仿真验证、上板排查到性能调优按我实际做项目的顺序完整拆一遍。内容会涉及 AXI 协议本身的握手规则、PCIe 的 TLP 与 BAR 机制、跨时钟域处理、MSI-X 中断、AXI VIP 仿真日志治理以及实测带宽怎么从几百 MB/s 拉到 1.5 GB/s 以上。适合已经能看懂 AXI 时序图、想把卡上资源和主机内存打通的人如果你第一次做 PCIe 板卡被 BAR、TLP、完成包绕得头晕这篇也能当一份可对照的施工图看。1. 先把这件事讲明白这颗 IP 核到底桥接了什么AXI Memory Mapped To PCI Express IP 核本质上是一台双向翻译机。它一侧挂着 AXI4 内存映射从机接口另一侧挂着 AXI4 内存映射主机接口中间夹着一套 PCIe 事务层逻辑。主机侧的 CPU 或者其它 PCIe 设备发过来的存储器读写 TLP会被翻译成 AXI 的读地址通道、读数据通道、写地址通道、写数据通道和写响应通道上的一组事务反过来FPGA 内部逻辑在 AXI 主机接口上发起的一次读或者写也会被打包成 PCIe 的存储器请求经链路送到主机内存或者别的设备上。理解这台翻译机关键是要承认它两侧说的根本不是同一种语言。AXI 是片内总线讲究的是地址、数据、响应三条线各自独立、逐拍握手、细颗粒度控制PCIe 是包交换网络讲究的是把请求打包、贴标签、按信用额度发送、靠完成包回传结果。这两套东西的差异不是快和慢的差别而是线性总线和分组交换网两种世界观。你在 AXI 侧看到的一个连续突发到了 PCIe 侧可能被拆成好几个 TLP你在 AXI 侧看到的一条写命令在 PCIe 侧根本不会得到数据已经落到内存里的确认。1.1 AXI 与 PCIe 的本质差异一张表说清我习惯先把这张表画出来贴在工作区墙上每次接新板卡都对照着看一遍比事后翻协议文档快得多。对比维度AXI4 内存映射PCIe 事务层传输模型五通道独立地址与数据分离包交换请求包与完成包分离地址语义片内线性地址本地释义全局地址空间先过 BAR 译码突发能力最多 256 拍不可跨 4KB 边界载荷受 MPS 限制不可跨 4KB 边界流控方式valid/ready 逐拍握手靠 stall 反压链路层基于 credit 的信用流控写响应B 通道显式返回写响应写是 posted链路收下即结束无完成包读响应R 通道最后一拍带 RLAST靠 tag 匹配完成包可能被拆成多段乱序能力同 ID 保序不同 ID 可乱序有若干排序规则完成包可乱序返回错误语义DECERR / SLVERR 两种从机错误UR / CA / 完成超时 / AER 上报这里最容易被忽略的是写是 posted这一条。AXI 的写事务有 B 通道你在 FPGA 侧看到 BVALID 拉高了会本能地认为数据已经到了。实际上 PCIe 的存储器写请求一旦被链路层接受桥就认为任务完成可以把 B 响应还给 AXI 侧了而这时数据可能还在路上、还在主机内存控制器的写缓冲里。这个提前完成的语义差异是所有写后立即读回校验失败问题的总根源。另外一条是 4KB 边界。AXI4 明确规定一个突发不能跨越 4KB 地址边界PCIe 也明确规定一个存储器请求不能跨越 4KB 边界。这两条规则看起来是巧合其实是桥内部能高效工作的基础AXI 的 128 位数据宽度下256 拍突发正好是 4096 字节一拍不多一拍不少刚好一页。所以我在配置时会把 AXI 突发长度和 PCIe 页边界对齐起来用而不是随手设一个 16 拍。1.2 桥的四条通路各自管什么这颗 IP 除了大家最关注的两条数据通路其实还有两条容易被忽略的通路少配一条就会卡住。第一条是 AXI 从机通路也就是主机访问卡内资源的那条路。主机上电后BIOS 或操作系统会给每个 BAR 分配一段 PCIe 地址空间之后 CPU 对这段地址的读写会被桥翻译成 AXI 侧的读写事务。你卡上的控制寄存器、状态寄存器、门铃寄存器、BRAM 窗口、DDR 窗口都是通过这条路被主机访问的。这条路的关键是地址译码哪些地址落到哪个 BARBAR 里的偏移怎么映射到片内 AXI 地址全靠你配置。第二条是 AXI 主机通路也就是卡内逻辑主动访问主机内存的那条路。想让 FPGA 把采集到的数据直接写进主机内存或者从主机内存读参数表就走这条路。它的地址是主机物理地址不能是虚拟地址通常由驱动把 DMA 缓冲区的物理地址写进卡上的一个寄存器FPGA 再去读这个寄存器发起事务。第三条是配置管理通路一般是 AXI4-Lite 接口用来读写 PCIe 配置空间的寄存器。很多新手会疑惑我明明写了 BAR 大小为什么主机看到的 BAR 是 0答案通常是配置空间里的 Memory Space Enable 位没开。主机枚举时先写 BAR 探测大小、再读回确认最后要写命令寄存器的第 1 位使能存储器访问BAR 才真正生效。如果你在 FPGA 侧做自测就得通过这条配置通路把命令寄存器打开。第四条是中断通路。传统 INTx 是靠拉电平共享、慢、驱动里还得做中断共享处理MSI 是写一个特定地址、带一个特定数据本质上是一次存储器写可靠得多MSI-X 支持多个向量每个向量有自己的地址和数据适合多队列并发。我现在的项目基本默认上 MSI-X哪怕只用一两个向量也比 INTx 省心。1.3 什么时候该用它什么时候该换方案这颗 IP 最适合的是主机要随机访问卡内资源加卡内逻辑要主动搬中等规模数据的组合场景。比如主机要频繁读卡上几十个寄存器做状态轮询同时 FPGA 要把每帧几 KB 到几 MB 的数据写进主机内存这种交互模式下它非常顺手地址空间清晰软件侧直接用 mmap 或者 ioremap 就能访问不用专门的驱动。但如果你面对的是持续几 GB/s 的单向数据流比如高速采样前端直接把数据灌进主机内存那这颗 IP 就不是最优解了。它的每个读事务都要走请求、完成包、tag 匹配这一整套协议开销在高吞吐场景下会吃掉不少带宽而且它是内存映射语义每个事务都要带地址、要等响应天生比流式接口重。这种情况我一般会换用带 AXI Stream 接口的 DMA 类方案让数据以流的形式直接搬运控制通路再单独走一条轻量的寄存器通道。还有一种情况是纯粹要一个控制通道数据量很小。这时候用 AXI GPIO 配几个寄存器接到同一套 AXI 互连上再通过这颗桥的 BAR 暴露给主机比专门设计一套寄存器逻辑省事得多。AXI GPIO 的好处是接口简单、文档齐全、可以直接在中途挂 ILA 观察缺点是位宽有限、没有握手能力只适合单向的状态和控制位。2. 设计前的关键决策参数选型与地址规划很多项目后期出现的带宽不达标和地址错乱其实在选型阶段就埋下了。这一节讲的是动手画第一行 RTL 之前必须敲定的三件事带宽闭环算不算得平、地址空间怎么分、时钟复位中断怎么走。2.1 带宽闭环计算别让 AXI 侧先成为瓶颈我见过太多次链路是 Gen2 x4实测只有 500 MB/s以为 IP 有问题的情况最后查出来是 AXI 侧位宽配成了 64 位、时钟只有 125 MHz理论峰值正好 1 GB/s再打个对折就到 500 MB/s 了。所以选型第一步一定是把两端的理论带宽算清楚让它们量级匹配。PCIe 侧的有效带宽算法是线速率乘通道数再乘编码效率再乘 TLP 协议效率。Gen2 是 5 GT/s 每通道用 8b/10b 编码编码效率 80%Gen3 是 8 GT/s 每通道用 128b/130b 编码编码效率约 98.5%。TLP 协议效率取决于最大载荷大小128 字节载荷时一个 TLP 除了载荷还要带 12 到 16 字节的头、4 字节的序列号、4 字节的链路 CRC有效载荷率大约在 84%加上链路层的确认包和流控更新包实测能到 70% 到 80% 就算不错了。具体算几个常见配置Gen2 x4 原始速率 20 Gb/s去掉编码开销剩 16 Gb/s也就是单向 2 GB/s再乘 0.75 左右的实际效率能跑到 1.5 GB/s 就说明设计和主机侧都做得不错了。Gen3 x8 原始速率 64 Gb/s去掉编码剩 63 Gb/s 约 7.9 GB/s实测能到 6 GB/s 上下。AXI 侧的理论带宽是位宽乘时钟频率64 位配 125 MHz 等于 8 Gb/s也就是 1 GB/s128 位配 125 MHz 等于 2 GB/s128 位配 250 MHz 等于 4 GB/s256 位配 250 MHz 等于 8 GB/s。把这两组数字摆在一起结论就很清楚了Gen2 x4 必须配 128 位 AXI 才不浪费链路配 64 位就是自己给自己限速Gen3 x8 至少要 256 位 AXI、时钟往 250 MHz 走否则链路再快也没数据可发。注意AXI 侧的实际可用带宽还要乘一个效率系数因为读写切换、地址拍、响应拍、以及片内互连的仲裁都会吃掉周期。连续大突发的效率能到 90% 以上但如果是大量单拍随机访问效率可能掉到 30% 以下。带宽估算时按 70% 折算比较稳妥。2.2 BAR 规划与地址映射表BAR 这个概念用一个生活类比就通了主机眼里你的板卡就是一排信箱。BAR 就是这排信箱的起始编号和容量信箱内部怎么分格子是你自己的事但编号和容量必须提前向主机申报主机才能在地址空间里给你划地盘。申报的容量必须是 2 的幂申报之后主机用写全 1 再读回的方式探测你实际需要多大读回来的低位 0 的个数就代表容量。地址映射的方向也要说清楚。主机发过来的 TLP 里带的是 PCIe 地址桥先拿这个地址去比对各个 BAR 的基址和容量落进哪个 BAR就减掉那个 BAR 的基址得到一个偏移再加上你在 IP 里配置的片内 AXI 基址最终送到 AXI 从机接口上。所以片内地址 片内基址 (PCIe 地址 − BAR 基址)。这个减法关系必须在你脑子里清清楚楚后期所有地址对不上的问题都是这个减法没算对。我给一个实际项目里的 BAR 划分表可以直接照着改BAR 编号类型与属性容量主机侧地址示例片内 AXI 起始地址用途BAR064 位存储器预取1 MB0x0000_0000_0000_00000x0000_0000控制寄存器、状态、门铃BAR264 位存储器预取16 MB0x0000_0000_0010_00000x0100_0000卡上 DDR 窗口BAR432 位存储器4 KB由主机分配0x0200_0000诊断与调试寄存器有几条经验必须写在这张表旁边。第一64 位 BAR 必须成对占用BAR0 和 BAR1 组一个 64 位 BAR所以下一个能用的编号是 BAR2不是 BAR1。第二MSI-X 的向量表和挂起位数组会占用一个 BAR 里的空间具体占用哪个、占多大IP 配置界面里会告诉你规划寄存器窗口时一定要把它让出来否则你的寄存器和向量表重叠中断会莫名其妙跑到别的地址上去。第三容量别申报过大1 MB 的寄存器窗口对绝大多数应用都绰绰有余申报 64 MB 只会让主机在地址空间里挖出一个大洞还可能与其它设备冲突。2.3 时钟、复位与中断方案时钟方面PCIe 参考时钟一般是 100 MHz差分输入必须从专用时钟引脚进来走 GT 的参考时钟管脚。IP 会输出一个 AXI 时钟Gen2 常见配置下是 125 MHz 或 250 MHz取决于通道数和数据位宽的组合。这个时钟就是整个 AXI 侧的时间基准你的用户逻辑要么直接跑在这个时钟上要么用异步 FIFO 跨过来绝对不能用组合逻辑或者同步器直接跨。这里顺带说一个同板多 IP 时常见的时钟规划问题。我遇到过 FFT IP 在某些吞吐配置下不允许输入小数频率时钟的情况比如想要 122.88 MHz 结果综合直接报错最后只能改 MMCM 的分频比走整数频率。PCIe 桥的 AXI 时钟是 IP 输出的没法改所以用户逻辑侧如果要跑别的频点必须提前把 MMCM 的输出频率算好确认能整除别等到综合报错才回头改架构。复位方面IP 会输出一个同步于 AXI 时钟的复位信号通常低有效。正确的上电顺序是参考时钟先稳定然后主机释放 PCIe 复位IP 内部完成链路训练接着 AXI 侧复位释放最后才是你的用户逻辑复位释放。我踩过的坑是用外部按键做一个全局复位按键一按AXI 侧复位拉低此时如果还有正在进行的读写事务握手信号被打断桥内部状态机就卡死了必须重新枚举链路才能恢复。所以用户逻辑的复位和 AXI 侧的复位要分开复位时先停止发起新事务等所有 outstanding 事务都返回了再复位。中断方案我现在的默认选择是 MSI-X。配置时要注意向量数量别贪多四到十六个足够覆盖多队列场景每个向量对应一个队列或者一类事件。中断产生的正确节奏是先写用户寄存器把状态和计数值更新好再写中断触发寄存器中断处理程序里先读状态寄存器判断事件来源清掉中断源再返回。千万别在 AXI 事务进行到一半的时候清中断否则会出现中断丢了但状态位还在的错位现象。3. 工程搭建实操从 IP 配置到综合参数确定之后就是动手环节。这一节我按实际搭建顺序讲配置每一栏为什么这么选逻辑侧怎么封装约束怎么写。3.1 IP 配置逐项说明与推荐值配置界面上的参数多但真正会影响后期能不能跑通、能跑多快的就那么十来项。我按重要性排一遍并给出我在实际项目里的惯用值。设备标识类参数厂商 ID、设备 ID、子系统 ID看着无所谓但驱动是拿它们做匹配的。我一般把厂商 ID 用一个固定值设备 ID 按板卡型号区分子系统 ID 留给不同版本的硬件。这些值一旦量产就不要改改了驱动就认不出设备装机的时候会很痛苦。通道数和最高链路速率这两项要跟硬件设计对齐板上实际布了几对差分线就配几通道配多了链路训练会失败配少了浪费硬件。最高速率建议配成硬件支持的上限协商过程会自动降到对端能接受的速度配低了就永远上不去。AXI 数据位宽按前面算的带宽闭环来定Gen2 x4 配 128 位Gen3 x8 配 256 位。地址位宽一定要开 64 位哪怕你的板卡插在 32 位系统上驱动程序也可能在 64 位地址空间里分配缓冲区。我早期有个项目图省事开了 32 位地址结果在服务器上跑起来读回来的全是垃圾数据因为主机分配的 DMA 缓冲区地址在 4 GB 以上高位被截掉了。最大载荷大小这一项配置值不能超过主机侧协商出来的值否则桥发出的 TLP 会因为超限被丢弃表现为完成超时。保守做法是配 256 或者 512让主机侧去协商。如果追求吞吐配 512 是个不错的平衡点128 字节的头开销摊薄到 512 字节载荷上是 3% 左右比 128 字节载荷时的 12% 好太多。未完成事务深度outstanding 深度决定了能同时挂多少个读请求在链路上。这个值配小了读延迟一高带宽就掉配大了桥内部缓冲面积增加时序也更容易紧张。Gen2 x4 的场景我一般配 16 到 32 之间实测 32 个 outstanding 配合 512 字节载荷能把链路跑到 1.4 GB/s 以上。MSI-X 向量数按队列数量配四到十六之间。中断类型选 MSI-X别用传统 INTx。提示配置改完之后一定要重新跑一遍综合和实现看时序报告里 AXI 时钟域的最差路径余量。我遇到过一次改大 outstanding 深度之后时序余量从 0.3 ns 掉到 −0.15 ns功能仿真完全正常上板跑一会儿就出错。参数变更和时序收敛必须一起看。3.2 用户逻辑侧的接口封装从机侧的逻辑我习惯封成地址译码 寄存器堆 窗口直通三段。地址译码把 AXI 地址的高位切出来判断落在哪个窗口寄存器堆处理读写注意读写使能和字节使能窗口直通把访问直接透传给 BRAM 或者 DDR 控制器。这里有一个特别容易翻车的点我必须单独拎出来讲从机接口必须能处理突发写。很多人做寄存器逻辑时只处理单拍看到 AWLEN 是 0 就以为万事大吉因为自己写测试激励时都是一拍一拍发的。但主机侧对 BAR 空间做一次 memcpyRoot Complex 会把这段内存写拆成若干个最大载荷大小的 TLP 发下来每个 TLP 到 AXI 侧就变成一个多拍突发。如果你的从机逻辑没处理 AWLEN 大于 0 的情况表现就是 memcpy 过去只有第一拍写进去了后面的全丢了而且不会报错。正确的做法是在从机侧用一个简单的写状态机AW 通道握手时锁存地址和突发长度W 通道每一拍写数据、地址自增写满长度后拉 BVALID。读侧同理AR 握手后按长度逐拍输出数据最后一拍拉 RLAST。长度计数器一定要用锁存值不要用 AXI 侧实时变化的信号。主机侧的逻辑封装我建议做成一个描述符 状态机的小引擎软件把源地址、目的地址、长度写进寄存器然后写门铃FPGA 侧状态机开始按突发长度切分逐个发出 AXI 读或者写事务完成后更新状态寄存器并触发中断。这样软件侧只需要下发一次命令不用介入每一笔事务效率高得多。一个简化的写命令状态机骨架大致是这样// 简化的 M_AXI 写发起状态机骨架仅示意流程 localparam S_IDLE 3d0, S_AW 3d1, S_W 3d2, S_B 3d3, S_NEXT 3d4, S_DONE 3d5; always (posedge axi_aclk) begin if (!axi_aresetn) begin state S_IDLE; awvalid_r 1b0; wvalid_r 1b0; beat_cnt d0; end else begin case (state) S_IDLE: begin if (doorbell) begin cur_addr desc_src_addr; left_len desc_len; state S_AW; end end S_AW: begin awvalid_r 1b1; // AXI 规则valid 拉高后不得撤销直到 ready 到来 if (awvalid_r m_axi_awready) begin awvalid_r 1b0; state S_W; end end S_W: begin wvalid_r 1b1; if (wvalid_r m_axi_wready) begin beat_cnt beat_cnt 1b1; if (beat_cnt burst_len - 1) begin wvalid_r 1b0; state S_B; end end end S_B: begin // 注意BVALID 只代表 TLP 已被链路接受不代表数据落到主机内存 if (m_axi_bvalid) begin state (left_len desc_len) ? S_NEXT : S_DONE; end end default: state S_IDLE; endcase end end这段骨架里最有价值的是那句注释。B 响应回来不代表数据到了内存如果你需要强一致的写后读必须在写完所有数据之后再补一次读事务做刷新确认或者让软件侧在读之前加一个屏障操作。3.3 约束、时序与跨时钟处理约束文件里必须有的几条参考时钟的 create_clockAXI 时钟的 create_generated_clock 或者直接由 IP 输出的时钟约束接管跨时钟域路径的 set_false_path 或者 set_clock_groups以及外部接口的 input/output delay。参考时钟和 AXI 时钟如果不是整数倍关系一定要用 set_clock_groups 声明异步别指望工具自己猜。时序收敛方面我要提醒的是别在 M_AXI 接口上放组合逻辑。这些信号扇出大、路径长在 250 MHz 下稍微加两层组合逻辑就可能负余量。我的做法是所有 M_AXI 输出信号都在发起侧打一拍寄存然后直连到 IP 端口输入信号也一样进来先寄存一级再做判断。这一条看似牺牲了一个周期延迟但换来的是干净的时序和可复现的收敛结果。跨时钟域的处理只有两种正确答案异步 FIFO或者双口 RAM 加握手机制。用两级触发器同步多比特信号是错误做法位与位之间的到达时间不一致会造成采样到中间态。数据宽度 128 位、频率差在几倍以内的场景用 IP 生成的异步 FIFO 最省事注意深度要能在最坏反压情况下不溢出一般给 32 到 64 深。IP 自带的一些调试观测信号链路训练状态、配置空间读写信号、内部错误标志建议在顶层引出来或者接进 ILA。上板初期这些东西比任何打印都有用链路起不来的时候看一眼状态机在哪一步卡住比盲猜快十倍。4. 仿真验证把桥跑通的第一道关仿真阶段的目的是在花掉几小时综合时间之前把所有协议级的问题揪出来。这一节讲环境怎么搭、日志怎么治、用例怎么设计、波形看哪里。4.1 验证环境搭建与 AXI VIP 日志治理验证环境有两种常见搭法。一种是直接用 IP 自带示例工程里的测试平台里面已经有主机侧的行为模型和 AXI 侧的激励源改改激励就能跑另一种是自建 UVM 环境AXI 侧用 Synopsys 的 VC AXI VIP 当激励源和监视器主机侧用简化的事务模型或者参考模型。前者上手快后者可控性强我一般是先跑通前者再往后者迁移。自建环境最大的痛点是日志。AXI VIP 默认会把每一条 transaction 的地址、长度、数据都打印出来仿真跑一个长突发压力用例日志文件能轻松涨到几十 GB磁盘写满不说仿真速度还会慢好几倍。我在 Synopsys AXI VIP 上压日志的套路是三层一起上。第一层是命令行 plusarg加上UVM_VERBOSITYUVM_NONE或者在仿真参数里写uvm_set_verbosity*,_ALL_,UVM_NONE,time,0。这一层的作用是把所有基于 verbosity 机制的 info 打印压掉VIP 里绝大多数 per-transaction 的打印都是 info 级别的压完就没了。这一层的好处是安全warning、error、fatal 这三个级别不受 verbosity 影响照常输出你不会因为压日志而漏掉真正的错误。第二层是在测试用例的 end_of_elaboration_phase 里针对 VIP 的各个 agent 单独调 verbosity把监视器压到 UVM_LOW只保留警告以上。这样做的意义是可以精细控制——比如只想看主机侧事务、不想看 AXI 侧的事务那就只压 AXI 侧的 agent保留主机侧的输出。第三层才是关键。有些 VIP 版本的 transaction 打印不走 uvm_info而是有自己的打印机机制开关藏在配置对象里。名字各版本不一样靠猜是猜不出来的最靠谱的办法是直接去 VIP 的安装目录里搜在源码根目录下执行grep -rn print --include*configuration* .或者grep -rn transaction_print .一般几分钟就能定位到那个配置位看清楚它是哪个配置类的成员在环境里把它置 0 就行。用这个方法我找到过好几个不同版本里的对应开关比翻手册快得多。还有一个立竿见影但经常被忽略的操作把仿真日志重定向到文件而不是打到终端。终端刷屏本身的开销极大一个每秒几万行的日志输出光终端刷新就能把仿真拖慢三到五倍。仿真命令后面加-l sim.log /dev/null之类的重定向速度立刻不一样。日志还是要留着的出了问题拿 grep 去捞错误比盯屏幕靠谱。注意压日志的时候不要一刀切把整个环境的 verbosity 都设成最低然后就不管了。我的习惯是保留 UVM_WARNING 以上并且专门保留 AXI 协议检查器protocol checker的输出。VIP 的协议检查器会报出握手违规、突发长度不匹配、4KB 边界跨越这类问题这些恰恰是仿真阶段最需要抓的东西压掉了等于白跑。4.2 关键用例设计仿真用例不能只写个读一个寄存器看看对不对就算完下面这几个场景必须覆盖每一个都对应过真实故障。第一个是单拍读写回环。往寄存器写一个随机值再读回来比对验证基本通路和地址译码。这个用例主要抓地址映射错误和字节序问题。第二个是跨 4KB 边界的长突发。比如从 BAR 基址加 0xFF0 的位置读 64 字节这个访问跨过了页边界。桥应该把它拆成两个请求返回的数据必须严格按地址顺序拼接。如果桥没拆而是一口气发出去仿真里主机会返回一个错误完成包上板后表现为数据错位。这个用例我每次必跑。第三个是背压场景。在 AXI 从机侧故意把 ready 延迟几十个周期才拉高观察桥的反应。这里要验证两点桥不能丢数据桥不能死锁。AXI 协议规定 valid 拉高后不能撤销ready 可以晚来所以延迟 ready 是合法的反压手段。如果桥内部的缓冲区管理有问题长时间反压之后可能会把请求丢掉或者卡住状态机。第四个是非对齐访问。地址不是数据位宽的整数倍比如 128 位数据宽度下访问地址末位是 0x4 的位置。这时字节使能信号必须正确处理只更新对应的那几个字节。非对齐访问是寄存器逻辑最常见的 bug 来源因为很多人写逻辑时只测对齐地址。第五个是压满 outstanding。连续发起几十个读请求不给间隙看桥是否在深度耗尽后正确反压以及标签回卷是否正常。这个用例能暴露内部标签管理的边界问题只在压力下才会出现。第六个是复位中断。在事务进行到一半时拉低 AXI 侧复位复位释放后重新发起事务验证桥能否恢复。这个用例上板时救过我一次因为硬件上确实存在复位按钮被误触的可能。4.3 波形上到底该看什么仿真波形不要漫无目的地扫看几个关键点就够了。先看写通道的握手顺序AWVALID 和 AWREADY 握手成功之后WVALID 才能拉高WVALID 和 WREADY 逐拍握手最后一拍 WLAST 拉高然后 BVALID 回来BRESP 应该是 OKAY。如果顺序反了说明桥内部实现有问题或者你的激励违反了 AXI 规则。再看读通道的 RLAST 是否与 ARLEN 匹配。ARLEN 是 0 表示单拍RLAST 应该在第一拍就拉高ARLEN 是 15 表示 16 拍RLAST 应该在第十六拍拉高。这个匹配关系对不上说明读数据通道的计数器有问题上板会表现为读回的数据块长度不对。第三看地址是否跨了 4KB 边界。把 ARADDR 和 ARLEN 换算出的结束地址算一下如果跨越了 4KB 页边界说明桥的拆分逻辑在仿真环境下没生效。正常的桥应该把一个跨页请求拆成两个背靠背的请求波形上表现为两组完整的地址和数据握手。第四看 outstanding 计数。如果仿真环境里有 VIP 的统计功能直接看它报的事务数和完成数是否一致没有的话就在波形里数 RLAST 的个数和发出去的 AR 个数比对能发现有没有丢完成。5. 常见问题与排查技巧实录上板之后的问题八成集中在枚举、数据正确性、性能和稳定性四类。这一节我把踩过的坑整理成速查表再逐类讲讲排查思路。5.1 枚举与链路类问题枚举类问题的现象很好认主机侧扫描设备看不到或者看到了但显示设备异常。排查顺序我固定成四步。现象可能原因定位方法处理方式完全扫不到设备参考时钟未接、频率偏、幅度不够示波器测参考时钟管脚看频率和幅度检查时钟源和匹配电路扫不到但时钟正常通道极性反了、通道映射错位查看链路训练状态寄存器交换通道极性或在 IP 里开启通道反转扫不到且复位未释放PCIe 复位信号未正确引出测量复位引脚电平确认复位信号在时钟稳定后释放设备可见但报错配置空间使能位未开通过配置通路读命令寄存器使能存储器空间访问位BAR 读回全 1BAR 未使能或地址译码未配读 BAR 寄存器探测结果补齐地址译码配置链路训练状态是最直接的线索。IP 一般会输出一个状态信号能看到它停在哪个训练阶段。如果停在检测阶段说明对端没检测到你的接收端重点查电气和时钟如果停在配置阶段说明链路宽度或速率协商没谈拢重点查通道数和速率配置。5.2 数据正确性类问题数据错是最难查的一类因为现象千奇百怪。我总结的排查顺序是先看地址再看字节序最后看字节使能。地址类问题最好查也最常犯。主机读 BAR 基址加 0x100 的寄存器读回来是别的寄存器的值说明片内基址和偏移的加法算错了。现场排查的方法是把片内 AXI 地址引到 ILA 上抓和主机侧读的地址做对比一眼就能看出来偏移了多少。我曾经在一个项目里发现片内基址配成了 0x2000_0000而逻辑里译码用的是 0x0000_0000差了整整一段改完就好。字节序问题在跨平台时会冒出来。FPGA 内部一般按字节序存取主机端如果是小端模式两边对 32 位字的拼装方式可能不一致。解决办法是在寄存器逻辑里显式做字节重排别指望工具自动处理。判断有没有这个问题写一个已知模式比如 0x11223344读回来如果是 0x44332211那就是字节序反了。字节使能问题通常和写操作有关。主机做一次非对齐的 32 位写AXI 侧的 WSTRB 只有部分位有效如果你的寄存器逻辑没用 WSTRB 而是每次写都整字更新就会把相邻的字节覆盖掉。这类 bug 在只做对齐访问的测试里看不出来一旦软件用结构体指针直接写寄存器就会出现。还有一个隐蔽的坑是写后立即读。前面说过 PCIe 的写是 posted写完立刻读可能读到旧值。软件侧的正确做法是在写之后读一次同一地址做刷新或者加一个完成确认机制。FPGA 侧如果要做内部自检也要注意这个顺序别自己把自己坑了。5.3 性能类问题带宽不达标的原因可以按影响程度排序最大载荷大小太小、突发长度太短、outstanding 深度不足、AXI 位宽或时钟不足、主机侧缓冲区跨页、CPU 直接做 PIO 访问。排第一的是 PIO 访问。很多人第一次做板卡主机侧代码直接对 BAR 空间做 memcpy 来测带宽测出来几十 MB/s 就以为卡有问题。实际上 CPU 对非缓存区的逐字访问每次都要等一次完整的 PCIe 往返延迟几百纳秒带宽能高才怪。正确做法是用 DMA把数据搬移交给 FPGA 侧发起的事务。排第二的是载荷和突发长度。MPS 配 128、AXI 突发配 16 拍协议开销能吃掉三成带宽。把 MPS 提到 512、突发长度按 128 位乘 256 拍对齐到 4KB带宽立刻上一个台阶。排第三的是 outstanding 深度。读操作的带宽受延迟和并行度共同限制单个读事务的往返延迟如果有 500 纳秒那么就算链路速率再高一个事务在飞的时候链路上也是空的。只有把 outstanding 深度提到延迟能覆盖的程度链路才能被填满。粗略估算链路速率 2 GB/s、往返延迟 500 纳秒需要 1 KB 的数据在飞按 512 字节载荷算就是 2 个事务起步考虑到各种开销和抖动配 16 到 32 个比较稳妥。5.4 稳定性与复位类问题跑一段时间出错、重启才能恢复的问题我遇到过几次原因基本都是复位和跨时钟域。一种是用户逻辑自己做了全局复位按键一按把正在进行的事务打断桥内部状态机卡在等待状态。解决办法是把用户逻辑的应用层复位和 AXI 接口层复位分开应用层可以随便复位接口层只在链路建立后统一释放一次。另一种是跨时钟域没做干净。用户逻辑跑在 200 MHzAXI 时钟 250 MHz两个时钟不是整数倍关系如果中间用了同步器传多比特数据偶尔会采到中间值表现为跑几十分钟出现一次数据错误。这类问题很难复现唯一的判断依据是看有没有用异步 FIFO。没有的话补上。还有一种是电源相关的。PCIe 链路的参考时钟抖动在长时间运行后累积误差或者供电纹波超标会导致链路降速甚至断开。这类问题仿真和短期测试都发现不了需要长时间跑压力测试同时监控链路状态寄存器有没有出现降速或者重训练事件。6. 性能调优把实测带宽推到理论值的七成以上功能通了之后下一个目标是把带宽做上去。这一节讲几个调优维度以及怎么定位真正的瓶颈在哪。6.1 outstanding 深度、载荷与突发长度的组合这三者的关系可以用一个类比说清楚载荷大小是每辆车的载货量突发长度是车队的编队长度outstanding 深度是同时在路上的车队数量。链路速率是公路的限速。想让公路跑满三者都得配够。实际调优时我会做一个二维扫描固定 AXI 突发长度为最大值128 位宽度下 256 拍正好 4KB然后扫描最大载荷大小 128、256、512、1024测每个配置下的实测带宽。一般能看到带宽随载荷增加明显上升到 512 之后增长放缓。然后固定最优载荷扫描 outstanding 深度 8、16、32、64看带宽什么时候饱和。饱和点就是性价比最高的配置再往上加只会增加面积和时序压力。需要注意的是最大载荷大小不能超过主机侧协商值。桥发出的 TLP 超过主机的接收能力会被丢弃表现为完成超时和重传增加带宽反而下降。所以调优之前先用配置通路把主机侧协商出来的载荷大小读出来以此为上限。6.2 多主设备共享 AXI 主机接口时的仲裁策略如果片内除了数据通路还有别的模块要访问主机内存就会出现多个主设备抢一个 AXI 主机接口的情况。这时候中间要放一个 AXI 互连或者仲裁器。仲裁器的选择有几个考虑。轮询仲裁公平但对高优先级事务不友好固定优先级简单但低优先级事务可能被饿死加权仲裁可以在两者之间取平衡。我的做法是给需要保带宽的流式数据通路配高权重给状态上报这类低频通路配低权重并且在仲裁器后面、桥前面加一个小的数据缓冲避免仲裁切换的开销直接暴露给桥。还有一个容易忽略的点AXI 互连本身会引入额外的延迟和面积。如果片内只有一个主设备在用这条通路直接连到桥的 AXI 主机接口别为了以后可能要扩展而预先塞一个互连进去。互连的仲裁延迟在低 outstanding 深度的配置下会明显拉低带宽等到真需要扩展的时候再加也不迟。如果片内还有 AXI GPIO 这类轻量从设备注意它们的地址空间别和 BAR 窗口重叠。我一般把 AXI GPIO 放在片内地址空间的高段和给主机的窗口隔开至少 1 MB避免地址译码的边界情况。6.3 实测与瓶颈定位方法调优不能靠猜要有明确的瓶颈定位手段。我常用的方法有三层。第一层是统计计数。在 FPGA 侧加一组计数器统计发出的读事务数、写事务数、总字节数读回来和主机侧软件统计的数值比对。如果 FPGA 侧发了 1 GB 而主机侧只收到 800 MB说明中间有丢失或者重传问题在协议层。第二层是时间戳。在发起第一个事务和收到最后一个完成之间打时间戳算出实际吞吐。这个方法比软件侧计时准确因为它排除了软件的开销。第三层是链路利用率。如果 IP 有性能监测端口直接看链路利用率。利用率上不去说明发送端不给力利用率满了但带宽还是低说明协议开销太大要去调载荷和突发长度。这三层组合起来能快速区分是发不出去还是发出去效率低。我遇到过利用率只有 40% 的情况查下来是 AXI 侧的异步 FIFO 深度只有 8一遇到反压就断流把深度加到 64 之后利用率立刻上到 85%。6.4 与片内其它模块协同的几条经验最后说几条和别的模块协同时的经验这些坑都很像但换一个接口就会再踩一次。凡是涉及复位序列的接口都要严格按模块要求的顺序来。比如高速串行收发类的 IP一定要等收发器复位释放之后若干周期再把通道复位释放顺序反了链路就起不来。这个规律和 PCIe 桥的复位顺序是一样的都是底层先稳、上层后放。凡是涉及时钟的模块频率约束要提前算。有些信号处理类的 IP 对输入时钟有整数倍要求不能随便给一个小数频率规划板级时钟树的时候要一起考虑别等综合报错才回头改 MMCM 的分频比。凡是涉及片内存储的模块调用时要注意初始化文件的路径和延迟配置。ROM 类 IP 的初始化文件路径如果写成绝对路径换台机器综合就会失败延迟配成 1 拍还是 2 拍会影响上层状态机的等待周期数。这类细节在单独验证模块时都正常一集成到系统里就出错原因是模块间的等待周期没对齐。我自己在做这几类板卡的这些年里最深的体会是PCIe 这条链路上的问题九成都能在仿真阶段用合适的用例提前发现剩下那一成需要上板并借助观测信号才能定位。所以与其把时间花在上板后反复抓瞎不如在前端把 4KB 边界、非对齐访问、背压和复位中断这几个用例老老实实跑完把日志压干净让仿真跑得快一点多跑几轮随机激励。板子上省下来的那几天比什么都值。
返回列表