Xilinx FPGA PCIe-XDMA的教程,基于PCIe-XDMA IP核,基于Alveo U280 FPGA开发板,该示例涵盖加速核与BRAM交互、XDMA与BRAM交互、XDMA驱动加速核的启动与停止、主机代码的编写
前置保姆级知识可以参考:https://github.com/Reconfigurable-Computing/Xilinx-FPGA-PCIe-XDMA-Tutorial
这段代码实现了一个加速核功能,通过PCIe-XDMA启动。其主要流程是从BRAM中读取数据A,将数据分块处理,每次读取BUFFER_SIZE大小的数据到本地缓冲区,对缓冲区中的每个数据元素左移两位(相当于乘以4) ,最后将处理后的数据写回BRAM的C中,完成数据的处理与存储更新。其中,BRAM我们使用AXI4进行读写。
#include <hls_math.h>
#include <ap_fixed.h>
#include <hls_stream.h>
#include <ap_int.h>
#define BUFFER_SIZE 256
#define DATA_SIZE 1024
const unsigned int c_len = DATA_SIZE / BUFFER_SIZE;
const unsigned int c_size = BUFFER_SIZE;
extern "C"
void vecmul_top(int* c, int* a, const int n_elements) {
#pragma HLS INTERFACE mode=s_axilite port=n_elements
#pragma HLS INTERFACE mode=s_axilite port=return // 这个还是挺重要的,把加速核的启动信号放在s_axilite接口上,而不是电平信号,方便xdma直接启动加速器
#pragma HLS INTERFACE mode=m_axi port = c bundle = gmem0 offset=slave
#pragma HLS INTERFACE mode=m_axi port = a bundle = gmem0 offset=slave
int bufferA[BUFFER_SIZE];
for (int i = 0; i < n_elements; i += BUFFER_SIZE) {
#pragma HLS LOOP_TRIPCOUNT min = c_len max = c_len
int size = BUFFER_SIZE;
if (i + size > n_elements) size = n_elements - i;
readA:
for (int j = 0; j < size; j++) {
#pragma HLS LOOP_TRIPCOUNT min = c_size max = c_size
bufferA[j] = a[i + j];
}
vmul_writeC:
for (int j = 0; j < size; j++) {
#pragma HLS LOOP_TRIPCOUNT min = c_size max = c_size
c[i + j] = bufferA[j] << 2;
}
}
}在综合报告中的 HW Interfaces 板块可以看到综合后接口的信息:
加速核的geme0被综合成为m_axi,其他参数例如控制信号CTRL(AP_START,AP_DONE,AP_IDLE,AP_READY等)、C、A、n_elements等被综合成为s_axi_control(也就是s_axi_lite)。
其中solution1/syn/verilog/vecmul_top_control_s_axi.v文件中能看到s_axi_control的地址信息情况,如下列:
//------------------------Address Info-------------------
// 0x00 : Control signals
// bit 0 - ap_start (Read/Write/COH)
// bit 1 - ap_done (Read/COR)
// bit 2 - ap_idle (Read)
// bit 3 - ap_ready (Read/COR)
// bit 7 - auto_restart (Read/Write)
// bit 9 - interrupt (Read)
// others - reserved
// 0x04 : Global Interrupt Enable Register
// bit 0 - Global Interrupt Enable (Read/Write)
// others - reserved
// 0x08 : IP Interrupt Enable Register (Read/Write)
// bit 0 - enable ap_done interrupt (Read/Write)
// bit 1 - enable ap_ready interrupt (Read/Write)
// others - reserved
// 0x0c : IP Interrupt Status Register (Read/TOW)
// bit 0 - ap_done (Read/TOW)
// bit 1 - ap_ready (Read/TOW)
// others - reserved
// 0x10 : Data signal of c
// bit 31~0 - c[31:0] (Read/Write)
// 0x14 : Data signal of c
// bit 31~0 - c[63:32] (Read/Write)
// 0x18 : reserved
// 0x1c : Data signal of a
// bit 31~0 - a[31:0] (Read/Write)
// 0x20 : Data signal of a
// bit 31~0 - a[63:32] (Read/Write)
// 0x24 : reserved
// 0x28 : Data signal of n_elements
// bit 31~0 - n_elements[31:0] (Read/Write)
// 0x2c : reserved
// (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)- 注意:接下来流程需要sudo权限,并且避免在虚拟环境中运行vivado(最好直接使用/tools/Xilinx/Vivado/2023.1/bin/vivado类似这样启动vivado)!否则会出现找不到fpga开发板或者运行失败的情况!
建立blockdesign
在左侧 Flow Navigator → IP Integrator 中点 Create Block Design ,然后指定该 block design 的名称,保持默认即可。然后点 OK。
添加并配置 PCIe-XDMA IP
在 Diagram 中点上方的 "+" (Add IP) ,输入 xdma ,然后双击 "DMA/Bridge Subsystem for PCIe",如下图 。
然后就可以看到 Diagram 中出现了一个叫 xdma_0 的 IP 。双击这个 xdma_0 ,配置这个 IP 的参数,该 IP 的配置一共有6页。其中第二页如下图,你可以指定 PCIe 和 AXI 接口的相关信息。其中大多数选项与下图保持一致即可,但有一些选项你可以根据需要去修改:
- Lane Width 可以自由指定,因为 NetFPGA 的 PCIe 为 x8,我们可以取 x1, x4, x8 。本例中取 x1 。
- Maximum Link Speed 代表的是 PCIe 的速率,可以自由指定,2.5 GT/s 代表 PCIe Gen1, 5.0 GT/s 代表 PCIe Gen2,8.0 GT/s 代表 PCIe Gen3 。本例中取 2.5 GT/s
- Reference Clock Frequency 是指 PCIe 参考时钟的频率,由于 FPGA 使用的是来自 PCIe 插口的,由电脑主板提供的 PCIe 参考时钟,而 PCIe 规定该时钟频率为 100MHz 。因此这里取 100MHz 。
- AXI Data Width 是 AXI 总线中数据总线的宽度,也即一个周期最多可以读/写的比特数量。可以自由指定,但要和 AXI slave 保持一致。本例中取 64 bit。
- AXI Clock Frequency 是 AXI 总线的时钟频率,可以自由指定,只要 AXI slave 能工作在这个频率下就行。本例中取 125 MHz 。

第三页的ID Initial Values是自动生成,无需手动更改,Class Code Lookup Assistant按照如图手动更改。

第四页按照如图配置。要勾选上AXI Lite,以便PCIe-XDMA可以控制加速核的启动与暂停。

同理添加并配置 Constant、AXI Interconnect、AXI BRAM Controller、Block Memory Gengerator、加速IP核
AXI BRAM Controller:该 IP 的功能是充当 AXI master 和 “裸BRAM” 之间的桥梁,可以理解为进行一个协议转换——接收 AXI 总线的命令,把它转化为对 "裸BRAM" 的读写。
- Data Width : 是 AXI 总线中数据总线的宽度,也即一个周期最多可以读/写的比特数量。可以自由指定。这里取 32 bit。
- Memory Depth : 存储器深度,决定了 BRAM 的容量,在blockdesign不可手动调节,需通过address editor调节,取8K即代表这里的深度是2048。这里我们取 2048,则 BRAM 容量 = Memory Depth * Data Width = 2048 * 32 bits = = 8 K Bytes.(这里其实就对应这后面address editor的配置)。
把 PCIe 相关的信号引出到外界
分配 AXI 地址映射
在 blockdesign 中,只要有 AXI master 和 AXI slave 对接的情况,就需要进行地址分配 。因为一个 AXI master 可能对应多个 AXI slave ,我们需要分配这些 AXI slave 在该 AXI master 中对应的地址 ——即使只有一个 AXI slave。
我们切换到 Diagram 旁边的 Address Editor。按照如图配置:
在这里,我把AXI-Bram的Master Base Address设置成为了0xC0000000,换句话说,当 xdma_0 通过 AXI 总线对 0xC0000000这个地址进行读写时,读写的就是 axi_bram_ctrl_0 的起始位置。
另外,AXI-Bram的Range是8K,对应着AXI BRAM Controller的深度2048。
blockdesign 验证模块设计验证
我们切换回 Diagram ,然后点击上方的 "Validate Design" ,软件会帮我们检查该 blockdesign 中是否有错误。它只能检查出一些低级错误(比如 AXI 总线两侧的位宽不匹配),而功能性的正确性还需开发者自己保证。
生成 blockdesign 的 HDL Wrapper
然后它就会生成一个名为 design_1_wrapper 的 Verilog 源文件。双击这个文件可以看到它的代码,我们发现它的模块输入输出接口就是我们之前引出到 blockdesign 的外部的那些信号(也即 PCIe 时钟、复位、信号),如下:
注意到在这里,PCIe 的参考驱动时钟 sys_clk_0 是单端的,而 FPGA 引脚上收到的的 PCIe 参考时钟实际上是差分的。因此这里还需要一个单端转差分的 clock buffer 。另外,还需要对复位信号 sys_rst_n_0 进行 IO buffer 。这一步将在以下 Verilog 顶层模块中实现。
下一步我们要编写一个 Verilog 模块,来调用这个 blockdesign 。同时实现 clock-buffer 和 IO-buffer 。
给工程添加一个 Verilog 文件,名为 fpga_top.v ,然后在其中编写代码如下。注意到:
- fpga_top.v 调用了我们在上一步生成的 block design 的 HDL wrapper 。
- fpga_top.v 调用了 IBUFDS_GTE4 和 IBUF ,分别是 clock-buffer 和 IO-buffer ,它们是 Xilinx 提供的原语 (primitive) ,对应着 Xilinx FPGA 种的资源。在 Vivado 的 Verilog 编程中不需要引入任何文件就能调用,但不能移植到其它 FPGA 厂商的 Verilog 工程中。
module fpga_top(
input wire i_pcie_rstn,
input wire i_pcie_refclkp, i_pcie_refclkn,
input wire [0:0] i_pcie_rxp, i_pcie_rxn,
output wire [0:0] o_pcie_txp, o_pcie_txn
);
wire pcie_rstn;
wire pcie_refclk;
wire pcie_refclk_gt;
wire pcie_refclk_div2;
// Reset input buffer ----------------------------------------------
IBUF sys_reset_n_ibuf (
.I ( i_pcie_rstn ),
.O ( pcie_rstn )
);
// Ref clock input buffer ----------------------------------------------
IBUFDS_GTE4 #(
.REFCLK_HROW_CK_SEL(2'b00)
) refclk_ibuf (
.CEB (1'b0), // 常使能
.I (i_pcie_refclkp), // 差分时钟P
.IB (i_pcie_refclkn), // 差分时钟N
.O (pcie_refclk_gt), // GT直接输出
.ODIV2(pcie_refclk_div2) // 分频输出
);
assign pcie_refclk = pcie_refclk_div2;
// block design's top (the HDL wrapper) ----------------------------------------------
design_1_wrapper design_1_wrapper_i (
.pcie_mgt_0_rxn ( i_pcie_rxn ),
.pcie_mgt_0_rxp ( i_pcie_rxp ),
.pcie_mgt_0_txn ( o_pcie_txn ),
.pcie_mgt_0_txp ( o_pcie_txp ),
.sys_clk_0 ( pcie_refclk ),
.sys_clk_gt_0 ( pcie_refclk_gt ),
.sys_rst_n_0 ( pcie_rstn )
);
endmodulecreate_clock -name sys_clk -period 10 [get_ports i_pcie_refclkp]
set_false_path -from [get_ports i_pcie_rstn]
set_property PULLUP true [get_ports i_pcie_rstn]
set_property IOSTANDARD LVCMOS18 [get_ports i_pcie_rstn]
set_property PACKAGE_PIN BH26 [get_ports i_pcie_rstn]
set_property CONFIG_VOLTAGE 1.8 [current_design]
set_property LOC [get_package_pins -of_objects [get_bels [get_sites -filter {NAME =~ *COMMON*} -of_objects [get_iobanks -of_objects [get_sites GTYE4_CHANNEL_X1Y7]]]/REFCLK0P]] [get_ports i_pcie_refclkp]
set_property LOC [get_package_pins -of_objects [get_bels [get_sites -filter {NAME =~ *COMMON*} -of_objects [get_iobanks -of_objects [get_sites GTYE4_CHANNEL_X1Y7]]]/REFCLK0N]] [get_ports i_pcie_refclkn]
set_false_path -to [get_pins -hier *sync_reg[0]/D]- 有用的东西:烧录完恢复原有的固件可以使用:
boot_hw_device [lindex [get_hw_devices] 0]
查看 PCIe XDMA 设备是否被识别
lspci | grep -i xilinx
运行 lspci 命令来看看 PCIe 设备是否被正常识别。如果发现其中有 "Memory controller: Xilinx..." ,说明识别成功。
- 注意:请尽量使用sudo来操控命令
https://github.com/Xilinx/dma_ip_drivers
可以跟着这个readme走: https://github.com/Xilinx/dma_ip_drivers/tree/master/XDMA/linux-kernel
也可以:
- cd dma_ip_drivers/XDMA/linux-kernel
- cd xdma
- sudo make install
- cd tools
- sudo make
- 如果想重新编译
- sudo rm *.o
- sudo rm *.ko
- sudo make install
- cd tests
- sudo ./load_driver.sh
如果驱动加载成功,则显示 "DONE" 。
然后我们运行以下命令,会发现 /dev 目录下出现一系列 xdma 设备。
$ ls /dev/xdma*
...
/dev/xdma0_c2h_0
...
/dev/xdma0_h2c_0
- 若无法在vivado找到板子,首先要检查是否在虚拟环境,确保不要在虚拟环境,其次是不是sudo打开
- 安装xdma及其容易出现问题,首先应该检查所使用的命令是否是sudo运行
- 其次若 sudo ./load_driver出现
可以尝试运行
- sudo rmmod xdma
- sudo modprobe xdma
来重新安装xdma,或许就可以了,有时候使用load_driver.sh脚本不会成功,但是手动使用这两个命令行就会成功,暂时原因不明。
驱动加载成功的标志是运行ls /dev/xdma*出现该有的设备,如:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#define min(a, b) ((a) < (b) ? (a) : (b))
//------------------------Address Info-------------------
// 0x00 : Control signals
// bit 0 - ap_start (Read/Write/COH)
// bit 1 - ap_done (Read/COR)
// bit 2 - ap_idle (Read)
// bit 3 - ap_ready (Read/COR)
// bit 7 - auto_restart (Read/Write)
// bit 9 - interrupt (Read)
// others - reserved
// 0x04 : Global Interrupt Enable Register
// bit 0 - Global Interrupt Enable (Read/Write)
// others - reserved
// 0x08 : IP Interrupt Enable Register (Read/Write)
// bit 0 - enable ap_done interrupt (Read/Write)
// bit 1 - enable ap_ready interrupt (Read/Write)
// others - reserved
// 0x0c : IP Interrupt Status Register (Read/TOW)
// bit 0 - ap_done (Read/TOW)
// bit 1 - ap_ready (Read/TOW)
// others - reserved
// 0x10 : Data signal of c
// bit 31~0 - c[31:0] (Read/Write)
// 0x14 : Data signal of c
// bit 31~0 - c[63:32] (Read/Write)
// 0x18 : reserved
// 0x1c : Data signal of a
// bit 31~0 - a[31:0] (Read/Write)
// 0x20 : Data signal of a
// bit 31~0 - a[63:32] (Read/Write)
// 0x24 : reserved
// 0x28 : Data signal of n_elements
// bit 31~0 - n_elements[31:0] (Read/Write)
// 0x2c : reserved
// (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)
//------------------------Address Info-------------------
// bram设备地址偏移量,来源于在vivado中自己的手动设置,具体是address editor中设置的
#define BRAM_S_AXI_BASE 0xC0000000
// 控制寄存器的地址偏移量,来源于在vivado中自己的手动设置,具体是address editor中设置的
#define CONTROL_S_AXI_BASE 0x00000000
// 寄存器地址偏移量(基于AXI-Lite地址映射),来源于上面👆,上面的信息来源于vitis HLS生成的s-axi的verilog文件:solution1/impl/verilog/vecmul_top_control_s_axi.v
#define CTRL_REG CONTROL_S_AXI_BASE+0x00 // 控制寄存器,控制加速核的启动和状态,不同的位表示不同的状态和控制信号
#define C_LOW_REG CONTROL_S_AXI_BASE+0x10 // C寄存器低32位,存储加速核的变量C的地址
#define C_HIGH_REG CONTROL_S_AXI_BASE+0x14 // C寄存器高32位,存储加速核的变量C的地址
#define A_LOW_REG CONTROL_S_AXI_BASE+0x1c // A寄存器低32位,存储加速核的变量A的地址
#define A_HIGH_REG CONTROL_S_AXI_BASE+0x20 // A寄存器高32位,存储加速核的变量A的地址
#define N_ELEMENTS_REG CONTROL_S_AXI_BASE+0x28 // N元素寄存器,存储加速核的变量N的值
// 控制寄存器位定义,对应上面👆的CTRL_REG寄存器
#define AP_START_BIT (1 << 0)
#define AP_DONE_BIT (1 << 1)
#define AP_IDLE_BIT (1 << 2)
// description : read data from device to local memory (buffer), (i.e. device-to-host),主要服务master axi
// parameter :
// dev_fd : device instance
// addr : source address in the device
// buffer : buffer base pointer
// size : data size
// return:
// int : 0=success, -1=failed
int dev_read (int dev_fd, uint64_t addr, void *buffer, uint64_t size) {
if ( addr != lseek(dev_fd, addr, SEEK_SET) ) // seek
return -1; // seek failed
if ( size != read(dev_fd, buffer, size) ) // read device to buffer
return -1; // read failed
return 0;
}
// description : write data from local memory (buffer) to device, (i.e. host-to-device),主要服务master axi
// parameter :
// dev_fd : device instance
// addr : target address in the device
// buffer : buffer base pointer
// size : data size
// return:
// int : 0=success, -1=failed
int dev_write (int dev_fd, uint64_t addr, void *buffer, uint64_t size) {
if ( addr != lseek(dev_fd, addr, SEEK_SET) ) // seek
return -1; // seek failed
if ( size != write(dev_fd, buffer, size) ) // write device from buffer
return -1; // write failed
return 0;
}
// m_axi_lite写操作,主要服务master axi lite
void write_reg(void* base, int offset, uint32_t val) {
*((volatile uint32_t*)(base + offset)) = val;
}
// m_axi_lite读操作,主要服务master axi lite
uint32_t read_reg(void* base, int offset) {
return *((volatile uint32_t*)(base + offset));
}
int main() {
int n_elements = 1024;
int fd_regs;
int fd_c2h, fd_h2c;
void *buffer_regs;
volatile uint32_t *buffer_c2h;
volatile uint32_t *buffer_h2c;
const char *device_regs = "/dev/xdma0_user"; // XDMA用户空间设备
const char *device_c2h = "/dev/xdma0_c2h_0"; // XDMA C2H设备
const char *device_h2c = "/dev/xdma0_h2c_0"; // XDMA H2C设备
/*=========打开设备,包括XDMA用户空间设备,XDMA C2H设备和DMA H2C设备=========*/
if ((fd_regs = open(device_regs, O_RDWR)) < 0) {
printf("打开用户空间设备设备失败\n");
close(fd_regs);
return EXIT_FAILURE;
}
if((fd_c2h = open(device_c2h, O_RDWR)) < 0) {
printf("打开C2H设备失败\n");
close(fd_c2h);
return EXIT_FAILURE;
}
if((fd_h2c = open(device_h2c, O_RDWR)) < 0) {
printf("打开H2C设备失败\n");
close(fd_h2c);
return EXIT_FAILURE;
}
/*=======在host主机分配缓冲空间buffer========*/
buffer_regs = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd_regs, 0);
if (buffer_regs == MAP_FAILED) {
printf("*** ERROR: mmap failed\n");
return EXIT_FAILURE;
}
buffer_h2c = (uint32_t*)malloc(1024 * 1024);
if (buffer_h2c == NULL) {
printf("*** ERROR: buffer_h2c failed to allocate buffer\n");
return EXIT_FAILURE;
}
buffer_c2h = (uint32_t*)malloc(1024 * 1024);
if (buffer_c2h == NULL) {
printf("*** ERROR: buffer_c2h failed to allocate buffer\n");
return EXIT_FAILURE;
}
/*========初始化H2C缓冲区========*/
for (int i = 0; i < n_elements; i++) {
buffer_h2c[i] = i; // 填充数据
}
if ( dev_write(fd_h2c, BRAM_S_AXI_BASE, buffer_h2c, n_elements * 4) ) {
printf("*** ERROR: failed to write\n");
return EXIT_FAILURE;
}
printf("H2C缓冲区: \n");
for (int i = 0; i < min(5 * 8, n_elements); i++) {
printf("\t%u ", buffer_h2c[i]);
if ((i + 1) % 8 == 0) {
printf("\n");
}
}
printf("\n");
/*========初始化加速核的参数列表=========*/
write_reg(buffer_regs, C_LOW_REG, 0xC0000000); // 写入bram的第一个地址,因为本示例中bram在address editor中配置的第一个地址是0xC0000000
write_reg(buffer_regs, C_HIGH_REG, 0x00000000);
write_reg(buffer_regs, A_LOW_REG, 0xC0000000); // 写入bram的第一个地址,因为本示例中bram在address editor中配置的第一个地址是0xC0000000
write_reg(buffer_regs, A_HIGH_REG, 0x00000000);
write_reg(buffer_regs, N_ELEMENTS_REG, n_elements);
printf("寄存器初始化完成...\n");
write_reg(buffer_regs, CTRL_REG, AP_START_BIT);
printf("加速器已启动...\n");
/*=======等待加速器完成===========*/
while (1) {
uint32_t ctrl_reg = read_reg(buffer_regs, CTRL_REG);
printf("==================\n");
printf("C寄存器状态: %08X%08X\n", read_reg(buffer_regs, C_HIGH_REG), read_reg(buffer_regs, C_LOW_REG));
printf("A寄存器状态: %08X%08X\n", read_reg(buffer_regs, A_HIGH_REG), read_reg(buffer_regs, A_LOW_REG));
printf("N元素寄存器状态: %u\n", read_reg(buffer_regs, N_ELEMENTS_REG));
printf("控制寄存器状态: \n");
printf("\tap_start: %d, ap_done: %d\n", (ctrl_reg & AP_START_BIT) != 0, (ctrl_reg & AP_DONE_BIT) != 0);
printf("\tap_idle: %d, ap_ready: %d\n", (ctrl_reg & AP_IDLE_BIT) != 0, (ctrl_reg & AP_DONE_BIT) != 0);
usleep(100); // 100us轮询间隔,1000000是1秒
if (ctrl_reg & AP_DONE_BIT) {
printf("加速器计算完成!\n");
printf("==================\n");
break;
}
}
/*========取出加速器计算结果===========*/
if ( dev_read(fd_c2h, BRAM_S_AXI_BASE, buffer_c2h, n_elements * 4) ) {
printf("*** ERROR: failed to read\n");
return EXIT_FAILURE;
}
printf("C2H缓冲区:\n");
for (int i = 0; i < min(5 * 8, n_elements); i++) {
printf("\t%u ", buffer_c2h[i]);
if ((i + 1) % 8 == 0) {
printf("\n");
}
}
printf("\n");
/*========释放空间==========*/
close(fd_c2h);
close(fd_h2c);
close(fd_regs);
free((void*)buffer_c2h);
free((void*)buffer_h2c);
if (munmap(buffer_regs, 4096) < 0) {
printf("*** ERROR: munmap failed\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}若host文件名为xdma_rw.c,则编译命令是:gcc xdma_rw.c -o xdma_rw,运行命令是sudo ./xdma_rw,以下是成功结果:





















