尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Xilinx XDMA PCIe驱动详解:从设备枚举到DMA环形描述符的完整实现

Xilinx XDMA PCIe驱动详解:从设备枚举到DMA环形描述符的完整实现 简介面向Linux驱动开发者的PCIe驱动源码包聚焦Xilinx FPGA的XDMA控制器覆盖PCIe设备枚举、DMA传输、中断处理、用户态交互等关键环节。包内共27个文件以C源码、头文件、Makefile为主另含配置界面图像与运行结果文件整体仅124KB结构清晰适合作为轻量级参考工程直接研读或二次移植。目前已有1227人学习下载。源码围绕XDMA驱动展开包括探测与移除、描述符环管理、基地址映射、SG模式传输等典型实现并带有用户态示例、配置界面框架及交叉编译所需的Makefile组织方式结合描述中的Linux PCIe架构与开发流程可帮助读者理解从设备识别、资源分配到DMA协同工作的完整驱动开发路径。无论是要调试Xilinx PCIe设备还是希望复用现有框架编写其他PCIe驱动都能从中获得直接可参考的代码范式与排错思路。1. 拿到 linux_driver.rar先搞清楚这套 Xilinx PCIe 驱动能干什么做 FPGA 板卡和 x86 主机通信的工程师十有八九翻过 Xilinx 官方的 XDMA 驱动源码。这份压缩包里的linux_driver目录就是一套完整的 Xilinx PCIe DMA 驱动实现包含xdma_base.c、xdma_bdring.c、xdma_user.c这几个核心源文件外加sguser.c、ConfigGui.c这类用户态测试程序。它的场景很明确你的 FPGA 里用 Xilinx 的 PCIe IP 核搭了端点Endpoint主机侧要有一个 Linux 驱动能枚举到它、映射 BAR 空间、建 DMA 通道、收发数据。这套代码解决的就是从「设备插上识别不了」到「用户态程序能直接读写 FPGA 寄存器」的完整链路。适合谁用两种人。一种是刚接手 FPGA 板卡开发、被 PCIe 枚举和 DMA 描述符折腾得头大的嵌入式工程师照着这份源码能把驱动骨架搭起来另一种是已经跑通基础通信、想理解 XDMA 环形描述符和 C2H/H2C 通道调度机制的熟手直接读xdma_bdring.c比看文档快得多。下面我按「驱动怎么加载 → DMA 怎么调度 → 用户态怎么交互 → 哪些坑必须绕开」的顺序拆开讲。2. 驱动加载与设备枚举从 insmod 到 probe 的完整链路2.1 先看驱动骨架pci_driver 结构体怎么注册这套驱动的入口在xdma_base.c核心是注册一个struct pci_driver。Linux 内核的 PCI 子系统枚举到设备后会比对 Vendor ID 和 Device ID匹配成功就回调驱动的probe函数。Xilinx XDMA IP 核默认的 Vendor ID 是0x10EEDevice ID 常见有0x9038、0x9039等具体要看你在 Vivado 里怎么配置的。static const struct pci_device_id xdma_pci_table[] { { XILINX_VENDOR_ID, XILINX_DMA_DEVICE_ID, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0 }, { 0 } }; MODULE_DEVICE_TABLE(pci, xdma_pci_table); static struct pci_driver xdma_driver { .name xdma, .id_table xdma_pci_table, .probe xdma_probe, .remove xdma_remove, }; module_pci_driver(xdma_driver);这个代码块是驱动和内核的「握手协议」。module_pci_driver宏把pci_register_driver和pci_unregister_driver都封装好了模块加载时自动注册。注意id_table里的匹配规则PCI_ANY_ID表示忽略子系统 ID如果你的板卡用了非默认 Device ID直接改这一行就行不用动其他地方。我见过有人改了 FPGA 逻辑里的 Device ID 忘了同步这里导致insmod成功但设备一直不绑定驱动。2.2 probe 函数做了什么从 pci_enable_device 到 ioremapprobe函数是驱动初始化的主战场。Xilinx XDMA 的xdma_probe流程大致分几步使能 PCIe 设备、获取 BAR 资源、映射寄存器空间、初始化 DMA 引擎、创建设备节点。下面把关键步骤摘出来static int xdma_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct xdma_dev *xdev; int err, bars; err pci_enable_device(pdev); if (err) { dev_err(pdev-dev, pci_enable_device failed: %d\n, err); return err; } /* 只映射 BAR0XDMA 的控制寄存器都在这块空间 */ bars pci_select_bars(pdev, IORESOURCE_MEM); err pci_request_selected_regions(pdev, bars, xdma); if (err) { dev_err(pdev-dev, pci_request_selected_regions failed: %d\n, err); goto err_disable; } /* DMA 掩码XDMA 支持 64 位地址但有些平台只能给 32 位 */ err pci_set_dma_mask(pdev, DMA_BIT_MASK(64)); if (err) { err pci_set_dma_mask(pdev, DMA_BIT_MASK(32)); if (err) { dev_err(pdev-dev, DMA mask set failed\n); goto err_regions; } } xdev-base pci_iomap(pdev, 0, pci_resource_len(pdev, 0)); if (!xdev-base) { dev_err(pdev-dev, ioremap failed\n); goto err_regions; } /* 初始化描述符环形缓冲、中断、sysfs 属性 */ err xdma_init(xdev); if (err) goto err_unmap; return 0; err_unmap: pci_iomap_unmap(pdev, xdev-base); err_regions: pci_release_selected_regions(pdev, bars); err_disable: pci_disable_device(pdev); return err; }这段代码最值得注意的参数是pci_set_dma_mask。XDMA 的 scatter-gather 引擎在 64 位地址下效率更高但如果你在 32 位主机或者某些只支持 40 位地址的 IOMMU 环境下DMA_BIT_MASK(64)会返回失败代码里的 fallback 到 32 位是必须的。另外pci_iomap映射的是 BAR0XDMA 的寄存器布局里BAR0 的低地址放的是中断、DMA 控制等关键寄存器。如果你的 Vivado 工程把 BAR 空间拆成了多个记得检查pci_resource_len(pdev, 0)返回的长度是否和你配置的一致——不一致多半是硬件里 AXI 地址映射和 PCIe BAR 大小对不上。2.3 加载顺序与设备绑定insmod、dmesg、lspci 三板斧驱动写好了上板验证时有一套固定的排查顺序。先把板卡插上开机后在终端里执行# 1. 确认 PCIe 设备被内核枚举到 lspci -d 10ee: # 2. 加载驱动模块 insmod xdma.ko # 3. 看内核日志确认 probe 是否成功 dmesg | tail -20 # 4. 确认设备节点已经创建 ls /dev/xdma*这里有几个容易翻车的点。lspci能看到设备但显示Kernel driver in use: none说明设备枚举成功但驱动没绑定先查id_table里的 VID/DID 是否匹配反过来insmod报Invalid parameters或者直接没反应先查dmesg里的pci_enable_device failed或ioremap failed前者通常是设备被 BIOS 的 PCIe 选项 ROM 占用了后者是 BAR 空间没分配上——常见原因是主板 BIOS 里Above 4G Decoding没开导致 BAR 落在 32 位地址空间之外。模块卸载也有讲究rmmod xdma前必须确保没有用户态进程占用/dev/xdma*节点否则remove回调里的资源释放会撞上还在跑的 DMA 传输内核直接 panic。我一般会先fuser -k /dev/xdma0把占用进程踢掉再卸载。3. XDMA 的环形描述符机制C2H 和 H2C 通道的数据调度核心3.1 描述符环形缓冲BD Ring的设计逻辑XDMA 的 DMA 传输用的是 scatter-gather 模式核心数据结构是xdma_bdring.c里维护的那组环形描述符。每个描述符BDBuffer Descriptor指向一块物理内存多个 BD 串成一个环硬件 DMA 引擎从环里取 BD 搬运数据搬完一个置一次状态软件这边通过中断或者轮询 BD 的完成标志来回收。这套机制的好处是 CPU 不用逐字节参与拷贝硬件自己沿着环跑。struct xdma_bd { __le32 next_lo; /* 下一个 BD 地址低 32 位 */ __le32 next_hi; /* 下一个 BD 地址高 32 位 */ __le32 addr_lo; /* 数据缓冲区地址低 32 位 */ __le32 addr_hi; /* 数据缓冲区地址高 32 位 */ __le32 control; /* 控制字长度、方向、结束标志等 */ __le32 status; /* 状态字硬件写回完成状态 */ __le32 rsvd0; __le32 rsvd1; };这个结构体就是 XDMA 硬件和驱动之间唯一的「协议」。control字段里最关键的是长度和EOP标志——上位机发的一包数据跨了多个 BD 时硬件靠这个标志知道哪块 buffer 是一包的结尾。status字段由硬件写回驱动在中断处理里检查它来判断该 BD 是否完成。注意next_hi/next_lo是链式指针Xilinx 的 XDMA 支持环形也支持链式驱动默认用环形即最后一个 BD 的next指回环形头。3.2 BD Ring 初始化和传输提交流程初始化一个通道的 BD Ring 时驱动要分配 DMA 一致性内存把 BD 数组刷成零然后把环形基地址写到硬件寄存器里。看xdma_bdring.c里的xdma_ring_alloc就明白这套流程了int xdma_ring_alloc(struct xdma_ring *ring, u32 len, u32 bd_size) { struct xdma_bd *bd; dma_addr_t dma_addr; int i; ring-bd kzalloc(bd_size * len, GFP_KERNEL); if (!ring-bd) return -ENOMEM; ring-dma_addr dma_map_single(ring-dev, ring-bd, bd_size * len, DMA_TO_DEVICE); if (dma_map_single_failed(ring-dma_addr)) { kfree(ring-bd); return -ENOMEM; } /* 把 BD 串成环形最后一个指回头部 */ for (i 0; i len; i) { bd ring-bd[i]; bd-next_lo lower_32_bits((u64)ring-dma_addr bd_size * ((i 1) % len)); bd-next_hi upper_32_bits((u64)ring-dma_addr bd_size * ((i 1) % len)); } ring-len len; ring-head 0; ring-tail 0; return 0; }这里有个不太容易察觉的坑dma_map_single映射的方向是DMA_TO_DEVICE但 BD 本身是要被硬件写回status字段的。理论上软件在回收描述符时读的是硬件写的数据方向应该是双向的。Xilinx 官方驱动在部分内核版本里这里用的是DMA_BIDIRECTIONAL如果你发现传输完成后status字段读出来是旧的大概率是 DMA 映射方向没配对改成双向映射能解决。另外lower_32_bits/upper_32_bits的位宽拆分是必须的XDMA 的寄存器字段是 32 位的64 位地址必须拆成两段写漏了高位地址在 64 位系统上会直接随机指向非法内存。提交一个传输任务时驱动要做的就是把用户传下来的 buffer 映射到 DMA 地址、填 BD 的addr_lo/addr_hi和control然后敲一下硬件寄存器上的 tail 指针。XDMA 的设计里软件写 tail 寄存器、硬件写 head 寄存器两个指针错开的部分就是待处理的 BD。这个生产者/消费者模型搞明白了DMA 调不通的 80% 问题都能一眼定位。3.3 中断处理MSI-X 和完成队列XDMA 驱动默认用 MSI-X 中断每个 DMA 通道一个中断号中断处理函数里做的事非常少读完成队列的 tail 指针、遍历完成的 BD、调用户注册的回调。代码在xdma_base.c里static irqreturn_t xdma_channel_irq(int irq, void *data) { struct xdma_channel *chan data; u32 tail; /* 读硬件写回的 tail 指针 */ tail xdma_read32(chan-base, chan-tail_reg); /* 遍历从 head 到 tail 之间的 BD逐个标记完成 */ while (chan-head ! tail) { xdma_bd_done(chan, chan-ring-bd[chan-head]); chan-head (chan-head 1) % chan-ring-len; } /* 写回 head 指针通知硬件可以复用这些 BD */ xdma_write32(chan-base, chan-head_reg, chan-head); return IRQ_HANDLED; }这个函数是 XDMA 吞吐量的命门。它必须在极短时间内处理完一轮 BD如果中断里做了太重的操作比如拷贝数据、打印日志中断延迟会直接压垮 DMA 带宽我见过有人在这里加了printk后每秒只能传几十 MB去掉后直接跑到 3GB/s。正确处理方式是中断里只回收 BD 并把 buffer 挂到 ready 队列真正的数据处理交给 kthread 或者 workqueue让中断处理时间控制在几十微秒以内。4. 用户态交互与 DMA 传输实战从 cdev 到 sguser 测试程序4.1 字符设备与 ioctl 接口用户态怎么操控 DMA 通道驱动给用户态开了什么口子决定了上层软件好不好写。XDMA 的经典做法是每个通道创建一个字符设备/dev/xdma0_h2c、/dev/xdma0_c2h用户态通过open、ioctl、mmap、read/write来操控。这里read/write是同步阻塞式收发底层的半双工 DSAdescriptor-based scatter-gather模式靠ioctl传递配置参数。看xdma_user.c里 ioctl 的派发逻辑static long xdma_user_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { struct xdma_channel *chan file-private_data; struct xdma_user_io __user *uarg (void __user *)arg; switch (cmd) { case XDMA_IOCTL_GET_CHANNEL_INFO: if (copy_to_user(uarg, chan-info, sizeof(chan-info))) return -EFAULT; break; case XDMA_IOCTL_START_DMA: /* 从用户态 DMA 地址区映射并提交传输 */ return xdma_user_start_dma(chan, uarg); case XDMA_IOCTL_STOP_DMA: return xdma_user_stop_dma(chan); default: return -ENOTTY; } }用户态程序拿到指令后通过ioctl(fd, XDMA_IOCTL_START_DMA, req)发起一次 DMA 传输。注意copy_to_user和copy_from_user一定要配对用用户态传进来的指针如果直接在内核态解引用会触发 segfault 或者被 SMAP 机制拦截这是新手写字符设备 ioctl 最常见的崩溃来源。4.2 sguser.c 示例程序一次完整的 DMA 读写操作压缩包里的sguser.c是 Xilinx 官方的 scatter-gather 用户态测试代表示例。它做的事很直接分配一块用户态 buffer提交给驱动做 DMA 写H2C再提交做 DMA 读C2H然后校验数据。跑通这个程序说明整条链路用户态 → 驱动 → DMA 引擎 → FPGA 逻辑是通的/* sguser.c 核心流程简化 */ int main(int argc, char **argv) { int fd; char *buf; struct xdma_user_io req; fd open(/dev/xdma0_h2c, O_RDWR); if (fd 0) { perror(open); return -1; } /* 分配页对齐的 bufferDMA 要求物理连续 */ posix_memalign((void **)buf, 4096, BUF_SIZE); /* 填充数据发起 H2C 传输 */ memset(buf, 0xAA, BUF_SIZE); req.buf buf; req.len BUF_SIZE; if (ioctl(fd, XDMA_IOCTL_START_DMA, req) 0) { perror(ioctl); return -1; } /* 等待完成并验证 */ close(fd); return 0; }这个示例有两个必须注意的边界。第一个posix_memalign的页对齐不是可选的而是必须的。XDMA 的 DMA 引擎要求 buffer 物理地址页对齐任意字节对齐会在硬件里直接报Address decode error。第二个ioctl传进去的req.buf必须是用户态虚拟地址驱动内部会调get_user_pages把它钉在内存里拿到物理地址如果你传的是mmap出来的设备地址或者内核态指针驱动会直接拒绝或者 GDB 都救不回来。4.3 通过 sysfs 查看设备状态驱动对外的信息窗口除了字符设备驱动还注册了一组 sysfs 属性用来暴露运行时状态。这套接口在调试时比printk好用得多不用翻内核日志就能确认通道状态和中断统计# 查看每个通道的中断计数确认中断是否正常触发 cat /sys/bus/pci/devices/0000:03:00.0/xdma/interrupt_count # 查看 DMA 通道的环形描述符深度 cat /sys/bus/pci/devices/0000:03:00.0/xdma/ring_size # 触发一次软复位调试时不用重新插拔板卡 echo 1 /sys/bus/pci/devices/0000:03:00.0/xdma/reset这三条命令分别对应中断健康度、描述符配置和复位操作。中断计数如果一直是 0 但 DMA 传输正常说明驱动跑的是轮询模式能传但效率低得回头查xdma_channel_irq有没有正确注册ring size 的默认值通常在xdma.h里定义一般 256 够用如果你想压满 PCIe Gen3 x8 的带宽试着拉大到 1024可以缓解描述符耗尽导致的吞吐抖动。5. 避坑与排查XDMA 驱动最常见的五个翻车现场5.1 DMA 传输永远停在第一个 BD环形描述符的预取问题现象驱动加载成功用户态程序提交了传输任务DMA 引擎只把第一个 BD 搬完就停住了状态寄存器显示Ring Empty但软件明明塞了很多 BD。原因XDMA 硬件和软件对 ring 的「生产-消费」指针理解不一致。软件写入 tail 寄存器后硬件只预取了固定数量的 BD通常是 8 个如果你一次性提交的 BD 数超过预取窗口硬件会在窗口耗尽后停等而它认为的 tail 指针是缓存的旧值需要软件再敲一次 tail 寄存器唤醒。解决在xdma_write32(chan-base, chan-tail_reg, tail)之后加一个 memory barrier再写一次同样的值。我一般会在提交函数的末尾强制xdma_write32(chan-base, chan-tail_reg, tail)两次第一次给硬件更新预取窗口第二次触发未决的 DMA 请求。这个动作不会破坏协议因为硬件的 tail 寄存器是覆盖写语义。5.2 C2H 方向数据全是 0DMA buffer 没正确映射现象H2C主机到 FPGA方向正常数据 FPGA 能收到但 C2HFPGA 到主机方向读回来的 buffer 全是零或者第一次读到正确数据、第二次开始全是旧值。原因C2H 方向用户态 read buffer 的 DMA 映射方向放错了。很多从 H2C 代码复制过来的实现给 C2H 的 buffer 也用了DMA_TO_DEVICE映射方向。硬件往这个 buffer 写数据时CPU 缓存里的旧数据不会被 DMA 写穿透读出来的就是缓存里的旧内容。解决C2H 的 buffer 必须用DMA_FROM_DEVICE方向映射并且每次传输完成后做一次dma_map_sync或者dma_unmap_single把 CPU 缓存清掉。注意get_user_pages拿到的页要调set_page_dirty否则 mmap 到用户态的文件页不会回写。5.3 insmod 报错No such deviceBoard 没被内核识别现象板卡插在 PCIe 插槽里lspci能列出设备但insmod直接报No such devicedmesg里没有任何 probe 调用记录。原因id_table里的 VID/DID 和实际板卡不匹配。Xilinx XDMA IP 的 Device ID 在 Vivado 配置里是可改的很多人改完硬件忘了同步驱动里的XILINX_DMA_DEVICE_ID宏定义。另外还有个隐蔽情况同一个 VID/DID 被另一个驱动先注册绑定了内核里一个设备只能绑定一个驱动后来的直接跳过。解决先lspci -nn看真实的 VID/DID 数值再回源码里grep XILINX_DMA_DEVICE_ID对比。如果确认被别的驱动抢了看ls /sys/bus/pci/drivers里哪个驱动占着设备echo掉再重新绑定或者用modprobe.blacklistother_driver屏蔽掉。5.4 DMA 带宽上不去只到几百 MB/s中断亲和性没设现象理论带宽应该能跑到 PCIe Gen3 x4 的 3.2GB/s实际只能到 800MB/s 左右而且波动很大。原因MSI-X 中断被内核调度到了多个 CPU 核上DMA 完成中断和用户态处理线程不在同一个核导致缓存反复失效。另外部分 BIOS 默认把 PCIe 设备的中断路由到一个负载很高的核上。解决把中断绑定到专门的核上。先cat /proc/interrupt | grep xdma找到中断号然后echo 0f /proc/irq/N/smp_affinity把中断都绑在 0-3 号核上。同时用户态处理线程用sched_setaffinity绑到同一个核组。这套组合拳一般能把带宽从 800MB/s 拉到 2.5GB/s 以上。5.5 拔卡后 rmmod 卡死热插拔的释放流程处理现象开发过程中频繁插拔 PCIe 板卡某次直接拔掉后执行rmmod xdma进程卡死无法退出过一会儿系统报BUG: scheduling while atomic或者直接 panic。原因驱动里中断线程还挂在被拔掉的硬件上中断处理函数里读的寄存器地址已经失效硬件返回的总线错误让驱动的等待循环永远转会不来。热插拔场景下pci_driver的remove回调会在设备消失后触发但和你手动rmmod的顺序冲突了。解决热插拔场景下强制先执行echo 1 /sys/bus/pci/devices/0000:03:00.0/remove让内核走一遍完整的 remove 流程再rmmod。另外在驱动的remove回调里第一件事是pci_disable_device并关掉所有中断确保没有新的中断进来然后再释放 DMA 资源和字符设备节点。6. 用 perf 数据验证驱动性能一套能直接抄的带宽测试方法驱动跑通了下一个问题就是怎么证明它没白写。我常用的验证手段是写一个用户态的 ping-pong 测试程序一边向 FPGA 发数据一边收数据用CLOCK_MONOTONIC计时算实际吞吐。基础版本长这样/* 简单的 C2H/H2C 往返带宽测试 */ #include stdio.h #include stdlib.h #include string.h #include time.h #include fcntl.h #include sys/ioctl.h #define BUF_SIZE (4 * 1024 * 1024) /* 4MB够大才能压住延迟开销 */ static double now_sec(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); return ts.tv_sec ts.tv_nsec * 1e-9; } int main(void) { int fd_h2c, fd_c2h; char *tx_buf, *rx_buf; double t0, t1; int i, ret; posix_memalign((void **)tx_buf, 4096, BUF_SIZE); posix_memalign((void **)rx_buf, 4096, BUF_SIZE); memset(tx_buf, 0x5A, BUF_SIZE); fd_h2c open(/dev/xdma0_h2c, O_RDWR); fd_c2h open(/dev/xdma0_c2h, O_RDWR); if (fd_h2c 0 || fd_c2h 0) { perror(open); return -1; } /* 一轮 H2C 加一轮 C2H测往返时间 */ t0 now_sec(); for (i 0; i 100; i) { ret write(fd_h2c, tx_buf, BUF_SIZE); if (ret ! BUF_SIZE) { perror(write); break; } ret read(fd_c2h, rx_buf, BUF_SIZE); if (ret ! BUF_SIZE) { perror(read); break; } } t1 now_sec(); /* 往返一次算 2 倍 buffer 数据量半双工场景直接按单项算 */ double bytes (double)BUF_SIZE * 100 * 2; printf(ping-pong %d rounds, %.2f s, %.2f MB/s (双向)\n, i, t1 - t0, bytes / (t1 - t0) / 1e6); return 0; }这段测试代码有个关键点write和read交替执行测的是「同步半双工」的往返能力不是单向峰值带宽。如果你的 FPGA 是 AXI 接口注意把 BUF_SIZE 加大到超过 PCIe 的 TLP 包大小256B否则单次传输的开销会被包处理时间掩盖。测试时顺便抓一下网卡的 PCIe 链路速度状态确认运行在 Gen3 而不是降级到了 Gen1带宽算出来差一个数量级。从这个测试成绩出发你还能判断驱动需不需要优化。如果单向带宽接近理论值但双向只有一半多半是 FPGA 侧的 AXI 互连带宽瓶颈或者 H2C/C2H 通道共用了一个 DMA 引擎导致仲裁冲突如果单向带宽就低多半要检查中断亲和性和 BD 深度——把两个通道的中断分开绑到不同核上或者加大 ring size能解决大部分性能不达标问题。我从那以后每次拿到新的 FPGA 板卡或者修改了 PCIe IP 配置都强制自己走一遍「先 lspci 确认枚举 → insmod 看 probe 日志 → 跑一遍 sguser 验证数据正确 → 再跑 ping-pong 量化带宽」的流程顺序不乱数据不靠猜。这套源码和测试程序配合起来从零到稳定传输通常半天内能搞定。希望这条路径能帮你少踩几个 D 的坑让 PCIe 驱动开发从玄学变成工程。本文还有配套的精品资源点击获取
返回列表