在FPGA上做PCIe DMA这件事,很多人的真实经历是这样的:板卡插到主机上,进系统一看设备管理器里没有未知设备,或者lspci根本刷不出你的Device ID;好不容易识别到了,驱动一加载,跑一次DMA回环,数据对不上,甚至直接把系统搞蓝屏。这种时候你很容易怀疑硬件焊接、怀疑金手指接触、怀疑电源纹波,然后在实验室里反复拔插板卡,浪费一整个下午。
后来我把调试顺序彻底反过来了——先用Vivado XDMA的仿真环境,把PCIe链路训练和DMA传输在波形上完整跑通,再上板。事实证明,绝大部分协议层、寄存器层、AXI接口层的问题,根本不需要硬件就可以提前暴露出来。这篇内容我就围绕Vivado里的XDMA IP,讲清楚仿真到底怎么搭、链路训练在波形上是如何一步步走到L0的、以及怎么在仿真里发起一次完整的DMA读写并验证数据。适合手里有XDMA项目、或者刚拿到PCIe板卡但一直识别不到的工程师参考。
1. 为什么不在板卡到手之后再调XDMA,而是先花时间做仿真
1.1 上板调试PCIe的痛点很现实
PCIe和普通的SPI、UART这类慢速接口不一样,它是一套完整的状态机协议,上电后链路训练、配置空间枚举、BAR分配、中断映射,每一步都发生在主机BIOS和操作系统里面。一旦链路训练没完成,板卡在主机看来就是“不存在的设备”,你连最基本的调试入口都没有。
更难受的是,PCIe是差分高速串行总线。你在实验室里想用示波器抓差分对,首先探头带宽要够,其次还要在正确的时刻触发。链路训练发生在复位释放后的几百微秒到几毫秒内,窗口很短,抓一次往往要靠运气。逻辑分析仪能抓协议层,但一台像样的PCIe协议分析仪价格不便宜,大多数团队根本不会为一次板卡调试去买。
1.2 仿真真正能帮你消灭哪一类问题
仿真不会帮你验证眼图、抖动、通道损耗这些物理层问题,但它能非常高效地验证协议层和逻辑层的问题。比如LTSSM是否按照Detect、Polling、Configuration的顺序推进,配置空间的BAR有没有被正确分配,XDMA的寄存器读写是否正常,DMA引擎是否生成了正确的AXI读写事务,这些在仿真波形里都可以看得清清楚楚。
换句话说,仿真把“板卡有没有被主机识别”“DMA数据为什么不对”这类问题,拆成了两个可以独立排查的层面。协议层的问题在仿真里先解决,剩下的物理层、信号完整性、驱动适配问题,再带上板去定位,范围一下就缩小了。
1.3 Xilinx已经给了你一套能“自问自答”的仿真环境
很多工程师做完XDMA IP之后,不知道仿真该怎么下手,总觉得要自己写一个PCIe Root Port模型,那确实很复杂。但实际上Vivado在生成XDMA IP时,自带了Example Design,里面已经例化好了Endpoint模型和Root Port模型,两者通过MGT差分信号连在一起,形成一个完整的PCIe链路。仿真Testbench里会模拟主机CPU去完成枚举、BAR分配、寄存器读写,你只需要打开仿真、跑起来、看波形,不需要从零搭测试平台。
所以我建议的路线是:第一步在Example Design基础上把XDMA仿真跑通,第二步修改用户侧逻辑,替换成你自己的AXI接口模块,第三步把链路宽度、速率调整成实际项目的配置,再做回归仿真。这样每一步的问题边界都很清楚。
2. XDMA仿真环境搭建:IP配置与Example Design初始化
2.1 仿真友好的IP配置参数
在Vivado IP Catalog里搜索XDMA,会找到名为“DMA/Bridge Subsystem for PCI Express”的IP。不同Vivado版本文档号不一样,老版本是PG195,新版本是PG346,但日常大家都还叫它XDMA IP。
创建IP时,几个关键参数虽然是给上板用的,但会直接影响仿真速度,我一般这样配:
| 配置项 | 仿真推荐值 | 上板正式值 | 原因 |
|---|---|---|---|
| PCIe链路宽度 | x1 | x4/x8 | 减少通道训练的状态复杂度,仿真更快 |
| PCIe链路速率 | Gen1 (2.5GT/s) | Gen3/Gen4 | 降低模型内部的时钟节拍,加速仿真 |
| AXI数据位宽 | 64-bit | 128-bit/256-bit | 位宽越低,仿真事件数越少 |
| AXI接口类型 | AXI Memory Mapped | 按需 | 最常用,仿真也直观 |
| 使能DMA通道 | H2C和C2H各1路 | 按需 | 通道越少,状态空间越小 |
这里有个常见误区:有人觉得仿真配置必须和上板完全一致,否则没有参考价值。实际上链路宽度和速率在仿真里只是协议流程的体现,x1 Gen1和x8 Gen3在LTSSM状态跳转逻辑上没有本质区别,只是通道协商细节和速率不同。先把流程跑通,再改回正式参数做回归,才是最高效的做法。
2.2 Example Design里到底帮你搭好了什么
生成XDMA IP之后,在Sources窗口右键IP核,选择Open IP Example Design,Vivado会生成一个独立的工程。打开这个工程,你会看到顶层结构里除了XDMA Endpoint,还有一个Root Port模型和一个testbench。
这套结构对仿真来说非常关键。Root Port模型负责模拟主板上的CPU复杂根节点,它会主动发起链路训练,并在链路进入L0后对Endpoint执行配置空间读写,完成BAR分配。Endpoint这边就是你的XDMA IP,它的用户侧接口会接一个AXI BRAM Controller,相当于模拟FPGA内部的用户逻辑。而testbench里还挂了一个模拟主机内存的BRAM模型,后续DMA传输时,RP模型会把PCIe TLP转换到对应的AXI接口上,让仿真里的“主机内存”可以被DMA引擎访问。
第一次打开这个工程时,我建议你花点时间把层次结构看一遍,搞清楚四个东西的位置:EP实例、RP实例、用户侧BRAM、主机侧内存模型。后面所有波形调试都要用到这些层次路径。
2.3 跑第一次仿真前必查的初始化信号
很多人第一次跑Example Design仿真,直接点了Run Simulation,然后等了几分钟,波形里user_lnk_up始终是0,链路一直没建起来。这时候最该查的是几个初始化信号。
第一个是sys_rst_n。PCIe仿真模型要求复位信号有一个完整的拉低、保持、释放过程,不能上来就一直是高电平。如果testbench里的复位拉低时间太短,或者根本没拉低,模型内部的初始化序列不会执行,链路就会卡在Detect状态。
第二个是sys_clk_p和sys_clk_n,这是给XDMA模型提供基准时钟的差分输入。要注意仿真模型的参考时钟频率和IP配置的参考时钟必须一致,常见是100MHz。
第三个是MGT差分对的连接。RP的TX要接到EP的RX,RP的RX要接到EP的TX,交叉连接不能错。如果连反了,链路训练也会失败。
把这些信号都检查一遍,再跑仿真,基本能在几百微秒的仿真时间内看到链路建立。
3. 仿真波形里观察LTSSM:从Detect到L0的完整链路训练过程
3.1 LTSSM是什么,为什么仿真要看它
LTSSM是PCIe物理层里的链路训练和状态机,全称Link Training and Status State Machine。它存在的意义,就是让链路两端的设备在上电后先完成一系列“握手”,确认对方存在、协商链路宽度和速率,最后进入L0状态,才能正常收发TLP数据包。
你可以把它理解成两个人打电话之前要先确认线路通不通:先拨号(Detect),验证对方有没有接听(Polling),然后确认用哪个号码、什么清晰度(Configuration),最后说“我们开始吧”(L0)。在仿真波形里看LTSSM,本质上就是在看这个握手过程是否正常走通。
3.2 在Vivado波形窗口里抓住哪个信号
在Example Design的仿真top层,找到pcie4_uscale_plus_ep实例,往下展开能找到ltssm_state信号。不同器件系列信号名可能略有差异,7系列可能是ltssmstate,UltraScale+系列通常是ltssm_state。这个信号一般是5到7位的总线,不同的数值对应不同的LTSSM状态。
我习惯在仿真开始前,就把这几个信号加入波形窗口:ltssm_state、user_lnk_up、axi_awvalid、axi_wvalid、axi_bvalid、axi_arvalid、axi_rvalid。这样链路状态和后续的DMA传输可以放到同一个时间轴上对照观察。
在Vivado的Waveform窗口里,选中ltssm_state信号,右键Radix改成Hexadecimal,然后对照IP参考手册里的LTSSM状态编码表,就能实时看出当前处于哪个状态。如果嫌一个个对编码麻烦,也可以在testbench里用$display输出状态名称,仿真跑完直接在Transcript窗口看状态跳转记录。
3.3 一次典型链路训练的波形解读
我跑过一次典型的UltraScale+ x1 Gen1仿真,链路建立的大致波形是这样的。复位释放后,ltssm_state先进入Detect.Quiet,这时候链路上没有任何信号活动,模型的接收检测电路在等待。很快状态跳到Detect.Active,这一步模型会通过在TX端发送一个小的脉冲检测接收端是否存在,也就是Receiver Detection。
检测通过之后进入Polling阶段,链路开始发送TS1和TS2序列。这是最关键的一步,发送端和接收端在Polling阶段完成位锁定和符号锁定。如果MGT差分连接正确、参考时钟正常,这个阶段会很快通过。从波形上看,ltssm_state会依次跳到Polling.Active、Polling.Configuration、Polling.Completion。
接下来是Configuration阶段,链路在这里做宽度协商和通道编号确认。x1配置下这个过程比较简单,状态会在Linkwidth和Lanenum之间走一遍,最终进入Configuration.Complete,然后跳转到L0。在ltssm_state进入L0的同时,user_lnk_up信号会拉高,表示从主机侧看,设备已经被识别为一个可用的PCIe链路。
整个链路训练过程在我的仿真环境里大约花了几十微秒的仿真时间。真实硬件上会更快,但仿真模型为了可控性,把部分时序做了拉伸,所以不要在仿真里期待几毫秒就走完,要有耐心。
4. 仿真中构造一次DMA传输:寄存器操作与AXI事务核对
4.1 XDMA的寄存器映射和BAR空间
链路进入L0只是第一步,真正要验证的是DMA数据通路。XDMA IP内部有一组控制寄存器,主机通过PCIe配置空间分配的BAR地址来访问这些寄存器。默认情况下,BAR0映射到XDMA的内部寄存器空间,BAR1映射到用户侧AXI接口,具体BAR分配可以在IP配置界面里调整。
寄存器映射方面,不同版本的XDMA有细微差异,一定要打开你当前版本的手册到寄存器章节核对一遍。下面这张表是我常用的H2C通道0的常见寄存器布局,作为参考足够。
| 偏移地址 | 寄存器名称 | 作用 |
|---|---|---|
| 0x0000 | DMA Control | 控制位:Run、中断使能等 |
| 0x0008 | DMA Status | 状态位:Busy、Done、Error |
| 0x0010 | H2C Source Address Low | 源地址低32位,Host侧内存地址 |
| 0x0014 | H2C Source Address High | 源地址高32位 |
| 0x0018 | H2C Destination Address Low | 目的地址低32位,FPGA侧BAR地址 |
| 0x001C | H2C Destination Address High | 目的地址高32位 |
| 0x0020 | H2C Length Low | 传输字节数低32位 |
| 0x0024 | H2C Length High | 传输字节数高32位 |
C2H通道的方向完全相反,寄存器偏移通常在0x1000开始。每次要发起DMA之前,我建议先把对应通道的Status寄存器读一遍,确认Busy位为0,确保上一个传输已经结束。
4.2 发起一次H2C DMA的完整操作序列
H2C方向,也就是Host to Card,数据从主机内存搬到FPGA用户侧。整个操作我在仿真Testbench里是这样做的。
第一步,等待user_lnk_up拉高,并且确认BAR配置完成。在Example Design的testbench里,这一步通常已经由仿真代码完成了,但如果你是自己在写Testbench,要等到RP模型发出配置写事务、把BAR Base Address寄存器写进去之后,才能访问寄存器。
第二步,在模拟主机内存的BRAM模型里填入测试数据,比如一个递增序列。这样DMA传输完成后,到用户侧BRAM里比对数据,能直观地看出字节有没有错位、有没有丢数据。
第三步,按顺序写寄存器。先后写Source Address、Destination Address、Length,最后写DMA Control触发Run位。为什么要最后一个写Control?因为寄存器写入是有时序的,如果你先触发Run位,DMA引擎可能已经在读取地址和长度的时候读到旧值,或者读到半更新的值,导致传输源地址或长度错误。
在SystemVerilog Testbench里,这段操作大致长这样:
// 等待链路建立 wait(user_lnk_up == 1'b1); // 检查通道忙状态 read_reg(32'h0000_0008, status_val); assert(!status_val[0]); // Busy 应为 0 // 写H2C DMA寄存器 write_reg(32'h0000_0010, host_mem_addr[31:0]); write_reg(32'h0000_0014, host_mem_addr[63:32]); write_reg(32'h0000_0018, fpga_bar_addr[31:0]); write_reg(32'h0000_001C, fpga_bar_addr[63:32]); write_reg(32'h0000_0020, transfer_len[31:0]); write_reg(32'h0000_0024, transfer_len[63:32]); // 触发Run位,启动DMA write_reg(32'h0000_0000, 32'h0000_0001);第四步,轮询Status寄存器,等Busy位清零,表示DMA传输完成。在仿真里也可以直接看中断相关信号的变化,XDMA在传输完成时会拉高对应的中断请求信号。
4.3 如何在AXI总线上确认传输真的完成了
寄存器轮询到Busy清零,只是拿到了DMA引擎的“我认为我完成了”,数据链路是不是真的对,还要到AXI接口上看实际事务。
对于H2C方向的DMA,FPGA侧的XDMA作为AXI Master,会通过M_AXI接口发起写操作,把数据写入用户侧BRAM。在波形上看,应该能看到一组典型的AXI写事务:AWVALID拉高,BRAM控制器回AWREADY,然后是WVALID和WREADY握手,每拍传输一拍数据,最后BVALID和BREADY握手表示整笔写事务完成。
这里有个小技巧:不要只看有没有握手,要数一下数据拍数。假设你设置的传输长度是1KB,AXI数据位宽是64bit,也就是8字节,那么一整个写事务应该包含128拍数据。如果拍数不对,说明Length寄存器配置或地址对齐有问题。
传输完成之后,在Testbench里从用户侧BRAM读回数据,和之前写入Host内存的递增序列做比对,一致就说明整条数据通路没问题了。这一步非常关键,很多人在仿真里看到AXI有握手就认为成功了,其实数据字节序可能因为WSTRB或地址偏移问题全乱了。
5. 排错实录:链路卡在Detect和DMA不完成的两类典型问题
5.1 链路一直卡在Detect状态,怎么查
这是XDMA仿真里最经典的问题,表现是跑了几百微秒,ltssm_state纹丝不动停在Detect,user_lnk_up始终不拉高。
我遇到过一次,最后定位到是复位释放的问题。testbench里sys_rst_n的释放条件是异步的,但PCIe仿真模型对接地、上拉的时序有要求,需要保证复位拉低时间足够长,并且在释放前这个模型内部的PLL已经锁定。如果释放太早,模型内部的初始状态没准备好,LTSSM就不会往下走。
排查这类问题时,我一般按三个步骤走:
- 先看
sys_clk_p/n是否正常跑起来,频率对不对。如果时钟没有,后面全部免谈。 - 再看
sys_rst_n的拉低时间,对比手册要求,至少保证复位有效时间覆盖几十个参考时钟周期。 - 最后看MGT差分信号对的连接和电平。在仿真里MGT被模型替代,连接关系虽然走内部信号,但如果顶层连接错误,模型会一直检测不到对端。
5.2 DMA发起后AXI握手正常,但用户侧数据全是X
另一个高频问题是:DMA寄存器配置了、AXI的握手也发生了,但读回用户侧BRAM,数据全是X不定态。
这种情况十有八九不是DMA引擎的问题,而是用户侧AXI Slave没有把数据真正写入BRAM。最常见的原因是替换Example Design的BRAM Controller、换上了你自己的用户逻辑之后,AXI写通道的WREADY和BVALID没有按规范拉高,或者WSTRB字节使能没有正确解析。
注意一个细节:AXI4写事务中,WSTRB和WDATA是配套的。如果BRAM Controller在接收数据时只把WDATA存进去了,却忽略了WSTRB,遇到非全字节使能的情况,BRAM里没有被使能的位置就不会被写入,返回来的数据里就会出现X或旧值残留。
解决方法是把仿真的用户侧AXI Slave接口严格对照AXI协议规范检查一遍,尤其关注:
AWREADY是否在AWVALID有效时及时拉高,不能无限等待WREADY是否和WVALID完成了逐拍握手BVALID是否在数据全部接收完成后拉高,而不是提前或延后- 读通道
RVALID和RLAST是否符合突发读的时序
5.3 仿真跑得太慢,如何大幅提速
XDMA的仿真在FPGA仿真里算比较重的,特别是链路训练阶段要跑几十微秒,XSim默认的记录所有信号配置下会很慢。我见过有人第一次跑XDMA仿真,挂了一晚上还在Detect,其实就是仿真性能问题。
提速的核心思路是减少XSim需要记录的信号事件量。在Vivado仿真设置里,把xsim.simulate.runtime设置为比如200us,同时在添加波形时,只添加核心观察信号,不要用Add All Signals in Design把所有信号全丢进去。XSim在记录波形时,信号越多、越深,性能下降越明显。
如果想进一步提速,可以在仿真Testbench里多用$display打印状态变化,而不用波形记录。比如LTSSM状态变化、DMA寄存器写入、AXI事务完成,都用$display打印到Transcript窗口。跑完之后看文本日志就够了,需要看时序细节再局部加波形信号。实测下来,把链路宽度配成x1、速率配成Gen1、关闭不需要的DMA通道、减少波形记录信号,仿真速度能提升好几倍。
6. 仿真结果与上板调试之间的差异,以及怎么衔接
6.1 仿真通过不等于上板一定通过,差异在哪里
这是所有做XDMA仿真的工程师都要清醒认识的一点。仿真模型里没有真实的物理层电路,没有传输线损耗,没有电源纹波,也没有CPU和操作系统驱动栈的复杂性。具体来说,有三个维度的差异最常导致上板翻车。
物理层和信号完整性是第一个差异。仿真里MGT的收发模型是理想化的,真实板卡上链路的插损、回损、串扰会影响误码率。链路训练能够完成,不代表在实际速率的长时间压力测试下不出错。对于Gen3以上速率,上板后最好用PCIe链路测试工具或驱动自带的误码检测跑一跑压力。
时钟域的差异是第二个。仿真里时钟都是对齐的,真实FPGA里AXI接口的跨时钟域、异步复位释放、时序收敛都是要命的问题。如果M_AXI接口和用户逻辑的时钟域交互处理不好,上板后会出现偶发性数据错误,这种问题在仿真里是复现不出来的。
系统软件栈的差异是第三个。仿真里的Root Port是模型,不会加载驱动,不会处理MSI中断,更不会涉及IOMMU的地址映射。上板后驱动分配的内存物理地址是否连续、页对齐,中断处理函数是否能及时响应,这些都是仿真覆盖不到的部分。
6.2 上板前建议补做的几件事
仿真在协议层把问题消灭了一大半,上板前我还会再补三个动作,让衔接更顺畅。
第一,把IP配置从仿真参数改回正式参数时,重新跑一次仿真回归。特别是链路宽度从x1改成x8、速率从Gen1改成Gen3后,Configuration阶段的通道协商逻辑会走完全不同的分支,寄存器配置、LTSSM时序都可能影响初始化流程。
第二,在代码里把user_lnk_up作为用户逻辑的全局复位释放条件。链路没建立之前,用户侧AXI逻辑不要处理任何DMA事务;链路建立之后,统一释放复位。这个习惯能避免很多上板时时序毛刺导致的异常。
第三,上板后在FPGA内部用ILA抓一次ltssm_state和user_lnk_up,和仿真波形对照。这样能快速确认真实硬件的链路状态机和仿真模型是否一致。如果真实硬件卡在某个状态,也有直观的信号做定位依据。
6.3 用仿真波形作为上板调试的参照物
我在实际调试中一个很深的体会是,仿真波形不只是开发阶段的验证工具,更是上板后的“标准答案”。比如驱动加载失败时,在ILA里看ltssm_state是不是稳定在L0;DMA回环数据不对时,对比ILA里的AXI事务波形和仿真波形,数一拍数据、看一个握手时序,很容易分辨是用户逻辑问题还是PCIe链路问题。
特别是user_lnk_up和ltssm_state这两组信号,在上板和仿真里几乎是一一对应的,你完全可以把仿真波形截图作为排查参考,一线一拍的对照。
我个人习惯在做XDMA项目时,把整个仿真工程和波形保存好,就算项目上线之后也会留着。一旦现场反馈数据异常,先跑一遍基线仿真,确认协议层没回归,再去怀疑硬件和驱动。这样做的好处是排查范围一次比一次窄,不会每次都在同样的问题上浪费一整天。仿真花掉的几个小时,最后都会在上板调试的顺利程度里拿回来。