本文继续基于PCIE4C IP核实现主机(RHEL 8.9)与FPGA(Xilinx Ultrascale+HBM VCU128开发板)间的DMA数据传输。本文分为四个部分:DMA设计、FPGA设计、仿真设计、驱动程序设计。
DMA设计
本文所涉及的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设计
上文介绍了FPGA通过cq和cc接口接收主机发来的请求报文,从而实现对自身内存单元的读写操作。本文介绍FPGA通过rq和rc接口向主机发出请求报文,从而实现对主机内存单元(DDR)的读写操作。
FPGA向主机发送请求使用PCIE4C的rq、rc接口实现,rq、rc为从机(FPGA)请求、主机(PC机)响应接口,FPGA将读/写地址报文通过rq接口通过握手方式发送到PC机,PC机将读地址对应的数据内容/写地址完成报文通过rc接口通过握手方式发送给FPGA。
rq接口
rq接口具有的信号及传输方向如图所示。需要注意的是,不同于标准PCIe报文格式,PCIE4C将部分PCIe报文头字段(描述符)放入tuser字段中。
同时,PCIE4C将剩余的PCIe报文头字段留在第一个传输tdata的前几个字节中,对于内存、IO、原子操作类型的PCIe报文,tdata头个传输字段划分如下图所示。
各字段解释可从产品手册找到。
对于128bit位宽AXIS流接口、DWORD对齐模式,一次写内存请求操作对应波形类似下图。
对于128bit位宽AXIS流接口,一次读内存请求操作对应波形类似下图。
rc接口
rc通道的接口信号如下图,当每次rq写请求操作结束后,FPGA侧会通过rc接口受到来自目标设备返回的写成功或写失败响应。
响应的每第一次传输的tdata的前几字节都被视为PCIe头字段(描述符),如下图
各字段解释可从产品手册找到。
对于128bit位宽AXIS流接口、DWORD对齐模式,一次读内存响应操作对应波形类似下图。
FPGA模块代码
FPGA部分代码包含positive_process和negative_process两个部分,其中negative_process为FPGA作为响应设备,处理其他设备发来的请求报文,已在 基于PCIE4C的数据传输(一)——寄存器读写访问 中介绍。positive_process为FPGA作为请求设备向PCIe总线发出请求报文,由其他PCIe设备(这里为根设备即主机)作出响应,一般用于进行大规模数据传输即DMA操作。
本文共利用PCIE4C IP核例化了四个功能设备,每个功能设备具有独立的ram区域。
1. DMA请求监听模块
这里列出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
2. DMA请求产生及处理模块
这里列出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
FPGA行为仿真
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报文打印出来。
PC驱动程序设计
作者使用的系统为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获取。