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(32'h01, 32'h00000007, 4'h1); 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)) , 4'h1);

以上这些语句均是对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, 4'hF); // 12'hD0

根据读出的数值判断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 = 5'h%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, 12'h10, P_ADDRESS_MASK, 4'hF); DEFAULT_TAG = DEFAULT_TAG + 1; TSK_TX_CLK_EAT(100); // Read BAR0 Range TSK_TX_TYPE0_CONFIGURATION_READ(DEFAULT_TAG, 12'h10, 4'hF); DEFAULT_TAG = DEFAULT_TAG + 1; TSK_WAIT_FOR_READ_DATA; BAR_INIT_P_BAR_RANGE[0] = P_READ_DATA;

其中12'h10(0x04)就对应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 0
bit2: 在Memory 中0表示32位地址空间,1表示64位地址空间。
bit3:在memory BAR中表示该设备是否允许prefetch,1表示可以预取。

对所有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] = 2'h0; // disable BAR end else begin BAR_INIT_P_BAR_ENABLED[ii] = 2'h2; // 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 & {1'b1,(BAR_INIT_P_BAR_RANGE[ii] & 32'hffff_fff0)};

在分析下一步结果之前,FNC_CONVERT_RANGE_TO_SIZE_32()的功能,他可以根据每段BAR的回读值为其分配起始地址:

case (BAR_INIT_P_BAR_RANGE[bar_index] & 32'hFFFF_FFF0) // AND off control bits 32'hFFFF_FFF0 : return_value = 33'h0000_0010; 32'hFFFF_FFE0 : return_value = 33'h0000_0020; 32'hFFFF_FFC0 : return_value = 33'h0000_0040; 32'hFFFF_FF80 : return_value = 33'h0000_0080; .....

所以起始地址是只在某一bit上置1的一个32bit数,它和BAR的回读值(高位全为1,剩下的低位全为0)取与后,要么得到0,要么得到原起始地址。

如果为0,说明已分配的空间小于此次申请的BAR空间,例如BAR0为1kb,BAR1申请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, 12'h10, BAR_INIT_P_BAR[0][31:0], 4'hF);

并且,同样像RP设备一样,开启使能并设置Max_Payload_Size

TSK_TX_TYPE0_CONFIGURATION_WRITE(DEFAULT_TAG, 12'h04, 32'h00000007, 4'h1);
TSK_TX_TYPE0_CONFIGURATION_WRITE(DEFAULT_TAG, DEV_CTRL_REG_ADDR, 32'h0000007f, 4'h1);

至此,TSK_BAR_INIT就全部执行完毕了。

任务TSK_XDMA_FIND_BAR紧接着开始执行,该任务的作用是在BAR0~6找到对应XDMA的BAR。

首先判断是否为32bit的mem bar,之后读取内存空间中该BAR对应的地址范围。16'h0说明是读BAR的起始位置。

if(board.RP.tx_usrapp.BAR_INIT_P_BAR_ENABLED[jj] == 2'b10) begin board.RP.tx_usrapp.TSK_TX_MEMORY_READ_32(board.RP.tx_usrapp.DEFAULT_TAG, board.RP.tx_usrapp.DEFAULT_TC, 11'd1, board.RP.tx_usrapp.BAR_INIT_P_BAR[jj][31:0]+16'h0, 4'h0, 4'hF);//[jj][31:0]是主机为该BAR分配的起始地址,16'h0代表偏移地址 board.RP.tx_usrapp.TSK_WAIT_FOR_READ_DATA; end

之后判断回读的值,可以查看XDMA的UG,如果偏移地址是16'h0,则其读出的是H2C通道的Channel Identifier,它的[31:16]位固定位0x1FC0。之后用xdma_bar记录是第几个BAR。

if (board.RP.tx_usrapp.P_READ_DATA[31:16] == 16'h1FC0) 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[256+0] = 8'h13; // -- Magic DATA_STORE[256+1] = 8'h00; DATA_STORE[256+2] = 8'h4b; DATA_STORE[256+3] = 8'had; DATA_STORE[256+4] = DMA_BYTE_CNT[7:0]; //-- Length lsb DATA_STORE[256+5] = DMA_BYTE_CNT[15:8];//-- Length msb DATA_STORE[256+6] = 8'h00; DATA_STORE[256+7] = 8'h00; DATA_STORE[256+8] = 8'h00; //-- Src_add [31:0] x0400 DATA_STORE[256+9] = 8'h04; DATA_STORE[256+10] = 8'h00; DATA_STORE[256+11] = 8'h00; DATA_STORE[256+12] = 8'h00; //-- Src add [63:32] DATA_STORE[256+13] = 8'h00; DATA_STORE[256+14] = 8'h00; DATA_STORE[256+15] = 8'h00;

8'h13是control字段,16'had4b是固定字段,表示描述符是否有效;这里的含义是该描述符完成后中断,且终止传输,因为我们只传输一组数据。

DMA_BYTE_CNT很好理解,就是数据长度,以字节为单位,这里的数值为16;Src_adr是H2C的起始地址这里定义为32'h0000_0400 = 32'd1024.

接下来是向指定的地址填入数据:

for (k = 0; k < 32; k = k + 1) begin $display(" **** Descriptor data *** data = %h, addr= %d\n", DATA_STORE[256+k], 256+k); #(Tcq); end for (k = 0; k < DMA_BYTE_CNT+64; k = k + 1) begin if( k < DMA_BYTE_CNT) begin #(Tcq) DATA_STORE[1024+k] = k; end else begin #(Tcq) DATA_STORE[1024+k] = 8'h00; end end

之后开始向寄存器H2C SGDMA Descriptor(0x80/0x84)填入描述符的起始地址,8'd256:

board.RP.tx_usrapp.TSK_XDMA_REG_WRITE(16'h4080, 32'h00000100, 4'hF);

之后就是开启传输,是通过写入H2C Channel Control寄存器实现的,写入的这一串数其实就是开启SGDMA引擎:

board.RP.tx_usrapp.TSK_XDMA_REG_WRITE(16'h0004, 32'hfffe7f, 4'hF);

至此,整体工作过程就很清晰了。PC先扫描设备,为其分配BAR空间,设置基地址。之后PC端驱动程序会准备描述符,将待传输数据(比如一幅图像)填入描述符。之后会继续配置BAR空间,向其中的寄存器写入描述符地址以及启动指令。之后就进行传输。

(PCIe switch会根据PC设置的地址判断属于哪个Card,FPGA上的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 (i=0; i<data_beat_count; i=i+1) 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] == 1'b1) ? board.EP.m_axi_wdata[127:120] : 8'h00), ((board.EP.m_axi_wstrb[14] == 1'b1) ? board.EP.m_axi_wdata[119:112] : 8'h00), ((board.EP.m_axi_wstrb[13] == 1'b1) ? board.EP.m_axi_wdata[111:104] : 8'h00), ((board.EP.m_axi_wstrb[12] == 1'b1) ? board.EP.m_axi_wdata[103:96] : 8'h00), ((board.EP.m_axi_wstrb[11] == 1'b1) ? board.EP.m_axi_wdata[95:88] : 8'h00),

之后就是RP存储在DATA_STORE中的原始值,需要注意重新排列字节,因为在128bit的数据排列中,高字节排列在高bit位:

128: begin for (i = 0; i < data_beat_count; i = i + 1) begin for (j=15; j>=0; j=j-1) begin DATA_STORE_512[i] = {DATA_STORE_512[i], DATA_STORE[1024+k+j]};//src是,0x0400位置处的 = 1024 end k=k+16; $display ("-- Data Stored in TB for H2C Transfer = %h--\n", DATA_STORE_512[i]); end end

之后会读取相关寄存器,检测执行状态以及完成描述符传输的次数。如果读取的状态是为32'h6、描述符完成的数量为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(16'h5080, 32'h00000300, 4'hF);

使能C2H传输:

board.RP.tx_usrapp.TSK_XDMA_REG_WRITE(16'h1004, 32'hfffe7f, 4'hF); // 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内的XDMA,DMA控制器会根据配置信息主动向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位宽)