Xilinx FPGA PCIe | XDMA 驱动编译与中断测试

注:本文为 "XDMA 驱动" 相关合辑。

图片清晰度受引文原图所限。

略作重排,未整理去重。

如有内容异常,请看原文。


一文掌握 XDMA IP 配置与应用方法

合芯电子 原创 | 修改时间:2024-08-06 13:12:07

一、相关知识

  1. 上位机通过 PCIe 接口向端点设备传输数据时,XDMA 完成数据解包与指令解析,提取读写指令及对应地址,随后对 DDR 执行读写操作。操作结果经由 AXI 接口回传至 XDMA,由 XDMA 完成数据组包后通过物理层发送,实现数据的 DMA 传输控制。
  2. 物理地址 (绝对地址) 计算公式:
    物理地址 = 段地址 × 16 + 偏移地址

数据读写分为两类,分别为业务数据读写与配置数据读写。业务数据读写过程中,DMA 借助 MIG 模块完成 DDR 的数据交互。配置数据读写依托 AXI-lite 总线实现与 BRAM 的通信,XDMA 将 PCIe 配置信息存储至 BRAM 内部。执行配置信息读写时,系统将主机地址映射至用户逻辑地址,并结合偏移地址完成地址解析。

DDR 空间无需额外地址偏移配置,该类配置会缩减可用内存容量。DDR 遵循写入位置与读取位置一一对应的规则,相关地址参数需设置为 0。

IP 内置接口说明

  • AXI-MM 接口:适用于高带宽、大吞吐量的数据传输场景。
  • AXI4-Stream 接口:面向低延迟流式数据传输。
  • AXI Lite Master 接口:为 AXI 简化版本,用于小容量数据交互,常应用于外设寄存器配置等轻量化传输场景。
  • DMA bypass:属于标准 PCIe 传输模式。数据传输长度较小时,其运行效率与 DMA 模式基本持平;传输长度较大时,DMA 模式性能更优。PCIe DMA bypass 占用独立 BAR 空间,由主机直接发起操作,相较于 DMA 模式会占用更多主机资源,在主机资源充足的前提下可选用该模式。

主机可通过两类接口直接访问用户逻辑:

  1. AXI4-Lite Master 配置接口:位宽固定为 32 位,用于非高实时性需求下的用户配置寄存器与状态寄存器访问。
  2. AXI Memory Mapped Master CQ 旁路(Bypass)端口:位宽与 DMA 通道数据位宽保持一致,用于点对点传输场景下的用户内存高带宽访问。

用户逻辑可通过 AXI4-Lite Slave 配置接口读取 XDMA 内部配置寄存器与状态寄存器,该接口收到的访问请求不会转发至 PCI Express 链路。

二、XDMA 接口说明

DMA/Bridge Subsystem for PCI Express (PCIe),简称 XDMA

端口名称 类型 ~~~~~ 说明
sys_clk 输入 由 Bank 差分时钟转换为单端时钟,经 BUFG 模块接入 IP 内部
sys_rst_n 输入 主机提供的冷复位信号,硬件设计中包含冷复位、温复位、热复位三类复位形式
pcie_cfg_mgmt 输入 PCIe 配置管理接口,支持配置空间地址访问、指定地址数据读写等操作
usr_irq_req 输入 用户自定义中断请求信号,用于向计算机发送中断标识,该信号不会触发 CPU 中断异常
M_AXI 输出 AXI 总线主机信号,为高速内存访问接口
M_AXI_ILTE 输出 AXI 接口链路错误状态信号,用于上报 AXI 总线链路、接口运行异常
pcie_mgt 输出 PCIe 物理层管理相关功能接口,涵盖 MDIO 接口、时钟与数据恢复等物理层管控任务
usr_lnk_up 输出 PCIe 链路状态指示信号,高电平代表物理链路初始化完成,可正常传输数据
axi_aclk 输出 全局同步时钟,为 AXI 读通道、写通道、控制通道提供时序基准,需驱动所有关联 AXI 互联模块的 aclk 信号,属于衍生时钟
axi_aresetn 输出 AXI 总线异步低电平复位信号,与 axi_aclk 配合使用,负责复位所有 AXI 相关模块与互联电路
usr_irq_ack 输出 用户中断应答信号,代表 PCIe 侧已完成中断发送
msi_enable 输出 MSI 中断功能使能信号,高电平表示开启 MSI 中断模式
msi_vector_width 输出 MSI 中断向量字段位宽标识,用于指示当前 MSI 中断向量的有效位数

三、IP 详细配置

3.1 速率与接口选择配置

PCIe 工作速率与用户逻辑侧 AXI 总线速率需保持匹配。

  • PCIe 侧速率:8 lane × 5 GT/s = 40 Gbit/s
  • 逻辑侧 AXI 速率:128 bit × 250 MHz = 3200 Mbit/s,结合字节换算关系 10 B/8 B,等效速率为 40 Gbit/s

Interface width 指代 AXI 接口位宽。Recommended FrequencyOptional Frequency 对应器件标称最高工作频率,实际工程配置可脱离标称参数执行。


3.2 PCIE ID 配置

器件 ID 由多项参数共同决定:

  • 器件系列:9 对应 UltraScale+ 系列,8 对应 UltraScale 系列,7 对应 7 系列器件
  • 工作模式:EP 模式或 RP 模式
  • 链路宽度:1 代表 x1,2 代表 x2,4 代表 x4,8 代表 x8,F 代表 x16
  • 链路速率:1 代表 Gen1,2 代表 Gen2,3 代表 Gen3,4 代表 Gen4

3.3 基地址寄存器配置

  1. 计算机内存地址与 PCIe 域地址存在映射关系,PCIe to AXI Translation 参数配置值,需要与 Address EditorAXI_Lite 配置的地址空间大小保持一致。

    PCIe to AXI Translation 用于实现 PCIe 地址至 AXI 地址的转换。示例:主机侧 BAR 地址为 0,用户逻辑侧 AXI Lite 总线基地址为 0x40000000。主机访问用户逻辑时,XDMA 会依据配置参数,将主机侧地址 0 转换为 AXI Lite 总线地址 0x40000000

    该参数存在两种配置方式:第一种为手动指定转换地址,再修改 AXI Lite 总线偏移地址;第二种为先确定 AXI Lite 总线偏移地址,再以此为依据设置转换地址。若将 AXI Lite 总线偏移地址设置为 0x40000000,则 PCIe to AXI Translation 同步配置为 0x40000000

  2. DMA 旁路功能需谨慎启用。该模式可访问用户逻辑寄存器与 FPGA 外接 DDR 存储设备,传输过程不启用 DMA 引擎,实现 PCIe 直通访问,适用于低延迟数据传输场景。

  3. Prefetchable(可预取)说明:预取机制是将内存数据提前缓存至缓冲区,以此提升读取效率。当两级 PCIe 总线通过网桥级联时,主机访问远端总线内存时,网桥会预先读取数据并存放至本地缓冲区。

    不可预取内存的数据移入网桥缓冲区后,原始内存区域的数据会被清除。若主机未及时读取缓冲区数据,会造成数据丢失。可预取内存不存在该类问题,数据安全性更高。

3.4 中断配置

  • 中断向量配置规则:

    • MSI 中断与 MSI-X 中断仅可选择其一,同时启用会触发配置报错。若选择 MSI 中断,可额外搭配 Legacy 中断;若选择 MSI-X 中断,必须关闭 MSI 中断,且 Legacy 中断需设置为 None。7 系列器件遵循上述规则,UltraScale 系列器件支持三类中断同时启用。

    • PCIe 标准中断包含 MSI-XMSILegacy 三种类型,user interrupt 为 FPGA 逻辑侧专属中断接口。中断响应优先级排序:MSI-X > MSI > Legacy

  • 中断信号交互规则:

    • 中断请求信号 req 需持续保持高电平,直至收到应答信号 ack 后方可拉低,随后发起下一次中断请求。逻辑侧中断与主机侧中断按优先级匹配,同一时刻仅能生效一类中断。

3.5 DMA 选项卡配置

DMA 通道用于区分不同数据源与数据类型,例如光纤采集数据、ADC 采样数据等。

多通道配置在 AXI Stream 模式下作用显著,该模式下多通道可分别对接独立数据源;AXI Memory Mapped 模式中,通道数量对整体运行效果影响较小。

3.6 共享时钟配置

单个 MGT-BANK 的参考时钟可供给相邻 3 个 MGT-BANK 使用,PCIe 输入参考时钟支持共享配置。本应用场景无时钟共享需求,因此不开启对应功能。

补充说明:XDMA 数据传输流程

XDMA 借助 PCIe 向主机内存写入数据的流程如下:

  1. 向操作系统申请内存空间,获取该内存的物理地址(Physical Address);
  2. 将内存物理地址与空间大小写入 XDMA 配置寄存器;
  3. 写入控制寄存器,启动 DMA 传输;
  4. XDMA 按照 PCIe 事务层数据包(Transaction Layer Packet, TLP)格式封装数据与目标内存地址,完成组包发送;
  5. 主机端 Root Complex(RC) 接收 TLP,将有效数据写入指定物理地址;TLP 的可靠传输由 PCIe 数据链路层 的 ACK/NAK 机制保障;
  6. XDMA 继续封装并发送下一组数据包,直至全部数据传输完成;
  7. 传输结束后,XDMA 发送 IRQ 中断信号,通知主机 DMA 传输任务已完成。

XDMA Linux 驱动编译与 ARM 平台部署(中断检测及测试程序)

拱-卒 2025-07-25 21:41:08

1 驱动源码获取

XDMA 驱动源码官方下载地址:https://github.com/Xilinx/dma_ip_drivers

下载仓库 master 分支源码,本文适配 XDMA IP 核 4.1 版本,非对应版本会出现功能异常。

2 驱动适配与编译

本节内容参考: xdma 驱动编译(给 arm 使用) 见下文

2.1 配置 Makefile 文件

修改文件路径:dma_ip_drivers-master/XDMA/linux-kernel/xdma/Makefile

注释原有条件编译代码段(包含末尾 endif 语句),在注释位置追加交叉编译配置内容,修改内容如下:

makefile 复制代码
# ifneq ($(KERNELRELEASE),)
# 	$(TARGET_MODULE)-objs := libxdma.o xdma_cdev.o cdev_ctrl.o cdev_events.o cdev_sgdma.o cdev_xvc.o cdev_bypass.o xdma_mod.o xdma_thread.o
# 	obj-m := $(TARGET_MODULE).o
# else
# 	BUILDSYSTEM_DIR:=/lib/modules/$(shell uname -r)/build
# 	PWD:=$(shell pwd)
# endif

$(TARGET_MODULE)-objs := libxdma.o xdma_cdev.o cdev_ctrl.o cdev_events.o cdev_sgdma.o cdev_xvc.o cdev_bypass.o xdma_mod.o xdma_thread.o
obj-m := $(TARGET_MODULE).o
BUILDSYSTEM_DIR:=/home/debian/Desktop/xiaguangbo/project/rk3588/project/kernel
PWD:=$(shell pwd)

说明:BUILDSYSTEM_DIR 填写目标平台对应的 Linux 内核源码路径。

2.2 执行驱动编译

终端执行环境变量配置与编译指令,指定架构与交叉编译器:

bash 复制代码
export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
make

3 配套测试工具编译

3.1 配置工具目录 Makefile

修改文件路径:dma_ip_drivers-master/XDMA/linux-kernel/tools/Makefile,指定交叉编译工具链:

makefile 复制代码
# CC ?= gcc
CC = aarch64-linux-gnu-gcc

3.2 执行工具编译

bash 复制代码
make

编译完成后,可执行 file 文件名 指令校验程序架构。若架构非 aarch64,需重新检查 Makefile 编译器配置。

4 XDMA 中断检测测试程序

4.1 中断检测程序源码

新建 pcie_irq.c 文件,写入如下代码:

c 复制代码
#include <assert.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdarg.h>
#include <syslog.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/sysinfo.h>
#include <dirent.h>

/* 大小端转换宏定义 */
/* ltoh: little to host */
/* htol: big to host */
#if __BYTE_ORDER == __LITTLE_ENDIAN
#  define ltohl(x)       (x)
#  define ltohs(x)       (x)
#  define htoll(x)       (x)
#  define htols(x)       (x)
#elif __BYTE_ORDER == __BIG_ENDIAN
#  define ltohl(x)     __bswap_32(x)
#  define ltohs(x)     __bswap_16(x)
#  define htoll(x)     __bswap_32(x)
#  define htols(x)     __bswap_16(x)
#endif

#define MAP_SIZE  (1024 * 1024UL)
#define MAP_MASK  (MAP_SIZE - 1)

static void *user_base;
static int start_en;
unsigned int user_irq_ack;

/**
 * @brief  内存映射函数
 * @param  fd        文件描述符
 * @param  mapsize   映射空间大小
 * @return 映射后的虚拟地址
 */
static void *mmap_control(int fd, long mapsize)
{
	void *vir_addr;
	vir_addr = mmap(0, mapsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	return vir_addr;
}

/**
 * @brief  寄存器写操作
 * @param  address   寄存器偏移地址
 * @param  val       待写入数据
 */
static void user_write(unsigned int address, unsigned int val)
{
    unsigned int writeval = htoll(val);
    *((unsigned int *)(user_base + address)) = writeval;
}

/**
 * @brief  寄存器读操作
 * @param  address   寄存器偏移地址
 * @return 读取到的寄存器数据
 */
static unsigned int user_read(unsigned int address)
{
    unsigned int read_result = *((unsigned int *)(user_base + address));
    read_result = ltohl(read_result);
    return read_result;
}

/**
 * @brief  事件0中断处理线程
 */
void *event0_process()
{
	int val;
	int h_event0;
	h_event0 = open("/dev/xdma0_events_0", O_RDWR | O_SYNC);
	if (h_event0 < 0)
	{
		printf("open event0 error\n");
	}
	else
	{
		printf("open event0\n");
		while (1)
		{
			if (start_en)
			{
				read(h_event0, &val, 4);
				if (val == 1)
					printf("event_0 done!\n");
				else
					printf("event_0 timeout!\n");
			}
			usleep(1);
		}
		close(h_event0);
	}
    return NULL;
}

/**
 * @brief  事件1中断处理线程
 */
void *event1_process()
{
	int val;
	int h_event1;
	h_event1 = open("/dev/xdma0_events_1", O_RDWR | O_SYNC);
	if (h_event1 < 0)
	{
		printf("open event1 error\n");
	}
	else
	{
		printf("open event1\n");
		while (1)
		{
			if (start_en)
			{
				read(h_event1, &val, 4);
				if (val == 1)
					printf("event_1 done!\n");
				else
					printf("event_1 timeout!\n");
			}
			usleep(1);
		}
		close(h_event1);
	}
    return NULL;
}

/**
 * @brief  事件2中断处理线程
 */
void *event2_process()
{
	int val;
	int h_event2;
	h_event2 = open("/dev/xdma0_events_2", O_RDWR | O_SYNC);
	if (h_event2 < 0)
	{
		printf("open event2 error\n");
	}
	else
	{
		printf("open event2\n");
		while (1)
		{
			if (start_en)
			{
				read(h_event2, &val, 4);
				if (val == 1)
					printf("event_2 done!\n");
				else
					printf("event_2 timeout!\n");
			}
			usleep(1);
		}
		close(h_event2);
	}
    return NULL;
}

/**
 * @brief  事件3中断处理线程
 */
void *event3_process()
{
	int val;
	int h_event3;
	h_event3 = open("/dev/xdma0_events_3", O_RDWR | O_SYNC);
	if (h_event3 < 0)
	{
		printf("open event3 error\n");
	}
	else
	{
		printf("open event3\n");
		while (1)
		{
			if (start_en)
			{
				read(h_event3, &val, 4);
				if (val == 1)
					printf("event_3 done!\n");
				else
					printf("event_3 timeout!\n");
			}
			usleep(1);
		}
		close(h_event3);
	}
    return NULL;
}

int main(int argc, char* argv[])
{
	static int h_c2h0;
	static int h_h2c0;
	static int h_user;
	pthread_t t_event0;
	pthread_t t_event1;
	pthread_t t_event2;
	pthread_t t_event3;

	char* user_name  = "/dev/xdma0_user";
	char* c2h0_name  = "/dev/xdma0_c2h_0";
	char* h2c0_name  = "/dev/xdma0_h2c_0";

	start_en = 0;

	h_c2h0 = open(c2h0_name, O_RDWR | O_NONBLOCK);
	if (h_c2h0 < 0)
	{
		printf("open c2h0 error\n");
	}

	h_h2c0 = open(h2c0_name, O_RDWR);
	if (h_h2c0 < 0)
	{
		printf("open h2c0 error\n");
	}

	h_user = open(user_name, O_RDWR | O_SYNC);
	if (h_user < 0)
	{
		printf("open user error\n");
	}

	user_base = mmap_control(h_user, MAP_SIZE);
	user_write(0x00000, 0xf);

	/* 创建中断监听线程 */
	pthread_create(&t_event0, NULL, event0_process, NULL);
	pthread_create(&t_event1, NULL, event1_process, NULL);
	pthread_create(&t_event2, NULL, event2_process, NULL);
	pthread_create(&t_event3, NULL, event3_process, NULL);

	usleep(100);
	user_irq_ack = 0xffff0000;
	user_write(0x00004, user_irq_ack);  // 使能中断
	start_en = 1;
	printf("start\n");

	/* 等待线程执行 */
	pthread_join(t_event0, NULL);
	pthread_join(t_event1, NULL);
	pthread_join(t_event2, NULL);
	pthread_join(t_event3, NULL);

	user_irq_ack = 0x00000000;
	user_write(0x00004, user_irq_ack);  // 关闭中断

	close(h_c2h0);
	close(h_h2c0);
	close(h_user);

	return 0;
}

4.2 编译中断测试程序

使用交叉编译器完成静态编译,链接线程库:

bash 复制代码
aarch64-linux-gnu-gcc pcie_irq.c -o pcie_irq -static -lpthread

XDMA AXI-Stream 回环测试

作者 :xiaguangbo

修订时间:2024-07-03 17:07:15

本文基于 FPGA 完成 XDMA AXI-Stream 回环逻辑设计,包含 IP 配置、Verilog HDL 代码、约束文件,同时提供 Linux 平台 Rust 测试程序,整套工程在 RK3588 设备完成功能验证。

1 XDMA AXI-Stream 回环逻辑

完成 XDMA AXI-Stream IP 配置并生成工程后,即可实现数据回环功能。

数据流向:PCIe RX → AXI-Stream Master → AXI-Stream Slave → PCIe TX

业务逻辑说明:

  1. 单独执行读操作或单独执行写操作会触发超时,需采用双线程分别执行读写操作。
  2. 主机发起读请求后,PCIe TX 侧的 AXI-Stream Slave 信号 ready 才会置有效,PCIe RX 侧的 AXI-Stream Master 方可向 Slave 传输数据。

2 PCIe 与 XDMA IP 配置

配置界面参考如下图片资源,工程必须开启 MSIX 功能,未开启会频繁出现超时问题。

3 FPGA 逻辑代码

3.1 功能逻辑模块 work.v

verilog 复制代码
module work #(
    parameter C_DATA_WIDTH = 64
) (
    // AXI streaming 接口
    output wire [C_DATA_WIDTH-1:0]   s_axis_c2h_tdata_0,
    output wire                      s_axis_c2h_tlast_0,
    output wire                      s_axis_c2h_tvalid_0,
    input  wire                      s_axis_c2h_tready_0,
    output wire [C_DATA_WIDTH/8-1:0] s_axis_c2h_tkeep_0,

    input  wire [C_DATA_WIDTH-1:0]   m_axis_h2c_tdata_0,
    input  wire                      m_axis_h2c_tlast_0,
    input  wire                      m_axis_h2c_tvalid_0,
    output wire                      m_axis_h2c_tready_0,
    input  wire [C_DATA_WIDTH/8-1:0] m_axis_h2c_tkeep_0,

    // 时钟与复位
    input  wire sys_rst_n,
    input  wire user_resetn,
    input  wire user_clk,
    input  wire user_lnk_up,

    // LED 指示灯
    output wire [3:0] leds
);

reg [25:0] user_clk_heartbeat;

assign sys_resetn = sys_rst_n;

// LED 信号赋值
assign leds[0] = sys_resetn;
assign leds[1] = user_resetn;
assign leds[2] = user_lnk_up;
assign leds[3] = user_clk_heartbeat[25];

// AXI-Stream 数据回环
assign s_axis_c2h_tdata_0  = m_axis_h2c_tdata_0;
assign s_axis_c2h_tlast_0  = 0;
assign s_axis_c2h_tvalid_0 = m_axis_h2c_tvalid_0;
assign s_axis_c2h_tkeep_0  = m_axis_h2c_tkeep_0;
assign m_axis_h2c_tready_0 = s_axis_c2h_tready_0;

// 心跳计数器
always @(posedge user_clk) begin
    if(!sys_resetn) begin
        user_clk_heartbeat <= 26'd0;
    end else begin
        user_clk_heartbeat <= user_clk_heartbeat + 1'b1;
    end
end

endmodule

3.2 顶层模块 main.v

verilog 复制代码
module main #(
    parameter PL_LINK_CAP_MAX_LINK_WIDTH = 2,
    parameter C_DATA_WIDTH               = 64
) (
    // PCIe 差分接口
    output [(PL_LINK_CAP_MAX_LINK_WIDTH - 1) : 0] pci_exp_txp,
    output [(PL_LINK_CAP_MAX_LINK_WIDTH - 1) : 0] pci_exp_txn,
    input  [(PL_LINK_CAP_MAX_LINK_WIDTH - 1) : 0] pci_exp_rxp,
    input  [(PL_LINK_CAP_MAX_LINK_WIDTH - 1) : 0] pci_exp_rxn,

    // 系统时钟、复位
    input  sys_clk_p,
    input  sys_clk_n,
    input  sys_rst_n,

    // LED 指示灯
    output [3:0] leds
);

localparam C_NUM_USR_IRQ = 1;

wire sys_clk;
wire sys_rst_n_c;

// 差分时钟缓冲
IBUFDS_GTE2 refclk_ibuf (
    .O(sys_clk),
    .ODIV2(),
    .I(sys_clk_p),
    .CEB(1'b0),
    .IB(sys_clk_n)
);
IBUF sys_reset_n_ibuf (
    .O(sys_rst_n_c),
    .I(sys_rst_n)
);

wire user_lnk_up;
wire user_clk;
wire user_resetn;

reg  [C_NUM_USR_IRQ-1:0] usr_irq_req = 0;

// AXI-Stream 信号线定义
wire [C_DATA_WIDTH-1:0]	  m_axis_h2c_tdata_0;
wire 			          m_axis_h2c_tlast_0;
wire 			          m_axis_h2c_tvalid_0;
wire 			          m_axis_h2c_tready_0;
wire [C_DATA_WIDTH/8-1:0] m_axis_h2c_tkeep_0;
wire [C_DATA_WIDTH-1:0]   s_axis_c2h_tdata_0;
wire                      s_axis_c2h_tlast_0;
wire                      s_axis_c2h_tvalid_0;
wire                      s_axis_c2h_tready_0;
wire [C_DATA_WIDTH/8-1:0] s_axis_c2h_tkeep_0;

// XDMA IP 例化
xdma_0 xdma_0_i (
    .sys_rst_n(sys_rst_n_c),
    .sys_clk  (sys_clk),

    .pci_exp_txn(pci_exp_txn),
    .pci_exp_txp(pci_exp_txp),
    .pci_exp_rxn(pci_exp_rxn),
    .pci_exp_rxp(pci_exp_rxp),

    .s_axis_c2h_tdata_0 (s_axis_c2h_tdata_0),
    .s_axis_c2h_tlast_0 (s_axis_c2h_tlast_0),
    .s_axis_c2h_tvalid_0(s_axis_c2h_tvalid_0),
    .s_axis_c2h_tready_0(s_axis_c2h_tready_0),
    .s_axis_c2h_tkeep_0 (s_axis_c2h_tkeep_0),
    .m_axis_h2c_tdata_0 (m_axis_h2c_tdata_0),
    .m_axis_h2c_tlast_0 (m_axis_h2c_tlast_0),
    .m_axis_h2c_tvalid_0(m_axis_h2c_tvalid_0),
    .m_axis_h2c_tready_0(m_axis_h2c_tready_0),
    .m_axis_h2c_tkeep_0 (m_axis_h2c_tkeep_0),

    .axi_aclk   (user_clk),
    .axi_aresetn(user_resetn),
    .user_lnk_up(user_lnk_up),

    .usr_irq_req(usr_irq_req),
    .usr_irq_ack(),
    .msix_enable()
);

// 回环逻辑模块例化
work #(
    .C_DATA_WIDTH(C_DATA_WIDTH)
) work_i (
    .s_axis_c2h_tdata_0 (s_axis_c2h_tdata_0),
    .s_axis_c2h_tlast_0 (s_axis_c2h_tlast_0),
    .s_axis_c2h_tvalid_0(s_axis_c2h_tvalid_0),
    .s_axis_c2h_tready_0(s_axis_c2h_tready_0),
    .s_axis_c2h_tkeep_0 (s_axis_c2h_tkeep_0),
    .m_axis_h2c_tdata_0 (m_axis_h2c_tdata_0),
    .m_axis_h2c_tlast_0 (m_axis_h2c_tlast_0),
    .m_axis_h2c_tvalid_0(m_axis_h2c_tvalid_0),
    .m_axis_h2c_tready_0(m_axis_h2c_tready_0),
    .m_axis_h2c_tkeep_0 (m_axis_h2c_tkeep_0),

    .user_clk(user_clk),
    .user_resetn(user_resetn),
    .sys_rst_n(sys_rst_n_c),
    .user_lnk_up(user_lnk_up),
    .leds(leds)
);

endmodule

3.3 约束文件 constr.xdc

LED 为高电平点亮。设备加载 FPGA 程序后,第 4 路 LED 持续闪烁,切换周期约 500 ms;上电后第 1 路 LED 常亮,延时约 3 s 后熄灭,随后第 1、2、3 路 LED 同时常亮,切换间隔约 500 ms。

xdc 复制代码
## 全局配置
set_property CFGBVS VCCO                         [current_design]
set_property CONFIG_VOLTAGE 3.3                  [current_design]
set_property CONFIG_MODE SPIx4                   [current_design]
set_property BITSTREAM.CONFIG.CONFIGRATE 50      [current_design]
set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 4     [current_design]
set_property BITSTREAM.CONFIG.SPI_FALL_EDGE YES  [current_design]
set_property BITSTREAM.GENERAL.COMPRESS TRUE     [current_design]
set_property BITSTREAM.CONFIG.UNUSEDPIN PULLNONE [current_design]

## PCIe 引脚约束
# 参考时钟
set_property PACKAGE_PIN F6 [get_ports sys_clk_p]

# 数据差分对
set_property PACKAGE_PIN D9  [get_ports {pci_exp_rxp[0]}]
set_property PACKAGE_PIN B10 [get_ports {pci_exp_rxp[1]}]
set_property PACKAGE_PIN D7  [get_ports {pci_exp_txp[0]}]
set_property PACKAGE_PIN B6  [get_ports {pci_exp_txp[1]}]

# 复位信号
set_property -dict {PACKAGE_PIN N15 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]

## LED 引脚约束
set_property -dict {PACKAGE_PIN V9 IOSTANDARD LVCMOS15} [get_ports {leds[0]}]
set_property -dict {PACKAGE_PIN Y8 IOSTANDARD LVCMOS15} [get_ports {leds[1]}]
set_property -dict {PACKAGE_PIN Y7 IOSTANDARD LVCMOS15} [get_ports {leds[2]}]
set_property -dict {PACKAGE_PIN W7 IOSTANDARD LVCMOS15} [get_ports {leds[3]}]

4 Linux 端 Rust 测试程序

4.1 主程序 main.rs

rust 复制代码
fn main() {
	use rand::Rng;
	use std::fs::OpenOptions;
	use std::io::{Read, Write};
    use std::sync::mpsc;
    use std::thread;

	let (read_tx, read_rx) = mpsc::channel();

	// 读数据线程
	thread::spawn(move || {
	    let mut buffer = vec![0u8; 200];
	    let mut file = OpenOptions::new()
	        .read(true)
	        .open("/dev/xdma0_c2h_0")
	        .unwrap();
	    file.read(buffer.as_mut_slice()).unwrap();

	    read_tx.send(buffer).unwrap();
	});

	// 写数据线程
	thread::spawn(move || {
	    let mut buffer = vec![0u8; 200];
	    for index in 0..(buffer.len() / 2) {
	        let rng: u16 = rand::thread_rng().gen_range(0..=1023);
	        buffer[index * 2] = (rng >> 8) as u8;
	        buffer[index * 2 + 1] = rng as u8;
	    }

	    let mut file = OpenOptions::new()
	        .write(true)
	        .open("/dev/xdma0_h2c_0")
	        .unwrap();
	    file.write(buffer.as_slice()).unwrap();
	});

	let buffer = read_rx.recv().unwrap();
	let mut data = vec![0u16; 100];
	for index in 0..(buffer.len() / 2) {
	    data[index] = ((buffer[index * 2] as u16) << 8) | (buffer[index * 2 + 1] as u16);
	}
	println!("xdma: {:?}", data);
}

4.2 工程配置 Cargo.toml

toml 复制代码
[dependencies]
rand = "*"

XDMA 驱动交叉编译(ARM 平台适配)

xiaguangbo 2025-11-05 19:46:17

本文介绍 dma_ip_drivers 驱动针对 aarch64 架构的适配流程,包含 Makefile 修改、交叉编译、内核模块部署与调试方法。编译生成的 .ko 内核模块可部署至目标设备根文件系统,结合 depmod 工具实现开机自动加载;也可通过 insmod 手动加载,配合 dmesg 查看运行日志完成调试。

1 驱动文件配置

驱动源码获取路径:https://github.com/Xilinx/dma_ip_drivers

修改文件路径:dma_ip_drivers-master/XDMA/linux-kernel/xdma/Makefile

注释原有本地编译代码段(包含末尾 endif),追加交叉编译配置:

makefile 复制代码
# ifneq ($(KERNELRELEASE),)
# 	$(TARGET_MODULE)-objs := libxdma.o xdma_cdev.o cdev_ctrl.o cdev_events.o cdev_sgdma.o cdev_xvc.o cdev_bypass.o xdma_mod.o xdma_thread.o
# 	obj-m := $(TARGET_MODULE).o
# else
# 	BUILDSYSTEM_DIR:=/lib/modules/$(shell uname -r)/build
# 	PWD:=$(shell pwd)
# endif

$(TARGET_MODULE)-objs := libxdma.o xdma_cdev.o cdev_ctrl.o cdev_events.o cdev_sgdma.o cdev_xvc.o cdev_bypass.o xdma_mod.o xdma_thread.o
obj-m := $(TARGET_MODULE).o
BUILDSYSTEM_DIR:=/home/debian/Desktop/xiaguangbo/project/rk3588/project/kernel
PWD:=$(shell pwd)

说明:BUILDSYSTEM_DIR 填写目标设备对应的内核源码路径,源码版本需与目标设备运行内核版本保持一致。

2 驱动编译指令

终端配置编译架构与交叉编译器,执行编译:

bash 复制代码
export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
make

3 工具程序配置与编译

3.1 修改工具目录 Makefile

文件路径:dma_ip_drivers-master/XDMA/linux-kernel/tools/Makefile

makefile 复制代码
# CC ?= gcc
CC = aarch64-linux-gnu-gcc

3.2 编译指令

bash 复制代码
make

编译结束后执行 file 文件名 校验程序架构,架构异常时重新核对编译器配置。

4 内核模块部署

  1. 查看目标设备内核版本,执行指令:uname -r,记录版本字符串。
  2. 在根文件系统创建对应目录:mkdir -p /lib/modules/内核版本号
  3. 将编译生成的 xdma.ko 复制至上述目录。
  4. 执行 depmod 生成模块依赖文件。
  5. 重启设备,驱动可自动加载。

自动加载异常时,可使用手动加载指令调试:

bash 复制代码
insmod /lib/modules/内核版本号/xdma.ko

日志查看指令:

bash 复制代码
dmesg | grep xdma
dmesg | grep pci

基于 XDMA 驱动与中断测试程序问题解析

一、驱动源码获取与版本说明

1.1 源码下载

XDMA 驱动源码官网下载:

下载最新版本的 XDMA 驱动源码,即 master 版本,否则会出现兼容性问题。

1.2 版本匹配原则

组件 匹配要求
XDMA IP 核版本 与 Linux 驱动版本一致
Vivado 版本 决定内置 XDMA IP 的默认版本,高版本 Vivado 内置新版 IP
驱动替换 若更换 Vivado 或 XDMA IP 版本,需同步替换对应版本的 Linux 驱动

注:Xilinx 早期 XDMA 驱动对高版本 Linux 内核存在接口兼容问题。

解决方向:

选用适配的低版本内核;

或前往 Xilinx 官网下载适配高版本内核的新版驱动;

或手动修改驱动源码适配新接口。

二、驱动编译(ARM64 交叉编译)

2.1 修改驱动的 Makefile

修改路径:dma_ip_drivers-master/XDMA/linux-kernel/xdma/Makefile

将原有条件编译块注释掉:

makefile 复制代码
# ifneq ($(KERNELRELEASE),)
# 	$(TARGET_MODULE)-objs := libxdma.o xdma_cdev.o cdev_ctrl.o cdev_events.o cdev_sgdma.o cdev_xvc.o cdev_bypass.o xdma_mod.o xdma_thread.o
# 	obj-m := $(TARGET_MODULE).o
# else
# 	BUILDSYSTEM_DIR:=/lib/modules/$(shell uname -r)/build
# 	PWD:=$(shell pwd)
# ...
# endif

替换为:

makefile 复制代码
$(TARGET_MODULE)-objs := libxdma.o xdma_cdev.o cdev_ctrl.o cdev_events.o cdev_sgdma.o cdev_xvc.o cdev_bypass.o xdma_mod.o xdma_thread.o
obj-m := $(TARGET_MODULE).o
BUILDSYSTEM_DIR:=/home/debian/Desktop/xiaguangbo/project/rk3588/project/kernel  # Linux 完整内核源码根目录
PWD:=$(shell pwd)

2.2 Linux 内核源码路径说明

配置项 BUILDSYSTEM_DIR 要求填写完整 Linux 内核源码根目录 ,并非系统自带的 linux-headers 头文件目录。

  • linux-headers:仅包含内核头文件、模块编译基础文件,不满足 XDMA 完整驱动编译要求
  • 完整内核源码:需单独下载、解压、配置编译后的内核工程,文中路径 /home/debian/Desktop/xiaguangbo/project/rk3588/project/kernel 即为 RK3588 平台专属完整内核源码目录。

交叉编译 ARM64 平台驱动时,必须提前准备与目标设备内核版本完全一致的完整源码,版本不一致会出现编译报错、模块加载失败。

2.3 编译执行

bash 复制代码
export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
make

编译指令运行在 PC 端 Linux 环境 (Ubuntu、Debian 等)。该流程为跨平台交叉编译,PC 端借助 aarch64 交叉编译器,生成适配 ARM64 架构的驱动模块,再将产物拷贝至 ARM 目标板(RK3588)运行。

无需额外新建工程。修改路径属于官方 XDMA 驱动源码自带目录,全程依托原厂驱动目录结构。

三、测试工具编译

3.1 修改测试工具 Makefile

修改路径:dma_ip_drivers-master/XDMA/linux-kernel/tools/Makefile

makefile 复制代码
# CC ?= gcc
CC = aarch64-linux-gnu-gcc

3.2 编译

bash 复制代码
make

编译之后用 file xxx 查看文件是否属于 aarch64 架构,如果不是则检查 Makefile 配置是否正确。

四、XDMA 中断检测上位机程序

4.1 完整代码

c 复制代码
#include <assert.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdarg.h>
#include <syslog.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/time.h>

#define FATAL do { fprintf(stderr, "Error at line %d, file %s (%d) [%s]\n" , __LINE__, __FILE__, errno, strerror(errno)); exit(1); } while(0)

#define MAP_SIZE (32*1024UL)
#define MAP_MASK (MAP_SIZE - 1)

#define user_irq_ack 0x1

void *map_base;
int fd;

int user_write(off_t target, unsigned long val)
{
    void *virt_addr;
    virt_addr = map_base + (target & MAP_MASK);
    *((unsigned long *)virt_addr) = val;
    return 0;
}

void *event0_process(void *arg)
{
    int val;
    int fd = open("/dev/xdma0_events_0", O_RDWR | O_SYNC);
    assert(fd >= 0);
    while (1) {
        read(fd, &val, 4);
        if (val == 1) {
            printf("event0 interrupt, val = %d\n", val);
            user_write(0x00004, user_irq_ack);  // 中断应答与清除
        } else {
            printf("event0 timeout, val = %d\n", val);
        }
    }
    close(fd);
    return NULL;
}

void *event1_process(void *arg)
{
    int val;
    int fd = open("/dev/xdma0_events_1", O_RDWR | O_SYNC);
    assert(fd >= 0);
    while (1) {
        read(fd, &val, 4);
        if (val == 1) {
            printf("event1 interrupt, val = %d\n", val);
            user_write(0x00004, user_irq_ack);
        } else {
            printf("event1 timeout, val = %d\n", val);
        }
    }
    close(fd);
    return NULL;
}

void *event2_process(void *arg)
{
    int val;
    int fd = open("/dev/xdma0_events_2", O_RDWR | O_SYNC);
    assert(fd >= 0);
    while (1) {
        read(fd, &val, 4);
        if (val == 1) {
            printf("event2 interrupt, val = %d\n", val);
            user_write(0x00004, user_irq_ack);
        } else {
            printf("event2 timeout, val = %d\n", val);
        }
    }
    close(fd);
    return NULL;
}

void *event3_process(void *arg)
{
    int val;
    int fd = open("/dev/xdma0_events_3", O_RDWR | O_SYNC);
    assert(fd >= 0);
    while (1) {
        read(fd, &val, 4);
        if (val == 1) {
            printf("event3 interrupt, val = %d\n", val);
            user_write(0x00004, user_irq_ack);
        } else {
            printf("event3 timeout, val = %d\n", val);
        }
    }
    close(fd);
    return NULL;
}

int main(int argc, char *argv[])
{
    int threads = 4;
    pthread_t tid[4];

    fd = open("/dev/xdma0_user", O_RDWR | O_SYNC);
    assert(fd >= 0);
    printf("PCIe BAR mapped at address %p.\n", map_base);
    map_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    assert(map_base != MAP_FAILED);

    pthread_create(&tid[0], NULL, event0_process, NULL);
    pthread_create(&tid[1], NULL, event1_process, NULL);
    pthread_create(&tid[2], NULL, event2_process, NULL);
    pthread_create(&tid[3], NULL, event3_process, NULL);

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_join(tid[2], NULL);
    pthread_join(tid[3], NULL);

    close(fd);
    munmap(map_base, MAP_SIZE);
    return 0;
}

4.2 代码结构说明

组件 功能
/dev/xdma0_user 用户 BAR 空间映射,用于寄存器读写(中断应答)
/dev/xdma0_events_0 ~ events_3 4 路中断事件节点,阻塞式监听中断
user_write(0x00004, user_irq_ack) 向中断应答寄存器写入 0x1,完成中断清除
4 个独立线程 每路中断一个线程,互不影响

五、中断与 C2H 数据通道关联问题

5.1 通道与中断的对应关系

XDMA 驱动加载成功后,会在 /dev 下生成两类设备节点:

  • xdma0_events_x中断事件节点,用于接收硬件中断上报(最多 16 路用户中断);
  • xdma0_c2h_xC2H(Card to Host,FPGA 到主机)数据通道节点,用于业务数据读写。

文中代码独立创建线程循环读取 /dev/xdma0_events_0 ~ /dev/xdma0_events_3,该操作为阻塞式中断监听

5.2 阻塞式中断监听

XDMA 驱动为每个用户中断创建的 /dev/xdma0_events_x 设备节点,其底层实现是一个阻塞型字符设备 。当用户态调用 read() 时:

  • 无中断发生时:进程进入睡眠状态,挂入该设备的等待队列,CPU 释放给其他任务;
  • 中断触发时 :内核 ISR 唤醒等待队列中的进程,read() 返回,用户态获得中断事件值。

这种设计使得无需轮询、无需忙等,程序在没有中断时零开销休眠。

阻塞式 vs 非阻塞式对比
方式 实现 适用场景 开销
阻塞式 read 直接 read(fd, &val, 4),无数据时进程挂起 中断频率不高、需简化代码逻辑的场景 中断到来前 CPU 占用率为 0
非阻塞式 read open(..., O_NONBLOCK) + 配合 poll/select/epoll 需同时处理多路中断或与其他 IO 复用 需额外事件循环代码
轮询(poll_mode=1 驱动内部轮询 DMA 描述符完成状态 中断资源紧张或高吞吐场景 持续占用 CPU

采用阻塞式的原因:代码最简单,且 4 路中断各自独立线程,互不干扰。read() 一行代码同时完成三件事:等待中断(进程休眠)、接收事件(返回中断值)、清除中断(驱动内部 ACK 寄存器)。

5.3 执行逻辑

程序逻辑为:检测到 event 中断事件后,可触发 C2H 数据读取操作。原文示例仅做中断状态打印,未附加数据读取逻辑,实际工程中可在 if (val == 1) 分支内调用 read 函数读取 /dev/xdma0_c2h_0 数据。

执行顺序:捕获中断事件 → 执行 C2H 数据接收

5.4 中断清除机制

内存在寄存器操作逻辑:

c 复制代码
user_write(0x00004, user_irq_ack);

该地址对应 XDMA 中断应答寄存器。完成一次中断对应的数据处理后,必须向该寄存器写入指定数值,完成中断确认与清除,否则硬件中断会持续触发,造成事件线程死循环。

六、中断事件清除方式详解

硬件外设产生的中断依赖寄存器完成状态交互,不存在完全不操作寄存器的清除方式 。根据实现形式分为常规清除临时屏蔽硬件复位三大类,同时区分显性操作与隐性操作。

6.1 常规中断清除(业务流程标准用法)

(1)显性写寄存器清零

为 FPGA、XDMA 设备最主流的设计,分为两种硬件规则:

  • 写 1 清零(W1C) :向中断应答/状态寄存器对应位写入 1,硬件自动清除该路中断标志。
  • 专用应答寄存器 :独立 IRQ ACK 寄存器,写入指定数值或掩码完成中断确认与标志清除,前文示例代码中 user_write(0x00004, user_irq_ack) 即为此类用法。

该方式可在内核态或用户态(mmap 寄存器映射)执行,正式产品建议在内核中断上下文完成。

(2)隐性寄存器操作(上层无需主动写指令)

本质仍为寄存器交互,仅对外表现形式不同:

  1. 读状态寄存器自动清零

    部分 FPGA 自定义中断逻辑设计为:软件读取中断状态寄存器的动作触发硬件自动清零标志位,仅需执行读操作即可完成清除,XDMA 标准逻辑中该设计较少使用。

  2. XDMA 事件节点读操作自动清零

    XDMA Linux 驱动会生成 /dev/xdma0_events_x 中断事件节点。用户态调用 read 读取该节点时,系统调用下沉至内核驱动,驱动内部自动完成中断应答寄存器读写,实现中断标志清除,也是前文中断检测程序的底层逻辑。

    通过 events 节点 read 进行隐性清除是 XDMA 驱动的推荐用法

6.2 临时屏蔽手段(非清除,仅阻断中断上报)

此类操作不会清空已置位的中断标志,仅关闭中断上报通路,不可替代正常清除流程:

  • 关闭中断使能位:操作中断掩码寄存器,关闭单路或全局中断使能。原有中断标志会持续保留,重新开启使能后,未处理中断会再次触发。
  • 关闭设备总中断开关:关停 XDMA 控制器全局中断,所有中断停止上报,标志位保持不变。

6.3 硬件复位(全局清空,非常规业务操作)

属于故障恢复、系统初始化场景的应急手段,会破坏正常业务链路:

  • 中断模块软复位:向复位寄存器下发指令,复位中断控制器,一次性清空所有中断标志与配置。
  • 设备全局复位:复位 XDMA IP 或整片 FPGA,所有硬件状态位清零。

6.4 小结

  1. 正常业务场景下,中断清除必然伴随寄存器读写,仅分为上层手动操作驱动底层封装操作两种形式;
  2. XDMA 推荐优先使用 events 节点 read 方式,由驱动自动完成中断清除;
  3. 中断屏蔽、硬件复位不属于标准中断清除方案,禁止用于常规中断处理流程。

七、内核中断处理函数(ISR)中清除中断标志位

Linux 内核要求:必须在中断上下文内完成中断标志清除,若延迟至应用层处理,会引发中断风暴,导致 CPU 占用异常。

7.1 内核中断标准执行流程

  1. 硬件触发中断,CPU 跳转至对应中断服务函数;
  2. 读取中断状态寄存器,判定当前触发的中断源;
  3. 依据硬件规则清除中断标志位;
  4. 执行业务逻辑(数据拷贝、唤醒等待队列、向上层上报事件等);
  5. 退出中断上下文。

7.2 主流清零方式与内核代码示例

驱动中通过 ioremap 将设备物理寄存器映射为内核虚拟地址,再使用 ioread32/iowrite32 完成寄存器操作。

(1)写 1 清零(W1C,XDMA/FPGA 通用)
c 复制代码
#include <linux/io.h>
#include <linux/interrupt.h>

// 寄存器偏移地址
#define IRQ_STAT_REG  0x00
#define IRQ_ACK_REG   0x04

irqreturn_t xdma_irq_isr(int irq, void *dev_data)
{
    struct xdma_dev *priv = dev_data;
    u32 irq_flags;

    // 读取中断状态,判断是否为本设备中断
    irq_flags = ioread32(priv->reg_base + IRQ_STAT_REG);
    if (!irq_flags) {
        return IRQ_NONE;
    }

    // 写 1 清零:向应答寄存器写入中断标志
    iowrite32(irq_flags, priv->reg_base + IRQ_ACK_REG);

    // 执行业务逻辑:唤醒等待队列、上报事件等
    // ...

    return IRQ_HANDLED;
}
(2)读寄存器自动清零

硬件设计为读取状态寄存器即自动清除标志,无需写操作:

c 复制代码
irqreturn_t xdma_irq_isr(int irq, void *dev_data)
{
    struct xdma_dev *priv = dev_data;
    u32 irq_flags;

    // 读取动作同步完成中断标志清除
    irq_flags = ioread32(priv->reg_base + IRQ_STAT_REG);
    if (!irq_flags)
        return IRQ_NONE;

    // 执行业务逻辑
    // ...

    return IRQ_HANDLED;
}
(3)XDMA 官方驱动原生逻辑

官方 XDMA 驱动将中断应答逻辑封装在 cdev_events 模块:

  1. 硬件中断触发 → 内核 ISR 标记中断事件、唤醒阻塞在 read 上的应用进程;
  2. 应用层读取 /dev/xdma0_events_x → 驱动内部执行寄存器操作,完成中断 ACK 与标志清除。

7.3 关键注意事项

  1. 中断上下文内禁止调用耗时函数、睡眠类函数;
  2. 多路中断共用一条中断线时,必须先读取状态寄存器区分中断源,再逐路清除标志;
  3. 未清除中断标志就退出 ISR,会造成中断反复触发。

八、用户态中断检测程序处理多个中断源

XDMA 驱动为每一路独立中断生成专属设备节点:/dev/xdma0_events_0 ~ /dev/xdma0_events_3(最多支持 16 路),每个节点对应一个独立中断源。结合中断数量、性能需求,提供三类实现方案。

8.1 方案一:多线程监听(适用于 4 路以内少量中断)

原理

为每一个中断节点创建独立子线程,单个线程独占一个文件描述符,阻塞 read 监听中断;中断触发后线程唤醒,执行业务逻辑。

特点

逻辑简单、代码易维护,单路中断故障不会影响其他通路;中断源数量增多后,线程资源开销会随之上升。

代码示例
c 复制代码
#include <pthread.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

void *event0_process(void *arg)
{
    int val;
    int fd = open("/dev/xdma0_events_0", O_RDWR | O_SYNC);
    while (1) {
        read(fd, &val, 4); // 读取事件,驱动自动清除中断
        printf("Interrupt 0 triggered\n");
        // 拓展:联动 C2H 通道读取数据
    }
    close(fd);
    return NULL;
}

void *event1_process(void *arg)
{
    int val;
    int fd = open("/dev/xdma0_events_1", O_RDWR | O_SYNC);
    while (1) {
        read(fd, &val, 4);
        printf("Interrupt 1 triggered\n");
    }
    close(fd);
    return NULL;
}

int main(int argc, char* argv[])
{
    pthread_t t0, t1;
    pthread_create(&t0, NULL, event0_process, NULL);
    pthread_create(&t1, NULL, event1_process, NULL);

    pthread_join(t0, NULL);
    pthread_join(t1, NULL);
    return 0;
}

8.2 方案二:单线程 + poll/select(适用于 4~16 路中断,兼容性优)

原理

单线程统一打开所有中断设备文件,通过 poll/select 实现 IO 多路复用,同时监听所有文件的可读事件;任意中断触发,对应文件描述符置位,程序区分中断源并处理。

特点

线程数量固定,资源占用低,兼容全版本 Linux 系统;存在轻微轮询延迟,不适合超高并发场景。

代码示例
c 复制代码
#include <poll.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

#define IRQ_COUNT 4
const char *dev_path[IRQ_COUNT] = {
    "/dev/xdma0_events_0",
    "/dev/xdma0_events_1",
    "/dev/xdma0_events_2",
    "/dev/xdma0_events_3"
};

int main(void)
{
    int fds[IRQ_COUNT];
    struct pollfd poll_fds[IRQ_COUNT];
    int i;

    // 打开设备并初始化 poll 结构体
    for (i = 0; i < IRQ_COUNT; i++) {
        fds[i] = open(dev_path[i], O_RDWR | O_SYNC);
        poll_fds[i].fd = fds[i];
        poll_fds[i].events = POLLIN;
        poll_fds[i].revents = 0;
    }

    // 循环监听多路中断
    while (1) {
        poll(poll_fds, IRQ_COUNT, -1);
        for (i = 0; i < IRQ_COUNT; i++) {
            if (poll_fds[i].revents & POLLIN) {
                int val;
                read(poll_fds[i].fd, &val, 4);
                printf("Interrupt %d triggered\n", i);
            }
            poll_fds[i].revents = 0;
        }
    }

    // 释放资源
    for (i = 0; i < IRQ_COUNT; i++)
        close(fds[i]);
    return 0;
}

8.3 方案三:单线程 + epoll(适用于多路/高实时性中断,性能最优)

原理

epoll 是 Linux 高性能 IO 多路复用机制,采用事件通知模型,相比 poll/select 效率更高,适合 8 路以上中断源、高并发、高实时性场景。

代码示例
c 复制代码
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

#define IRQ_COUNT 4
const char *dev_path[IRQ_COUNT] = {
    "/dev/xdma0_events_0",
    "/dev/xdma0_events_1",
    "/dev/xdma0_events_2",
    "/dev/xdma0_events_3"
};

int main(void)
{
    int epoll_fd = epoll_create1(0);
    int fds[IRQ_COUNT];
    struct epoll_event ev, events[IRQ_COUNT];
    int i, n;

    // 注册所有文件句柄至 epoll
    for (i = 0; i < IRQ_COUNT; i++) {
        fds[i] = open(dev_path[i], O_RDWR | O_SYNC);
        ev.events = EPOLLIN;
        ev.data.fd = fds[i];
        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fds[i], &ev);
    }

    // 循环监听事件
    while (1) {
        n = epoll_wait(epoll_fd, events, IRQ_COUNT, -1);
        for (i = 0; i < n; i++) {
            int val;
            int fd = events[i].data.fd;
            read(fd, &val, 4);
            printf("Interrupt detected\n");
        }
    }

    // 释放资源
    for (i = 0; i < IRQ_COUNT; i++)
        close(fds[i]);
    close(epoll_fd);
    return 0;
}

8.4 方案选型建议

场景 推荐方案 说明
中断源 ≤ 4 路、调试或简易项目 多线程方案 开发成本最低
中断源 4~16 路、常规工业应用 poll/select 系统兼容性最佳
中断源 >16 路、高实时/高并发场景 epoll 综合性能最优

九、驱动加载报错:模块安装成功,但未识别设备

9.1 报错信息

复制代码
insmod xdma.ko interrupt_mode=2
Error: The Kernel module installed correctly, but no devices were recognized.

附加现象:lspci 可识别到 Xilinx 7028 设备,PCIe 物理链路正常。

9.2 分层排查方案

  1. FPGA 端排查

    • 确认 Vivado 工程内 XDMA IP 已正确配置,MSI-X 中断功能已开启
    • 检查 FPGA 比特流文件已正常烧录,PCIe 链路初始化完成。
  2. 驱动版本匹配排查

    • 若 IP 版本与驱动版本不匹配,会出现模块加载成功、但 /dev 节点无法生成的问题。
  3. 内核与编译环境排查

    • 确认编译驱动使用的内核源码版本和 ARM 目标板运行内核版本一致;
    • 重新检查 Makefile 内 BUILDSYSTEM_DIR 路径配置。
  4. 系统日志排查

    在加载驱动后执行指令查看详细报错:

    bash 复制代码
    dmesg | grep xdma
    dmesg | grep pci

    日志会输出设备枚举、寄存器初始化、中断注册失败等具体原因。

  5. 启动模式参数排查

    • 加载指令指定 interrupt_mode=2(Legacy 中断模式),可尝试切换中断模式重新加载模块。

    • XDMA 驱动支持的 interrupt_mode 参数定义如下:

      模式 说明
      0 Auto 自动探测(默认)
      1 MSI 消息信号中断
      2 Legacy 传统 PCI 中断(INTx)
      3 MSI-X 扩展消息信号中断(推荐)
    • 此外,poll_mode 参数(0 为中断驱动,1 为轮询模式)与 interrupt_mode 为两个独立参数,不可混淆。

总结

  1. 驱动编译 :下载 Xilinx 官方 master 版本驱动,修改 Makefile 指向完整内核源码,使用 aarch64-linux-gnu- 交叉编译器生成 ARM64 驱动模块。
  2. 测试工具 :修改 tools/Makefile 中的 CC 为交叉编译器,编译后使用 file 命令验证架构。
  3. 中断检测 :通过 4 个独立线程阻塞读取 /dev/xdma0_events_x,检测到中断后通过 mmap/dev/xdma0_user 写入应答寄存器完成清除。
  4. 中断清除 :所有硬件中断的清除均依托寄存器交互实现,XDMA 优先使用 events 节点 read 完成隐性清除;中断屏蔽、硬件复位仅用于异常场景,不做常规使用。
  5. 内核中断处理 :必须在中断上下文内,通过 ioread32/iowrite32 操作寄存器清除标志,遵循硬件「写 1 清零」或「读清零」规则。
  6. 用户态多中断源处理 :依据中断数量与性能要求,分别采用多线程、poll/selectepoll 三种方案,通过独立 events 设备节点区分不同中断源。
  7. 驱动参数规范insmod xdma.ko 加载时,interrupt_mode0=Auto, 1=MSI, 2=Legacy, 3=MSI-X)与 poll_mode0=中断, 1=轮询)为两个独立参数,参数名使用下划线连接,不可含空格。

reference