资讯动态

Linux PCI驱动开发:从设备匹配到DMA与中断实现

发布时间:2026/9/19 6:21:39 来源:尧图企业网站定制
简介面向Linux驱动开发与嵌入式系统工程师的PCI驱动开发参考资料以PCI9054桥接芯片为例系统讲解在Linux内核2.4环境下PCI驱动的设计思路与实现流程。内容涵盖PCI总线规范、PCI9054的配置空间与BAR0~BAR5基址寄存器、字符设备驱动框架、设备探测与资源分配、初始化与中断处理以及将驱动静态编译进内核的方法并给出硬件测试要点适合需要从零搭建PCI设备驱动的开发者参考。资源为单篇PDF论文共1个文件压缩包约304KB内容精炼便于快速浏览核心知识点。已有124人浏览学习可用于快速理解PCI驱动开发的关键框架与实现步骤。1. Linux下的PCI驱动开发先匹配设备再谈读写寄存器Linux 下的 PCI 驱动开发是内核里套路最固定的一类工作。x86 服务器上的网卡、数据采集卡、GPU 卡驱动要做的核心事情都一样用 Vendor ID / Device ID 匹配设备拿到 BAR 空间使能 DMA 与中断再把能力暴露给用户态。很多人卡在不知道从哪下手其实内核已经完成了总线枚举和配置空间解析驱动作者只需要填好pci_driver结构体让probe在设备出现时被正确调用。这篇文章用最小可编译的代码把这条路走通先讲框架和生命周期再落地 Makefile 与资源访问然后处理 DMA 和中断这两个性能关键点最后给出调试和验证手段。2. PCI驱动开发的核心pci_driver 结构体与 probe 生命周期2.1 设备与驱动怎么匹配Vendor ID 与 pci_device_id先不写代码得弄清内核如何把一段硬件和一段驱动「撮合」在一起。PCI 设备的配置空间里固定存放着 16 位 Vendor ID 和 Device ID这是出厂烧死的身份标识。内核在启动时扫描 PCI 总线读出每个设备的 ID再遍历所有已注册的pci_driver用驱动声明的id_table与设备 ID 做匹配。匹配成功就调用probe失败则继续等下一个驱动或者交给modprobe自动加载对应模块。这个机制决定了开发的第一步不是写读写函数而是确认你的板卡 ID。用lspci -n能看到设备编号lspci -n # 输出示例01:00.0 0200: 8086:1539 (rev 04) # 前四位 8086 是 Vendor ID后四位 1539 是 Device ID第 2 个字段0200是 Class Code表示这是一张以太网控制器。开发时把这两个 ID 填进pci_device_id数组驱动就能和设备对上。如果lspci里根本没有这个 BDFBus:Device.Function问题就不在驱动而在 BIOS/UEFI 的 PCI 枚举阶段得先解决设备不出来这件事再谈驱动开发。2.2 一个最小 pci_driver 结构体pci_driver是驱动与 PCI 总线层之间的唯一接口内核文档里叫 low-level driver interface。关键成员不多但每个回调用时机必须清楚probe在设备与驱动匹配成功时调用remove在设备拔出或驱动卸载时调用shutdown在系统关机时调用suspend/resume用于电源管理。大部分数据采集卡、网卡驱动只实现前两个就够用。#include linux/module.h #include linux/pci.h #define MY_VENDOR_ID 0x1234 #define MY_DEVICE_ID 0x5678 static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id) { pr_info(my_pci: probe called for %04x:%04x\n, id-vendor, id-device); return 0; } static void my_remove(struct pci_dev *pdev) { pr_info(my_pci: remove called\n); } static struct pci_device_id my_ids[] { { PCI_DEVICE(MY_VENDOR_ID, MY_DEVICE_ID) }, { 0 } }; MODULE_DEVICE_TABLE(pci, my_ids); static struct pci_driver my_driver { .name my_pci, .id_table my_ids, .probe my_probe, .remove my_remove, }; module_pci_driver(my_driver); MODULE_LICENSE(GPL);四个容易被忽略的细节。第一MODULE_DEVICE_TABLE不只是给内核用的它让模块带上别名信息udev 在设备出现时才能根据 sysfs 里的 modalias 自动modprobe对应模块缺了它手动insmod没问题热插拔场景不会自动绑定。第二PCI_DEVICE()宏只匹配 Vendor/Device忽略 Subsystem ID如果同一 Device ID 下存在多个子型号、行为却不同改用PCI_DEVICE_SUB()精确到 Subsystem Vendor 和 Subsystem Device。第三.name不参与匹配只是驱动目录名但最好和模块名一致不然/sys/bus/pci/drivers/下的目录会让排错的人困惑。第四module_pci_driver展开后自动生成module_init和module_exit分别对应pci_register_driver和pci_unregister_driver省掉两段样板代码。2.3 probe 里按什么顺序拿资源probe是驱动开发的重头戏。很多人第一步就搞混pci_enable_device是把 PCI 配置空间 Command 寄存器里的 Memory/IO 访问使能位置位不等于已经占用了资源pci_request_regions才是向内核申请这段区域的所有权防止两个驱动同时操作同一个 BAR。static int my_probe(struct pci_dev *pdev, const struct pci_device_id *id) { void __iomem *regs; int err; err pcim_enable_device(pdev); /* 设备使能带有自动清理 */ if (err) return err; err pcim_iomap_regions(pdev, BIT(0), my_pci); /* 申请并映射 BAR0 */ if (err) return err; regs pcim_iomap_table(pdev)[0]; /* 拿到映射后的虚拟地址 */ pci_set_master(pdev); /* 开启 bus masterDMA 前置 */ return 0; }这里刻意用了pcim_前缀的接口而不是裸的pci_enable_devicepci_request_regionsioremap组合。pcim_系列会将资源挂到设备生命周期上后面代码出错或设备拔出时内核自动释放不会泄漏。pci_set_master只做一件事置位 PCI Command 寄存器的 Bus Master 位允许设备主动发起 DMA。不做 DMA 的驱动可以不调但绝大多数 PCI 设备的性能都依赖 DMA建议放在 probe 成功路径上。顺序不能乱先使能再申请映射最后开 Master反了会出现资源申请失败或 DMA 超时。3. 从零编译可加载的 PCI 驱动Makefile、模块参数与 BAR 访问3.1 Kbuild 外部模块的 Makefile 写法PCI 驱动开发不一定需要把代码编进内核外部模块方式加载完全够用调试迭代还更快。Kbuild 体系下最小 Makefile 只要三条规则obj-m : my_pci.o KERNELDIR ? /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KERNELDIR) M$(PWD) modules clean: $(MAKE) -C $(KERNELDIR) M$(PWD) clean不需要自己写gcc规则。make -C进入内核源码目录读取顶层 MakefileM$(PWD)让 Kbuild 切回当前目录编译外部模块。前置条件是系统装了 kernel-devel 或 linux-headers 包版本必须和当前运行的内核一致。obj-m表示目标是可加载模块写成obj-y就是编进内核镜像一个模块由多个源文件组成时用my_pci-objs : core.o ring.o聚合。编出来的my_pci.ko直接insmod配合uname -r检查内核版本是最常见的排错起点。3.2 用模块参数覆盖设备 IDPCI 驱动开发中常遇到「手上有好几款板卡ID 表写死之后要反复改源码」。常见做法是用module_param提供运行时覆盖能力static unsigned short vendor_override; static unsigned short device_override; module_param(vendor_override, ushort, 0444); module_param(device_override, ushort, 0444); MODULE_PARM_DESC(vendor_override, force vendor ID to probe); static int __init my_init(void) { if (vendor_override device_override) { my_ids[0].vendor vendor_override; my_ids[0].device device_override; } return pci_register_driver(my_driver); } module_init(my_init);module_param第三个参数0444决定 sysfs 权限0444 表示用户态只读。加载时用insmod my_pci.ko vendor_override0x8086 device_override0x1539驱动就会去匹配另一张卡适合开发期验证不同板卡。生产环境不建议留这个后门一不留神可能绑到别人的网卡上。这里需要自己写module_init因为module_pci_driver不会给你插入参数覆盖逻辑的机会。3.3 用 ioremap 与 readl/writel 访问寄存器所谓「读写寄存器」读写的是映射之后的虚拟地址。BAR 里存放的是物理地址必须用ioremap转成内核虚拟地址再访问直接对物理地址解引用会触发 page fault这是新手最容易踩的坑。static void __iomem *regs; err pcim_iomap_regions(pdev, BIT(3), my_pci); /* 请求并映射 BAR3 */ if (err) return err; regs pcim_iomap_table(pdev)[3]; u32 version readl(regs 0x00); /* 读版本寄存器 */ pr_info(my_pci: hw version 0x%08x\n, version); writel(0x01, regs 0x10); /* 写控制寄存器 */ readl(regs 0x10); /* 回读确保写到达设备 */经常被忽略的是最后一行回读。PCI 的写操作是 posted writeCPU 发出写指令后不等设备响应就返回在某些 FPGA 卡上表现为「写了但设备还没处理紧接着读状态却读到旧值」。readl自带 memory barrier回读一次能保证之前的写操作已经被设备接收。另一个常见坑是 BAR 类型有的设备 BAR0 是 IO 空间而不是 Memory 空间这时要用pci_iomap替代ioremap它会根据 BAR 类型自动选择内存映射或 IO 端口映射方式。加载后验证资源分配sudo insmod my_pci.ko dmesg | tail -n 20 cat /proc/iomem | grep -i my_pci ls -l /sys/bus/pci/drivers/my_pci//sys/bus/pci/drivers/my_pci/下出现设备 BDF 符号链接说明匹配成功没有就去核对lspci -n的 ID。4. DMA 与中断PCI驱动开发的性能核心4.1 一致性映射还是流式映射设备通过 Bus Master 直接读写主存驱动要做的就是分配一段物理上连续的缓冲区、把物理地址告诉设备。内核提供两种映射方式选错会导致数据损坏或性能劣化。一致性映射用dma_alloc_coherent分配的内存 CPU 和设备都能访问硬件保证缓存一致性适合周期性交换的控制结构、描述符环struct my_hw_desc *desc; dma_addr_t desc_dma; desc dma_alloc_coherent(pdev-dev, sizeof(*desc), desc_dma, GFP_KERNEL); if (!desc) return -ENOMEM; writel(lower_32_bits(desc_dma), regs DMA_ADDR_LOW); writel(upper_32_bits(desc_dma), regs DMA_ADDR_HIGH);lower_32_bits/upper_32_bits是把 64 位 DMA 地址拆成两个 32 位寄存器写入的标准做法兼容 32 位和 64 位寻址的 BAR。流式映射用dma_map_single配合dma_unmap_single适合一次性收发的大块数据比如网卡收发包它不保证缓冲区连续也不做缓存同步驱动必须在操作前后显式调用dma_sync_single_for_cpu/dma_sync_single_for_device。判断依据很简单缓冲区生命周期长、CPU 持续读写用一致性映射一次性大块传输、用完即弃用流式映射。把流式映射用在环形缓冲区上会频繁同步性能反而更差。4.2 中断注册从 INTx 到 MSI-XPCI 中断有两条路线传统 INTx 是共享中断线多个设备共用同一个 IRQ中断处理函数得先读硬件状态确认中断是否属于自己MSI 是设备直接往 CPU 发送中断消息每个设备有独立中断号没有共享干扰。现代 PCI 驱动开发基本都用 MSI-X因为它支持多队列每个队列可以绑不同 CPU。int irq; err pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); if (err 0) err pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_LEGACY); irq pci_irq_vector(pdev, 0); err request_threaded_irq(irq, my_hard_irq, my_thread_fn, IRQF_SHARED, my_pci, pdev);pci_alloc_irq_vectors的参数是最少向量数、最多向量数、支持的中断类型标志。只传PCI_IRQ_MSI表示仅接受 MSI加PCI_IRQ_MSIX允许 MSI-X带PCI_IRQ_LEGACY则允许回退 INTx。多队列设备把最小和最大向量数都设为队列数就能一次拿到多个中断号。request_threaded_irq的第三个参数是线程化中断处理硬中断里只做最小确认读中断状态寄存器、清中断耗时工作放到内核线程避免把中断上下文卡死硬中断返回IRQ_WAKE_THREAD时内核才唤醒线程函数。4.3 常用 PCI 驱动开发 API 参数速查函数作用关键参数含义pcim_enable_device(pdev)使能设备 Memory/IO 访问相比非 pcim 版本带自动清理pcim_iomap_regions(pdev, mask, name)申请并映射指定 BARmask 是BIT(n)n 为 BAR 序号pcim_iomap_table(pdev)[n]取第 n 个 BAR 的映射地址必须在 iomap_regions 之后调用pci_set_master(pdev)置位 Bus MasterDMA 前置条件dma_alloc_coherent(dev, size, dma, gfp)分配一致性 DMA 内存gfp 常用GFP_KERNEL中断上下文用GFP_ATOMICpci_alloc_irq_vectors(pdev, min, max, flags)分配中断向量flags 取PCI_IRQ_MSIX/PCI_IRQ_MSI/PCI_IRQ_LEGACYpci_irq_vector(pdev, n)取第 n 个中断号配合irq_set_affinity_hint可做绑核一个常见错误是把pci_set_master放在 DMA 内存分配之后。设备没开 Bus Master 时DMA 地址即便写进设备寄存器设备发出的读写请求也会被总线拒绝表现为 DMA 超时。标准顺序是使能设备 → 申请并映射 BAR →pci_set_master→ 分配 DMA 内存 → 注册中断。调换任意两步都可能踩到「资源申请失败」或「中断触发但数据没来」的玄学问题。5. 调试三板斧dmesg 时序、动态调试与 udev 固定设备节点5.1 用 dmesg 确认 probe 与 remove 的调用时序PCI 驱动开发里最常用的工具就是dmesg。加载模块、绑定设备、卸载模块三个动作各留一条日志就能判断生命周期是否完整dmesg -wT sudo sh -c echo 1 /sys/bus/pci/devices/0000:01:00.0/remove sudo sh -c echo 1 /sys/bus/pci/rescanremove文件写入 1 模拟热拔出内核会调用驱动的remove回调rescan写入 1 触发重新枚举probe应该再次被调用。如果rescan后没有 probe 日志多半是id_table没匹配上或上一个remove里资源没释放干净。5.2 打开动态调试不用重新编译内核开了CONFIG_DYNAMIC_DEBUG时pr_debug可以在运行时按文件、函数、行号精确开关echo file drivers/my_pci.c p /sys/kernel/debug/dynamic_debug/control sudo cat /proc/sys/kernel/printk注意printk级别pr_info对应 6 级默认控制台不一定显示dmesg里一定能看到如果dmesg里也没有先查模块是否真的加载了执行lsmod | grep my_pci。调试 DMA 超时这类问题在中断处理函数和 DMA 完成回调里各加一条带时间戳的pr_info拿dmesg -T对比时间差是最直接的定位手段。5.3 用 udev 固定设备节点PCI 驱动开发最后一步往往是把/dev下的设备节点固定下来。动态主设备号会导致每次加载节点号漂移用户态脚本没法依赖。在/etc/udev/rules.d/99-my_pci.rules里写KERNELmy_pci*, SUBSYSTEMmy_pci, OWNERroot, GROUPstaff, MODE0660, SYMLINKmy_pci0SYMLINK保证用户态始终访问/dev/my_pci0MODE0660配合GROUPstaff把访问权限限制在特定用户组。改完规则执行udevadm control --reload和udevadm trigger再insmod模块验证节点是否按预期生成。驱动里创建字符设备时用class_create注册一个struct classSUBSYSTEM才会是my_pci否则 udev 规则匹配不到。链路验证的方法很简单用户态打开/dev/my_pci0用ioctl触发一次寄存器回读拿dmesg对照驱动里打印的硬件版本号这一步通了整条 PCI 驱动开发链路就算闭环。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价