简介:面向 Linux 下 PCIe 驱动开发与 Xilinx FPGA 应用场景,这套代码包主要服务于内核驱动开发者、FPGA 工程师以及需要为 Xilinx 器件移植驱动的嵌入式人员。内容覆盖 PCIe 设备枚举、资源分配、总线驱动与设备驱动分层、probe 设备匹配、中断注册、DMA 掩码设置、ioremap 地址映射、固件加载和模块卸载清理等环节,适合从整体上把握 Linux PCIe 驱动框架。包内共 27 个文件,以 C 源码和头文件为主体,另有 Makefile 构建脚本、运行结果记录和 logo 示意图,整体仅 124KB,便于快速阅读和对照编译。已有 1227 人学习下载。借助 xdma 驱动骨架、用户态 sysfs/ioctl 交互接口和调试辅助文件,读者可以了解 lspci、dmesg、ethtool 等工具的观察点,并通过内核日志定位初始化失败原因,掌握从设备匹配到数据通路建立的排错思路,为后续裁剪或移植 Xilinx PCIe 驱动提供一个可落地的起点。
1. Linux PCIe 驱动:为什么 probe 进不来,比怎么写代码更值得先搞清楚
把 linux_driver.rar 解压、make 出一个 .ko,insmod 显示加载成功,但 lspci -k 里就是没有你的驱动名,probe 死活不调用——这是 Linux PCIe 驱动开发最常见的第一道坎。标题这套组合词(linux pcie 驱动 / pcie driver / linux驱动pcie)问的就是一件事:怎么让一块 PCIe 设备(网卡、加速卡、采集卡)在 Linux 里被识别、被驱动起来,并能稳定地做 DMA 和中断。我按实际调板卡的顺序来写:先讲枚举与 pci_driver 骨架,再讲编译与匹配,最后落到热插拔、AER 和稳定性排查。适合正在做板卡 bringup、写内核模块,或者拿到别人的 linux_driver.rar 想快速跑起来的人。
2. 从枚举到 probe:Linux PCIe 驱动是怎么找到你的设备的
很多人拿到驱动源码第一反应是读 probe 函数,但 probe 之所以能被调用,依赖的是内核在启动阶段完成的 PCIe 总线枚举。不理解枚举,你连报错日志都不知道去哪里看。
2.1 pcie枚举过程:配置空间、BDF 与设备树,驱动作者必须知道的三个事实
PCIe 是树形总线。系统上电后,Root Complex 从 Bus 0 开始,向每个 Device/Function 组合读 Vendor ID,读到 0xffffffff 就认为槽位为空,继续扫下一个。扫描完成后给设备分配 BDF(Bus:Device.Function)、BAR 地址和中断号,这就是整个 pcie枚举过程。你看到的 lspci 输出,就是内核把枚举结果解析成人话。
第二个事实是配置空间。标准配置空间前 256 字节兼容 PCI,BAR0~5 在 0x10~0x24,Capability 链表指针在 0x34。MSI、MSI-X、PM、AER 这些能力全部通过 Capability 结构暴露,而探寻它们不是用指针访问,而是用 pci_read_config_byte/word/dword 系列接口。扩展配置空间到 4KB,AER 的更多状态在 0x100 之后,lspci -vvv 输出的就是内核解析完的配置空间摘要。
第三个事实和平台相关。x86 下 PCIe 控制器由 ACPI 描述,ARM 平台则常见设备树里的 pcie 节点,compatible 通常写 pcie-host-ecam-generic,配合 bus-range、ranges、dma-ranges、msi-map 描述总线范围和地址映射。如果在设备树平台调驱动,prpbe 之前得先确认 dts 里有没有这个节点,不然内核根本不会去扫描这条总线。
先用这三条命令确认设备到底在不在系统里:
# 看整棵 PCIe 树拓扑,确认设备挂在哪条总线 lspci -tv # 看单个设备的能力和当前链路状态 lspci -vvv -s 01:00.0 # 打印 vendor:device,驱动匹配 id_table 用的就是这两个数字 lspci -n -s 01:00.0lspci -tv 输出里能看到 Root Port、Switch、Endpoint 的层级关系;lspci -vvv 能看到 LnkCap/LnkSta、MSI 能力、BAR 大小;lspci -n 给出的 1234:5678 这样的编号,直接对应驱动里 pci_device_id 表的 vendor/device。我一般先跑这三条,再决定要不要继续读源码。
2.2 最小可用的 pci_driver 骨架:id_table、probe 与 remove 的正确写法
注册一个 PCIe 驱动,内核不看 module_init,只看你的 pci_driver 结构体。下面是一个最小但完整可编译的骨架,虚拟 vendor 0x1234、device 0x5678。
#include <linux/module.h> #include <linux/pci.h> #define DRV_NAME "my_pcie_driver" static int my_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id) { int ret; // 1. 使能设备:打开 MMIO/IO 访问和总线主控能力 ret = pci_enable_device(pdev); if (ret) return ret; // 2. 使能 bus mastering,DMA 必需,只做 PIO 可以不加 pci_set_master(pdev); // 3. 申请配置空间里的资源(BAR),避免与其他驱动冲突 ret = pci_request_regions(pdev, DRV_NAME); if (ret) { pci_disable_device(pdev); return ret; } // 4. 映射 BAR0 到内核虚拟地址空间 void __iomem *bar0 = pci_ioremap_bar(pdev, 0); if (!bar0) { pci_release_regions(pdev); pci_disable_device(pdev); return -ENOMEM; } dev_info(&pdev->dev, "probe: vendor=%04x device=%04x bar0=%px\n", id->vendor, id->device, bar0); return 0; } static void my_pcie_remove(struct pci_dev *pdev) { dev_info(&pdev->dev, "remove\n"); pci_disable_device(pdev); } static const struct pci_device_id my_pcie_ids[] = { { PCI_DEVICE(0x1234, 0x5678) }, { } }; MODULE_DEVICE_TABLE(pci, my_pcie_ids); static struct pci_driver my_pcie_driver = { .name = DRV_NAME, .id_table = my_pcie_ids, .probe = my_pcie_probe, .remove = my_pcie_remove, }; module_pci_driver(my_pcie_driver); MODULE_LICENSE("GPL");核心逻辑在最后一行:module_pci_driver 宏自动生成 module_init/module_exit,把 pci_driver 注册进内核的 PCI 总线。probe 在设备与 id_table 匹配时被调用,remove 在设备拔出或驱动卸载时被调用。id_table 必须用空条目结尾,这个空条目表示“表结束”,漏掉它内核会越界读,编译能过但运行时会翻车。
参数说明:PCI_DEVICE(0x1234, 0x5678) 宏检查 vendor 和 device 都相等;如果只知道 vendor,可以用 PCI_DEVICE 只填 vendor 再配合 .class 字段做粗匹配。pci_enable_device 内部会设置 Command 寄存器的 Memory/IO 位和 Bus Master 位,所以顺序一定是先 enable 再 request_regions 再 ioremap。probe 失败时要把前面已经成功申请的逆序释放,很多人只写 pci_enable_device 成功分支,忘了失败分支也返回错误码,导致设备虽然报错却已被标记为“驱动绑定中”,后续重试和 lspci -k 表现都很奇怪。
3. 把驱动编进内核或独立编译:Kconfig、Makefile 与模块加载
驱动源码拿到手,真正第一笔开销往往在编译环境。内核模块和外层应用程序不一样,它编译时依赖运行内核的构建目录,版本不一致会出现各种匪夷所思的报错,这个环节我先讲清楚怎么搭。
3.1 把 linux_driver.rar 跑成 .ko:外部模块 Makefile 与三个关键点
如果你拿到的是 linux_driver.rar 这类打包,解压后里面一般有源码和厂商自带 Makefile。先别急着 make,先确认两件事:源码树的 include 路径和你当前内核是否匹配,以及 obj-m 是否指定了正确的目标名。大多数情况下,自带的 Makefile 需要依赖内核构建目录,也就是下面这个变量:
KVER ?= $(shell uname -r) KDIR ?= /lib/modules/$(KVER)/build obj-m += my_pcie_driver.o all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean这个 Makefile 的逻辑是切换到内核构建目录执行 modules 目标,同时把当前目录作为 M 参数传入。内核的 kbuild 系统会编译当前目录下所有 obj-m 指定的模块目标。my_pcie_driver.o 这个名字必须和源码文件名一致,如果你的源文件是 pcie_main.c,就写 obj-m += pcie_main.o,而不是乱改目标名。
参数说明:KDIR 指向 /lib/modules/$(uname -r)/build,这个目录是一个符号链接,真正指向内核头文件与构建中间文件所在位置。装完内核头文件包才有这个链接,否则 make 会报 “no such file or directory”。常见的做法是在 Ubuntu 系先装 linux-headers-$(uname -r),在嵌入式环境则直接让 KDIR 指向你交叉编译用的内核源码树。
编译产物是 my_pcie_driver.ko,接下来按顺序加载:
# 加载模块,建议先看 dmesg 尾部输出 sudo insmod my_pcie_driver.ko dmesg | tail -20 # 查看模块是否真的绑定了设备 lspci -k -s 01:00.0如果 lspci -k 的 Kernel driver in use 显示 my_pcie_driver,说明 probe 成功;如果显示设备没有驱动,probe 没被调用或者调用后失败了。insmod 只看模块本身加载,不保证设备绑定,很多新手在这里误判“驱动已经加载成功”。想让 modprobe 自动装载,需要把 .ko 拷贝到 /lib/modules/$(uname -r)/extra/,然后执行 depmod -a,后续用 modprobe my_pcie_driver 加载。
3.2 probe 不触发的排查顺序:id_table、CONFIG_PCI_DEBUG 与 driver_override
这一节是无数人的共同痛点。按下面顺序排查,能解决九成 probe 失联问题。
# 第一步:确认设备真实 id lspci -n -s 01:00.0 # 第二步:看内核有没有报绑定相关日志 dmesg | grep -i pci | tail -30 # 第三步:确认驱动是否注册成功 ls /sys/bus/pci/drivers/my_pcie_driver/第一个坑是 vendor/device 对不上。厂商手写 id_table 时经常把 device id 写错一位,尤其是转贴过来的驱动源码。用 lspci -n 看真实编号,对照驱动里 PCI_DEVICE 的参数,这是最快的定位。第二个坑是内核开了 CONFIG_PCI_DEBUG 才能打印更多匹配信息,如果没开,dmesg 里可能什么都没有。第三个坑是设备被其他驱动或 pci_stub 抢先占了,lspci -k 会显示另一个驱动的名字,解决方法是先卸载那个驱动,或者用 driver_override 强制绑定。
第四个坑在设备树平台。设备树里的 compatible 要和驱动里的 of_match_table 对得上,否则即使 id_table 正确,probe 也可能因为 of_node 匹配失败而绕开。常见做法是在驱动里同时提供 pci_device_id 和 of_device_id,并让 .driver.of_match_table 指向后者。最后一个坑是驱动编译进了内核而不是模块,但设备在 init 之后才出现,probe 时机对不上。如果设备是后插的,驱动需要在子系统注册时重新扫描,这时 driver_override 配合 echo 到 /sys/bus/pci/drivers_probe 可以把设备重新“踢”进探测流程。
我一般建议把排查顺序固定成一条命令链:先 lspci -n 对 id,再 dmesg 扫错误,最后看 /sys/bus/pci/drivers 下的绑定关系。这比反复 insmod/rmmod 高效得多。
4. 热插拔与 AER:PCIe 驱动稳定性的两道坎
驱动能 probe 只是开始。PCIe 设备真正上线后,死在热插拔和错误处理上的概率比死在初始化上的高得多。初始化崩了是搞笑段子,remove 崩了才是线上事故。
4.1 pcie热插拔功能为什么是驱动杀手:remove、shutdown 与电源管理回调
PCIe 热插拔分成两种场景:一种是系统管理的热插拔,由 hotplug 驱动控制槽位供电和复位,顺序可控;另一种是 surprise removal,卡被直接拔掉,内核甚至来不及通知你,驱动代码正在访问的设备瞬间从总线上消失。对驱动作者来说,内核只保证一点:设备还在总线上的时候,remove 会被调用,资源释放顺序必须严谨。
常见的 remove 清理顺序如下,这个顺序不能乱:
static void my_pcie_remove(struct pci_dev *pdev) { struct my_dev *dev = pci_get_drvdata(pdev); // 1. 先摘中断,确保没有新中断进来 free_irq(pdev->irq, dev); // 2. 等待可能还在跑的工作队列 cancel_work_sync(&dev->work); // 3. 释放 BAR 映射 pci_iounmap(pdev, dev->bar0); // 4. 释放配置空间资源 pci_release_regions(pdev); // 5. 关闭设备 pci_disable_device(pdev); }顺序的逻辑是:中断处理函数和工作队列都会读写硬件,如果先 iounmap 再 free_irq,中断来临时访问已释放的映射,轻则报错,重则触发不可纠正错误。free_irq 还附带一个语义:它同步等待所有执行中的中断处理函数返回,所以放在第一步等于把并发入口先关掉。IRQF_SHARED 共享中断场景下,free_irq 的第三个参数 dev_id 必须和 request_irq 传的完全一致,否则内核无法区分是你的中断还是别人的。
电源管理回调同样容易被忽略。完整驱动应该在 suspend 里做 pci_save_state 和 pci_set_power_state,在 resume 里做 pci_restore_state 和重新使能。缺了这组回调,系统休眠再唤醒后经常出现设备无法访问、DMA 不工作、链路掉速这类“玄学问题”,实际上只是上下电时序没人管。
4.2 AER 与链路降速:掉卡、降 speed/lane 的正确诊断姿势
AER 是 PCIe 的进阶错误报告机制,分 Corrected 和 Uncorrected 两类。Uncorrected 又分 Fatal 和 Non-Fatal。内核里 AER 驱动负责接收和处理这些错误,部分错误会导致内核直接把设备从系统中隔离,表现就是驱动还在,设备已经没了。
掉卡和降速是 PCIe 稳定性问题的高频词。降速指的是链路协商从 Gen3 x4 掉到 Gen2 x1,甚至 Gen1 x1;掉卡则是设备从 lspci 输出里消失。这两类问题先查三样东西:
# 查当前协商速度和宽度 lspci -vvv -s 01:00.0 | grep -E "LnkCap|LnkSta" # 查 AER 错误日志 dmesg | grep -i "PCIe Bus Error" | tail -20 # 查设备是否还挂在总线上 lspci -s 01:00.0LnkSta 显示 Speed 8GT/s、Width x4,说明链路健康;如果变成 2.5GT/s、x1,多半是信号完整性或电源问题,驱动里改参数解决不了。AER 日志出现 Corrected 大量累积,常见原因是 PCIe 参考时钟的 SSC 展频配置不一致,或者连接器/金手指氧化接触不良。有的卡本身被锁在 PCIe 2.0 x1 上(比如一些矿卡改的显卡),那是固件限制,驱动再怎么写也不会超速,不必浪费时间。
在 x86 平台排查链路问题,我一般会先用内核启动参数关掉 ASPM 做对照实验:pcie_aspm=off。如果关闭后掉卡频率明显下降,说明是电源管理链路训练和板卡兼容性的问题,优先查 BIOS 设置和板端供电,而不是驱动。驱动侧能做的补救是:在 suspend/resume 后重新确认链路状态,必要时重新初始化 DMA 描述符。AER 注入测试在支持 CONFIG_PCIEAER_INJECT 的内核里可以做,但线上排查用 dmesg 加 lspci 已经够定位九成问题。
5. 避坑:PCIe 驱动稳定性最常见的 5 个问题(现象、原因、解决)
这一章全是实战里反复出现的翻车点,每条都按现象、原因、解决三步写。遇到过的人会庆幸早看见,没遇到过的会在某个深夜突然明白。
5.1 probe 成功但 remove 就崩溃
现象:模块卸载或设备热拔出时,系统报空指针、内核 panic,或者 remove 之后设备在 lspci 里变成半死状态。
原因:probe 里用了 devm_kzalloc 分配私有数据,又手动在 remove 里 kfree 了一次。devm 类函数注册的资源在设备销毁时自动释放,重复释放导致双重释放。另一种常见原因是 probe 里写死了 bar0 映射,但 remove 里 pci_iounmap 之前已经释放了私有结构体,后续 dev_info 访问空指针。
解决:私有数据全部用 devm_kzalloc,中断用 devm_request_irq,BAR 映射用 devm_ioremap,然后 remove 里什么都不用做,只做非 devm 资源的清理。如果坚持手动管理,就做一个约定:probe 失败的每个错误分支都逆序回滚,remove 只释放 probe 成功路径里申请过的东西。
5.2 中断申请失败后设备处于半开状态
现象:probe 返回 -EIO,模块加载失败,但之后 lspci -k 显示设备仍被这个驱动绑定,怎么 insmod 都不再触发 probe。
原因:probe 里先 pci_enable_device 成功,再 request_irq 失败,返回错误码之前没有调用 pci_disable_device。内核认为设备已经和驱动绑定,后续重新探测时发现已有驱动持有该设备,不再调用 probe。
解决:把 probe 改成单出口模式,用 goto err 标签统一回滚,失败分支一定执行 pci_disable_device 和 pci_release_regions。顺手养成习惯:probe 成功最后再用 pci_set_drvdata 保存私有数据,避免中间态。
5.3 DMA 地址被截断,数据写到了错误位置
现象:DMA 传输能完成,但目标内存里的数据是乱的,或者 DMA 写坏了附近的内存。
原因:没有调用 dma_set_mask_and_coherent。内核默认 DMA 掩码是 32 位,如果设备支持 64 位地址而驱动没设置,高地址被截断,硬件拿到的是错位地址。
解决:probe 里尽早加上:
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));同时用 dma_alloc_coherent 或 pci_alloc_consistent 分配 DMA 缓冲区,而不是普通 kmalloc。kmalloc 出来的内存物理地址不连续,做分散聚合时更容易出错。
5.4 读 BAR 触发 Uncorrected,整个系统直接挂起
现象:probe 一执行 readl/ioread32,dmesg 瞬间刷出 Uncorrected (Non-Fatal) 或直接死机。
原因:resource 冲突,BAR 对应的物理地址被其他驱动或内核子系统占用,pci_request_regions 返回了错误但驱动没检查,照样 ioremap 访问。
解决:probe 里必须检查 pci_request_regions 的返回值,失败就返回错误。排查时先 lspci -vvv 看 BAR 地址范围,再用 lspci -xxx 看配置空间确认 BAR 是否被正确分配。如果板子和别人共用内存映射区,检查设备树里有没有预留对应地址段。
5.5 热插拔后中断处理函数还在跑,设备已经不在了
现象:拔卡瞬间系统 panic,日志停在中断处理函数附近的地址。
原因:中断处理函数没有持有设备状态锁,remove 已经把设备标记为移除,中断线仍被触发。对于 surprise removal,物理上设备已消失,中断处理里读 BAR 直接触发总线错误。
解决:中断处理函数第一步检查设备是否还在线,配合 dev->removed 标志位;remove 里先置标志、再 free_irq、再 cancel_work_sync。如果硬件支持,套用 pci_channel_io_normal/io_error 状态机判断链路健康,比裸读寄存器安全。
6. 进阶验证:从 lspci 到仿真压测,确认驱动真的可靠
驱动写完只是第一步,真正要回答的是“它能不能在长时间、高负载、热插拔下不翻车”。我一般把验证拆成四层,从配置空间一直压到故障注入。
第一层验证配置空间和 BAR 映射。用 lspci -vvv 核对寄存器能力,用 devmem 读 BAR 对应的物理地址,确认硬件寄存器值和驱动读到的一致。这一步能抓出 BAR 偏移算错、ioremap 地址错误、寄存器位宽读错三类基础问题。
第二层验证中断和 DMA 在压力下的稳定性。跑 DMA 压测的同时观察中断计数:
# 压测前 grep my_pcie_driver /proc/interrupts # 连续读写几 GB 数据后再看一次 grep my_pcie_driver /proc/interrupts中断次数应随数据量线性增长,如果卡住或增长异常,多半是中断丢失或 DMA 描述符更新失败。压测时同时查 dmesg 里有没有被 AER 上报的 Corrected 错误,累积速度异常就要回头查链路。
第三层是故障注入。支持 CONFIG_PCIEAER_INJECT 的内核可以把构造的 AER 错误注入到设备,验证驱动在收到不可纠正错误后能否正确进入错误分支,而不是 panic。常见做法是触发一次 Non-Fatal 错误,观察驱动是重试、复位设备还是优雅失败。
第四层是仿真。逻辑层面用 QEMU 的 q35 机型模拟 PCIe 拓扑,可以观察枚举和热插拔行为;FPGA 场景下,Xilinx PCIe IP 的仿真环境能提前验证链路训练和 BAR 访问逻辑,不用等板卡回来。真机上配合 ftrace 跟踪 irq_handler_entry 和 pci_probe 的调用时间,能看出性能瓶颈是在驱动还是硬件。
前几年我调一块 FPGA 加速卡时,probe 里 ioremap 后直接 readl 读状态寄存器,一读就触发 Uncorrected,查了两天发现是另一个驱动占了这段 BAR 地址,而 pci_request_regions 返回错误时我没检查返回值,硬是访问了别人的资源。从那以后我每次板卡 bringup 都先用 lspci -xxx 核对配置空间再写驱动,宁可慢十分钟也不要深夜 debug。希望帮到你。
本文还有配套的精品资源,点击获取