XDMA官方仿真文件解读

📅 2026/7/15 19:33:48
XDMA官方仿真文件解读
目录一、获取仿真文件二、仿真解析2.1、RP设备初始化2.2、EP设备初始化2.3、数据传输流程2.4、Xilinx PCIe通道解读一、获取仿真文件在配置完xdma ip后右键Open IP Example Design可以得到一个官方示例工程。这份工程的最大价值是其附带了一份Xilinx官方提供的XDMA仿真文件其完整模拟了PCIe RP节点的工作过程。搞清楚这份仿真工程对于了解PCIe工作机理进行高效仿真都是大有好处的。仿真工程的结构如下其中CLK_GEN_RP/CLK_GEN_EP都是时钟生成相关的EP是用户自己的XDMA工程可以不用关心。本文档着重解读RP一些关键部分的重要逻辑。二、仿真解析2.1、RP设备初始化工程启动后先运行TSK_SYSTEM_INITIALIZATION(usp_pci_exp_usrapp_tx.v)比较关键的是下面三行的配置board.RP.cfg_usrapp.TSK_WRITE_CFG_DW(32h01, 32h00000007, 4h1); board.RP.cfg_usrapp.TSK_READ_CFG_DW(DEV_CTRL_REG_ADDR/4); board.RP.cfg_usrapp.TSK_WRITE_CFG_DW(DEV_CTRL_REG_ADDR/4,( board.RP.cfg_usrapp.cfg_rd_data | (DEV_CAP_MAX_PAYLOAD_SUPPORTED * 32)) , 4h1);以上这些语句均是对RP自身空间的配置也就是Type1型的Header。注意这里每一个地址对应4字节所以这里实际上是向0x04的位置也就是向Command的位置写入了0x0007。根据Command部分的功能这实际上是开启了I/O Space Enable、Memory Space Enable以及Bus Master。DEV_CTRL_REG_ADDR的值为0x78(对应0x1E)这部分空间不在Header部分其对应寄存器以上写入操作是在维持其他配置不变的情况下向bit 5写入1也就是向MAX_Payload_Size写入001对应256bytes这设置了TLP包有效载荷部分的最大数据量。最后开启另一个任务TSK_SYSTEM_CONFIGURATION_CHECK.在第一行读取LINK_CTRL_REG寄存器(0x80)一次读取32bit所以实际上还读出了Link_Status_Reg(0x82)。TSK_TX_TYPE0_CONFIGURATION_READ(DEFAULT_TAG, LINK_CTRL_REG_ADDR, 4hF); // 12hD0根据读出的数值判断EP设备支持的Link Speed以及Lane数目if (P_READ_DATA[19:16] MAX_LINK_SPEED) begin if (P_READ_DATA[19:16] 1) $display([%t] : Check Max Link Speed 2.5GT/s - PASSED, $realtime); else if(P_READ_DATA[19:16] 2) $display([%t] : Check Max Link Speed 5.0GT/s - PASSED, $realtime); else if(P_READ_DATA[19:16] 3) $display([%t] : Check Max Link Speed 8.0GT/s - PASSED, $realtime); else if(P_READ_DATA[19:16] 4) $display([%t] : Check Max Link Speed 16.0GT/s - PASSED, $realtime); end else begin $display([%t] : Check Max Link Speed - FAILED, $realtime); $display([%t] : Data Error Mismatch, Parameter Data %x ! Read Data %x, $realtime, MAX_LINK_SPEED, P_READ_DATA[19:16]); end if (P_READ_DATA[24:20] LINK_CAP_MAX_LINK_WIDTH) $display([%t] : Check Negotiated Link Width 5h%x - PASSED, $realtime, LINK_CAP_MAX_LINK_WIDTH); else $display([%t] : Data Error Mismatch, Parameter Data %x ! Read Data %x, $realtime, LINK_CAP_MAX_LINK_WIDTH, P_READ_DATA[24:20]);类似的接下来会检查Device_Id等参数是否一致这里不赘述。2.2、EP设备初始化所有检查通过后执行TSK_BAR_INIT这里主要是对BAR空间的处理都是对于EP设备的Type0 Header进行的配置。TSK_BAR_INIT的第1个为TSK_BAR_SCAN流程如下TSK_TX_TYPE0_CONFIGURATION_WRITE(DEFAULT_TAG, 12h10, P_ADDRESS_MASK, 4hF); DEFAULT_TAG DEFAULT_TAG 1; TSK_TX_CLK_EAT(100); // Read BAR0 Range TSK_TX_TYPE0_CONFIGURATION_READ(DEFAULT_TAG, 12h10, 4hF); DEFAULT_TAG DEFAULT_TAG 1; TSK_WAIT_FOR_READ_DATA; BAR_INIT_P_BAR_RANGE[0] P_READ_DATA;其中12h100x04就对应BAR0表示向BAR空间写入全1并回读。如果PCIe设备的空间大小只需要20bit的地址空间他应当将BAR的[19:0]设置为只读。RP在扫描时会向BAR空间写入全1再回读。从左到右找第一个不为1的bit位从该位开始到0bit就是实际的地址空间。尽管[3:0]bit有特殊用途但是由于PCIe规定BAR空间最小为128Bytes也就是需要[6:0]范围的bit并不影响扫描。[3:0]的用途bit0:0表示memory空间、1表示IO空间。bit1: reserved 0bit2: 在Memory 中0表示32位地址空间1表示64位地址空间。bit3:在memory BAR中表示该设备是否允许prefetch1表示可以预取。对所有BAR都做如上处理并使用BAR_INIT_P_BAR_RANGE记录每个BAR的回读值这可以确定每块BAR申请的地址范围。第2个任务是TSK_BUILD_PCIE_MAP它会根据之前BAR的回读值确认Memory或IO类型以及32位或64位空间。之后根据每块BAR的申请为其分配地址空间由于我使用的是32bit的Memory空间因此重点分析这一分支if (ii ! 6) begin NUMBER_OF_MEM32_BARS NUMBER_OF_MEM32_BARS 1; // not counting erom space //if (pio_check_design (~BAR_ENABLED[ii])) begin if (pio_check_design (NUMBER_OF_MEM32_BARS 6)) begin $display([%t] Testbench will disable BAR %x,$realtime, ii); BAR_INIT_P_BAR_ENABLED[ii] 2h0; // disable BAR end else begin BAR_INIT_P_BAR_ENABLED[ii] 2h2; // bar is mem32 mapped $display([%t] Testbench is enabling MEM32 BAR %x,$realtime, ii); end end这里首先判断BAR总数是否超过7个不超过则为BAR_INIT_P_BAR_ENABLED分配2该参数其实是记录一个索引值后面会通过该索引值从BAR_INIT_MESSAGE确定其类型。BAR_INIT_MESSAGE[0] DISABLED; BAR_INIT_MESSAGE[1] IO MAPPED; BAR_INIT_MESSAGE[2] MEM32 MAPPED; BAR_INIT_MESSAGE[3] MEM64 MAPPED;之后会为该BAR分配地址空间BAR_INIT_P_BAR会记录每个BAR的起始地址BAR_INIT_P_BAR的每个值都是33bit的所以检测其第32bit即可得知地址是否超限(OUT_OF_LO_MEM)。回读的BAR数值和0xffff_fff0取与可见忽略了低4bit。这段再和当前的起始地址取与。if (!OUT_OF_LO_MEM) begin BAR_INIT_TEMP BAR_INIT_P_MEM32_START {1b1,(BAR_INIT_P_BAR_RANGE[ii] 32hffff_fff0)};在分析下一步结果之前FNC_CONVERT_RANGE_TO_SIZE_32()的功能他可以根据每段BAR的回读值为其分配起始地址case (BAR_INIT_P_BAR_RANGE[bar_index] 32hFFFF_FFF0) // AND off control bits 32hFFFF_FFF0 : return_value 33h0000_0010; 32hFFFF_FFE0 : return_value 33h0000_0020; 32hFFFF_FFC0 : return_value 33h0000_0040; 32hFFFF_FF80 : return_value 33h0000_0080; .....所以起始地址是只在某一bit上置1的一个32bit数它和BAR的回读值(高位全为1剩下的低位全为0)取与后要么得到0要么得到原起始地址。如果为0说明已分配的空间小于此次申请的BAR空间例如BAR0为1kbBAR1申请4kb。此时系统会为当前BAR分配4k至8k的空间作为其BAR空间。所以将其起始地址设置为4kb并且将下一字段的起始地址设置为8kb。反之如果取与后不为0。说明新的BAR申请的空间小于当前已经分配的空间例如BAR2申请了2kb的空间此时将其起始地址设置为8kb且设置下一字段的起始地址为10kb。第3个任务是TSK_DISPLAY_PCIE_MAP它会打印出每段BAR的地址范围(BAR_INIT_P_BAR_RANGE)和其类型(BAR_INIT_MESSAGE)。最后第3个任务是TSK_BAR_PROGRAM它会将每段BAR的起始地址写回到BAR寄存器。TSK_TX_TYPE0_CONFIGURATION_WRITE(DEFAULT_TAG, 12h10, BAR_INIT_P_BAR[0][31:0], 4hF);并且同样像RP设备一样开启使能并设置Max_Payload_SizeTSK_TX_TYPE0_CONFIGURATION_WRITE(DEFAULT_TAG, 12h04, 32h00000007, 4h1);TSK_TX_TYPE0_CONFIGURATION_WRITE(DEFAULT_TAG, DEV_CTRL_REG_ADDR, 32h0000007f, 4h1);至此TSK_BAR_INIT就全部执行完毕了。任务TSK_XDMA_FIND_BAR紧接着开始执行该任务的作用是在BAR0~6找到对应XDMA的BAR。首先判断是否为32bit的mem bar之后读取内存空间中该BAR对应的地址范围。16h0说明是读BAR的起始位置。if(board.RP.tx_usrapp.BAR_INIT_P_BAR_ENABLED[jj] 2b10) begin board.RP.tx_usrapp.TSK_TX_MEMORY_READ_32(board.RP.tx_usrapp.DEFAULT_TAG, board.RP.tx_usrapp.DEFAULT_TC, 11d1, board.RP.tx_usrapp.BAR_INIT_P_BAR[jj][31:0]16h0, 4h0, 4hF);//[jj][31:0]是主机为该BAR分配的起始地址16h0代表偏移地址 board.RP.tx_usrapp.TSK_WAIT_FOR_READ_DATA; end之后判断回读的值可以查看XDMA的UG如果偏移地址是16h0,则其读出的是H2C通道的Channel Identifier它的[31:16]位固定位0x1FC0。之后用xdma_bar记录是第几个BAR。if (board.RP.tx_usrapp.P_READ_DATA[31:16] 16h1FC0) begin //Mask [15:0] which will have revision number. xdma_bar jj; xdma_bar_found 1; $display ( XDMA BAR found : BAR %d is XDMA BAR\n, xdma_bar); end else begin $display ( XDMA BAR : BAR %d is NOT XDMA BAR\n, jj); end在找到XDMA BAR后根据bit15判断是stream还是mm接口本例使用的是mm所以是开启dma_test0.之后开始执行test.vh,也就是sample_tests.vh。这里只关注dma_test0部分2.3、数据传输流程test部分首先运行任务TSK_INIT_DATA_H2C该任务会定义H2C的描述符。关于描述符它会为DMA控制器指明需要搬用数据的起始地址(H2C)、目标地址(C2H)、数据长度。可以从XDMA文档中看到描述符长这个样子其中Nxt_adr表示下一个描述符的起始地址Nxt_adj表示之后还有多少描述符。很自然的的一个问题第一个描述符的地址是多少它存储在寄存器H2C SGDMA Descriptor(0x80/0x84)中。接下来看代码(部分)$display( **** TASK DATA H2C ***\n); $display( **** Initilize Descriptor data ***\n); DATA_STORE[2560] 8h13; // -- Magic DATA_STORE[2561] 8h00; DATA_STORE[2562] 8h4b; DATA_STORE[2563] 8had; DATA_STORE[2564] DMA_BYTE_CNT[7:0]; //-- Length lsb DATA_STORE[2565] DMA_BYTE_CNT[15:8];//-- Length msb DATA_STORE[2566] 8h00; DATA_STORE[2567] 8h00; DATA_STORE[2568] 8h00; //-- Src_add [31:0] x0400 DATA_STORE[2569] 8h04; DATA_STORE[25610] 8h00; DATA_STORE[25611] 8h00; DATA_STORE[25612] 8h00; //-- Src add [63:32] DATA_STORE[25613] 8h00; DATA_STORE[25614] 8h00; DATA_STORE[25615] 8h00;8h13是control字段16had4b是固定字段表示描述符是否有效这里的含义是该描述符完成后中断且终止传输因为我们只传输一组数据。DMA_BYTE_CNT很好理解就是数据长度以字节为单位,这里的数值为16Src_adr是H2C的起始地址这里定义为32h0000_0400 32d1024.接下来是向指定的地址填入数据for (k 0; k 32; k k 1) begin $display( **** Descriptor data *** data %h, addr %d\n, DATA_STORE[256k], 256k); #(Tcq); end for (k 0; k DMA_BYTE_CNT64; k k 1) begin if( k DMA_BYTE_CNT) begin #(Tcq) DATA_STORE[1024k] k; end else begin #(Tcq) DATA_STORE[1024k] 8h00; end end之后开始向寄存器H2C SGDMA Descriptor(0x80/0x84)填入描述符的起始地址8d256board.RP.tx_usrapp.TSK_XDMA_REG_WRITE(16h4080, 32h00000100, 4hF);之后就是开启传输是通过写入H2C Channel Control寄存器实现的写入的这一串数其实就是开启SGDMA引擎board.RP.tx_usrapp.TSK_XDMA_REG_WRITE(16h0004, 32hfffe7f, 4hF);至此整体工作过程就很清晰了。PC先扫描设备为其分配BAR空间设置基地址。之后PC端驱动程序会准备描述符将待传输数据(比如一幅图像)填入描述符。之后会继续配置BAR空间向其中的寄存器写入描述符地址以及启动指令。之后就进行传输。(PCIe switch会根据PC设置的地址判断属于哪个CardFPGA上的PCIe硬核会判断具体属于哪个BAR)。到这里你可能会奇怪为什么没有看到TLP包的影子其实前文中提到的配置、读取、写入凡事设计RP、EP间交互的命令都涉及到了TLP包比如TSK_TX_TYPE0_CONFIGURATION_READ TSK_TX_TYPE0_CONFIGURATION_WRITE TSK_TX_MEMORY_READ_32 TSK_TX_MEMORY_WRITE_32 TSK_XDMA_REG_READ TSK_XDMA_REG_WRITE。如果去看其底层的实现方式可以发现其本质都是TLP包的传输。只不过Xilinx会将其封装成axi_stream的接口至于axi_stream和TLP的具体对应关系可以去看相关文档这里不多介绍了。之后该仿真工程还提供了数据比较的功能COMPARE_DATA_H2C它会比较FPGA收到的值和PC发送的值是否一致。XDMA会通过AXI-MM接口向外输出这里设置的C_DATA_WIDTH为128该任务会通过READ_DATA记录该值 (posedge board.EP.m_axi_wvalid) ; //valid data comes at wvalid for (i0; idata_beat_count; ii1) begin (negedge board.EP.user_clk); //samples data wvalid and negedge of user_clk if ( board.EP.m_axi_wready ) begin //check for wready is high before sampling data case (board.C_DATA_WIDTH) 64: ... 128: READ_DATA[i] {((board.EP.m_axi_wstrb[15] 1b1) ? board.EP.m_axi_wdata[127:120] : 8h00), ((board.EP.m_axi_wstrb[14] 1b1) ? board.EP.m_axi_wdata[119:112] : 8h00), ((board.EP.m_axi_wstrb[13] 1b1) ? board.EP.m_axi_wdata[111:104] : 8h00), ((board.EP.m_axi_wstrb[12] 1b1) ? board.EP.m_axi_wdata[103:96] : 8h00), ((board.EP.m_axi_wstrb[11] 1b1) ? board.EP.m_axi_wdata[95:88] : 8h00),之后就是RP存储在DATA_STORE中的原始值需要注意重新排列字节因为在128bit的数据排列中高字节排列在高bit位128: begin for (i 0; i data_beat_count; i i 1) begin for (j15; j0; jj-1) begin DATA_STORE_512[i] {DATA_STORE_512[i], DATA_STORE[1024kj]};//src是0x0400位置处的 1024 end kk16; $display (-- Data Stored in TB for H2C Transfer %h--\n, DATA_STORE_512[i]); end end之后会读取相关寄存器检测执行状态以及完成描述符传输的次数。如果读取的状态是为32h6、描述符完成的数量为1就代表传输没有问题。至此H2C的传输和验证就完成了。接下来是C2H的传输部分类似的首先是数据初始化部分TSK_INIT_DATA_C2H数据量同样是16字节因为是FPGA向PC写所以重点关注一下Dst Adr这里设置为0x0800 2048。描述符在DATA_STORE中的起始地址是768 0x300。之后设置C2H SGDMA寄存器用于告知XDMA描述符起始地址:board.RP.tx_usrapp.TSK_XDMA_REG_WRITE(16h5080, 32h00000300, 4hF);使能C2H传输board.RP.tx_usrapp.TSK_XDMA_REG_WRITE(16h1004, 32hfffe7f, 4hF); // Enable C2H DMA之后是通过COMPARE_DATA_C2H比较二者数据是否一致。注意这里整个仿真工程是回环测试用户侧逻辑会将H2C传入的数据再传回去所以这个task中仍然是和DATA_DTORE的1024位置开始的数据比较。2.4、Xilinx PCIe通道解读那么C2H通道送到HOST的数据在哪呢答案是CQ通道这里有必要介绍一下xilinx的PCIe功能涉及的4个数据通道分别是Xilinx的PCIe功能模块都会涉及这4个通道在用户侧这个被封装在XDMA内部并不对外暴漏。而我们现在聊的4个通道是来自RP节点的这是仿真工程模拟的RP节点的PCIe功能。这些通道均是面向用户侧、仿真侧的RP、EP节点的互联仍是通过PCIe接口。那么RP、EP的这些通道分别起到什么作用呢可以参考下图前文中提到的RP去配置BAR空间、读BAR空间等操作均是通过RP RQ -RP CQ通道实现的而EP又通过CC通道将数据给到RP的RC以上均可以在RP仿真功能中看到明确的定义。接下来是2个重要的通道正是这两个通道实现了真正的FPGA、PC间的DMA数据传输。我们设计数据传输是往往是上位机程序主动去向FPGA写数据或者去读FPGA侧的数据似乎数据搬运的过程是由PC去做的实际上不是这样。当PC想读FPGA时它只是先配置描述符配置BAR空间寄存器(写入描述符起始地址、使能DMA等)之后CPU就去忙其他的事情了而真正的执行者是FPGA内的XDMADMA控制器会根据配置信息主动向PC端传输数据(通过EP RQ - RP-CQ通道)不需要CPU的参与这正是DMA的优势所在。所以本质上PC对FPGA的读其实是XDMA向主机内存的写。类似的PC对FPGA的写最终会转化为XDMA对主机内存的读。CPU同样先配置描述符和BAR寄存器之后XDMA发起读请求(EP.RQ - RP.CQ)内存控制器直接将数据给到FPGA(RP.CC-EP.RC)。至此便可以理解为什么RP的读数据反映在CQ通道了。还有一点值得注意那就是CQ的第一个数据是包含TLP Header的所以在实际数据比较时要去除这一部分READ_DATA_C2H_512[i][511:0] board.RP.m_axis_cq_tdata [511:128];数据排列(RP端仿真中设置为512位宽)