ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux与FPGA PCIE通信设备驱动:DMA环设计与中断处理实战

Linux与FPGA PCIE通信设备驱动:DMA环设计与中断处理实战 简介这份资源面向嵌入式Linux驱动开发者与FPGA工程师提供一套带DMA功能的PCIe设备驱动实现用于解决FPGA与主机之间大数据量高速通信时CPU负载过高的问题。内容围绕PCIe分层协议、DMA读写机制与Linux内核PCI子系统展开涵盖设备初始化、中断处理、DMA通道配置、数据传输及资源释放等关键环节适合具备一定内核基础、希望深入理解PCIe驱动开发的中高级读者。压缩包共11个文件约27KB以C源码与头文件为核心另含内核模块、Makefile编译配置及加载卸载脚本结构紧凑便于直接编译验证。目前已有4714人学习下载可作为理解DMA驱动流程、搭建FPGA与Linux通信链路的实用参考。1. Linux 与 FPGA PCIE 通信的设备驱动为什么 DMA 是绕不开的那道坎做过 FPGA 加速卡、采集卡或者边缘网关的工程师大概率都经历过这样一个场景板子插上主机lspci能看到设备BAR 空间也映射上了寄存器读写一切正常但一旦开始搬数据CPU 占用率直接飙到 100%吞吐量却卡在几十 MB/s 上不去。问题往往不在 FPGA 逻辑而在驱动没有把 DMA 用对。Linux 与 FPGA 之间走 PCIE 通信本质上是让主机和 FPGA 通过总线交换数据而 DMA 决定了这场交换是「CPU 一字节一字节搬」还是「硬件自己搬、CPU 只发指令」。这篇笔记就围绕这个标题把设备驱动从枚举、BAR 映射、DMA 环设计到中断收尾的完整链路拆开讲适合正在做 FPGA 板卡驱动、想把数据通路跑通的嵌入式与驱动开发者。热词里常出现的 pcie 枚举过程、dma buffer、字符设备驱动框架都会在对应章节落到具体代码和参数上。先说清楚一个判断如果你的 FPGA 只是被主机读写几个控制寄存器那用ioremap加readl/writel就够了不需要 DMA。但只要涉及图像、ADC 采样、高速采集这类连续数据流DMA 就是必选项。原因很直接——PCIE 的带宽是靠 TLP 包堆出来的CPU 逐字节搬运会消耗大量周期在地址翻译和总线事务上而 DMA 引擎让 FPGA 直接对主机内存发起读写CPU 只负责准备缓冲区和处理完成中断。这也是为什么几乎所有正经的 FPGA PCIE 方案驱动里都会有一个 DMA 描述符环。下面按「先立住原理、再动手复现、最后讲坑」的顺序展开。2. 从 PCIE 枚举到字符设备驱动骨架怎么搭2.1 先搞懂 FPGA 在 Linux 眼里是什么FPGA 通过 PCIE 接入主机后对 Linux 来说它首先是一个 PCI 设备有 Vendor ID、Device ID、BAR 空间和配置空间。内核启动或热插拔时PCIE 枚举过程会扫描总线、分配总线号、读取配置头把设备挂到对应的总线树上。你可以先用命令确认设备是否被正确识别# 查看设备是否被枚举到重点看 Vendor/Device ID 和 BAR 分配 lspci -nn -vv | grep -A 20 Processing accelerators\|Memory controller # 查看 BAR 空间映射情况确认 mmio 起始地址和长度 cat /sys/bus/pci/devices/0000:01:00.0/resourcelspci -nn里的方括号数字就是 Vendor ID 和 Device ID驱动注册时要和它们对上。resource文件里每一行对应一个 BAR格式是起始地址、结束地址、标志位如果某行全是 0说明这个 BAR 没被分配常见原因是 FPGA 的 BAR 长度配置和主机桥窗口不匹配。这一步是后面所有工作的前提枚举不对后面ioremap一定失败。2.2 字符设备驱动框架的最小骨架FPGA 这类设备通常不做成块设备而是字符设备因为数据访问是流式的、需要 ioctl 控制。字符设备驱动框架的核心是file_operations结构体把 open、release、read、write、ioctl、mmap 这些入口挂上去。下面是一个可编译的最小骨架重点看 probe 里怎么拿 BAR 和注册设备#include linux/pci.h #include linux/cdev.h #include linux/fs.h #define FPGA_VENDOR_ID 0x10ee /* 换成你板子的实际 ID */ #define FPGA_DEVICE_ID 0x7021 struct fpga_dev { struct pci_dev *pdev; void __iomem *bar0; /* 控制寄存器区 */ void __iomem *bar1; /* DMA 寄存器区 */ struct cdev cdev; dev_t devno; }; static int fpga_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct fpga_dev *fdev; int ret; ret pci_enable_device(pdev); /* 使能设备打开内存空间 */ if (ret) return ret; ret pci_request_regions(pdev, fpga_dma); if (ret) goto err_disable; /* 映射 BAR0 控制区和 BAR1 DMA 区长度按实际 BAR 大小改 */ fdev-bar0 pci_iomap(pdev, 0, 0); fdev-bar1 pci_iomap(pdev, 1, 0); if (!fdev-bar0 || !fdev-bar1) { ret -ENOMEM; goto err_regions; } pci_set_master(pdev); /* 关键打开 bus masterDMA 才能发起 */ /* 注册字符设备省略 cdev_init/cdev_add 细节 */ return 0; err_regions: pci_release_regions(pdev); err_disable: pci_disable_device(pdev); return ret; }这里有几个参数必须对上pci_enable_device负责唤醒设备并分配资源pci_request_regions声明 BAR 归属防止冲突pci_iomap把物理 BAR 映射成内核虚拟地址供readl/writel使用。最容易被忽略的是pci_set_master它设置配置空间的 Bus Master Enable 位不开这个位FPGA 发起的 DMA 读写会被主机桥直接丢弃现象就是 DMA 永远不完成。BAR 长度参数写 0 表示映射整个 BAR如果只想映射一部分要按实际寄存器区大小传。2.3 控制通路先跑通再谈数据在写 DMA 之前建议先用控制寄存器验证通路。典型做法是 FPGA 里放一个版本寄存器和一个自增计数器驱动读出来打印。这一步能排除掉 BAR 映射错误、字节序问题、地址偏移算错这三类高频故障。确认readl(fdev-bar0 0x0)能读出预期版本号再往下做 DMA否则后面 DMA 出问题你会分不清是描述符错了还是 BAR 根本没通。这个顺序是我踩过坑之后固定下来的习惯。3. DMA 环怎么设计描述符、缓冲区与地址映射3.1 DMA 的两种方向与描述符环结构FPGA 和主机之间的 DMA 分两个方向主机到 FPGAH2C和 FPGA 到主机C2H。采集类应用主要是 C2H加速类应用两个方向都有。高效的做法不是一次一包而是维护一个描述符环FPGA 按环里的描述符依次搬运搬完一个产生一次中断。描述符里通常包含源地址、目的地址、长度和控制位。主机侧要准备两块东西描述符数组本身以及描述符指向的数据缓冲区。#define DESC_NUM 64 #define BUF_SIZE (1 20) /* 每个缓冲区 1MB */ struct dma_desc { u64 src_addr; /* FPGA 侧地址或主机物理地址 */ u64 dst_addr; u32 len; u32 ctrl; /* bit0 有效bit1 完成bit2 中断使能 */ } __attribute__((packed)); struct dma_ring { struct dma_desc *desc; /* 描述符数组需 DMA 一致性内存 */ dma_addr_t desc_phys; /* 描述符数组的物理地址写给 FPGA */ void *buf[DESC_NUM]; /* 数据缓冲区 */ dma_addr_t buf_phys[DESC_NUM]; };__attribute__((packed))不能省否则编译器可能插入填充字节导致主机和 FPGA 对描述符布局的理解不一致这是血泪经验。描述符数组本身也要让 FPGA 能访问所以必须用 DMA 一致性内存分配不能用普通kmalloc。3.2 用 dma_alloc_coherent 分配一致性内存数据缓冲区和描述符数组都要用 DMA 可访问的内存。常见做法是dma_alloc_coherent它保证 CPU 和设备的视图一致不需要手动刷 cachestatic int dma_ring_alloc(struct device *dev, struct dma_ring *ring) { int i; /* 描述符数组一致性内存返回虚拟地址和总线地址 */ ring-desc dma_alloc_coherent(dev, DESC_NUM * sizeof(struct dma_desc), ring-desc_phys, GFP_KERNEL); if (!ring-desc) return -ENOMEM; for (i 0; i DESC_NUM; i) { ring-buf[i] dma_alloc_coherent(dev, BUF_SIZE, ring-buf_phys[i], GFP_KERNEL); if (!ring-buf[i]) return -ENOMEM; /* 初始化描述符目的地址填缓冲区物理地址长度填 BUF_SIZE */ ring-desc[i].dst_addr ring-buf_phys[i]; ring-desc[i].len BUF_SIZE; ring-desc[i].ctrl 0; } return 0; }dma_alloc_coherent返回的desc_phys和buf_phys是设备视角的总线地址写给 FPGA 的必须是这个地址不是 CPU 的虚拟地址也不是virt_to_phys的结果。在带 IOMMU 的平台上总线地址和物理地址还不一样所以一定要用 DMA API 返回的地址。缓冲区大小BUF_SIZE要和 FPGA 侧描述符长度字段的位宽匹配如果 FPGA 里长度寄存器只有 16 位那单包最大只能 64KB写 1MB 会被截断。3.3 把描述符环地址写给 FPGA 并启动分配完之后把描述符数组的总线地址写到 FPGA 的 DMA 控制寄存器然后置启动位/* 假设 BAR1 偏移 0x00 是描述符基地址低 32 位0x04 是高 32 位 */ writel(lower_32_bits(ring-desc_phys), fdev-bar1 0x00); writel(upper_32_bits(ring-desc_phys), fdev-bar1 0x04); writel(DESC_NUM, fdev-bar1 0x08); /* 描述符数量 */ writel(0x1, fdev-bar1 0x0c); /* 启动 DMA */写完启动位后FPGA 会从描述符环里取描述符、发起 PCIE 写事务把数据写进主机内存完成后按描述符的 ctrl 位决定是否发中断。这里要注意寄存器偏移和位定义必须和 FPGA 逻辑严格一致建议在 FPGA 侧也放一个可读的调试寄存器回读描述符基地址确认写进去了。如果启动后没有任何中断先回读这个寄存器能快速判断是主机没写对还是 FPGA 没取到。4. 中断处理与数据回收别让完成事件丢了4.1 申请中断与中断处理的上半部DMA 完成中断是驱动回收数据的触发点。PCIE 设备的中断通常是 MSI 或 MSI-X比传统 INTx 更可靠。申请中断用pci_alloc_irq_vectors和request_irqret pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI | PCI_IRQ_MSIX); if (ret 0) return ret; ret request_irq(pci_irq_vector(pdev, 0), fpga_irq_handler, IRQF_SHARED, fpga_dma, fdev);中断处理函数里不要做耗时操作只做状态读取和唤醒工作队列。典型做法是读 FPGA 的中断状态寄存器判断是哪个描述符完成清中断然后调度下半部static irqreturn_t fpga_irq_handler(int irq, void *data) { struct fpga_dev *fdev data; u32 status readl(fdev-bar1 0x10); /* 中断状态寄存器 */ if (!(status DMA_DONE)) return IRQ_NONE; writel(status, fdev-bar1 0x10); /* 写回清中断 */ tasklet_schedule(fdev-dma_tasklet); /* 下半部处理数据 */ return IRQ_HANDLED; }IRQF_SHARED在 MSI 场景下通常不需要但保留不影响。关键是清中断要写回状态寄存器很多 FPGA 逻辑设计成写 1 清零如果驱动只读不清中断会一直触发系统直接卡死。4.2 描述符回收与缓冲区轮转下半部里遍历描述符找出 ctrl 位标记完成的那些把数据交给上层然后重置描述符让 FPGA 继续用static void dma_tasklet(unsigned long data) { struct fpga_dev *fdev (struct fpga_dev *)data; int i; for (i 0; i DESC_NUM; i) { struct dma_desc *d fdev-ring.desc[i]; if (!(d-ctrl DESC_DONE)) continue; /* 数据在 ring.buf[i] 里长度 d-len交给上层队列 */ push_to_queue(fdev, fdev-ring.buf[i], d-len); d-ctrl 0; /* 清完成位重新有效 */ d-dst_addr fdev-ring.buf_phys[i]; d-len BUF_SIZE; } /* 通知 FPGA 描述符已回收可以继续搬 */ writel(0x2, fdev-bar1 0x0c); }这里有个容易翻车的点d-len在完成时可能被 FPGA 改写为实际搬运长度如果你在重置时直接覆盖成BUF_SIZE要确认上层已经拷走数据。另外描述符回收和 FPGA 取描述符之间存在竞争正规做法是用门铃寄存器通知而不是让 FPGA 轮询 ctrl 位具体取决于 FPGA 逻辑实现两边要对齐协议。4.3 用 mmap 把缓冲区暴露给用户态如果用户态程序要直接处理数据避免内核到用户态的又一次拷贝可以在驱动里实现 mmap把 DMA 缓冲区映射到用户空间static int fpga_mmap(struct file *filp, struct vm_area_struct *vma) { struct fpga_dev *fdev filp-private_data; unsigned long size vma-vm_end - vma-vm_start; unsigned long pfn; if (size BUF_SIZE) return -EINVAL; pfn virt_to_phys(fdev-ring.buf[0]) PAGE_SHIFT; return remap_pfn_range(vma, vma-vm_start, pfn, size, vma-vm_page_prot); }注意dma_alloc_coherent分配的内存做 mmap 时cache 属性要和用户态访问方式匹配否则会出现用户态读到旧数据的情况。如果平台不支持一致性映射到用户态退而求其次用dma_mmap_coherent它内部处理了这些属性。5. 避坑与排查DMA 不工作时先看这几处5.1 现象DMA 启动后永远不完成无中断原因通常是pci_set_master没调用或者 FPGA 侧 Bus Master Enable 位没生效。解决确认 probe 里调用了pci_set_master并用lspci -vv查看设备配置空间的 Bus Master 位是否为。如果 FPGA 逻辑自己控制该位检查逻辑是否正确置位。5.2 现象中断疯狂触发系统卡死原因是中断状态寄存器没有正确清除或者 FPGA 中断是电平触发而驱动按边沿处理。解决确认清中断的写操作地址和值正确电平触发中断要在处理函数里彻底清源必要时在 FPGA 侧加中断屏蔽位处理完再打开。5.3 现象数据搬过来了但内容是错的或错位原因是描述符结构体没加packed或者主机和 FPGA 对地址字段的字节序理解不一致。解决检查sizeof(struct dma_desc)是否等于两边约定的字节数PCIE 是小端FPGA 侧如果用了大端寄存器要转换。另外确认写给 FPGA 的是 DMA API 返回的总线地址不是虚拟地址。5.4 现象跑一段时间后 DMA 报错或系统 OOM原因是描述符或缓冲区泄漏或者dma_alloc_coherent分配的内存在 remove 时没释放。解决在 remove 里成对调用dma_free_coherent描述符环的每个缓冲区都要释放。如果用了 IOMMU还要确认映射没有泄漏。5.5 现象吞吐量远低于预期原因是描述符数量太少导致频繁中断或者单包长度太小。解决增大DESC_NUM和BUF_SIZE让每次 DMA 搬更多数据减少中断频率。同时确认 PCIE 链路速率和宽度用lspci -vv看LnkSta是否协商到了预期速率链路降速会直接限制 DMA 带宽。6. 进阶技巧用门铃与多队列把 DMA 压榨到链路极限单描述符环在中等速率下够用但要做到接近 PCIE 链路极限得考虑多队列和门铃机制。多队列的思路是每个 CPU 核绑定一个描述符环和一条 MSI-X 中断减少锁竞争和 cache bouncing。实现上把dma_ring做成数组每个环独立申请中断向量#define QUEUE_NUM 4 for (i 0; i QUEUE_NUM; i) { dma_ring_alloc(pdev-dev, fdev-rings[i]); /* 每个环一个 MSI-X 向量 */ request_irq(pci_irq_vector(pdev, i), fpga_irq_handler, IRQF_SHARED, fpga_dma, fdev-rings[i]); }门铃机制则是主机写完描述符后往一个特定寄存器写一个值通知 FPGA而不是让 FPGA 轮询描述符有效位。这样 FPGA 逻辑更简单延迟也更低。门铃寄存器和描述符环要一一对应多队列时每个环一个门铃。验证 DMA 是否真的跑满别只看应用层吞吐要在驱动里统计。我一般会在描述符回收时累加字节数和中断次数通过 debugfs 暴露出来# 假设驱动在 debugfs 下建了 fpga_dma 目录 cat /sys/kernel/debug/fpga_dma/stats # 输出示例bytes1073741824 irqs1024 queues4用bytes除以irqs得到每次中断平均搬运量如果这个值远小于BUF_SIZE说明中断太频繁该加大缓冲区或合并中断。用总字节除以时间得到实际带宽和lspci里的链路理论带宽对比差距大就查描述符环深度和 FPGA 侧仲裁逻辑。最后说个我自己的习惯每次改完 DMA 相关代码先用小缓冲区跑通功能再逐步加大到目标值中间用dmesg盯着有没有DMA-API相关的警告。DMA 的问题往往不是一次暴露的是跑一段时间才冒出来所以压力测试至少跑够几轮缓冲区轮转。这套方案值不值得做取决于你的数据量——只要单次传输超过几十 KB 且是连续的DMA 带来的 CPU 解放和吞吐提升就完全值得投入。希望帮到你。本文还有配套的精品资源点击获取
返回列表