1. PCIe 驱动开发到底在搞什么
Linux 下的 PCI 设备驱动,说白了就是让内核认识一块插在主板上的板卡,并且给它分配好资源、挂上中断、暴露接口,最后让用户态程序能读写它。听起来简单,但真正动手写的时候,你会发现从设备枚举到 BAR 空间映射,从 MSI 中断到 DMA 一致性,每一步都有坑。我做了十多年底层开发,经手的 PCIe 设备从网卡、采集卡到自定义加速器都有,踩过的坑比写过的驱动还多。这篇文章就把这些经验整理出来,给正在做或者准备做 PCIe 驱动的朋友一个参考。
先明确一下适用范围。如果你用的是标准外设,比如常见的 Realtek PCIe GbE 网卡,内核里已经有现成驱动,你只需要确认设备 ID 匹配、固件加载正常就行。但如果你面对的是自定义 FPGA 板卡、专用采集设备或者老设备移植,那就得自己写驱动。本文主要面向后者,同时也会覆盖调试和排查的通用方法,因为不管写不写驱动,PCIe 设备出问题时排查思路是一样的。
核心关键词先摆出来:PCIe、Linux、PCI、设备驱动、BDF。BDF 是 Bus-Device-Function 的缩写,这是 PCIe 设备的身份证,后面会反复用到。整个驱动开发的主线就是:找到设备、分配资源、注册驱动、处理中断、实现数据通路。下面按这个顺序展开。
2. 从 BDF 到驱动匹配:设备是怎么被内核认出来的
2.1 BDF 编号与配置空间
每个 PCIe 设备在系统里都有一个唯一地址,格式是domain:bus:device.function,通常 domain 是 0000,所以简写成bus:device.function,比如03:00.0。这个编号不是随便定的,是硬件拓扑决定的。CPU 通过 Host Bridge 发起配置周期,内核枚举时从 bus 0 开始扫描,发现桥就继续往下扫,最终给每个设备分配一个 BDF。
配置空间是每个 PCIe 设备都有的 256 字节(PCIe 扩展后是 4KB)寄存器区域,前 64 字节是标准头部,里面有关键字段:Vendor ID、Device ID、Class Code、BAR(Base Address Register)、中断引脚等。内核就是靠 Vendor ID 和 Device ID 来匹配驱动的。你可以用lspci -nn看到这些信息:
lspci -nn 03:00.0 Ethernet controller [0200]: Realtek Semiconductor Co., Ltd. RTL8111/8168/8411 PCI Express Gigabit Ethernet Controller [10ec:8168] (rev 15)方括号里的10ec:8168就是 Vendor ID 和 Device ID。驱动里用pci_device_id结构体声明支持的设备:
static const struct pci_device_id my_pci_ids[] = { { PCI_DEVICE(0x10ec, 0x8168) }, { 0, } }; MODULE_DEVICE_TABLE(pci, my_pci_ids);PCI_DEVICE宏会自动填充 Vendor 和 Device,MODULE_DEVICE_TABLE则是让用户态的udev或depmod知道这个驱动支持哪些设备,方便自动加载。这里有个细节:如果你只填 Vendor ID 不填 Device ID,会匹配该厂商所有设备,一般不建议这么干,容易误匹配。
2.2 驱动注册与 probe 流程
驱动注册用pci_register_driver,核心是提供一个pci_driver结构体:
static struct pci_driver my_pci_driver = { .name = "my_pci_drv", .id_table = my_pci_ids, .probe = my_pci_probe, .remove = my_pci_remove, }; module_pci_driver(my_pci_driver);当内核枚举到一个设备,并且它的 Vendor/Device ID 跟某个驱动的 id_table 匹配上,就会调用该驱动的probe函数。probe 是驱动初始化的主战场,你要在这里做几件事:使能设备、申请 BAR 资源、映射寄存器、注册中断、初始化硬件、创建字符设备或 sysfs 节点。顺序很重要,后面会细说。
remove函数则在设备被移除或驱动卸载时调用,负责释放资源。很多人写驱动时 probe 写得很仔细,remove 随便糊弄,结果热插拔或者模块卸载时各种 oops。记住一句话:probe 里申请了什么,remove 里就要按相反顺序释放什么。
2.3 枚举过程与热插拔
PCIe 枚举是内核在启动时或者热插拔时触发的。启动时的枚举由 BIOS/UEFI 先做一遍,内核再重新扫描一遍,确保资源分配合理。热插拔则是通过 PCIe 的 Hot-Plug 信号或者用户手动触发echo 1 > /sys/bus/pci/rescan来重新扫描总线。
热插拔功能在实际项目中非常有用,尤其是服务器场景,可以在不关机的情况下更换板卡。但热插拔对驱动的要求更高:remove 必须能干净地释放所有资源,probe 必须能处理设备重新出现的情况。我遇到过不少驱动在热插拔后第二次 probe 失败,原因通常是第一次 remove 时没释放干净,比如中断没注销、DMA 缓冲区没释放、sysfs 节点没删除。
排查热插拔问题的一个实用技巧是看内核日志:
dmesg | grep -i pci热插拔时会有pci 0000:03:00.0: [10ec:8168] type 00 class 0x020000这样的日志,如果 remove 或 probe 出错,也会有相应的报错信息。另外/sys/bus/pci/devices/下每个设备目录里都有remove和rescan文件,可以手动触发测试。
3. 资源分配与 BAR 空间映射:驱动的地基
3.1 BAR 是什么,为什么重要
BAR 是 PCIe 设备向系统申请的地址窗口。一个设备最多有 6 个 BAR(32 位系统)或更多(64 位系统),每个 BAR 可以映射到内存空间或者 I/O 空间。设备内部的寄存器、FIFO、DMA 描述符等都会映射到某个 BAR 里,驱动通过读写这些地址来控制硬件。
BAR 的大小和类型由硬件设计决定,内核在枚举时会读取 BAR 寄存器,确定它需要多大空间,然后分配一段物理地址给它。驱动在 probe 里要用pci_request_regions申请这段资源,再用pci_iomap映射到内核虚拟地址:
ret = pci_request_regions(pdev, "my_pci_drv"); if (ret) { dev_err(&pdev->dev, "Failed to request regions\n"); return ret; } bar0 = pci_iomap(pdev, 0, 0); if (!bar0) { dev_err(&pdev->dev, "Failed to map BAR0\n"); pci_release_regions(pdev); return -ENOMEM; }pci_iomap的第三个参数是映射长度,传 0 表示映射整个 BAR。映射完成后,你就可以用ioread32/iowrite32等函数读写寄存器了。注意不要直接用指针解引用,因为 BAR 空间可能是 I/O 映射的,必须用专门的 I/O 访问函数。
3.2 资源分配的常见坑
第一个坑是 BAR 大小不匹配。有些 FPGA 板卡在设计时 BAR 大小没对齐,比如声明需要 4KB 但实际只用了 1KB,内核分配时可能因为对齐要求分配失败。解决办法是在硬件设计阶段就确保 BAR 大小是 2 的幂,并且跟实际使用量匹配。
第二个坑是 64 位 BAR 的处理。64 位 BAR 占用两个 BAR 寄存器,低 32 位在前,高 32 位在后。驱动里用pci_resource_start和pci_resource_len获取资源时,内核已经帮你处理好了,但如果你自己解析配置空间,就要注意这个细节。
第三个坑是 I/O 空间和内存空间的区分。现在大多数 PCIe 设备都用内存空间,I/O 空间基本淘汰了。但如果你遇到老设备,可能还是 I/O 空间,这时候要用inb/outb系列函数,而不是ioread32/iowrite32。判断方法是看 BAR 的最低位:0 表示内存空间,1 表示 I/O 空间。
3.3 DMA 与一致性
PCIe 设备做数据传输通常用 DMA,驱动需要分配 DMA 缓冲区,并且保证 CPU 和设备的视图一致。Linux 提供了两类 DMA API:一致性 DMA 和流式 DMA。
一致性 DMA 用dma_alloc_coherent分配,返回的缓冲区在 CPU 和设备侧都能直接访问,不需要额外的同步操作,适合长期存在的描述符环。流式 DMA 用dma_map_single或dma_map_sg映射,适合一次性传输,传输完成后要dma_unmap解除映射。
dma_addr_t dma_handle; void *cpu_addr = dma_alloc_coherent(&pdev->dev, size, &dma_handle, GFP_KERNEL); if (!cpu_addr) { dev_err(&pdev->dev, "Failed to allocate DMA buffer\n"); return -ENOMEM; }这里有个关键点:dma_alloc_coherent分配的地址是设备可见的物理地址,驱动要把dma_handle写到设备的 DMA 寄存器里,设备才能正确访问。如果写错了,设备会 DMA 到错误的内存区域,轻则数据错乱,重则系统崩溃。
另外要注意 DMA 掩码。有些设备只能访问 32 位地址空间,这时候要用dma_set_mask_and_coherent设置掩码:
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32)); if (ret) { dev_err(&pdev->dev, "Failed to set DMA mask\n"); return ret; }如果不设置,内核可能分配 64 位地址,设备访问不了,就会出现 DMA 失败。这个问题在嵌入式平台上特别常见,因为很多 SoC 的 PCIe 控制器只支持 32 位地址。
4. 中断处理与数据通路:让设备真正跑起来
4.1 中断类型与注册
PCIe 设备支持三种中断方式:INTx、MSI、MSI-X。INTx 是传统的中断引脚,通过 IOAPIC 路由,共享中断线,效率低。MSI 是消息信号中断,设备直接写一个特定地址触发中断,不共享。MSI-X 是 MSI 的扩展,支持更多中断向量,并且每个向量可以独立配置。
现代 PCIe 设备基本都用 MSI-X,驱动里要优先尝试 MSI-X,失败再退到 MSI,最后才用 INTx:
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY); if (ret < 0) { dev_err(&pdev->dev, "Failed to allocate IRQ vectors\n"); return ret; } ret = request_irq(pci_irq_vector(pdev, 0), my_irq_handler, 0, "my_pci_drv", priv); if (ret) { dev_err(&pdev->dev, "Failed to request IRQ\n"); pci_free_irq_vectors(pdev); return ret; }pci_alloc_irq_vectors的第二个和第三个参数是最小和最大向量数,这里只申请一个。如果设备需要多个中断向量,比如多队列网卡,就要申请多个,并且每个向量单独注册处理函数。
中断处理函数要做的事情通常很简单:读取设备的中断状态寄存器,判断是不是自己的中断,清除中断标志,然后唤醒等待队列或者调度下半部。不要在中断上下文里做耗时操作,比如大量内存拷贝或者睡眠,这些要放到 tasklet 或工作队列里。
4.2 数据通路的实现模式
PCIe 设备的数据通路一般有两种模式:PIO 和 DMA。PIO 是 CPU 直接读写 BAR 空间,适合小数据量、低频率的场景。DMA 是设备直接访问内存,适合大数据量、高频率的场景。
以字符设备为例,用户态通过read/write系统调用触发数据传输。驱动里要实现file_operations的read和write函数。对于 DMA 传输,典型流程是:
- 用户态提供缓冲区,驱动用
copy_from_user拷贝到内核缓冲区,或者直接用用户态缓冲区做 DMA(需要get_user_pages固定页面)。 - 驱动把缓冲区物理地址写到设备的 DMA 描述符里。
- 驱动写设备寄存器,启动 DMA。
- 设备完成传输后触发中断,中断处理函数唤醒等待队列。
- 驱动检查传输状态,返回结果给用户态。
这里有个性能优化点:如果数据量大,可以用mmap把 DMA 缓冲区映射到用户态,避免拷贝。但要注意缓存一致性问题,mmap映射的内存要设置为非缓存或者写合并,否则 CPU 缓存和 DMA 数据可能不一致。
4.3 中断与轮询的取舍
有些场景下中断开销太大,比如高速采集卡每秒产生几十万次中断,CPU 全耗在中断处理上了。这时候可以用轮询模式,驱动主动查询设备状态,而不是等中断。Linux 的 NAPI 机制就是中断和轮询的结合:中断触发后关闭中断,用轮询处理一批数据,处理完再打开中断。
对于自定义设备,你可以实现类似的机制:在中断处理函数里调度一个工作队列,工作队列里循环读取设备 FIFO 直到为空,然后再重新使能中断。这样既保证了低延迟,又避免了中断风暴。
5. 调试与问题排查:掉卡、降速、AER 怎么破
5.1 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 设备不识别 | 供电不足、链路训练失败、驱动未加载 | lspci看是否枚举到,dmesg看链路状态 |
| 掉卡 | 链路不稳定、电源波动、散热问题 | dmesg看 AER 报错,检查电源和散热 |
| 降速 | 链路训练到低速率、信号完整性差 | lspci -vv看 LnkSta,检查走线和连接器 |
| 降 lane | 链路宽度协商失败、金手指脏污 | lspci -vv看 LnkSta,清洁金手指 |
| AER 报错 | 传输错误、ECC 错误、超时 | dmesg看 AER 详情,检查硬件和驱动 |
| DMA 失败 | 地址掩码不匹配、IOMMU 配置问题 | 检查dma_set_mask,看 IOMMU 日志 |
| 中断不触发 | MSI-X 配置错误、中断路由问题 | cat /proc/interrupts看中断计数 |
5.2 链路问题排查实战
掉卡、降速、降 lane 是 PCIe 最让人头疼的问题,因为它们往往是硬件和软件交织在一起的。我遇到过一个案例:一块 FPGA 板卡在实验室跑得好好的,到了现场就频繁掉卡。用dmesg看日志,发现有大量 AER 报错:
pcieport 0000:00:1c.0: AER: Corrected error received: 0000:03:00.0 pcieport 0000:00:1c.0: AER: PCIe Bus Error: severity=Corrected, type=Physical LayerCorrected error 说明链路有错误但被纠正了,还不致命。但如果出现 Uncorrected error,设备就会掉。排查步骤是:
- 用
lspci -vv看链路状态:
lspci -vv -s 03:00.0 | grep -A 10 "LnkSta" LnkSta: Speed 8GT/s (downgraded), Width x4 (downgraded)如果显示 downgraded,说明链路没有协商到最高速率或最大宽度。
检查信号完整性。用示波器看差分信号的眼图,如果眼图闭合,说明信号质量差。常见原因是走线太长、阻抗不匹配、连接器接触不良。
检查电源。PCIe 设备对电源纹波很敏感,尤其是高速率下。用万用表测 12V 和 3.3V 电源,纹波要控制在几十毫伏以内。
检查散热。有些 FPGA 或高速网卡发热量大,温度过高会导致链路不稳定。摸一下散热片,如果烫手就要加风扇。
软件侧可以尝试限制链路速率:
setpci -s 03:00.0 CAP_EXP+10.w=0002:000f这个命令把目标链路速率设为 5GT/s(Gen2),如果降速后稳定了,说明是信号完整性问题。
5.3 AER 机制与错误处理
AER(Advanced Error Reporting)是 PCIe 的错误报告机制,能报告 Corrected、Uncorrected Non-Fatal、Uncorrected Fatal 三类错误。驱动里可以注册 AER 处理函数,在错误发生时做恢复:
static pci_ers_result_t my_error_detected(struct pci_dev *pdev, pci_channel_state_t state) { switch (state) { case pci_channel_io_normal: return PCI_ERS_RESULT_CAN_RECOVER; case pci_channel_io_frozen: return PCI_ERS_RESULT_NEED_RESET; case pci_channel_io_perm_failure: return PCI_ERS_RESULT_DISCONNECT; } return PCI_ERS_RESULT_NONE; }然后在pci_driver里注册err_handler:
static const struct pci_error_handlers my_err_handler = { .error_detected = my_error_detected, .slot_reset = my_slot_reset, .resume = my_resume, };AER 处理的关键是区分可恢复和不可恢复错误。Corrected error 一般不用管,内核会记录但不会影响设备。Uncorrected Non-Fatal 可以尝试 reset 恢复,Uncorrected Fatal 通常需要重新枚举设备。
5.4 实操心得与避坑技巧
第一个心得:probe 里一定要检查返回值。我见过太多驱动在pci_request_regions失败后继续往下走,结果访问了未映射的地址,直接 oops。每个可能失败的调用都要检查,失败就按相反顺序清理已申请的资源。
第二个心得:中断处理函数要快进快出。不要在中断里做printk,因为printk可能睡眠,而且大量打印会拖慢系统。调试时可以用pr_debug或者 tracepoint,生产环境一定要关掉。
第三个心得:DMA 缓冲区要用dma_alloc_coherent分配,不要用kmalloc然后手动转换物理地址。kmalloc返回的地址在开启 IOMMU 后可能不是设备可见的物理地址,会导致 DMA 失败。
第四个心得:热插拔测试一定要做。很多驱动在冷启动时正常,热插拔后就出问题。测试方法是反复echo 1 > /sys/bus/pci/devices/0000:03:00.0/remove和echo 1 > /sys/bus/pci/rescan,看驱动是否能正常 probe 和 remove。
第五个心得:用dev_err/dev_info而不是printk。dev_*系列函数会自动带上设备名称和 BDF,排查问题时能快速定位是哪个设备出的错。
6. 从零写一个 PCIe 驱动的完整流程
6.1 环境准备与骨架搭建
先确认内核头文件已安装:
apt install linux-headers-$(uname -r)然后创建驱动骨架:
#include <linux/module.h> #include <linux/pci.h> #include <linux/fs.h> #include <linux/cdev.h> #define DRV_NAME "my_pci_drv" static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { dev_info(&pdev->dev, "Probe called\n"); return 0; } static void my_pci_remove(struct pci_dev *pdev) { dev_info(&pdev->dev, "Remove called\n"); } static const struct pci_device_id my_pci_ids[] = { { PCI_DEVICE(0x1234, 0x5678) }, { 0, } }; MODULE_DEVICE_TABLE(pci, my_pci_ids); static struct pci_driver my_pci_driver = { .name = DRV_NAME, .id_table = my_pci_ids, .probe = my_pci_probe, .remove = my_pci_remove, }; module_pci_driver(my_pci_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("My PCIe Driver");Makefile:
obj-m += my_pci_drv.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean编译加载:
make insmod my_pci_drv.ko dmesg | tail如果设备 ID 匹配,应该能看到 probe 被调用。
6.2 完善 probe 与资源管理
在 probe 里按顺序做以下事情:
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) { int ret; void __iomem *bar0; dma_addr_t dma_handle; void *dma_buf; ret = pci_enable_device(pdev); if (ret) { dev_err(&pdev->dev, "Failed to enable device\n"); return ret; } ret = pci_request_regions(pdev, DRV_NAME); if (ret) { dev_err(&pdev->dev, "Failed to request regions\n"); goto err_disable; } bar0 = pci_iomap(pdev, 0, 0); if (!bar0) { dev_err(&pdev->dev, "Failed to map BAR0\n"); ret = -ENOMEM; goto err_release; } ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)); if (ret) { dev_err(&pdev->dev, "Failed to set DMA mask\n"); goto err_unmap; } dma_buf = dma_alloc_coherent(&pdev->dev, 4096, &dma_handle, GFP_KERNEL); if (!dma_buf) { dev_err(&pdev->dev, "Failed to allocate DMA buffer\n"); ret = -ENOMEM; goto err_unmap; } ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY); if (ret < 0) { dev_err(&pdev->dev, "Failed to allocate IRQ vectors\n"); goto err_free_dma; } ret = request_irq(pci_irq_vector(pdev, 0), my_irq_handler, 0, DRV_NAME, pdev); if (ret) { dev_err(&pdev->dev, "Failed to request IRQ\n"); goto err_free_irq; } pci_set_drvdata(pdev, bar0); dev_info(&pdev->dev, "Probe successful\n"); return 0; err_free_irq: pci_free_irq_vectors(pdev); err_free_dma: dma_free_coherent(&pdev->dev, 4096, dma_buf, dma_handle); err_unmap: pci_iounmap(pdev, bar0); err_release: pci_release_regions(pdev); err_disable: pci_disable_device(pdev); return ret; }remove 函数按相反顺序清理:
static void my_pci_remove(struct pci_dev *pdev) { void __iomem *bar0 = pci_get_drvdata(pdev); free_irq(pci_irq_vector(pdev, 0), pdev); pci_free_irq_vectors(pdev); pci_iounmap(pdev, bar0); pci_release_regions(pdev); pci_disable_device(pdev); dev_info(&pdev->dev, "Remove done\n"); }6.3 中断处理与数据读写
中断处理函数:
static irqreturn_t my_irq_handler(int irq, void *dev_id) { struct pci_dev *pdev = dev_id; void __iomem *bar0 = pci_get_drvdata(pdev); u32 status; status = ioread32(bar0 + REG_INT_STATUS); if (!(status & INT_MASK)) { return IRQ_NONE; } iowrite32(status, bar0 + REG_INT_CLEAR); /* 唤醒等待队列或调度下半部 */ return IRQ_HANDLED; }数据读写通过字符设备接口暴露给用户态,这里不展开完整代码,核心是copy_from_user/copy_to_user和 DMA 传输的配合。
6.4 测试与验证
加载驱动后,用lspci -vv确认设备状态,用cat /proc/interrupts确认中断注册成功,用dmesg看驱动日志。如果设备有回环测试模式,可以先在驱动里做自测,确认寄存器读写和 DMA 通路正常。
性能测试可以用dd或者自己写测试程序,测量吞吐量和延迟。如果吞吐量不达标,检查 DMA 描述符环大小、中断合并配置、PCIe 链路速率和宽度。
7. 几个容易被忽略的细节
7.1 电源管理与运行时 PM
PCIe 设备支持多种电源状态:D0(全开)、D1/D2(中间态)、D3hot/D3cold(低功耗)。驱动里可以实现suspend/resume回调,在系统休眠时保存设备状态,唤醒时恢复。如果设备支持运行时 PM,还可以在空闲时自动进入低功耗状态。
static int my_pci_suspend(struct device *dev) { struct pci_dev *pdev = to_pci_dev(dev); /* 保存寄存器状态 */ return 0; } static int my_pci_resume(struct device *dev) { struct pci_dev *pdev = to_pci_dev(dev); /* 恢复寄存器状态 */ return 0; } static const struct dev_pm_ops my_pm_ops = { .suspend = my_pci_suspend, .resume = my_pci_resume, };然后在pci_driver里设置.driver.pm = &my_pm_ops。注意 suspend 时要确保没有正在进行的 DMA 传输,否则恢复后数据会错乱。
7.2 多设备与多实例
如果系统里有多个相同设备,驱动要支持多实例。每个设备对应一个私有数据结构,通过pci_set_drvdata保存,中断处理函数通过dev_id区分是哪个设备。字符设备可以用主设备号加次设备号区分,或者用cdev_add动态分配。
7.3 国产平台与嵌入式适配
在国产 Linux 平台或者嵌入式 SoC 上做 PCIe 驱动,要注意几点:一是 PCIe 控制器可能只支持 32 位 DMA 地址,必须设置 DMA 掩码;二是中断控制器可能不支持 MSI-X,要退到 MSI 或 INTx;三是设备树里要正确配置 PCIe 控制器的 ranges 和 interrupt-map,否则枚举会失败。
我在全志平台上移植过一个 PCIe 采集卡驱动,踩的最大的坑就是 DMA 掩码没设,导致设备 DMA 到高地址内存失败。后来加上dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32))就正常了。另外国产平台的 PCIe 时钟和复位配置也跟标准 x86 不同,需要仔细看 SoC 手册。
7.4 调试工具与技巧
除了lspci和dmesg,还有几个工具很有用:
setpci:直接读写配置空间,调试链路速率、BAR 大小很方便。pcimem:读写 BAR 空间,验证寄存器读写是否正常。perf:分析中断和 DMA 的性能瓶颈。ftrace:跟踪内核函数调用,定位驱动里的耗时操作。
我习惯在 probe 和 remove 里加dev_dbg,调试时打开动态调试:
echo "file my_pci_drv.c +p" > /sys/kernel/debug/dynamic_debug/control这样只有这个文件的调试信息会打印,不会刷屏。
8. 最后分享几个实战技巧
第一个技巧:probe 里加一个自检步骤,读写几个已知寄存器,确认硬件真的在工作。我遇到过设备枚举成功但寄存器读写全是 0xFFFFFFFF 的情况,原因是 FPGA 没加载比特流。自检能提前发现这类问题。
第二个技巧:中断处理函数里加一个计数器,通过 sysfs 暴露出来。调试时看计数是否增长,能快速判断中断有没有触发。如果计数不增长,检查 MSI-X 配置和中断路由。
第三个技巧:DMA 传输加超时机制。设备可能因为各种原因不触发完成中断,如果没有超时,用户态会一直阻塞。用wait_event_interruptible_timeout代替wait_event_interruptible,超时后返回错误并复位设备。
第四个技巧:热插拔测试用脚本自动化,反复插拔几百次,看有没有内存泄漏或者资源未释放。用slabtop看内核内存变化,用ls /sys/bus/pci/devices/看设备目录是否残留。
第五个技巧:如果设备支持 AER,一定要注册错误处理函数。即使不做复杂恢复,至少把错误信息打印出来,方便定位问题。AER 报错里会包含错误类型、错误位置、建议的恢复动作,是排查链路问题的第一手资料。
这些经验都是我在实际项目中一点点积累的,有些是踩坑之后才明白的。PCIe 驱动开发没有捷径,多写多调多测,遇到问题看日志、查手册、做实验,慢慢就有感觉了。