
简介这份资源面向嵌入式Linux驱动开发者与FPGA工程师提供一套带DMA功能的PCIe设备驱动实现用于解决FPGA与主机之间高速数据传输时CPU负载过高的问题。内容围绕PCIe分层协议、DMA读写机制与Linux内核PCI子系统展开涵盖设备初始化、中断处理、DMA通道配置、数据传输及资源释放等关键环节适合具备一定内核驱动基础、希望深入掌握高速总线通信的中高级开发者参考。压缩包共11个文件约27KB以C源码与头文件为核心另含内核模块、Makefile编译配置及加载卸载脚本结构紧凑、便于直接编译验证。目前已有4714人学习下载。通过阅读源码与配套脚本读者可理解PCIe设备探测与DMA映射的完整流程掌握用户空间与设备交互的测试思路并借鉴其模块化组织方式为自研FPGA加速卡驱动提供可复用的工程模板与排错参考。1. Linux 与 FPGA 的 PCIE 通信驱动从枚举到 DMA 打通的一条龙拆解手里有一块 FPGA 板卡PCIE 金手指插进主机lspci能看到设备但/dev下什么都没有数据搬不动——这是很多做嵌入式 Linux 的工程师第一次碰 FPGA PCIE 时的真实处境。这份资源就是冲着这个场景来的一套 Linux 侧 PCIE 设备驱动核心是把 FPGA 当 PCIE 端点Endpoint在主机侧完成枚举、BAR 空间映射、中断注册再通过 DMA 把批量数据在主机内存和 FPGA 之间搬起来。它适合三类人正在做 FPGA 加速卡、采集卡、边缘网关的驱动开发者需要给自研板卡写字符设备接口的嵌入式工程师以及想把 PCIE 协议从「考试知识点」变成「能跑起来的代码」的熟手。下面按「资源是什么 → 怎么用 → 坑在哪」的顺序拆开讲代码和参数都能直接抄。2. PCIE 端点驱动骨架从 probe 到字符设备注册2.1 为什么先讲枚举和 BAR而不是直接写 DMA很多人拿到驱动源码第一反应是翻 DMA 那段结果改半天发现设备根本没被probe。PCIE 设备驱动的入口不是module_init里的注册而是内核在枚举阶段匹配到vendor ID和device ID后回调的probe函数。枚举过程由主机侧 Root Complex 发起扫描总线、分配总线号、读取配置空间最终把设备的 BARBase Address Register映射到系统内存地址。FPGA 作为端点配置空间里的vendor ID、device ID、class code通常由 IP 核或硬件逻辑写死驱动里的pci_device_id表必须和它一致否则probe永远不会被调用。常见做法是先用lspci -nn确认设备是否被识别拿到[xxxx:yyyy]格式的 ID再回填到驱动。BAR 空间则决定了你能访问 FPGA 内部哪些寄存器BAR0 一般放控制寄存器BAR1/BAR2 放数据缓冲或 DMA 描述符区。驱动里用pci_resource_start和pci_resource_len拿到物理地址和长度再ioremap成内核虚拟地址之后读写寄存器就走这个映射。这一步没做对后面 DMA 全是空中楼阁。2.2 驱动骨架代码与参数说明下面这段是典型的 PCIE 字符设备驱动骨架保留了 probe、BAR 映射、字符设备注册三条主线去掉了具体硬件相关的寄存器偏移方便你替换成自己的。#include linux/module.h #include linux/pci.h #include linux/fs.h #include linux/cdev.h #include linux/uaccess.h #define DEV_NAME fpga_pcie #define FPGA_VENDOR_ID 0x10ee // 替换为你的 FPGA vendor ID #define FPGA_DEVICE_ID 0x7021 // 替换为你的 device ID static struct pci_device_id fpga_pci_ids[] { { PCI_DEVICE(FPGA_VENDOR_ID, FPGA_DEVICE_ID) }, { 0, } }; MODULE_DEVICE_TABLE(pci, fpga_pci_ids); struct fpga_dev { struct pci_dev *pdev; void __iomem *bar0; // 控制寄存器 void __iomem *bar1; // 数据缓冲/DMA 描述符 resource_size_t bar0_len; resource_size_t bar1_len; 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; fdev devm_kzalloc(pdev-dev, sizeof(*fdev), GFP_KERNEL); if (!fdev) return -ENOMEM; fdev-pdev pdev; pci_set_drvdata(pdev, fdev); ret pci_enable_device(pdev); // 使能设备打开内存/IO 空间 if (ret) return ret; ret pci_request_regions(pdev, DEV_NAME); // 声明 BAR 资源防止冲突 if (ret) goto err_disable; fdev-bar0_len pci_resource_len(pdev, 0); fdev-bar0 pci_iomap(pdev, 0, fdev-bar0_len); if (!fdev-bar0) { ret -ENOMEM; goto err_regions; } fdev-bar1_len pci_resource_len(pdev, 1); fdev-bar1 pci_iomap(pdev, 1, fdev-bar1_len); if (!fdev-bar1) { ret -ENOMEM; goto err_unmap_bar0; } pci_set_master(pdev); // 关键使能 bus masterDMA 才能发起 ret alloc_chrdev_region(fdev-devno, 0, 1, DEV_NAME); if (ret) goto err_unmap_bar1; cdev_init(fdev-cdev, fpga_fops); fdev-cdev.owner THIS_MODULE; ret cdev_add(fdev-cdev, fdev-devno, 1); if (ret) goto err_chrdev; dev_info(pdev-dev, FPGA PCIE probed, BAR0 len%llu BAR1 len%llu\n, fdev-bar0_len, fdev-bar1_len); return 0; err_chrdev: unregister_chrdev_region(fdev-devno, 1); err_unmap_bar1: pci_iounmap(pdev, fdev-bar1); err_unmap_bar0: pci_iounmap(pdev, fdev-bar0); err_regions: pci_release_regions(pdev); err_disable: pci_disable_device(pdev); return ret; }逻辑上分四步pci_enable_device唤醒设备并分配中断线pci_request_regions把 BAR 资源锁住避免和其他驱动抢pci_iomap把物理 BAR 映射成内核虚拟地址之后ioread32/iowrite32就能操作寄存器pci_set_master是 DMA 的开关不调用它设备发起的 bus master 读写会被主机拒绝DMA 描述符写进去也没反应。字符设备注册用alloc_chrdev_region动态拿主设备号cdev_add挂上file_operations用户态就能通过/dev/fpga_pcie节点访问。参数上要盯紧三个FPGA_VENDOR_ID和FPGA_DEVICE_ID必须和lspci -nn输出一致BAR 索引要和硬件设计对应别把 BAR0 当数据区pci_resource_len拿到的长度用于ioremap如果硬件 BAR 声明的是 1MB映射时别只映射 4KB否则访问偏移超过 4KB 就翻车。2.3 字符设备接口read/write/ioctl 怎么落到 FPGA 寄存器字符设备是用户态和驱动之间的门。read通常用于从 FPGA 读数据write用于下发配置或写数据ioctl用于控制类操作比如启动 DMA、查询状态、复位。下面这段file_operations把三条路径都留了钩子。static long fpga_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct fpga_dev *fdev filp-private_data; void __user *uarg (void __user *)arg; u32 val; switch (cmd) { case FPGA_GET_STATUS: val ioread32(fdev-bar0 REG_STATUS); if (copy_to_user(uarg, val, sizeof(val))) return -EFAULT; break; case FPGA_START_DMA: iowrite32(1, fdev-bar0 REG_DMA_CTRL); // 写控制寄存器启动 DMA break; case FPGA_RESET: iowrite32(0x1, fdev-bar0 REG_RESET); break; default: return -ENOTTY; } return 0; } static ssize_t fpga_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { struct fpga_dev *fdev filp-private_data; // 这里通常配合 DMA 完成后的中断把数据从内核缓冲拷到用户态 // 简化示例直接从 BAR1 读 if (count fdev-bar1_len) count fdev-bar1_len; if (copy_to_user(buf, fdev-bar1, count)) return -EFAULT; return count; } static const struct file_operations fpga_fops { .owner THIS_MODULE, .read fpga_read, .unlocked_ioctl fpga_ioctl, .open fpga_open, .release fpga_release, };ioctl里的cmd是自己定义的魔数常见做法是用_IOR/_IOW宏生成避免和其他驱动冲突。ioread32/iowrite32操作的是 BAR 映射后的虚拟地址偏移量REG_STATUS、REG_DMA_CTRL要和 FPGA 逻辑里的寄存器地址对齐。read里直接从 BAR1 拷数据只适合小数据量大批量必须走 DMA否则 CPU 占用和延迟都很难看。copy_to_user返回非零说明用户态地址非法直接返回-EFAULT别继续往下走。3. DMA 通道搭建描述符、一致性内存与中断收尾3.1 DMA 在 PCIE 场景下到底怎么工作PCIE 的 DMA 和片上 DMA 不太一样。FPGA 作为端点要主动读写主机内存必须通过 bus master 发起 Memory Read/Write TLP 包目标地址是主机侧的物理地址。驱动要做的是在内核里分配一块物理连续或 IOMMU 映射后的缓冲把它的物理地址告诉 FPGAFPGA 按描述符搬完数据后发中断驱动在中断里收尾并唤醒等待的进程。这里有两个关键点一是缓冲的物理地址必须让 FPGA 能访问二是缓存一致性——CPU 和 FPGA 都可能读写这块内存不处理 cache 就会读到旧数据。常见做法是用dma_alloc_coherent分配一致性内存它返回虚拟地址和总线地址dma_addr_t总线地址就是给 FPGA 用的。如果数据量大且不想长期占用一致性内存也可以用dma_map_single做流式映射但每次传输前后要dma_sync_single_for_device/cpu处理 cache。描述符环descriptor ring是另一种常见结构驱动在内存里建一组描述符每个描述符含缓冲地址、长度、控制位FPGA 按环顺序取完成后写回状态位并中断。3.2 一致性缓冲分配与描述符初始化代码#define DMA_BUF_SIZE (4 * 1024 * 1024) // 4MB 缓冲 #define DESC_NUM 64 struct dma_desc { u64 buf_addr; // 主机侧总线地址 u32 len; u32 flags; // bit0: 有效, bit1: 完成 }; struct fpga_dma { void *cpu_addr; // CPU 虚拟地址 dma_addr_t dma_addr; // 给 FPGA 的总线地址 struct dma_desc *desc; // 描述符环 dma_addr_t desc_dma; int desc_head; }; static int fpga_dma_init(struct fpga_dev *fdev) { struct fpga_dma *dma fdev-dma; dma-cpu_addr dma_alloc_coherent(fdev-pdev-dev, DMA_BUF_SIZE, dma-dma_addr, GFP_KERNEL); if (!dma-cpu_addr) return -ENOMEM; dma-desc dma_alloc_coherent(fdev-pdev-dev, DESC_NUM * sizeof(struct dma_desc), dma-desc_dma, GFP_KERNEL); if (!dma-desc) { dma_free_coherent(fdev-pdev-dev, DMA_BUF_SIZE, dma-cpu_addr, dma-dma_addr); return -ENOMEM; } for (int i 0; i DESC_NUM; i) { dma-desc[i].buf_addr dma-dma_addr i * (DMA_BUF_SIZE / DESC_NUM); dma-desc[i].len DMA_BUF_SIZE / DESC_NUM; dma-desc[i].flags 0; } // 把描述符环基地址写到 FPGA 寄存器 iowrite32(lower_32_bits(dma-desc_dma), fdev-bar0 REG_DESC_LO); iowrite32(upper_32_bits(dma-desc_dma), fdev-bar0 REG_DESC_HI); iowrite32(DESC_NUM, fdev-bar0 REG_DESC_NUM); return 0; }dma_alloc_coherent的第三个参数是输出参数拿到的是总线地址直接写进 FPGA 寄存器即可。描述符环里每个描述符的buf_addr是总线地址不是 CPU 虚拟地址写错的话 FPGA 会往错误的主机物理地址搬数据轻则数据错乱重则踩到内核内存直接 panic。REG_DESC_LO/HI是 FPGA 侧接收描述符基地址的寄存器64 位地址分两次写低 32 位在前还是高 32 位在前要看硬件约定写反了描述符地址就变成另一个值。DESC_NUM告诉 FPGA 环的大小硬件逻辑按这个数取模循环。3.3 中断处理与 DMA 完成通知DMA 搬完数据后FPGA 拉中断线Linux 侧的中断处理程序负责确认状态、更新描述符、唤醒等待队列。PCIE 中断可以是 INTx 传统中断也可以是 MSI/MSI-X后者更常见因为支持多向量且不需要共享中断线。static irqreturn_t fpga_irq_handler(int irq, void *dev_id) { struct fpga_dev *fdev dev_id; u32 status ioread32(fdev-bar0 REG_IRQ_STATUS); if (status IRQ_DMA_DONE) { // 清除中断标志 iowrite32(IRQ_DMA_DONE, fdev-bar0 REG_IRQ_CLR); // 标记描述符完成唤醒等待的读进程 fdev-dma.desc[fdev-dma.desc_head].flags | 0x2; wake_up_interruptible(fdev-dma_wq); } return IRQ_HANDLED; } static int fpga_request_irq(struct fpga_dev *fdev) { int ret pci_alloc_irq_vectors(fdev-pdev, 1, 4, PCI_IRQ_MSI | PCI_IRQ_MSIX); if (ret 0) return ret; ret request_irq(pci_irq_vector(fdev-pdev, 0), fpga_irq_handler, 0, DEV_NAME, fdev); if (ret) pci_free_irq_vectors(fdev-pdev); return ret; }pci_alloc_irq_vectors优先申请 MSI/MSI-X失败再退回 INTx。pci_irq_vector拿到具体的中断号传给request_irq。中断处理里先读状态寄存器判断是不是自己的中断再清标志顺序反了可能丢中断。wake_up_interruptible唤醒在read里等 DMA 完成的进程等待队列的初始化和wait_event_interruptible配合使用别在中断上下文里做耗时操作。4. 避坑与排查枚举不到、DMA 不动、中断不来的血泪经验4.1 现象lspci 看不到设备驱动 probe 不执行原因通常有三层硬件层 FPGA 配置没加载PCIE 核没起来链路层 PERST 信号时序不对主机侧没完成链路训练配置空间里 vendor/device ID 是默认值或全 F。解决顺序是先量 PERST 和参考时钟确认 FPGA 配置完成再用lspci -vvv看链路状态LnkSta显示速度宽度是否正常最后核对 IP 核里配置空间的 ID 是否和驱动pci_device_id表一致。常见做法是在 FPGA 逻辑里把 ID 做成可配置寄存器避免每次改 ID 都要重新综合。4.2 现象probe 成功但读写 BAR 寄存器全返回 0xFFFFFFFF这是典型的 BAR 映射失败或地址偏移错误。0xFFFFFFFF表示主机读不到目标地址可能是pci_iomap返回的虚拟地址没真正映射到 BAR也可能是 BAR 索引写错——比如硬件把控制寄存器放在 BAR2驱动却映射 BAR0。排查时先cat /proc/iomem看 BAR 是否被正确分配再用dev_info打印pci_resource_start/len和lspci -vvv里的 Region 信息对比。如果 BAR 长度是 0说明硬件配置空间里 BAR 没使能需要检查 FPGA 侧 PCIE 核的 BAR 配置。4.3 现象DMA 描述符写进去了但数据不动、中断不来先确认pci_set_master是否调用没调用的话设备发起的 bus master 事务会被主机拒绝FPGA 侧可能连 TLP 都发不出去。再检查描述符的总线地址是否正确——dma_alloc_coherent返回的dma_addr_t在 32 位系统上可能是 32 位在 64 位系统上是 64 位写寄存器时高低位顺序要和硬件约定一致。如果描述符地址对但数据不动看 FPGA 侧是否真的启动了 DMA 引擎有些设计需要先写启动位再写描述符地址顺序反了引擎不跑。中断不来则检查 MSI 使能位和中断屏蔽寄存器pci_alloc_irq_vectors返回的向量数要大于 0。4.4 现象DMA 数据偶发错乱跑一段时间后 panic大概率是 cache 一致性问题。用dma_alloc_coherent分配的一致性内存不会有这个问题但如果用了dma_map_single或自己kmalloc后拿virt_to_phys当总线地址CPU 写的数据可能还在 cache 里没落到内存FPGA 读到的就是旧数据。解决方法是流式映射前后调用dma_sync_single_for_device和dma_sync_single_for_cpu或者干脆统一用一致性内存。另一个可能是描述符环的 head/tail 指针竞争驱动和 FPGA 同时改同一个描述符需要硬件写回状态位、驱动只读或者用原子操作保护。4.5 现象卸载模块时卡死或报资源占用remove函数里释放顺序反了。正确顺序是先关中断、释放 IRQ 向量再停 DMA、释放 DMA 缓冲然后cdev_del、unregister_chrdev_region接着pci_iounmap两个 BARpci_release_regions最后pci_disable_device。如果先pci_disable_device再访问 BAR就会读到异常值甚至卡总线。另外dma_free_coherent的参数必须和分配时一致长度、虚拟地址、总线地址都要对上否则内核会报警告。5. 进阶技巧用 msi 多向量 分散聚集把吞吐拉满单向量中断加单块一致性内存在小数据量下够用但要做高速采集或图像传输瓶颈很快出现。两个进阶方向MSI-X 多向量和分散聚集Scatter-GatherDMA。MSI-X 允许设备申请多个中断向量每个向量对应不同的完成事件比如向量 0 报 DMA 完成、向量 1 报错误、向量 2 报定时器驱动侧分别注册处理函数减少单处理函数里的分支判断。pci_alloc_irq_vectors的第二个参数设成期望的向量数第三个参数带上PCI_IRQ_MSIX返回实际拿到的数量然后循环request_irq绑定每个向量。分散聚集则是把多个不连续的缓冲拼成一条 DMA 链FPGA 按描述符逐个搬不用要求一大块连续物理内存。Linux 侧可以用dma_map_sg把scatterlist映射成总线地址数组填进描述符环。下面是一个简化的 SG 描述符填充片段。struct scatterlist *sg; int nents, i; nents dma_map_sg(fdev-pdev-dev, fdev-sgl, fdev-sg_nents, DMA_FROM_DEVICE); if (!nents) return -EIO; for_each_sg(fdev-sgl, sg, nents, i) { fdev-dma.desc[i].buf_addr sg_dma_address(sg); fdev-dma.desc[i].len sg_dma_len(sg); fdev-dma.desc[i].flags 0x1; // 有效 } iowrite32(nents, fdev-bar0 REG_DESC_NUM); iowrite32(1, fdev-bar0 REG_DMA_CTRL);dma_map_sg返回实际映射的条目数可能小于传入的sg_nents因为相邻的物理页会被合并。sg_dma_address和sg_dma_len拿到每个条目的总线地址和长度填进描述符。传输完成后要dma_unmap_sg解除映射方向参数DMA_FROM_DEVICE表示数据从设备到主机写反了 cache 同步方向就错了。验证吞吐时我一般会在用户态用dd或自己写的小工具连续读/dev/fpga_pcie同时用perf看中断频率和 CPU 占用。如果中断频率过高把描述符环加大、每次中断处理多个完成描述符如果 CPU 占用高但吞吐上不去检查是不是每次read都触发一次 DMA改成批量提交。还有一点容易忽略PCIE 的 Max Payload Size 和 Max Read Request Size 在链路训练时协商FPGA 侧 IP 核如果限制成 128 字节吞吐会被卡住常见做法是在配置空间里把这两个值设成 256 或 512前提是主机侧也支持。从那以后我每次调 PCIE DMA都强制先跑一遍lspci -vvv确认链路和 BAR再在驱动里打印一致性内存的总线地址最后才动 DMA 引擎。这套顺序帮我省掉了至少三次通宵排查。希望帮到你。本文还有配套的精品资源点击获取