简介:面向Linux驱动开发与FPGA嵌入式开发者,这份资源聚焦PCIe设备驱动开发,尤其针对Xilinx FPGA的PCIe通信场景,系统梳理从设备枚举、驱动模型、中断处理到DMA传输、固件加载等完整知识链路。压缩包共27个文件,以12个C头文件、7个C源文件和5个Makefile构建脚本为主,并附带PNG/GIF示意图与results结果文件,约124KB,便于在嵌入式环境中快速查阅与编译实践。已有1227人学习浏览。代码中包含xdma驱动框架、sguser用户态示例、ConfigGui设备配置界面及xpmon监控工具等模块,覆盖从硬件寄存器操作到用户空间ioctl交互的典型路径,可帮助开发者理解PCIe驱动的骨架代码、DMA通道配置和Makefile组织方式,同时为Xilinx PCIe驱动的裁剪与移植提供直接参考。
1. 从 linux_driver.rar 说起:Linux PCIe 驱动到底在驱动什么
很多人解压过名为 linux_driver.rar 的压缩包,里面躺着 demo_pcie.c、Makefile、几个 .h 头文件,注释偶尔还是乱码。如果只会 insmod 和 rmmod,这个包和一堆废纸没区别。Linux PCIe 驱动的核心不是"加载模块",而是把 PCIe 协议栈暴露给你的那部分资源——配置空间、BAR 映射出来的寄存器、中断和 DMA 通道——正确接管起来。这篇文章从 PCIe 枚举开始,走到 probe、BAR 映射、中断与 DMA 的落地代码,再落到 dmesg、lspci、devmem 三板斧和五个高频翻车场景。适合两类人:一类是 FPGA 或 ASIC 工程师要写 Linux 端驱动,另一类是嵌入式或服务器工程师要排查 PCIe 设备起不来、掉速、热插拔后不恢复的问题。
2. 动手前先看懂 PCIe 枚举与设备模型:驱动代码落在哪一层
写 PCIe 驱动之前,先让 lspci 说话。这步不是浪费时间,而是把你从驱动代码的细节里拉出来,先确认设备在 PCIe 协议层面有没有存在感。我见过的"驱动不工作"问题里,九成追到最后都发现是链路根本没建立起来,或者枚举阶段就被跳过了。这一章把枚举、配置空间和启动时序拆开讲,这些是写驱动代码前必须有的背景。
2.1 PCIe 枚举过程:RC 怎么一层层找到 EP
PCIe 枚举不是 Linux 驱动的职责,而是固件和内核共同完成的。上电后,Root Complex 从 bus 0 开始发起配置空间读取,每读到一个非全 0xFF 的 Vendor ID,就认定这个位置有设备,随即给它分配一个 BDF(Bus:Device.Function)。如果发现这个设备是 Switch 或 Bridge,还要继续往下分配新的 bus number,递归扫描。这一整套动作就是常说的 pcie 枚举过程,结果会直接写进内核的 device tree。
对驱动开发者来说,枚举结果决定你该盯哪个 BDF。最常见的翻车现场是:FPGA 板卡插上后 lspci 里什么都看不到,于是开始怀疑驱动、怀疑 DMA、怀疑人生。实际上大多数情况是枚举阶段就没过,根本不关驱动的事。
lspci -t # 输出示例(树形结构) # -+-[0000:00]-+-00.0 Host Bridge # +-01.0 PCIe Root Port # \-[0000:01]-+-00.0 FPGA Devicelspci 的数据来源于内核枚举完成后建立的设备树,不是实时访问硬件。所以 lspci 看不到设备,基本可以断定固件或内核在配置阶段就没发现它。这时候再看 Root Port 的链路状态:如果 Link Status 显示 Down,问题在物理层;如果显示 Up 但 Speed 停在 2.5GT/s,则是链路训练协商出了问题,后面专门讲。
枚举阶段还有一个容易忽略的角色——PCIe Switch。服务器或工控机上挂多个 EP 时,RC 先枚举 Switch 的上游口,再给下游口分配 bus 段,EP 挂在最末端。驱动代码里并不会直接感知 Switch 的存在,链路中的 Switch 对软件透明,但排查问题时要意识到中间还隔着一层转发逻辑。
2.2 配置空间与 BAR:驱动拿到的第一手"身份证"
每个 PCIe 设备都有 4KB 配置空间,前 256 字节是标准头,包含 Vendor ID、Device ID、Class Code 和 BAR 寄存器。Linux 驱动匹配设备靠的就是 Vendor ID 和 Device ID 组成的 id_table。BAR 寄存器存放设备内部寄存器或内存区域的基地址,这个地址由 RC 在枚举时分配,驱动拿到后要映射才能访问。
# dump 完整的 4KB 配置空间 lspci -xxxx -s 01:00.0 # 读取 BAR0 的低 32 位,注意 .l 表示按 32 位读 setpci -s 01:00.0 BAR0.lsetpci 的 BAR0.l 输出是 RC 分配的物理地址,但驱动不能直接拿这个地址去访问,需要 pci_iomap 把它映射到内核虚拟地址空间。64 位 BAR 会占用两个 BAR 槽位,FPGA 设备上很常见:BAR0 和 BAR1 组合成一个 64 位地址区间,写驱动时取 pci_resource_start(pdev, 0) 就能拿到低 32 位,取 pci_resource_start(pdev, 1) 拿高 32 位,两者拼起来才是完整地址。这点不搞清楚,驱动里看到的地址和 lspci 输出的对不上,会很困惑。
Class Code 也值得看。它告诉内核这个设备是网卡、存储控制器还是自定义加速器。写自定义 FPGA 设备驱动时,Class Code 通常是 0xFF0000(未分类),不会自动绑定到内核现有子系统,所以必须自己写 pci_driver。
2.3 EP 先启动还是 RC 先启动:PERST 与参考时钟的时序账
经常有人问 pcie ep 先启动还是 rc 先启动。规范层面的答案是:EP 必须等 PERST 信号释放后才能开始链路训练,而 PERST 释放之前参考时钟必须稳定。实践层面更复杂:FPGA EP 的 bitstream 加载需要时间,如果 PERST 释放太早,RC 在另一端等待链路训练,FPGA 还没准备好,训练就直接超时。超时后 RC 不会无限重试,链路就保持 Down。
我调试过一块 Xilinx FPGA 板卡,最初 CPLD 里只做了 50ms 上电延时就释放 PERST,结果十次有八次枚举不到。查了几天,最后发现是 bitstream 加载要 120ms 左右,PERST 释放时 FPGA 的 PCIe IP 还没起来。解决思路有两种:最干净的是从硬件时序上改,CPLD 等 FPGA config_done 信号有效后再释放 PERST;软件侧也有补救手段,在 Root Port 上触发链路重训练,但这是事后后悔药,不能当饭吃。
# 在 Root Port 上强制触发链路重训练,多数 x86 平台支持 sudo setpci -s 00:01.0 0x88.w=0x20000x88 是 Bridge Control 相关寄存器,0x2000 对应 Link Retrain 位,具体偏移以平台芯片手册为准。不同厂商的 Root Port 对重训练的支持不一样,有的平台写下去没反应,有的平台需要先置 Secondary Bus Reset。我的习惯是先在硬件上保证时序,软件重训练只在实验室里用来做验证。
3. 写一个最小 PCIe 驱动:probe、BAR 映射与中断
这一章给出一份可以直接编译加载的 PCIe 驱动骨架。目标不是实现业务逻辑,而是把设备"接管"过来:注册成功、BAR 可访问、中断能触发、DMA 能跑。以此为模板改寄存器偏移和中断处理,就能适配绝大多数 FPGA 板卡。
3.1 pci_driver 结构体与 module_pci_driver:驱动的注册入口
一个 PCIe 驱动的入口是 pci_driver 结构体,里面最关键的是 id_table 和 probe 回调。id_table 声明这个驱动认领哪些设备,probe 在设备被枚举且 id 匹配时执行。先看最小可编译的框架:
#include <linux/module.h> #include <linux/pci.h> #define DRV_NAME "demo_pcie" static int demo_probe(struct pci_dev *pdev, const struct pci_device_id *id) { dev_info(&pdev->dev, "probe: vendor=0x%04x device=0x%04x\n", id->vendor, id->device); return 0; } static void demo_remove(struct pci_dev *pdev) { dev_info(&pdev->dev, "remove\n"); } static struct pci_device_id demo_ids[] = { { PCI_DEVICE(0x10ee, 0x9038) }, { 0 } }; MODULE_DEVICE_TABLE(pci, demo_ids); static struct pci_driver demo_driver = { .name = DRV_NAME, .id_table = demo_ids, .probe = demo_probe, .remove = demo_remove, }; module_pci_driver(demo_driver); MODULE_LICENSE("GPL");PCI_DEVICE(0x10ee, 0x9038) 里的 0x10ee 是 Xilinx 的 Vendor ID,0x9038 是某款 FPGA 的 Device ID。实际使用时替换成你设备的 ID,可以通过 lspci -n 查到。MODULE_DEVICE_TABLE 的作用是让 modprobe 在插入设备时能自动加载对应模块,不开这个,手 insmod 也能跑,但热插拔场景下设备插入后驱动不会自动绑定。
Makefile 按内核模块标准写法:
obj-m := demo_pcie.o KERN_DIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KERN_DIR) M=$(PWD) modules clean: $(MAKE) -C $(KERN_DIR) M=$(PWD) clean编译后 insmod,dmesg 里能看到 probe 打印。这里有一个容易踩的坑:probe 返回 0 不代表设备已经被正确接管,只代表你这个驱动声明认领了它。很多人 insmod 看到日志就以为完事了,实际上 BAR 没映射、中断没注册、DMA 没配置,设备完全没被驱动起来,后续应用层访问还是失败。
3.2 使能设备与映射 BAR:probe 里第一个完整动作
probe 里要做的事按顺序来:使能设备、申请资源、映射 BAR、设置 DMA mask、注册中断。顺序反了会出各种怪问题,比如先映射 BAR 再 pci_enable_device,某些平台上 ioremap 返回的地址访问时会触发总线错误。
#include <linux/pci.h> #include <linux/io.h> #include <linux/slab.h> #define DEMO_INT_STATUS 0x1000 #define DEMO_INT_CLEAR 0x1004 #define DEMO_INT_MASK 0x00000001 #define DMA_ADDR_LOW 0x2000 #define DMA_ADDR_HIGH 0x2004 #define DMA_LEN 0x2008 #define DMA_START 0x200C #define DMA_BUF_SIZE (4 * 1024 * 1024) struct demo_dev { struct pci_dev *pdev; void __iomem *bar0; int irq; void *dma_buf; dma_addr_t dma_handle; u64 dma_count; u64 dma_bytes; }; static int demo_probe(struct pci_dev *pdev, const struct pci_device_id *id) { struct demo_dev *priv; int ret; priv = kzalloc(sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->pdev = pdev; ret = pci_enable_device(pdev); if (ret) { dev_err(&pdev->dev, "pci_enable_device failed: %d\n", ret); goto err_free; } ret = pci_request_regions(pdev, DRV_NAME); if (ret) { dev_err(&pdev->dev, "pci_request_regions failed: %d\n", ret); goto err_disable; } priv->bar0 = pci_iomap(pdev, 0, pci_resource_len(pdev, 0)); if (!priv->bar0) { dev_err(&pdev->dev, "pci_iomap bar0 failed\n"); ret = -ENOMEM; goto err_release; } pci_set_master(pdev); ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)); if (ret) { ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32)); if (ret) { dev_err(&pdev->dev, "no usable DMA mask\n"); goto err_unmap; } } pci_set_drvdata(pdev, priv); return 0; err_unmap: pci_iounmap(pdev, priv->bar0); err_release: pci_release_regions(pdev); err_disable: pci_disable_device(pdev); err_free: kfree(priv); return ret; } static void demo_remove(struct pci_dev *pdev) { struct demo_dev *priv = pci_get_drvdata(pdev); if (priv->bar0) pci_iounmap(pdev, priv->bar0); pci_release_regions(pdev); pci_disable_device(pdev); kfree(priv); }pci_enable_device 会打开设备的主内存访问和 IO 访问位,不做这一步,后面读写 BAR 地址会直接触发总线错误。pci_request_regions 是向内核申请这段 BAR 资源,防止别的驱动重复认领。pci_iomap 返回的是内核虚拟地址,这个地址不能直接当物理地址传给设备侧逻辑,它只给 CPU 访问用。
DMA mask 的设置在 FPGA 驱动里很容易被忽略。dma_set_mask_and_coherent 告诉内核 DMA 引擎能寻址的地址范围,不设置的话默认可能只有 32 位。FPGA 板卡如果用 64 位地址,高 32 位会丢,DMA 到了 FPGA 侧就成了截断地址,数据读写全乱。
3.3 中断选择:MSI-X 和 INTx 的取舍
PCIe 中断有两条路线:传统的 INTx 和消息信号中断 MSI/MSI-X。INTx 是边带信号,需要把中断请求通过物理信号线送到中断控制器,共享中断时需要判断是否是自己设备的中断。MSI-X 直接在 PCIe 事务里发中断消息,可以做到每个队列一个独立中断,是高性能设备的标配。
FPGA 板卡做 DMA 时,我一般优先让设备实现 MSI-X,一个完成队列对应一个中断向量,中断处理里不用轮询多个描述符,CPU 占用率能降一大截。驱动侧分配中断向量的代码:
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI | PCI_IRQ_INTX); if (ret < 0) { dev_err(&pdev->dev, "pci_alloc_irq_vectors failed: %d\n", ret); goto err_unmap; } priv->irq = pci_irq_vector(pdev, 0); ret = request_irq(priv->irq, demo_irq_handler, 0, DRV_NAME, priv); if (ret) { dev_err(&pdev->dev, "request_irq failed: %d\n", ret); goto err_vectors; }pci_alloc_irq_vectors 的第一个参数是设备,后两个参数是想要的中断向量数量范围,最后一个参数是类型标志。PCI_IRQ_MSI 表示接受 MSI,PCI_IRQ_INTX 表示退回 INTx。这里没有加 PCI_IRQ_MSIX,如果设备支持 MSI-X 且驱动需要多队列,应该把它加上,让内核优先分配 MSI-X。
中断处理函数遵循"读状态寄存器判断是否属于本设备,是就处理并清除,不是就返回 IRQ_NONE"的模式:
static irqreturn_t demo_irq_handler(int irq, void *dev_id) { struct demo_dev *priv = dev_id; u32 status = ioread32(priv->bar0 + DEMO_INT_STATUS); if (!(status & DEMO_INT_MASK)) return IRQ_NONE; iowrite32(status & DEMO_INT_MASK, priv->bar0 + DEMO_INT_CLEAR); priv->dma_count++; /* 在这里唤醒等待队列或调度 tasklet 处理数据 */ return IRQ_HANDLED; }DEMO_INT_STATUS 和 DEMO_INT_CLEAR 是 FPGA 侧寄存器的偏移,分别代表中断状态和中断清除。实际使用时换成你自己 FPGA 的寄存器地址。iowrite32 写入 DEMO_INT_CLEAR 是清中断的动作,必须放在处理逻辑之前还是之后取决于硬件设计,如果硬件在清除后立刻拉低中断线,那就先读状态、再处理数据、最后清中断。
3.4 DMA 缓冲分配:coherent 与 streaming 的边界
DMA 缓冲有两种分配方式:coherent 映射和 streaming 映射。coherent 分配的内存在 CPU 和 DMA 访问之间保持一致,不需要手动同步,适合设备持续读写、CPU 频繁访问的场景,缺点是分配时可能涉及页表修改,开销较大。streaming 映射适合一次性传输,数据从 CPU 交给设备后再收回来,需要手动做 sync。
FPGA 板卡的驱动里,我惯用 dma_alloc_coherent 分配一块固定大小的 DMA 缓冲区,因为 FPGA 的逻辑通常在固定地址做搬运,CPU 侧也要频繁切片访问:
priv->dma_buf = dma_alloc_coherent(&pdev->dev, DMA_BUF_SIZE, &priv->dma_handle, GFP_KERNEL); if (!priv->dma_buf) { dev_err(&pdev->dev, "dma_alloc_coherent failed\n"); ret = -ENOMEM; goto err_irq; } /* 把 DMA 地址拆成高低两个 32 位,写入 FPGA 的地址寄存器 */ iowrite32(lower_32_bits(priv->dma_handle), priv->bar0 + DMA_ADDR_LOW); iowrite32(upper_32_bits(priv->dma_handle), priv->bar0 + DMA_ADDR_HIGH); iowrite32(DMA_BUF_SIZE, priv->bar0 + DMA_LEN); iowrite32(1, priv->bar0 + DMA_START);lower_32_bits 和 upper_32_bits 是内核提供的宏,专门用来拆分 dma_addr_t。FPGA 侧通常就两个 32 位寄存器保存 DMA 起始地址,拼起来就是完整的 64 位地址。这里最容易错的是把 dma_handle 和高 32 位搞反,或者只写低 32 位,高 32 位没写导致设备实际访问到了错误的高地址区域。
dma_alloc_coherent 返回的地址是经过平台 DMA 层映射的,不是物理地址,尤其是开了 IOMMU 的 x86 平台上,不要再对它做 virt_to_phys 转换。cleanup 时用 dma_free_coherent 释放,参数和分配时一一对应。remove 函数里要在释放 BAR 映射之前释放 DMA 缓冲,否则先 iounmap 再去 free,设备侧可能还在访问这块内存。
4. 驱动调试三板斧:dmesg、lspci 与 devmem
驱动写完了,能不能跑,靠三板斧就能验证七成。dmesg 看事件顺序,lspci 看链路状态,devmem 看寄存器现场。这三条命令是我排障时最先敲的,比任何调试器都好使。
4.1 dmesg 过滤 PCIe 日志:枚举与驱动加载的现场记录
内核启动和运行期间的大量 PCIe 事件都进了内核日志,只是被其他消息淹没了。先按关键字过滤一遍,能快速找到设备枚举、链路训练、驱动绑定相关的记录:
sudo dmesg | grep -iE "pcie|pci 0000"重点看两段:一段是启动早期的枚举,形如 "pci 0000:01:00.0: [10ee:9038] type 00 class 0xffffff",这说明枚举阶段设备已被发现;另一段是你的驱动的 probe 打印,形如 "demo_pcie 0000:01:00.0: probe: vendor=0x10ee device=0x9038"。如果只有前一段没有后一段,说明 id_table 没匹配上或者 probe 里提前 return 了错误。
注意:dmesg 日志会被 ring buffer 覆盖,现场复现后先 dmesg > /tmp/pci.log 存一份,再开始改动。
还有一种情况是 dmesg 里出现 "AER: Corrected error received",这代表链路上有可恢复错误在持续发生,暂时不影响功能,但带宽可能已经降级了,需要结合 lspci 一起看。
4.2 lspci -vvv 看链路速率与带宽:ubuntu 查看 PCIe 是 4.0 还是 5.0
linux 系统如何查看 pcie 设备带宽,lspci -vvv 是最直接的方式。这个命令的输出里有两段关键信息:LnkCap 表示设备支持的链路能力,LnkSta 表示当前实际协商的结果。ubuntu 查看 pcie 是 4.0 还是 5.0,看 LnkSta 里的 Speed 字段就能判断。
lspci -vvv -s 01:00.0LnkSta 输出的 Speed 对应关系如下:
| LnkSta Speed | PCIe 代数 | 单通道原始速率 | x4 理论带宽(有效) |
|---|---|---|---|
| 2.5GT/s | Gen1 | 2.5 GT/s | 约 1.0 GB/s |
| 5GT/s | Gen2 | 5 GT/s | 约 2.0 GB/s |
| 8GT/s | Gen3 | 8 GT/s | 约 3.94 GB/s |
| 16GT/s | Gen4 | 16 GT/s | 约 7.88 GB/s |
| 32GT/s | Gen5 | 32 GT/s | 约 15.75 GB/s |
有效带宽要扣除编码开销:Gen1 和 Gen2 是 8b/10b 编码,有效率 80%;Gen3 及以上是 128b/130b,有效率约 98.5%。x4 链路的有效带宽大致是 单通道速率 × 4 × 编码效率。
如果 LnkSta 的 Speed 显示 "2.5GT/s (downgraded)",说明设备当前运行在降速状态,链路训练时出过错。旁边括号里的 downgraded 字样非常关键,看到了就要怀疑物理层信号质量。Width 字段同理,显示 "x1 (downgraded)" 说明本来支持 x4 但只协商到 x1,常见原因是金手指接触不良。
4.3 devmem 直接读写寄存器:绕过驱动验证硬件逻辑
设备起不来或者驱动 probe 失败了,不代表就没办法验证硬件逻辑。devmem 可以直接按物理地址读写,只要知道 BAR 对应的物理地址,就能绕过驱动直接访问设备寄存器,这在驱动还没加载起来时尤其有用。
先查设备 BAR0 的物理地址:
sudo cat /proc/iomem | grep "01:00.0" # 输出示例:f0000000-f00fffff : 0000:01:00.0拿到起始地址后,用 devmem 读写:
# 读 BAR0 + 0x1000 处的 32 位寄存器 sudo devmem 0xf0001000 32 # 写 0x1 到 BAR0 + 0x200C(DMA_START) sudo devmem 0xf000200c 32 0x1devmem 的第二个参数是访问宽度,支持 8、16、32,多数寄存器按 32 位访问。这个命令在调试早期特别有用:驱动还没写好,可以先手动给 FPGA 下发一个 DMA 命令,看设备是否响应,把硬件问题从驱动代码里剥离出来。但 devmem 没有总线错误保护,地址写错可能直接死机或者触发 MCE,只建议在内核启动参数加了 iommu=pt 或者确认物理地址准确的实验室环境里用。
注意:devmem 绕过了一切内核资源管理,生产环境禁止使用。它只是临时验证手段,不是长期运维工具。
5. PCIe 驱动避坑指南:热插拔、PERST 与供电的五个翻车现场
这一章从真实调试里挑五个高频问题,按现象、原因、解决的顺序写。每一条都是我或同行在 PCIe 驱动调试路上实际撞过的墙,希望能帮你省下几天的定位时间。
5.1 枚举不到设备:PERST 时序没等够
现象:FPGA 板卡插入服务器,lspci 完全看不到设备,dmesg 里只有 Root Port 的 Link Down 信息,驱动 insmod 后 probe 根本不触发。用示波器抓 PCIe 差分对,找不到链路训练波形。
原因:PCIe 规范要求 PERST 释放前参考时钟必须稳定,释放后 EP 才能开始链路训练。很多 FPGA 方案的 PERST 由 CPLD 控制,CPLD 只做了简单的上电延时,而 FPGA 的 bitstream 加载可能要好几十毫秒。PERST 释放时 FPGA 的 PCIe IP 还没起来,RC 一侧训练超时后链路就停在 Down 状态。
解决:让 PERST 释放时机往后挪,等 FPGA config_done 信号有效后再释放 PERST,或者把 CPLD 延时调到 200ms 以上。软件侧可以用 setpci 在 Root Port 上触发链路重训练,但这是事后补救,治标不治本。改硬件时序才是正路。
5.2 链路降速到 Gen1:连接器与走线的经典问题
现象:设备能枚举到,但 lspci -vvv 里 LnkSta 显示 Speed 2.5GT/s,两边明明都支持 Gen3 或 Gen4。驱动能跑,但 DMA 吞吐只有预期的几十分之一。
原因:链路训练按双方共同的最低能力协商降级。最常见的是金手指氧化或插入不到位,某些差分对信号质量差,CRC 错误太多,RC 和 EP 自动降速重训。其次是转接卡或线缆质量差,尤其是用转接线延长时,衰减严重也会触发降级。
解决:重新插拔板卡,用无水乙醇清洁金手指,确认卡扣到位。换了物理连接还不行的,检查 LnkCap 里 EP 支持的最大速率,如果 LnkCap 本身就是 2.5GT/s,说明 FPGA 工程的 PCIe IP 把 max link speed 配成了 Gen1,要去改硬件工程重新综合。
5.3 热插拔后驱动不恢复:remove 和错误处理不完整
现象:支持 pcie 热插拔功能的服务器上,驱动正常工作时拔卡,再插回同一槽位,lspci 能看到设备但 probe 不执行。有时候模块卸载重载报 "Device or resource busy"。
原因:热插拔走的是 pciehp 或 acpiphp 流程,内核会调用驱动的 remove 回调。如果 remove 里没有完整释放中断、DMA 缓冲和 BAR 映射,资源还挂在总线上。另一个隐蔽问题是很多 FPGA 驱动只注册了 probe 和 remove,没注册 err_handler,链路产生 AER 错误后设备被内核标记为不可恢复,后续热插拔事件来了也不重新绑定。
解决:remove 回调里把所有申请的资源对称释放,中断、DMA、iounmap、release_regions、disable_device 一个都不能少。对 FPGA 类设备,建议在 pci_driver 里补上 err_handler 的 reset 回调,配合 AER 做链路恢复。插回后不自动 probe 时,可以先试:
echo 1 | sudo tee /sys/bus/pci/rescan这个命令会触发一次重新枚举,能把新插入的设备纳入管理。
5.4 DMA 读回全零:dma mask 与 IOMMU 的坑
现象:驱动加载正常,寄存器读写正常,发起 DMA 后 FPGA 侧报了完成中断,但 CPU 读 buffer 全是 0。用 devmem 读 FPGA 的 DMA 地址寄存器,发现地址和预期不一致。
原因:两个常见根因。第一个是没调 dma_set_mask_and_coherent,64 位 DMA 地址被截断成 32 位写到 FPGA,而 FPGA 实际分配在 64 位地址区。第二个是 IOMMU 开启时,dma_alloc_coherent 返回的是经 IOMMU 映射的 DMA 地址,不是物理地址,如果设备侧按物理地址概念处理就必然出错。还有一种是 cache 一致性问题,用了 dma_map_single 但忘记在 CPU 读之前 dma_unmap_single。
解决:probe 里 dma_set_mask_and_coherent 检查返回值,FPGA 侧地址寄存器按 64 位拆成 low 和 high 两个 32 位写。开了 IOMMU 的系统,全程使用 dma_alloc_coherent 返回的 dma_addr_t,不要自己做物理地址转换。调试阶段可以用 iommu=pt 关闭 IOMMU 排除干扰,但上线前必须带着 IOMMU 完整测一遍。
5.5 FPGA 板卡反复复位:3.3V aux 供电不足
现象:板卡插上后系统能起来,但运行几分钟到几十分钟后设备突然消失,dmesg 出现 PCIe 错误风暴,lspci 里设备时有时无。用手摸 FPGA 散热片明显烫手。
原因:PCIe 插槽的 3.3V aux 供给能力有限,规范里 aux 电源主要给管理电路和 wake 逻辑用,大功耗 FPGA 板卡的 PCIe 核心逻辑需要独立供电。很多人问 pcie 为何还需要单独供电,因为插槽 12V 主供电有功率上限,标准 x16 槽大约 75W 到 150W 视平台而定,FPGA 峰值功耗超过这个值就会把电源拉垮。
解决:核对板卡峰值功耗与插槽供电能力,超过 75W 的板卡必须设计辅助供电接口。驱动里加温度监控,超过阈值主动告警或降频。调试时先降低 DMA 带宽负载确认是功耗问题还是驱动问题。这类问题驱动代码本身解决不了,但可以通过监控接口把温度暴露给用户态,做到系统级防护。
6. 把驱动从"能跑"做到"可靠":AER 错误检查与带宽实测
驱动能加载、能 DMA 只是第一步,可靠性才是 PCIe 驱动真正烧时间的地方。我交付前会做三件事:翻 AER 错误记录、实测 DMA 带宽、检查链路健康参数。
第一件事是看 AER。内核的 AER 驱动会记录链路上的可恢复和不可恢复错误,错误类型包括 Bad TLP、接收端溢出、CRC 错误等:
sudo dmesg | grep -i "aer"看到 Corrected Error 持续增长,先怀疑信号完整性;看到 Uncorrected Error,赶紧查硬件。有条件的话装 rasdaemon,它会把 AER 错误落到数据库里方便做趋势对比。
第二件事是实测带宽,而不是只看 LnkSta。链路显示 16GT/s x4 只是速率上限,实际 DMA 吞吐还受中断频率、描述符处理效率、FPGA 侧缓冲深度影响。我的做法是在驱动里加一个 debugfs 节点,统计 DMA 次数和字节数:
static ssize_t demo_dma_stat_read(struct file *fp, char __user *ubuf, size_t len, loff_t *off) { struct demo_dev *priv = fp->private_data; char buf[128]; snprintf(buf, sizeof(buf), "count=%llu bytes=%llu\n", priv->dma_count, priv->dma_bytes); return simple_read_from_buffer(ubuf, len, off, buf, strlen(buf)); }跑 30 分钟压测,用字节数除以实际时间,和理论带宽对比。16GT/s x4 的理论有效带宽约 7.88GB/s,实际跑到 60%-70% 就算健康。
第三件事是检查链路健康参数。部分平台支持读取接收端信号裕量,类似 rxmargin 的概念,这个参数能提前暴露金手指接触不良。不同芯片的访问方式不同,ARM 平台通常由固件暴露,x86 平台可以通过 setpci 读扩展配置空间的特定偏移,具体以芯片手册为准。
我的习惯是每次调试开始前,先把 lspci -vvv 的 LnkCap 和 LnkSta、dmesg 的枚举日志存一份快照,问题复现后立刻对比。九成的 PCIe 问题在第一次就能定位到物理层还是软件层。如果你正在为手里的 FPGA 板卡写 Linux 驱动,这条路从枚举走到 DMA 压测,跑完一遍,PCIe 驱动对你就不再是黑匣子。希望帮到你。
本文还有配套的精品资源,点击获取