3. 设备ID匹配基础
在Linux内核驱动模型中,驱动与设备的绑定依赖于设备ID的匹配机制。不同的总线与平台使用不同的设备描述结构体,其中struct device_id、struct of_device_id和struct acpi_device_id是三种最核心的匹配标识。理解它们的作用域、数据来源与匹配逻辑,是编写兼容性驱动的第一步。
3.1 三种设备ID结构体概览
下表从定义位置、匹配总线、数据来源和典型用途四个维度,对比三种结构体的核心差异:
| 结构体 | 定义头文件 | 匹配总线/框架 | 数据来源 | 典型用途 |
|---|---|---|---|---|
struct device_id |
linux/mod_devicetable.h |
PCI、USB、I2C、SPI 等传统总线 | 硬件厂商分配的 Vendor ID、Device ID、Revision | PCI 网卡、USB 存储、I2C 传感器 |
struct of_device_id |
linux/of_device.h |
Device Tree (OF) 平台设备 | 设备树源文件中的 compatible 字符串 | ARM/ARM64 嵌入式平台、RISC-V SoC |
struct acpi_device_id |
linux/acpi.h |
ACPI 枚举设备 (x86/ARM64) | ACPI 表 (DSDT/SSDT) 中的 _HID 和 _CID | x86 笔记本、服务器、ACPI 电源管理 |
device_id 使用数值ID匹配,of_device_id 使用字符串匹配,acpi_device_id 使用字符串但遵循 ACPI 命名规范。驱动可以同时携带多种ID表,以支持跨平台兼容。
3.2 struct device_id:传统总线数值匹配
struct device_id 是内核中最基础的设备标识结构,主要用于 PCI、USB、I2C、SPI 等传统枚举总线。其定义通常包含总线特定的匹配字段:
// 以 PCI 设备为例 (include/linux/mod_devicetable.h)
struct pci_device_id {
__u32 vendor, device; // 厂商ID和设备ID
__u32 subvendor, subdevice; // 子系统厂商和设备ID
__u32 class, class_mask; // 设备类别与掩码
unsigned long driver_data; // 驱动私有数据
};
// 以 I2C 设备为例
struct i2c_device_id {
char name[I2C_NAME_SIZE]; // 设备名称字符串
kernel_ulong_t driver_data;
};
匹配流程:总线核心在枚举设备时,读取硬件寄存器中的 Vendor ID、Device ID 等数值,然后遍历驱动注册的 id_table,进行精确或掩码匹配。
lspci -nn 命令查看。
3.3 struct of_device_id:设备树字符串匹配
在 Device Tree 架构下,硬件描述不再依赖总线枚举,而是通过设备树源文件 (.dts) 中的节点属性定义。驱动使用 struct of_device_id 声明自己支持的 compatible 字符串:
// include/linux/of_device.h
struct of_device_id {
char name[32]; // 设备名称(通常不用于匹配)
char type[32]; // 设备类型(通常不用于匹配)
char compatible[128]; // 兼容性字符串,核心匹配字段
const void *data; // 驱动私有数据指针
};
设备树节点示例:
// imx6ul.dtsi 中的 I2C 控制器节点
i2c1: i2c@021a0000 {
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "disabled";
};
匹配规则:内核按照 compatible 属性中的字符串顺序,依次与驱动表中的 of_device_id 进行字符串精确匹配。匹配成功即停止,并使用该驱动的 probe 函数。
"ti,ads1015"、"nxp,pca9685"。支持多个兼容型号时,将最具体的型号放在最前面。
3.4 struct acpi_device_id:ACPI 命名空间匹配
在 x86 和部分 ARM64 平台上,ACPI 取代设备树成为硬件描述标准。驱动通过 struct acpi_device_id 声明支持的 ACPI 硬件ID:
// include/linux/acpi.h
struct acpi_device_id {
__u8 id[ACPI_ID_LEN]; // ACPI _HID 字符串,固定9字节
kernel_ulong_t driver_data;
__u32 cls;
__u32 cls_msk;
};
ACPI 命名规范:_HID 必须遵循 PNP ID 或 ACPI 厂商ID格式:
- PNP ID:3个大写字母 + 4个十六进制数字,例如
"PNP0C50"(HID 传感器集线器) - ACPI 厂商ID:4个大写字母 + 4个十六进制数字,例如
"INT33F0"(Intel 智能音频)
匹配流程:ACPI 固件在启动时枚举 DSDT/SSDT 表中的设备,提取每个设备的 _HID 和 _CID。内核 ACPI 核心将设备ID与驱动注册的 acpi_device_id 表进行字符串匹配。
3.5 三种ID匹配机制的关系图
下面的SVG图展示了三种设备ID结构体在驱动匹配流程中的位置与关系:
3.6 驱动中同时使用多种ID表
现代内核驱动通常需要支持多种硬件描述方式。以下是一个同时支持设备树和传统平台匹配的驱动示例:
// drivers/iio/adc/ti-ads1015.c 简化示例
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of_device.h>
#include <linux/acpi.h>
/* 传统 I2C 设备ID表 */
static const struct i2c_device_id ads1015_id[] = {
{ "ads1015", 0 },
{ "ads1115", 1 },
{ }
};
MODULE_DEVICE_TABLE(i2c, ads1015_id);
/* 设备树 compatible 表 */
static const struct of_device_id ads1015_of_match[] = {
{ .compatible = "ti,ads1015", .data = (void *)0 },
{ .compatible = "ti,ads1115", .data = (void *)1 },
{ }
};
MODULE_DEVICE_TABLE(of, ads1015_of_match);
/* ACPI 设备ID表 */
static const struct acpi_device_id ads1015_acpi_match[] = {
{ "TIADS1015", 0 },
{ "TIADS1115", 1 },
{ }
};
MODULE_DEVICE_TABLE(acpi, ads1015_acpi_match);
/* I2C 驱动结构体 */
static struct i2c_driver ads1015_driver = {
.driver = {
.name = "ads1015",
.of_match_table = ads1015_of_match,
.acpi_match_table = ads1015_acpi_match,
},
.probe = ads1015_probe,
.id_table = ads1015_id,
};
module_i2c_driver(ads1015_driver);
acpi_match_table 匹配,在 ARM 设备树平台上通过 of_match_table 匹配,在老旧内核或特殊总线上通过 id_table 匹配。三种表互不冲突,内核自动选择当前平台支持的匹配路径。
3.7 匹配优先级与内核实现
内核在调用驱动 probe 之前,按照以下优先级顺序尝试匹配:
- 设备树匹配 (OF):如果设备节点包含 compatible 属性,且驱动注册了
of_match_table,优先使用 of_device_id 匹配。 - ACPI 匹配:如果设备由 ACPI 枚举,且驱动注册了
acpi_match_table,使用 acpi_device_id 匹配。 - 传统ID表匹配:如果以上均不适用,回退到
id_table进行数值或名称匹配。
内核源码中 driver_match_device() 函数实现了这一优先级逻辑:
// drivers/base/dd.c
int driver_match_device(struct device_driver *drv, struct device *dev)
{
if (drv->bus->match) {
return drv->bus->match(dev, drv);
}
return 1; // 默认匹配成功
}
// 以 platform_bus 为例的匹配实现
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* 1. 尝试 OF 匹配 */
if (of_driver_match_device(dev, drv))
return 1;
/* 2. 尝试 ACPI 匹配 */
if (acpi_driver_match_device(dev, drv))
return 1;
/* 3. 尝试传统 ID 表匹配 */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* 4. 尝试名称匹配 */
return strcmp(pdev->name, drv->name) == 0;
}
echo 'file drivers/base/dd.c +p' > /sys/kernel/debug/dynamic_debug/control 打开匹配过程的调试日志,查看内核选择了哪种ID表进行匹配。
3.8 总结与对比清单
| 对比维度 | struct device_id | struct of_device_id | struct acpi_device_id |
|---|---|---|---|
| 匹配字段类型 | 数值 (vendor/device/class) | 字符串 (compatible) | 字符串 (_HID/_CID) |
| 匹配精度 | 精确 + 掩码模糊匹配 | 精确字符串匹配 | 精确字符串匹配 |
| 平台依赖 | 所有平台 (通用) | ARM/ARM64/RISC-V (设备树) | x86/ARM64 (ACPI) |
| 模块导出宏 | MODULE_DEVICE_TABLE | MODULE_DEVICE_TABLE(of, ...) | MODULE_DEVICE_TABLE(acpi, ...) |
| 驱动注册位置 | .id_table 字段 | .driver.of_match_table | .driver.acpi_match_table |
| 热插拔支持 | 是 (总线枚举触发) | 是 (设备树节点创建触发) | 是 (ACPI 命名空间枚举触发) |
掌握这三种设备ID结构体的作用与区别,是编写跨平台兼容驱动的基石。在实际开发中,建议驱动开发者同时提供 of_match_table 和 acpi_match_table,并保留传统的 id_table 作为回退,以覆盖最广泛的硬件平台。