ARTICLE DETAIL

资讯详情

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

Linux下FPGA PCIe驱动开发:DMA传输与稳定性调优

Linux下FPGA PCIe驱动开发:DMA传输与稳定性调优 简介面向嵌入式Linux驱动开发者的一份PCIe设备驱动示例工程以Altera FPGA为对端实现带DMA的高速数据传输尤其适合需要理解Linux PCI子系统框架与DMA编程的进阶学习者。包内共11个文件约27KB包含C源码与头文件、Makefile构建配置、编译生成的内核模块ko以及加载、卸载、运行、用户测试等辅助脚本便于快速装载驱动并通过用户空间程序验证通信。驱动代码完整覆盖PCI设备探测、资源分配、中断处理、DMA通道申请与传输映射、设备卸载释放等关键路径可对照学习PCIe协议分层、DMA读写机制以及内核模块与用户空间的交互流程。这份工程既能作为课程设计与项目起步的模板也可为FPGA与Linux主机高性能通信提供一套可运行的驱动骨架。目前已有4745人学习浏览参考价值得到较多开发者认可。1. 先搞清这套 PCIE 驱动到底在解决什么问题一块 FPGA 板卡插到 x86 服务器上lspci 能看到设备但没有驱动内核只会给它分配一个默认的“未知设备”占位IO 地址和中断全都没人接管。更现实的问题是就算你写了个最简单的字符设备驱动能读写寄存器数据搬运也走 PIOprogrammed I/O方式CPU 被拖死PCIe 带宽利用率常常不到 20%。这套“Linux 与 FPGA PCIE 通信的设备驱动带 DMA”的方案解决的就是把 FPGA 当成一个高速外设挂到主机上让数据从 FPGA 片上 RAM 直达主机内存中间不经过 CPU 逐字节复制。适合做数据采集卡、软件无线电、图像采集加速、高速信号回放这类项目的嵌入式 Linux 工程师。读完你至少能搭出一个可用的 PCIe 驱动骨架配好 DMA 传输路径并且知道链路掉速、AER 报错这些坑在哪儿。2. 从枚举到握住设备PCIe 地址空间与驱动骨架2.1 枚举阶段先认清你的设备lspci 与 BAR 空间PCIe 设备上电后由 Host BridgeRC负责枚举给每个设备分配 Bus/Device/Function 号并读取配置空间里的 Vendor ID、Device ID、Class Code 等信息。驱动写的第一件事就是在探测阶段找到这个设备并确认它的资源布局。常见的做法是先通过 lspci 看设备有没有被正确枚举lspci -vvv -s 03:00.0重点看两个段落LnkSta显示的Speed 8GT/s, Width x4表示链路工作在 Gen3 x4 状态带宽上限 4GB/s如果显示Speed 2.5GT/s那就是 Gen1多半是链路训练没搞好。另一段是Region开头的 BAR 空间比如Region 0: Memory at 0x...这决定了你在驱动里要映射哪些物理地址窗口。PCIe 配置空间里最多有 6 个 BARBase Address RegisterFPGA 厂商的参考设计里通常只用 BAR0 放控制寄存器BAR1 或 BAR2 放数据缓冲区或 DMA 描述符环。BAR 空间的物理地址由 RC 在枚举时分配驱动拿到的是物理地址必须通过ioremap映射成内核虚拟地址才能读写绝对不能用__va直接把物理地址转成虚拟地址PCIe 设备的 BAR 空间不在内核线性映射范围内。2.2 字符设备驱动骨架probe/release/ioctl 最少可加载代码驱动整体分成 PCIe 驱动部分和字符设备部分。PCIe 驱动负责探测、映射、申请中断字符设备负责把 DMA 能力和用户态打通。下面是最小可加载的骨架我用的是 kernel 6.x 的接口写static int fpga_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct fpga_dev *fpga; int ret; fpga kzalloc(sizeof(*fpga), GFP_KERNEL); if (!fpga) return -ENOMEM; ret pci_enable_device(pdev); if (ret) goto err_free; ret pci_request_regions(pdev, DRV_NAME); if (ret) goto err_disable; fpga-pdev pdev; fpga-bar0 pci_ioremap_bar(pdev, 0); /* 控制寄存器 */ if (!fpga-bar0) { ret -ENOMEM; goto err_release; } /* 保存 BAR 的物理地址和长度DMA 描述符配置要用 */ fpga-bar0_phys pci_resource_start(pdev, 0); fpga-bar0_len pci_resource_len(pdev, 0); pci_set_master(pdev); pci_set_drvdata(pdev, fpga); ret fpga_setup_chardev(fpga); if (ret) goto err_unmap; dev_info(pdev-dev, FPGA PCIe probe ok, BAR0 phys0x%llx len%d\n, fpga-bar0_phys, (int)fpga-bar0_len); return 0; err_unmap: iounmap(fpga-bar0); err_release: pci_release_regions(pdev); err_disable: pci_disable_device(pdev); err_free: kfree(fpga); return ret; }注意几个必须做的动作。pci_enable_device会请求设备所需的 IO/MEM 资源并开启 bus master 相关配置必须在pci_request_regions之前调用。pci_set_master(pdev)这一行必不可少不使能 bus master设备就无法发起 DMA 读写在 PCIe 总线上这是新手最容易漏掉的一步。pci_ioremap_bar是ioremap的封装直接拿 BAR 的物理地址做映射比手动pci_resource_startioremap少出错。probe 里还有一个隐藏点fpga_setup_chardev注册字符设备和misc设备节点。用misc_register比register_chrdev_region方便节点名固定用户态/dev/fpga0出现即说明驱动加载成功。用户态通过 open/ioctl/read/write 操作这个节点ioctl 通常用来触发 DMA 启动、复位 FPGA、读取状态寄存器read/write 可以走阻塞式 DMA 传输。2.3 BAR 寄存器读写参数偏移与位宽要跟 FPGA 端对齐FPGA 端的控制寄存器一般按 32 位对齐排列偏移地址在 RTL 里用case语句定义。内核侧读写推荐用readl/writel而不是ioread32/iowrite32二者在强一致内存模型下行为相同但readl/writel在 ARM 平台上的内存屏障语义更明确。#define FPGA_REG_CTRL 0x00 #define FPGA_REG_STATUS 0x04 #define FPGA_REG_DMA_ADDR 0x08 #define FPGA_REG_DMA_LEN 0x0C static void fpga_start_dma(struct fpga_dev *fpga, dma_addr_t bus_addr, u32 len) { writel(lower_32_bits(bus_addr), fpga-bar0 FPGA_REG_DMA_ADDR); writel(upper_32_bits(bus_addr), fpga-bar0 FPGA_REG_DMA_ADDR 4); writel(len, fpga-bar0 FPGA_REG_DMA_LEN); writel(0x01, fpga-bar0 FPGA_REG_CTRL); /* 启动 DMA */ }64 位 DMA 地址必须分两次写入高 32 位和低 32 位且要先写高 32 位再写低 32 位否则 FPGA 端在写入中间态可能抓到错误的地址组合这是 RTL 与驱动协同设计时默认的握手规矩。FPGA_REG_STATUS一般会有一个 done 位如果支持中断就通过中断通知不支持就只能轮询状态寄存器轮询间隔建议不少于 1ms否则 CPU 占用率会异常升高。3. DMA 传输的实现dma_engine 子系统与手动寄存器方式3.1 为什么不能用 PIO 搬运数据PIO 方式下CPU 执行readl从 FPGA 的 BAR 空间读数再把数据memcpy到用户态缓冲区。一次 32 位读要经历 PCIe 事务的地址段、数据段、完成等环节有效吞吐率大概只有几百 MB/s而且 CPU 占用率接近 100%。DMA 方式下FPGA 的 DMA 引擎直接把数据写入主机内存地址CPU 只需要在传输完成中断里做一次tasklet或 workqueue 调度去唤醒用户态进程。这也是这套方案的核心价值数据搬运不消耗 CPU带宽能跑到链路物理上限的 80% 以上。实现 DMA 有两条路线内核标准dma_engine子系统以及完全手动操作 DMA 寄存器配合 FPGA 侧的 DMA 引擎。标准子系统抽象了 DMA 控制器和设备之间的通道好处是驱动代码不依赖特定 DMA 控制器硬件缺点是如果 FPGA 侧有自己的 DMA 引擎且不兼容标准 DMAEngine 协议反而多一层。我一般建议如果是 Xilinx/Intel 官方参考设计配套的驱动优先用标准dma_engine它的dma_request_channel接口在主流 SoC 上都能跑通如果 FPGA 侧是自定义 DMA 控制器就手动管理描述符和地址映射代码更直接。3.2 用 dma_engine 子系统申请通道、映射缓冲、提交事务标准路径分三步申请 DMA 通道、准备 buffer、提交并等待完成。下面这段是从一个 Xilinx AXI DMA 参考驱动简化出来的流程struct dma_chan *fpga_dma_chan; struct dma_async_tx_descriptor *tx; dma_cookie_t cookie; dma_addr_t buf_addr; u32 *buf_virt; /* 1. 申请 DMA 通道 */ fpga_dma_chan dma_request_chan(pdev-dev, fpga_dma0); if (IS_ERR(fpga_dma_chan)) return PTR_ERR(fpga_dma_chan); /* 2. 分配一致内存适合 DMA 读/写且需要 CPU 访问的场景 */ buf_virt dma_alloc_coherent(pdev-dev, BUF_SIZE, buf_addr, GFP_KERNEL); if (!buf_virt) return -ENOMEM; /* 3. 准备一次 DMA 读事务 */ tx dmaengine_prep_dma_cyclic(fpga_dma_chan, buf_addr, BUF_SIZE, PERIOD_SIZE, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT); if (!tx) { dma_free_coherent(pdev-dev, BUF_SIZE, buf_virt, buf_addr); return -ENOMEM; } tx-callback fpga_dma_done_cb; tx-callback_param fpga_dev; cookie dmaengine_submit(tx); dma_async_issue_pending(fpga_dma_chan);业务触发 DMA 读的流程是FPGA 侧 DMA 引擎发起读请求把数据从 FPGA 内部 BRAM 搬到主机内存里buf_addr指定的地址。这里的buf_addr是物理地址不是内核虚拟地址因为 PCIe 设备在总线上只认物理地址。dma_alloc_coherent返回的buf_virt是 CPU 侧的虚拟地址驱动和用户态传递数据时需要再配合dma_sync_single_for_cpu做一次同步或者干脆用 mmap 把这块 buffer 映射给用户态省掉一次 copy。参数里比较敏感的是PERIOD_SIZE。它决定中断频率周期越小中断越频繁CPU 负担高但时延低周期越大中断少但用户态拿到数据的等待时间长。数据采集类应用一般设 4KB64KB 之间图像类应用设成一帧大小。千万不要让PERIOD_SIZE小于缓存行大小否则 DMA 和 CPU 的缓存一致性处理会把你折磨到怀疑人生。3.3 FPGA 侧寄存器约定描述符还是连续突发如果走手动寄存器方式FPGA 端的 DMA 控制器有两种常见设计。第一种是“简单突发模式”驱动把目的地址和长度写入 BAR 寄存器FPGA 直接从自己的地址空间发起连续突发读第二种是“描述符链模式”主机内存里放一组描述符表每个描述符包含地址、长度、下一跳指针FPGA 的 DMA 引擎按链遍历。前者适合单次大块传输后者适合流式多包数据。描述符链方式需要驱动在初始化时分配描述符表struct fpga_desc __iomem *desc_virt; dma_addr_t desc_phys; desc_virt dma_alloc_coherent(pdev-dev, sizeof(struct fpga_desc) * DESC_NUM, desc_phys, GFP_KERNEL); for (int i 0; i DESC_NUM - 1; i) { writel(lower_32_bits(buf_phys[i]), desc_virt[i].src_addr); writel(len[i], desc_virt[i].len); writel(0x01, desc_virt[i].ctrl); writel(desc_phys sizeof(struct fpga_desc) * (i 1), desc_virt[i].next); } writel(lower_32_bits(desc_phys), fpga-bar0 FPGA_REG_DESC_BASE);描述符里的src_addr也必须填物理地址。这里最容易踩的坑是dma_alloc_coherent分配的内存放在 32 位地址空间之外而 FPGA 侧寄存器和描述符字段又只有 32 位宽。解决方法是分散分配多个小于 4GB 的 buffer或者在 FPGA 端把地址字段扩展到 64 位两边约定好高地址字段的偏移。4. 中断、内存一致性与 IOMMU决定速度上限的三个黑匣子4.1 MSI-X 中断注册、申请与触发路径PCIe 中断有 INTx、MSI、MSI-X 三种。INTx 是共享中断线多个设备共享一条线Linux 里的中断处理要判断是否是自己的设备触发效率低。MSI-X 是首选因为每个队列可以有独立中断号多队列 DMA 场景下可以把不同通道的中断绑到不同 CPU 上。注册代码很简单ret pci_alloc_irq_vectors(pdev, 1, 4, PCI_IRQ_MSIX); if (ret 0) return ret; for (int i 0; i ret; i) { ret request_irq(pci_irq_vector(pdev, i), fpga_irq_handler, IRQF_SHARED, DRV_NAME, fpga_dev); if (ret) goto err_irq; }pci_alloc_irq_vectors的第一个参数是最少向量数第三个是最多向量数最后一个参数指定允许 MSI-X。返回值是实际分配到的向量数可能比请求的少。FPGA 侧要配合生成 MSI-X 中断需要往 MSI-X Table 的地址写中断消息这部分逻辑在 FPGA 的 RTL 里实现驱动侧只需要检查中断状态寄存器来判断中断来源。中断处理函数里不要做重活。标准做法是disable_irq_nosync之后调用tasklet或者schedule_work在软中断或进程上下文里唤醒用户态读取数据。实测中直接在中断上下文里copy_to_user会引发报错而且导致吞吐率下降。4.2 dma_alloc_coherent 与 streaming DMA什么时候用哪个dma_alloc_coherent分配的内存是一致缓存映射的CPU 和 DMA 设备看到的数据始终一致代价是分配时可能禁止部分缓存优化适用于控制结构、描述符、环形缓冲区等频繁访问的小对象。dma_map_single/dma_map_sg属于 streaming DMA 映射适用于数据块传输可以通过dma_sync_single_for_cpu/dma_sync_single_for_device手动控制同步时机。数据块的正确姿势是用户态发起读 → 驱动把用户 buffer 映射到内核用get_user_pages或pin_user_pages→dma_map_sg映射成 device 地址 → FPGA 写入这个地址 → 完成后dma_unmap_sg并释放页。这里有性能分水岭每次 DMA 都做pin_user_pages代价极高高频小包场景会吃掉一大半带宽应该用dma_alloc_coherent分配固定 buffer再 mmap 给用户态然后把这个 buffer 物理地址直接告诉 FPGA后续每次 DMA 都是固定地址不用反复映射。static int fpga_mmap(struct file *filp, struct vm_area_struct *vma) { struct fpga_dev *fpga filp-private_data; return dma_mmap_coherent(fpga-pdev-dev, vma, fpga-buf_virt, fpga-buf_phys, BUF_SIZE); }4.3 IOMMU 的影响地址转换与直通模式服务器平台上默认启用 IOMMU/VT-d 时PCIe 设备发出的 DMA 地址会先经过 IOMMU 做地址转换。也就是说 FPGA 在描述符里填写的“物理地址”实际上是 IOMMU 映射后的 IOVA不是真正的 DRAM 物理地址。如果驱动用dma_alloc_coherent分配内存内核已经自动完成了 IOVA 映射你把这个地址写进 FPGA 寄存器是安全的但如果你从/proc/iomem手动取了物理地址填给 FPGAIOMMU 会直接拦掉总线访问DMA 永远完不成。调试时如果怀疑 IOMMU 问题可以先用iommupt启动参数看待测平台把 IOMMU 设为直通模式DMA 地址等于物理地址但不能作为生产方案。生产环境要保留 IOMMU驱动里所有传给 FPGA 的地址必须是通过 dma 接口拿到的 device 地址不能是virt_to_phys的结果。这也是驱动最多见的问题来源用virt_to_phys转换内核虚拟地址给 DMA 用在开启 IOMMU 的内核上 100% 翻车。5. 稳定性排查掉卡、降速与 AER 错误的现场处理5.1 hotspotdmesg 持续刷 AER 错误链路掉到 Gen1现象是dmesg刷PCIe Bus Error: severityCorrected或直接 Uncorrected 错误随后lspci显示LnkSta Speed 2.5GT/s链路协商速率掉到 Gen1和之前 Gen3 相比带宽直接降到 1/8。原因通常是 FPGA 侧的电平标准、时钟芯片配置和链路训练参数有问题常见于 PCIe 金手指接触不良、参考时钟抖动过大或者 FPGA 内部 PCIe 硬核 IP 的 TX 均衡参数没调好。软件层面只能补救确认主板 BIOS 中 ASPM主动电源管理是否关闭ASPM 开启时 PCIe 链路会自动降速省电和 FPGA 这种持续高速设备冲突很大建议在 BIOS 里 Disable 或在内核启动参数加pcie_aspmoff。另外检查pcie_link_speed参数可以在启动时固定链路速度pcie_link_speed3限制 Gen3。如果硬件本身不稳定软件只能降速稳链路无法根治。解决步骤是先断开链路复位 FPGA重新插拔或复位后看dmesg是否正确重训到 Gen3如果稳定复现重点查 PCIe 插槽的供电和 REFCLK 差分信号质量软件上先关 ASPM再用pcie_link_speed逐级往下降找稳定点。稳定的最高速才是可用配置追求标称带宽但跑十分钟就掉卡不如稳在 Gen2 x4 跑一整天。5.2 DMA 完成后数据整体错位或随机字节错误但链路没有报错现象是 FPGA 端明明完成了 DMA主机内存里拿到数据的前几个字节对后面全部错位或者每一帧相差固定字节数。原因多数不是 PCIe 链路问题而是地址对齐和长度字段不匹配。FPGA 的 DMA 引擎通常要求目的地址按 64 字节对齐数据长度按 burst length 的整数倍。如果驱动传给 FPGA 的len不是 burst 的整数倍FPGA 会多传几个字节或停在不该停的位置。还有一种情况是upper_32_bits的写入顺序反了32 位地址时低地址对但高地址错数据落在完全错误的内存区域。解决把 buffer 地址强制做ALIGN(buf_phys, 64)长度做ALIGN(len, 64)FPGA 端可传的块长按 64 字节设计。驱动初始化时加一个自检写入一块固定 patternDMA 回读比对错位直接打印出实际偏移量能快速定位是地址问题还是长度问题。这是最实用的排错手段别一上来就盯着 RTL 代码看先把边界条件卡死。5.3 驱动卸载或 FPGA 复位时系统死机或卡死现象表现在rmmod驱动后系统马上 hang 住或者 FPGA 热复位之后内核 panicdmesg 最后一行停在某个 ioremap 地址附近。原因是驱动的 remove 路径没有把设备上的 DMA 活动停下来。FPGA 还在持续往主机内存写数据但驱动已经释放了 buffer 和中断DMA 写到已释放页面上触发内核访问非法地址。标准写法是 remove 里先fpga_stop_dma设置控制寄存器停止位等待 FPGA 确认停止然后再free_irq、dma_free_coherent、iounmap、pci_release_regions。static void fpga_remove(struct pci_dev *pdev) { struct fpga_dev *fpga pci_get_drvdata(pdev); writel(0x00, fpga-bar0 FPGA_REG_CTRL); /* 停止 DMA */ mdelay(10); /* 等待 FPGA 真正停住 */ free_irq(pci_irq_vector(pdev, 0), fpga); dma_free_coherent(pdev-dev, BUF_SIZE, fpga-buf_virt, fpga-buf_phys); iounmap(fpga-bar0); pci_release_regions(pdev); pci_disable_device(pdev); kfree(fpga); }mdelay(10)看着土但实际很管用。FPGA 的 DMA 引擎通常要几个毫秒排空内部 FIFO如果驱动侧不等直接释放资源等于在枪口上写数据。这里的教训是驱动 remove 的顺序必须反向于 probe 的分配顺序每个资源释放前先确认设备侧已经停止访问它。5.4 大块 DMA 测速跑不到理论带宽常卡在 1.5GB/s 左右现象是 FPGA 报完成很快但用户态dd或自写测速工具实测只有 1GB/s 上下跟 Gen3 x8 的理论带宽相差一大截。原因有几个叠加。第一驱动里每次 DMA 都pin_user_pagesdma_map_sg映射开销占了传输时间的一半第二用户态读走 copy 路径read函数里 DMA buffer 到用户 buffer 的copy_to_user把吞吐打回解放前第三中断处理在 CPU 0 上集中跑单核处理中断达到瓶颈。解决固定 DMA buffer 加 mmap 直通数据路径上一次拷贝都不要。中断开启irq_affinity绑定到多个 CPUIRQ 和用户态处理线程错开。实测固定 buffer mmap 后Gen3 x4 链路能到 3GB/s 左右已经接近链路效率上限。带宽上不去的多数项目都属于“驱动设计里多了一次本不必要的内存拷贝”。5.5 FPGA 配置复位后BAR 寄存器全部回归出厂值驱动失联现象是程序跑着跑着没有报错但下一次 ioctl 操作寄存器没有反应lspci 看设备还在但 BAR0 区域的寄存器全部变成复位默认值。原因是 FPGA 加载的 bitstream 里 PCIe IP 配置被重新加载或者 PCIe IP 的复位由外部引脚触发设备在系统视角下没有真正下线但内部寄存器和 DMA 引擎已经回到初值。解决方法是驱动里检测一个版本寄存器或 magic number每次 ioctl 之前读它判断 FPGA 是否在线如果发现值变了就做一次完整的驱动软复位流程pci_reset_function(pdev)重新映射 BAR重新配置 DMA 描述符链把通道的状态恢复到已知初值。注意pci_reset_function会触发设备冷复位之后所有寄存器和配置空间都会丢驱动必须清除原来的映射重建而不是直接继续写旧地址。6. 用字符设备和常见工具做 DMA 测速把带宽调进理论值区间驱动写完后第一件事不是接应用而是测速。有一种简单方案驱动里加一个 ioctl 命令触发 DMA 从 FPGA 端拉 N 字节数据到固定 buffer同时在用户态用clock_gettime记录时间。这样避开了文件系统 IO 的开销测的是纯驱动 DMA 带宽。struct fpga_dma_test { unsigned long len; unsigned long time_ns; }; static int fpga_ioctl_dma_test(struct fpga_dev *fpga, unsigned long arg) { struct fpga_dma_test test; ktime_t start, end; if (copy_from_user(test, (void __user *)arg, sizeof(test))) return -EFAULT; start ktime_get(); fpga_start_dma(fpga, fpga-buf_phys, test.len); wait_for_completion(fpga-dma_done); end ktime_get(); test.time_ns ktime_to_ns(ktime_sub(end, start)); if (copy_to_user((void __user *)arg, test, sizeof(test))) return -EFAULT; return 0; }用户态测试程序对这个 ioctl 测速并对不同长度做扫描会看到典型规律4KB 传输时带宽只有几百 MB/s瓶颈在中断延迟和 DMA 引擎启动开销到 1MB 以上带宽会接近 PCIe 稳定值。如果大块传输的带宽还上不去再看一次perf里的中断耗时或者用iostat看 CPU 空闲率确认瓶颈在驱动侧还是FPGA 侧。这个测试路径会在交付给应用团队时保留因为任何一次 FPGA bitstream 改动后有没有把接口带宽跑回去一组测速数据就能说明白不用靠感觉。调优参数时的经验和优先级排序是先保证内存一致性和地址映射正确性然后做长传测速再根据短板调中断合并策略或描述符链节数。不要一上来就调PERIOD_SIZE那只是锦上添花数据错位时调参数毫无意义。这套方案里也藏了不少玄学成分比如 FPGA 端 TREADY 信号的时序和链路均衡参数的配合往往要来回调几天才能稳定在标称带宽。每次遇到这类问题我习惯把驱动侧能排除的因素先全部排除再找硬件同事查 RTL。希望帮到你。本文还有配套的精品资源点击获取
返回列表