赞
踩
本文继续基于PCIE4C IP核实现主机(RHEL 8.9)与FPGA(Xilinx Ultrascale+HBM VCU128开发板)间的DMA数据传输。本文分为四个部分:DMA设计、FPGA设计、仿真设计、驱动程序设计。
本文所涉及的DMA操作指FPGA设备不需要主机CPU的参与,独立对主机内存进行读写。具体而言,主机会向FPGA的指定地址写入DMA描述符,之后FPGA会根据DMA描述符的内容进行FPGA存储与主机内存的数据交换。DMA描述符的具体格式如下。
DMA读描述符地址 | 功能 |
---|---|
0x100 | 读PCIe空间基地址低32位 |
0x104 | 读PCIe空间基地址高32位 |
0x108 | 写FPGA存储器基地址低32位 |
0x10c | 写FPGA存储器基地址高32位 |
0x110 | 写长度 |
0x114 | 待完成读ID |
0x118 | 已完成读ID(DMA结束后更新为待完成读ID,CPU根据其与待完成读ID的值判断DMA操作是否完成) |
DMA写描述符地址 | 功能 |
---|---|
0x200 | 写PCIe空间基地址低32位 |
0x204 | 写PCIe空间基地址高32位 |
0x208 | 读FPGA存储器基地址低32位 |
0x20c | 读FPGA存储器基地址高32位 |
0x210 | 读长度 |
0x214 | 待完成写ID |
0x218 | 已完成写ID(DMA结束后更新为待完成写ID,CPU根据其与待完成写ID的值判断DMA操作是否完成) |
上文介绍了FPGA通过cq和cc接口接收主机发来的请求报文,从而实现对自身内存单元的读写操作。本文介绍FPGA通过rq和rc接口向主机发出请求报文,从而实现对主机内存单元(DDR)的读写操作。
FPGA向主机发送请求使用PCIE4C的rq、rc接口实现,rq、rc为从机(FPGA)请求、主机(PC机)响应接口,FPGA将读/写地址报文通过rq接口通过握手方式发送到PC机,PC机将读地址对应的数据内容/写地址完成报文通过rc接口通过握手方式发送给FPGA。
rq接口具有的信号及传输方向如图所示。需要注意的是,不同于标准PCIe报文格式,PCIE4C将部分PCIe报文头字段(描述符)放入tuser字段中。
同时,PCIE4C将剩余的PCIe报文头字段留在第一个传输tdata的前几个字节中,对于内存、IO、原子操作类型的PCIe报文,tdata头个传输字段划分如下图所示。
各字段解释可从产品手册找到。
对于128bit位宽AXIS流接口、DWORD对齐模式,一次写内存请求操作对应波形类似下图。
对于128bit位宽AXIS流接口,一次读内存请求操作对应波形类似下图。
rc通道的接口信号如下图,当每次rq写请求操作结束后,FPGA侧会通过rc接口受到来自目标设备返回的写成功或写失败响应。
响应的每第一次传输的tdata的前几字节都被视为PCIe头字段(描述符),如下图
各字段解释可从产品手册找到。
对于128bit位宽AXIS流接口、DWORD对齐模式,一次读内存响应操作对应波形类似下图。
FPGA部分代码包含positive_process和negative_process两个部分,其中negative_process为FPGA作为响应设备,处理其他设备发来的请求报文,已在 基于PCIE4C的数据传输(一)——寄存器读写访问 中介绍。positive_process为FPGA作为请求设备向PCIe总线发出请求报文,由其他PCIe设备(这里为根设备即主机)作出响应,一般用于进行大规模数据传输即DMA操作。
本文共利用PCIE4C IP核例化了四个功能设备,每个功能设备具有独立的ram区域。
这里列出positive_process.sv的DMA监听处理状态机代码,这里的状态机代码负责监听negative_process模块对bar0的写操作,如果涉及到对DMA待完成读写描述符的读写操作则进行一次判断。如果待完成描述符与已完成描述符不同则发起一次新的DMA操作,并等待DMA操作结束后更新已完成描述符的值。
always_comb begin case (snoop_r) IDLE: begin if (|{dma_watchdogs_s}) begin snoop_s = REQDECODE; end else begin snoop_s = IDLE; end end REQDECODE: begin snoop_s = READDMAINFO; end READDMAINFO: begin if (readdma_cnt_r == 'd5) begin snoop_s = GENDMAREQ; end else begin snoop_s = READDMAINFO; end end GENDMAREQ: begin if (dma_trans_valid_r) begin snoop_s = DMABUSY; end else begin snoop_s = UPDATEDMASTATUS; end end DMABUSY: begin if (dma_trans_finish_s) begin snoop_s = UPDATEDMASTATUS; end else begin snoop_s = DMABUSY; end end UPDATEDMASTATUS: begin snoop_s = IDLE; end default: snoop_s = IDLE; endcase end
这里列出dma_simple.sv的DMA请求产生及处理状态机代码,这里的状态机代码负责对positive_process模块发起的DMA操作进行处理,如果为DMA读请求则根据DMA读描述符通过rq发送报文对指定PCIe空间进行读操作,并将rc收到的数据保存到FPGA的指定存储器空间中。如果为DMA写请求则根据DMA写描述符通过rq发送报文对指定PCIe空间进行写操作。
always @(*) begin case (cs) 0: begin ns = 1; end 1: begin ns = 2; end 2: begin if (dma_trans_valid) begin ns = 7; end else begin ns = 2; end end 7: begin // delay wait fetch data from ram if (s_axis_rq_tready[0]) begin ns = 8; end else begin ns = 7; end end 8: begin // delay if (s_axis_rq_tready[0]) begin ns = 3; end else begin ns = 8; end end 3: begin if (s_axis_rq_tvalid && s_axis_rq_tready[0]) begin if (dma_trans_mode) begin // dma_trans_direction) begin ns = 4; end else begin ns = 5; end end else begin ns = 3; end end 4: begin // wr if (s_axis_rq_tvalid && s_axis_rq_tready[0] & s_axis_rq_tlast) begin ns = 6; end else begin ns = 4; end end 5: begin // rd cpl if (~cpl_start & cpl_done & last_trans_flag_r) begin ns = 6; end else begin ns = 5; end end 6: begin // write finish flag ns = 0; end default: begin ns = 0; end endcase end
PCIe仿真利用Alex Forencich编写的cocotb pcie仿真库进行,核心代码如下。主要对Bar0的地址空间写入DMA写描述符,等待一段时间后向地址空间写入DMA读描述符,判断读写两处内容是否一致。DMA描述符格式见 DMA设计 部分
# write pcie read descriptor await dev_pf0_bar0.write_dword(0x000100, (mem_base+0x0000) & 0xffffffff) await dev_pf0_bar0.write_dword(0x000104, (mem_base+0x0000 >> 32) & 0xffffffff) await dev_pf0_bar0.write_dword(0x000108, 0x100) await dev_pf0_bar0.write_dword(0x000110, 0x400) await dev_pf0_bar0.write_dword(0x000114, 0xAA) await Timer(2000, 'ns') # read status val = await dev_pf0_bar0.read_dword(0x000118) tb.log.info("Status: 0x%x", val) # assert val == 0x800000AA assert val == 0x000000AA # write pcie write descriptor await dev_pf0_bar0.write_dword(0x000200, (mem_base+0x1000) & 0xffffffff) await dev_pf0_bar0.write_dword(0x000204, (mem_base+0x1000 >> 32) & 0xffffffff) await dev_pf0_bar0.write_dword(0x000208, 0x100) await dev_pf0_bar0.write_dword(0x000210, 0x400) # await dev_pf0_bar0.write_dword(0x000210, 0x400) await dev_pf0_bar0.write_dword(0x000214, 0x55) await Timer(2000, 'ns') # read status val = await dev_pf0_bar0.read_dword(0x000218) tb.log.info("Status: 0x%x", val) # assert val == 0x80000055 assert val == 0x00000055 tb.log.info("%s", mem.hexdump_str(0x1000, 64)) assert mem[0:1024] == mem[0x1000:0x1000+1024]
本文使用QuestaSim作为仿真器,Cocotb编译指令如下,需要在tb目录下进行,此外也支持VCS等其他仿真器(可参考Cocotb文档):
cp runsim.do.questa sim_build/runsim.do
make SIM=questa WAVES=1
vsim vsim.wlf
在终端界面,仿真器会将运行过程中所发送和接收的PCIe报文打印出来。
作者使用的系统为RHEL8.9,PCIe驱动基于linux内核进行开发,PCIe与PCI设备的驱动代码基本一致。可参考kernel官网PCI设备开发教程(https://docs.kernel.org/PCI/pci.html)。对于WIndows而言可参考MSDN相关页面进行开发。
进行DMA测试的核心代码如下:
dma_cpuregion_addr = dma_alloc_coherent(&dev->dev, 0x400, &dma_rcregion_addr, GFP_KERNEL | __GFP_ZERO); if (dma_cpuregion_addr == NULL) { goto dma_alloc_err; } for (i = 0; i < 400; i++) { // set initial value *((u8*)dma_cpuregion_addr + i) = i; } printk("rc base addr %llx\n", dma_rcregion_addr); iowrite32((dma_rcregion_addr + 0x0000) & 0xffffffff, (u8*)bar32 + 0x000100); iowrite32(((dma_rcregion_addr + 0x0000) >> 32) & 0xffffffff, (u8*)bar32 + 0x000104); iowrite32(0x00, (u8*)bar32 + 0x000108); iowrite32(0, (u8*)bar32 + 0x00010C); iowrite32(0x50, (u8*)bar32 + 0x000110); iowrite32(0xAA, (u8*)bar32 + 0x000114); usleep_range(1000000, 2000001); printk("Read status of writing data"); printk("%08x\n", ioread32((u8*)bar32 + 0x000114)); printk("%08x\n", ioread32((u8*)bar32 + 0x000118)); usleep_range(1000, 2001); printk("start copy to host"); iowrite32((dma_rcregion_addr + 0x0100) & 0xffffffff, (u8*)bar32 + 0x000200); // cpu region lo addr iowrite32(((dma_rcregion_addr + 0x0100) >> 32) & 0xffffffff, (u8*)bar32 + 0x000204); // cpu region hi addr iowrite32(0x00, (u8*)bar32 + 0x000208); // fpga region lo addr iowrite32(0, (u8*)bar32 + 0x00020C); // fpga region hi addr iowrite32(0x50, (u8*)bar32 + 0x000210); // len iowrite32(0x55, (u8*)bar32 + 0x000214); // id usleep_range(1000000, 2000001); printk("Read status of reading data"); printk("%08x\n", ioread32((u8*)bar32 + 0x000214)); printk("%08x\n", ioread32((u8*)bar32 + 0x000218)); printk("Read data from original DMA region %p\n", ((u8*)dma_cpuregion_addr + 0x0000)); for (i = 0; i < 32; i++) { printk("%u ", *((u8*)dma_cpuregion_addr + 0x0000 + i)); } printk("\n"); printk("Read data from new DMA region %p\n", ((u8*)dma_cpuregion_addr + 0x0400)); for (i = 0; i < 32; i++) { printk("%u ", *((u8*)dma_cpuregion_addr + 0x0100 + i)); } printk("\n"); for (i = 0; i < 32; i++) { if (*((u8*)dma_cpuregion_addr + 0x0100 + i) != *((u8*)dma_cpuregion_addr + 0x0000 + i)) { printk("reading mismatch starting at address %d\n", i); dma_mismatch = 1; break; } } if (!dma_mismatch) { printk("dma all matched\n"); printk("\n"); }
make编译驱动,使用insmod加载驱动后,dmesg查看调试信息,即printk的输出结果。
测试完成后,使用rmmod卸载驱动,释放变量。
make
sudo insmod test_driver
sudo dmesg
sudo rmmod test_driver
完整工程可于同名公众号回复PCIE4C_DMA获取。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。