当前位置:   article > 正文

【PCIE】基于PCIE4C的数据传输(二)—— DMA_pcie dma

pcie dma

  本文继续基于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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

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]

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

  本文使用QuestaSim作为仿真器,Cocotb编译指令如下,需要在tb目录下进行,此外也支持VCS等其他仿真器(可参考Cocotb文档):

cp runsim.do.questa sim_build/runsim.do
make SIM=questa WAVES=1
vsim vsim.wlf
  • 1
  • 2
  • 3

  在终端界面,仿真器会将运行过程中所发送和接收的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");
      }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65

实机测试

  make编译驱动,使用insmod加载驱动后,dmesg查看调试信息,即printk的输出结果。

  测试完成后,使用rmmod卸载驱动,释放变量。

make
sudo insmod test_driver
sudo dmesg 
sudo rmmod test_driver
  • 1
  • 2
  • 3
  • 4

图片

图片

图片

工程文件

  完整工程可于同名公众号回复PCIE4C_DMA获取。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号