1. 项目概述从“硬调”到“软仿”的PCIE验证思路转变最近在做一个基于Xilinx FPGA的PCIE接口项目和很多同行一样在功能验证阶段遇到了不小的麻烦。PCIE这东西协议栈复杂物理层速率又高直接上板用ChipScope抓一次编译下载就得十几二十分钟效率低得让人抓狂。早期我也试过在Testbench里手动模拟IP核出来的并行总线时序光是搞懂TLP事务层包、DLLP数据链路层包的格式和排序规则就头大写出来的仿真模型自己心里都没底。直到后来深入研究了Xilinx官方提供的那套仿真环境才发现原来官方早就把“模拟主机”和“链路训练”这些最磨人的部分都做好了我们只需要在它搭建好的舞台上专注测试自己的用户逻辑就行。这套方法的核心就是用Modelsim在RTL级构建一个包含两个PCIE设备的虚拟系统一个作为Root PortRP模拟主机行为另一个作为EndpointEP即我们自己的设计让它们通过虚拟的差分链路“对话”。这不仅能大幅提升调试效率更能让你在代码烧进芯片前就对整个数据通路和状态机有透彻的理解。这篇文章我就结合最近一次在Virtex-6上折腾PCIE IP核仿真的实际经历把从环境搭建、用例编写到结果分析的完整流程和踩过的坑毫无保留地拆解一遍。2. 仿真环境搭建库文件与工程配置的魔鬼细节仿真环境搭建是第一步也是最容易出错的一步。很多“找不到库”或者“链接错误”的问题根源都出在这里。2.1 编译Xilinx仿真库版本匹配是关键Xilinx的IP核比如PCIE的核并不是纯Verilog/VHDL代码它内部调用了很多已编译好的底层原语Primitive和硬核模块这些内容都以预编译库的形式存在。因此在Modelsim里仿真前必须为你的ISE/Vivado版本和Modelsim版本编译对应的仿真库。我用的环境是ISE 13.2和Modelsim SE 10.1c。这里有个关键点ISE 13.2自带的PCIE IP核v6.6d或更高需要Modelsim 6.6d以上的版本支持。如果你用的是更老的Modelsim可能会遇到无法识别某些语法或功能的问题。编译库的路径在开始菜单 - Xilinx ISE Design Suite - ISE Design Tools - Accessories - Simulation Library Compilation Wizard。运行后你需要指定仿真工具选择ModelSim SE。编译库路径建议新建一个单独的、路径中无空格的文件夹例如D:\Xilinx_libs\13.2。这便于管理也避免一些因路径空格导致的诡异问题。语言根据你的设计语言选择Verilog或VHDL。我的设计是Verilog所以选Verilog。器件家族务必勾选你设计所用的所有器件系列。我的Virtex-6项目就需要勾选virtex6。如果设计里还用到了其他系列的IP比如时钟管理单元也要一并勾选。点击“Compile”后这个过程会比较耗时耐心等待完成。编译成功后会在你指定的目录下生成一系列文件夹如virtex6里面包含了modelsim.ini文件和编译好的.mdo等库文件。注意很多教程会忽略这一点——编译库的modelsim.ini文件不能直接替换你Modelsim安装目录下的那个。我们只需要从中提取关键信息。2.2 配置Modelsim的ini文件指向正确的库编译完成后需要让Modelsim知道这些库在哪里。找到你的Modelsim安装目录下的modelsim.ini文件通常是C:\modeltech_10.1c\modelsim.ini先备份然后用文本编辑器打开。你需要把刚才编译生成的库信息添加进去。找到文件中[Library]这个部分。编译库生成的modelsim.ini里会有类似下面的内容secureip $MODEL_TECH/../xilinx/secureip unisims_ver $MODEL_TECH/../xilinx/unisims_ver xilinxcorelib_ver $MODEL_TECH/../xilinx/xilinxcorelib_ver ...你需要将这些行合并到你自己的modelsim.ini文件的[Library]部分。关键操作是修改路径$MODEL_TECH/../xilinx/这个相对路径很可能不对。你应该将其修改为绝对路径指向你刚才编译库的文件夹。例如你编译到D:\Xilinx_libs\13.2那么就应该修改为secureip D:/Xilinx_libs/13.2/secureip unisims_ver D:/Xilinx_libs/13.2/unisims_ver xilinxcorelib_ver D:/Xilinx_libs/13.2/xilinxcorelib_ver simprims_ver D:/Xilinx_libs/13.2/simprims_ver unisim D:/Xilinx_libs/13.2/unisim xilinxcorelib D:/Xilinx_libs/13.2/xilinxcorelib ...注意Windows路径用反斜杠\但在INI文件里通常用正斜杠/也可以或者用双反斜杠\\转义。用正斜杠更保险。实操心得这里最容易出的错就是路径不对或者库名不匹配。modelsim.ini中左边的库名如unisims_ver必须和编译时生成的库名严格一致。一个快速检查的方法是去编译输出目录如D:\Xilinx_libs\13.2\unisims_ver里看是否存在一个同名的文件夹。这个文件夹名就是库名。2.3 导入并编译示例工程解决glbl.v的编译问题Xilinx Core Generator在生成PCIE IP核时会提供一个示例设计Example Design。这个设计是仿真入门的绝佳起点。我以Virtex-6 FPGA Gen2 x1的Endpoint模式为例。首先在ISE中打开IP核的示例工程目录。关键路径在你的项目目录\pcie_1x1_example\ipcore_dir\pcie\simulation\functional。这个文件夹里包含了仿真所需的所有文件特别是simulate_mti.doModelSim宏命令文件。启动Modelsim并切换工作目录打开Modelsim在Transcript窗口使用cd命令切换到上述functional文件夹。例如cd E:/loongson/PCIE/Xilinx_PCIe_V6/pcie_1x1_example/ipcore_dir/pcie/simulation/functional。编译glbl.v模块这是一个全局性的仿真模块包含全局复位GSR和全局三态控制GTS等对于Xilinx器件仿真至关重要。它的路径通常在ISE安装目录下如$XILINX\ISE_DS\ISE\verilog\src\glbl.v。你需要手动将它编译到work库。 在Modelsim的Transcript窗口输入vlog -work work C:/Xilinx/13.2/ISE_DS/ISE/verilog/src/glbl.v请替换为你的实际路径。修改do文件用文本编辑器打开simulate_mti.do。找到加载仿真顶层模块的那一行通常是vsim -t ps -L work -L ... glbl pcie_1x1_example_tb。你会发现glbl已经被包含在命令中了。但是因为我们刚才已经手动将glbl.v编译到了work库而do文件里可能试图从其他路径加载它这会导致冲突。一个稳妥的做法是从vsim命令的参数列表中移除glbl然后在我们手动编译后它已经被关联了。或者更简单的方法是注释掉do文件中编译glbl.v的那一行如果存在的话。例如找到类似vlog .../../glbl.v的行在前面加#注释掉。执行仿真在Modelsim中执行命令do simulate_mti.do。这个do文件会自动编译整个示例工程的所有源文件包括RP和EP的代码然后启动仿真。3. 仿真结构深度解析RP与EP如何共舞执行仿真后如果一切顺利你会看到仿真波形并且Transcript窗口打印出大量信息。先别急着看波形理解这个仿真环境的结构才能事半功倍。3.1 顶层架构Board、EP与RP整个仿真环境的顶层模块通常叫board在示例设计中可能叫pcie_1x1_example_tb。它主要实例化了两个核心部分Endpoint (EP)这就是你要测试的用户设计部分。在Xilinx的PIOProgrammed I/O示例中它是一个实现了简单读写操作的PCIE设备。在实际项目中你需要逐步用自己的逻辑替换掉这个PIO示例。Root Port (RP)这是Xilinx提供的测试模块用于模拟主机CPU/芯片组的PCIE端口行为。它藏在simulation/functional和dsport文件夹里。dsportDownstream Port这个名字就指明了它的身份。RP模块负责发起链路训练、生成配置读写Configuration Read/Write、内存读写Memory Read/Write等TLP事务。它们之间通过虚拟的差分信号线pci_exp_txp/n,pci_exp_rxp/n连接但这些信号在仿真中并不是真正的串行差分信号而是经过了行为级模型最终在逻辑上表现为并行总线接口如trn_td,trn_tsrc_rdy_n等方便我们观察。3.2 测试激励的产生pci_exp_usrapp_tx/rx.v这是整个仿真环境最精华的部分。RP模块的行为是由pci_exp_usrapp_tx.v和pci_exp_usrapp_rx.v这两个文件中的任务Task来驱动的。它们不是可综合的RTL而是纯验证用的SystemVerilog或Verilog行为级代码。pci_exp_usrapp_tx.v定义了各种产生TLP包的任务。例如TSK_SIMULATION_TIMEOUT设置仿真超时。TSK_BAR_INIT这是关键它模拟主机进行枚举发现EP设备并配置其Base Address RegisterBAR。只有执行了这个任务EP的BAR空间才被正确映射后续的内存读写操作才能找到目标地址。这就是为什么很多新手仿真时只有链路训练波形没有数据交易波形的根本原因。TSK_MEM_READ_32发起一个32位内存读请求。TSK_MEM_WRITE_32发起一个32位内存写请求。TSK_CFG_WRITE/READ发起配置空间的读写。pci_exp_usrapp_rx.v定义了处理来自EP的响应包Completion TLP的任务。测试用例的入口在tests.v文件中。simulate_mti.do在启动仿真时会通过vsim命令的-gTESTNAME参数指定运行哪个测试。在tests.v中有一个case语句根据TESTNAME选择执行不同的测试块。例如在PIO示例中默认的TESTNAME可能是sample_test1。在这个测试中它会依次调用TSK_BAR_INIT初始化BAR。TSK_MEM_WRITE_32向EP的某个BAR地址写入数据。TSK_MEM_READ_32从同一地址读取数据并验证是否与写入值一致。3.3 仿真执行流程与结果解读当你运行sample_test1后Transcript窗口会打印出类似下面的信息[时间] TSK_BAR_INIT: 开始枚举... [时间] TSK_BAR_INIT: 发现设备 Vendor IDXXXX, Device IDXXXX [时间] TSK_BAR_INIT: 配置BAR0地址为 XXXXXXXX [时间] TSK_MEM_WRITE_32: 向地址XXXXXXXX写入数据DDDDDDDD [时间] TSK_TX_CLK_EAT: ... [时间] TSK_MEM_READ_32: 从地址XXXXXXXX读取数据得到DDDDDDDD匹配成功 [时间] Simulation PASSED!同时在波形窗口中你可以看到初始阶段sys_reset_n撤销后链路训练开始可以看到trn_lnk_up_n信号从高变低表明链路训练成功。BAR初始化阶段在RP侧能看到它发出配置写TLPtrn_tsof_n拉低trn_td上是对应的TLP头和数据。数据读写阶段随后能看到RP发出内存写TLP目标地址是刚才配置的BAR空间EP收到后返回一个完成包Completion。接着RP发出内存读TLPEP返回一个带数据的完成包。注意事项仿真运行时Modelsim可能会弹出一个对话框提示“是否停止在宏文件xxx处”。一定要选择“No”。这是Modelsim在遇到$display或$stop等系统任务时的交互提示选择“No”会让仿真继续运行下去。4. 集成用户自定义逻辑从PIO示例到真实设计PIO示例只是一个演示我们的最终目标是将自己的逻辑集成进去。这个过程需要细心避免破坏已有的仿真框架。4.1 修改EP顶层模块假设你的用户逻辑模块名为user_logic它通过类似Avalon-ST或AXI-Stream接口与PCIE IP核的传输层TRN接口对接。你需要做的是在ISE/Vivado中用你的user_logic模块替换掉PIO示例设计中的pio模块或类似的控制模块。确保user_logic的接口与PCIE IP核的TRN接口正确连接。关键信号包括trn_td,trn_trem_n,trn_tsof_n,trn_teof_n,trn_tsrc_rdy_n,trn_tdst_rdy_n(发送端)trn_rd,trn_rrem_n,trn_rsof_n,trn_reof_n,trn_rsrc_rdy_n,trn_rdst_rdy_n(接收端)trn_lnk_up_n(链路状态)特别注意时钟和复位PCIE IP核输出的用户时钟trn_clk和复位trn_reset_n必须用来驱动你的user_logic内部的所有同步逻辑。不要引入额外的异步时钟域除非你很清楚如何做跨时钟域处理。4.2 适配测试激励tests.vPIO示例的测试是向固定的BAR地址写一个数再读回来。你的user_logic功能肯定不同。你需要修改tests.v中的测试序列使其符合你设计的通信协议。例如你的user_logic可能是一个DMA控制器那么测试序列可能变为TSK_BAR_INIT。TSK_MEM_WRITE_32向DMA控制器的“源地址寄存器”写入一个主机内存地址。TSK_MEM_WRITE_32向“目的地址寄存器”写入一个FPGA内部缓冲区地址。TSK_MEM_WRITE_32向“长度寄存器”写入传输长度。TSK_MEM_WRITE_32向“启动寄存器”写入1触发DMA传输。等待一段时间可以用# delay或等待某个状态信号然后通过TSK_MEM_READ_32读取“状态寄存器”来检查DMA是否完成。最后可以再发起一个读操作从FPGA缓冲区地址读取数据验证传输的正确性。你需要在pci_exp_usrapp_tx.v中已有的任务基础上组合出你需要的测试流程。如果现有任务不满足比如需要64位地址或更大突发长度你可能需要参考现有任务的代码自己编写新的任务。4.3 处理多BAR与设计检查Xilinx的示例仿真环境默认可能只使能和一个BAR并且开启了设计检查pio_check_design。如果你的设计使用了多个BAR或者你的用户逻辑行为与PIO示例不同可能会在仿真中遇到断言错误。多BAR支持在pci_exp_usrapp_tx.v文件中找到TSK_BAR_INIT任务。里面有一个变量可能叫pio_check_design默认值为1。这个检查可能会对多BAR支持不完善。一个常见的做法是将这个变量的值改为0跳过这部分检查。但前提是你必须自己清楚你的BAR空间映射关系并在测试激励中正确使用。修改顶层模块名如果你重命名了示例设计的顶层模块比如从pcie_1x1_example改成了my_pcie_design那么simulate_mti.do文件中的编译和仿真命令就会找不到对应的模块。你需要用文本编辑器打开这个do文件将所有对旧模块名的引用特别是在vlog编译和vsim仿真命令中更新为你的新模块名。5. 常见问题排查与调试技巧实录即使按照步骤操作仿真过程也难免遇到问题。下面是我总结的几个典型问题及排查思路。5.1 仿真只有链路训练没有数据交易这是最常见的问题。症状波形里只有trn_lnk_up_n变低之后没有任何TLP事务trn_tsof_n/trn_rsof_n再也没有脉冲。原因与排查没有执行TSK_BAR_INIT检查你的tests.v文件确认测试序列的第一条有效指令是TSK_BAR_INIT。确保TESTNAME参数传递正确并且成功进入了包含TSK_BAR_INIT的测试分支。BAR初始化失败在Transcript窗口搜索“BAR”相关打印。如果看到“BAR size is 0”或“BAR assignment failed”等错误说明RP在枚举配置时出了问题。检查IP核配置中BAR的设置大小、类型、预取等是否与TSK_BAR_INIT任务中的期望值匹配。可能需要根据你的IP核配置手动修改TSK_BAR_INIT任务中的配置写数据。链路训练未真正成功虽然trn_lnk_up_n变低了但可能内部状态机有问题。可以查看IP核内部的一些状态信号如cfg_link_up或者PCIE IP核自带的调试信号在IP核配置中启用调试端口。5.2 编译错误找不到库或模块症状执行do simulate_mti.do时vlog或vsim命令报错提示“未定义的模块”或“找不到库”。原因与排查库路径错误再次检查modelsim.ini文件中库的路径是否为绝对路径并且路径确实存在编译好的库文件。库名不匹配modelsim.ini中声明的库名如unisims_ver必须与编译库生成的文件夹名完全一致区分大小写。glbl.v问题确保glbl.v已正确编译到work库并且simulate_mti.do中没有重复编译或错误引用它。可以尝试在Modelsim命令行手动输入vsim work.glbl看是否能找到该模块。5.3 运行时错误数据校验失败或断言报警症状仿真能运行但最后打印“Simulation FAILED!”或者Transcript窗口出现红色的断言错误信息。原因与排查用户逻辑时序错误这是最可能的原因。你的user_logic在响应RP的请求时违反了TRN接口的时序。例如在trn_tsrc_rdy_n有效时你没有在规定的时钟周期内给出trn_tdst_rdy_n或者你发送的TLP包格式错误。仔细对照Xilinx用户手册如UG517中的TRN接口时序图用波形窗口仔细检查每一个交互。重点关注sof包开始、eof包结束、src_rdy源端就绪、dst_rdy目的端就绪这几个握手信号。测试激励与设计不匹配你发起的TLP请求地址、长度、属性超出了你用户逻辑的处理能力。比如你发起一个64位地址的读写但你的逻辑只支持32位或者你发起一个超过最大负载长度Max Payload Size的请求。调整测试激励使其符合你设计的规格。时钟域问题确保你的用户逻辑完全运行在trn_clk域内。如果必须用到其他时钟必须对跨时钟域的信号进行正确的同步处理如双触发器同步、异步FIFO并在仿真中验证其正确性。5.4 调试技巧让问题无所遁形善用波形保存与过滤PCIE仿真信号极多全部抓取会导致波形文件巨大加载缓慢。在simulate_mti.do或vsim命令中可以指定只保存你关心的模块层次和信号。例如在vsim命令后添加-view vsim.wlf然后使用add wave命令有选择地添加信号。或者在Testbench中使用$dumpfile和$dumpvars系统任务进行选择性存储。理解Transcript打印信息pci_exp_usrapp_tx/rx.v中有大量的$display语句它们打印了每个关键步骤。这是最直接的调试信息。根据错误信息定位到相应的任务再结合波形分析。分阶段仿真不要一开始就把所有功能都加上。先确保最基本的链路训练和BAR初始化能通过。然后只测试一个最简单的内存写操作看你的用户逻辑能否正确接收并响应。通过后再测试读操作。逐步增加复杂度。参考官方文档与代码遇到协议或接口时序问题时Virtex-6 FPGA Integrated Block for PCI Express User Guide (UG517)是最权威的参考。同时仔细阅读pci_exp_usrapp_tx.v中的任务实现它是Xilinx提供的标准行为模型是你编写自己测试激励的最佳范本。仿真通过并不意味着板上一定能工作但它能排除掉绝大部分逻辑设计错误尤其是协议交互和状态机方面的错误。将仿真环境搭建好、用熟是进行复杂FPGA系统设计特别是涉及高速接口设计时不可或缺的一项核心技能。它节省的调试时间远比搭建它所花费的时间要多得多。