Linux 下“发现设备”的机制主要分几大类。你可以先记住一个总原则:
谁发现设备,谁创建对应的 device 对象;你就要写对应的 driver 框架去匹配它。
也就是说,发现机制不同,最终驱动框架就不同。
1. 第一类:总线协议自己能枚举设备
这类设备本身有标准枚举机制,Linux 可以主动扫描/枚举出来。
典型包括:
| 发现机制 | 典型设备 | Linux 设备对象 | 驱动框架 |
|---|---|---|---|
| PCI / PCIe 枚举 | PCIe FPGA、网卡、NVMe、GPU | struct pci_dev | struct pci_driver |
| USB 枚举 | U盘、USB摄像头、USB串口 | struct usb_device/ USB interface | struct usb_driver |
| SCSI/SATA/SAS 扫描 | 硬盘、光驱、存储设备 | SCSI device / block device | SCSI / libata / block 框架 |
| NVMe 枚举 | NVMe SSD | PCI device + NVMe controller | PCI + NVMe 框架 |
比如 PCIe 设备,主机上电后 Root Complex 会枚举 PCIe 总线,读取设备配置空间,拿到 Vendor ID、Device ID、BAR、中断能力等信息。Linux PCI core 会据此创建struct pci_dev,然后拿它去匹配struct pci_driver。Linux PCI 驱动文档也说明,PCI 驱动通过注册pci_driver,由 PCI 子系统在匹配设备时调用驱动的probe()。(Linux内核文档)
所以你的 PCIe FPGA 板卡属于这一类:
PCIe 枚举 ↓ Linux 创建 struct pci_dev ↓ 匹配 struct pci_driver ↓ 调用 pci_driver.probe()因此驱动框架就是:
structpci_driver而不是platform_driver、i2c_driver或者单纯字符设备驱动。
2. 第二类:Firmware 描述设备
有些设备总线本身不能自动枚举,Linux 需要通过固件描述知道它们存在。
主要有两种:
Device Tree ACPI2.1 Device Tree 发现设备
常见于 ARM、Zynq、嵌入式 Linux。
比如设备树里写:
my_dma@43c00000 { compatible = "mycompany,my-dma"; reg = <0x43c00000 0x10000>; interrupts = <0 61 4>; };这段意思是:
这里有一个设备 寄存器基地址是 0x43c00000 大小是 0x10000 中断号是 61 匹配字符串是 "mycompany,my-dma"Linux 启动时解析 Device Tree,创建对应的platform_device,然后用compatible字符串去匹配platform_driver。Linux 的 Device Tree 文档说明,Device Tree 是 Linux 描述硬件布局的重要方式;platform 驱动文档也说明,driver core 会在 device 和 driver 匹配后调用probe()。(Linux内核文档)
所以这种路径是:
Device Tree ↓ Linux 创建 platform_device ↓ 匹配 platform_driver ↓ 调用 platform_driver.probe()对应驱动框架是:
structplatform_driver2.2 ACPI 发现设备
常见于 x86 PC、服务器、笔记本。
ACPI 表由 BIOS/UEFI 提供,里面描述了平台设备、电源管理、中断、I2C/SPI/UART 等设备信息。Linux ACPI 文档说明,ACPI 可以枚举 platform、SPI、I2C 以及某些 UART 后面的设备,并创建对应的物理设备对象。(Linux内核文档)
路径类似:
BIOS/UEFI ACPI table ↓ Linux ACPI core 解析 ↓ 创建 platform_device / i2c_client / spi_device 等 ↓ 匹配对应 driver所以 ACPI 不一定只创建platform_device,它也可能创建 I2C/SPI 子设备。
3. 第三类:父设备驱动创建子设备
这是非常重要的一类。
有些设备不是一开始就被系统直接看到,而是:
先发现一个父设备 父设备驱动 probe 后 再创建它下面的子设备典型场景:
| 父设备 | 子设备 | 说明 |
|---|---|---|
| PCIe 板卡 | 板卡上的 I2C 芯片 | PCIe 驱动起来后,注册内部 I2C adapter,再创建 i2c_client |
| USB 设备 | 多个 USB interface | USB core 把一个物理设备拆成多个 interface 给不同驱动 |
| MFD 芯片 | GPIO / RTC / regulator / codec | 一个芯片内部有多个功能块 |
| MDIO 控制器 | Ethernet PHY | MAC 驱动注册 MDIO bus,再扫描 PHY |
| SCSI HBA | 磁盘 LUN | HBA 先被发现,再扫描下面的磁盘 |
| FPGA Manager / Overlay | 动态加载后的逻辑外设 | FPGA 配置后再生成新设备 |
比如一个 PCIe FPGA 板卡上,FPGA 内部实现了一个 I2C 控制器。流程可能是:
PCIe 枚举发现 FPGA ↓ pci_driver.probe() ↓ 驱动映射 BAR ↓ 发现 FPGA 内部有 I2C controller ↓ 注册 i2c_adapter ↓ I2C core 再创建 i2c_client ↓ 匹配 i2c_driver所以这里有两层发现:
第一层:PCIe 发现 FPGA 板卡 第二层:FPGA 驱动创建内部子设备这也是为什么复杂设备里会出现“一个物理设备对应多个 Linux 驱动”的情况。
4. 第四类:I2C / SPI 这类“不能自发现”的总线
I2C 和 SPI 很特殊。
它们不像 PCIe、USB 那样可以标准枚举。大多数 I2C/SPI 从设备不会主动告诉 Linux:
我是谁 我在哪里 我的型号是什么所以 Linux 需要通过其他方式知道:
哪个总线上 哪个地址/片选 挂了什么芯片4.1 I2C 设备发现方式
I2C 设备通常有几种实例化方式:
| 方式 | 说明 |
|---|---|
| Device Tree | 嵌入式最常见 |
| ACPI | x86/笔记本/服务器常见 |
| board info | 老式板级代码 |
| 父驱动显式创建 | 比如 PCIe/USB 设备内部挂 I2C 芯片 |
| sysfs 手动创建 | 调试用,例如new_device |
Linux I2C 文档明确提到,I2C 设备可以显式实例化,例如填充struct i2c_board_info并调用i2c_new_client_device()。(Linux内核文档)
I2C 的典型路径是:
Device Tree / ACPI / board info / 父驱动创建 ↓ Linux 创建 struct i2c_client ↓ 匹配 struct i2c_driver ↓ 调用 i2c_driver.probe()所以 I2C 驱动框架是:
structi2c_driver但注意:
I2C 设备不是靠 I2C 总线自己扫描出来的,通常是被描述出来或者被父驱动创建出来的。
4.2 SPI 设备发现方式
SPI 也类似。
SPI 设备通常由:
Device Tree ACPI board info 父驱动创建来描述。
路径是:
固件/板级信息/父驱动 ↓ 创建 struct spi_device ↓ 匹配 struct spi_driver ↓ 调用 spi_driver.probe()SPI 也不能像 PCIe 那样自动读出 Vendor ID / Device ID。
5. 第五类:platform_device 静态注册
这是老式或者简单嵌入式系统里常见的方式。
设备不是通过 Device Tree,也不是通过 ACPI,而是内核板级代码里直接注册:
platform_device_register(&my_device);或者注册一组:
platform_add_devices(...);然后驱动用:
platform_driver_register(&my_driver);匹配方式通常是:
device name driver name或者通过of_match_table/acpi_match_table。
这类方式现在在很多新项目里被 Device Tree / ACPI 替代了,但内核里仍然存在。
路径是:
板级代码注册 platform_device ↓ platform bus ↓ 匹配 platform_driver ↓ 调用 probe()6. 第六类:热插拔 / 重新扫描
有些设备不是开机时就存在,而是运行中插入或重新扫描出来。
典型包括:
| 机制 | 例子 |
|---|---|
| PCIe Hotplug | 插入 PCIe 热插拔设备 |
| USB Hotplug | 插入 U盘、USB串口 |
| Thunderbolt Hotplug | 外接扩展坞 |
| PCI rescan | 手动触发 PCIe 总线重新扫描 |
| SCSI scan | 手动扫描新磁盘 |
| Device Tree overlay | 运行时加载新的 DT overlay |
| FPGA reconfiguration | FPGA 重新配置后出现新逻辑外设 |
例如 PCIe 可以通过 rescan 重新发现设备:
echo 1 > /sys/bus/pci/rescan但本质仍然是:
PCI core 重新扫描 ↓ 发现新 pci_dev ↓ 匹配 pci_driver也就是说,热插拔只是“发现时机”不同,不改变驱动框架。
7. 第七类:用户手动创建设备
有些设备 Linux 不会自动知道,可以由用户手动告诉内核。
比如 I2C 调试时,可以手动创建一个设备:
echotmp102 0x48>/sys/bus/i2c/devices/i2c-1/new_device这会告诉 I2C core:
在 i2c-1 总线上,0x48 地址有一个 tmp102 设备然后 Linux 创建i2c_client,再匹配i2c_driver。
这种方式常用于:
调试 验证 没有设备树/ACPI 描述的临时场景不太适合作为正式产品的主要发现方式。
8. 第八类:虚拟设备 / 软件总线
还有一些设备不是传统物理硬件,而是由虚拟化或软件框架创建。
典型包括:
| 机制 | 设备 |
|---|---|
| virtio | 虚拟网卡、虚拟磁盘、虚拟 console |
| vhost | 虚拟化后端设备 |
| rpmsg | 异构多核通信设备 |
| remoteproc | 远程处理器设备 |
| auxiliary bus | 一个大设备拆出的辅助子功能 |
| MFD | 多功能芯片拆出的子设备 |
这类设备的发现路径通常是:
父框架/虚拟化层/远程处理器框架创建 device ↓ 挂到对应 bus ↓ 匹配对应 driver9. 重要区分:udev不是硬件发现机制
很多人会把udev和设备发现混在一起。
严格说:
内核负责发现硬件并创建 device udev 负责根据内核事件创建设备节点、设置权限、加载规则例如:
PCIe 设备被 PCI core 发现 ↓ pci_driver probe 成功 ↓ 驱动注册字符设备 ↓ 内核产生 uevent ↓ udev 创建 /dev/mydma所以:
udev不是发现 PCIe 设备的人,它只是用户空间里的设备节点管理器。
10. 也要区分:字符设备不是硬件发现机制
比如你写:
register_chrdev();misc_register();cdev_add();这只是创建用户态访问入口:
/dev/xxx它不是发现硬件。
对于 PCIe FPGA DMA,正确结构是:
PCIe 发现机制 ↓ pci_driver 绑定硬件 ↓ probe 里初始化 BAR / DMA / IRQ ↓ 注册字符设备 ↓ 用户通过 /dev/mydma 访问所以不能说:
我写字符设备驱动,所以不用 pci_driver更准确是:
底层用 pci_driver 发现和绑定 PCIe 设备 上层用字符设备给用户态提供接口11. 总结表:Linux 常见设备发现机制
| 发现机制 | 谁发现设备 | 创建什么对象 | 驱动框架 | 例子 |
|---|---|---|---|---|
| PCI/PCIe 枚举 | PCI core | pci_dev | pci_driver | FPGA PCIe 板卡、网卡、NVMe |
| USB 枚举 | USB core | usb_device/ interface | usb_driver | U盘、USB摄像头 |
| Device Tree | OF/DT core | platform_device/i2c_client/spi_device | platform/I2C/SPI | ARM SoC 外设 |
| ACPI | ACPI core | platform/I2C/SPI/UART 等设备 | 对应 driver | x86 平台设备 |
| board info | 板级代码 | platform/I2C/SPI device | 对应 driver | 老式嵌入式 |
| 父驱动创建 | parent driver | 子 device | 子系统 driver | MFD、MDIO、FPGA 内部 I2C |
| 总线重新扫描 | bus core | 新 device | 对应 driver | PCI rescan、SCSI scan |
| 用户手动创建 | sysfs/configfs | 对应 device | 对应 driver | I2Cnew_device |
| 虚拟化/软件总线 | virtio/rpmsg/auxiliary 等 | 虚拟 device | 对应 driver | virtio-net、rpmsg |
12. 对你最重要的判断方法
你以后看到一个硬件,先不要问“我要写什么模板”,而是按这个顺序判断:
1. lspci 能不能看到? 能 → PCIe 设备 → pci_driver 2. lsusb 能不能看到? 能 → USB 设备 → usb_driver 3. 设备树里有没有 compatible/reg/interrupts? 有 → 多数是 platform_driver,也可能是 i2c_driver/spi_driver 4. 是 I2C 地址设备吗? 是 → i2c_driver,但设备通常要由 DT/ACPI/父驱动/手动创建 5. 是 SPI 片选设备吗? 是 → spi_driver,但设备通常要由 DT/ACPI/父驱动创建 6. 是 SoC 内部寄存器外设吗? 是 → platform_driver 7. 是 PCIe 设备内部又挂了子功能吗? 是 → 先 pci_driver,再由父驱动创建子设备 8. 只是想给用户态 /dev 接口? 那是字符设备接口,不是硬件发现路径13. 一句话记忆
能自报家门的总线,比如 PCIe/USB,靠总线枚举发现;不能自报家门的设备,比如 platform/I2C/SPI,靠 Device Tree、ACPI、板级信息或父驱动创建;用户态的
/dev只是访问入口,不负责发现硬件。
对你的 PCIe FPGA 来说,最关键就是:
发现机制:PCIe 枚举 内核对象:struct pci_dev 驱动框架:struct pci_driver 匹配依据:Vendor ID / Device ID 资源来源:BAR / IRQ / DMA capability 用户接口:再额外注册字符设备