3. 设备ID匹配基础

在Linux内核驱动模型中,驱动与设备的绑定依赖于设备ID的匹配机制。不同的总线与平台使用不同的设备描述结构体,其中struct device_id、struct of_device_id和struct acpi_device_id是三种最核心的匹配标识。理解它们的作用域、数据来源与匹配逻辑,是编写兼容性驱动的第一步。

核心概念:设备ID是驱动向内核声明“我能支持哪些硬件”的契约。内核通过遍历驱动表中的ID列表,与枚举到的设备硬件信息进行比对,匹配成功则调用驱动的probe函数。

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,进行精确或掩码匹配。

注意:数值ID匹配要求驱动开发者准确获取硬件厂商分配的ID。错误的 Vendor ID 会导致驱动永远无法匹配到设备。对于 PCI 设备,可以使用 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 函数。

最佳实践:compatible 字符串应遵循“厂商,型号”的命名规范,例如 "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结构体在驱动匹配流程中的位置与关系:

设备ID匹配机制关系图 硬件描述来源 PCI/USB 总线枚举 Device Tree (.dts) ACPI 表 (DSDT/SSDT) 设备ID结构体 struct device_id struct of_device_id struct acpi_device_id 匹配方式 数值ID + 掩码匹配 compatible 字符串匹配 _HID / _CID 字符串匹配 驱动兼容性设计要点 1. 一个驱动可以同时携带三种ID表,实现跨平台兼容 2. 匹配优先级:设备树优先于 ACPI,ACPI 优先于传统总线枚举 3. 使用 MODULE_DEVICE_TABLE 宏将ID表导出到模块元数据 4. 内核根据编译配置 (CONFIG_OF / CONFIG_ACPI) 决定启用哪种匹配路径

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);
兼容性策略:上述驱动在 x86 ACPI 平台上通过 acpi_match_table 匹配,在 ARM 设备树平台上通过 of_match_table 匹配,在老旧内核或特殊总线上通过 id_table 匹配。三种表互不冲突,内核自动选择当前平台支持的匹配路径。

3.7 匹配优先级与内核实现

内核在调用驱动 probe 之前,按照以下优先级顺序尝试匹配:

  1. 设备树匹配 (OF):如果设备节点包含 compatible 属性,且驱动注册了 of_match_table,优先使用 of_device_id 匹配。
  2. ACPI 匹配:如果设备由 ACPI 枚举,且驱动注册了 acpi_match_table,使用 acpi_device_id 匹配。
  3. 传统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 作为回退,以覆盖最广泛的硬件平台。