3、驱动分离模式:Platform驱动与设备分离机制

驱动分离,说白了就是把「驱动代码」和「设备信息」解耦。我早年做嵌入式时,经常看到驱动里硬编码了寄存器地址、中断号。换一个板子,就得改驱动源码重新编译。这哪叫工程?这叫搬砖。

Linux 内核为了解决这个问题,引入了 Platform 驱动模型。它的核心思想就一句话:驱动只管操作逻辑,设备只管描述硬件资源。两者通过名字(name)来匹配。

3.1 为什么需要驱动分离?

你想想看,一个 I2C 控制器,在 A 板子上挂的是温度传感器,在 B 板子上挂的是加速度计。如果驱动和设备不分离,你得写两个驱动。分离之后呢?驱动只负责控制器的读写时序,设备信息(挂载什么芯片、地址多少)由板级描述决定。

我遇到过最典型的例子:某项目换了 Flash 型号,只改了设备树里的分区表,驱动一行没动。这就是分离带来的好处。

驱动分离的核心收益:
  • 可移植性:驱动代码与硬件资源解耦,换板子只需改设备描述
  • 可维护性:驱动逻辑和硬件配置可以独立演进
  • 代码复用:同一驱动可服务于多个不同硬件实例
  • 标准化:设备资源通过统一接口(platform_data / 设备树)传递

3.2 Platform 驱动与设备匹配流程

Platform 驱动模型的工作流程,其实就三步:

  1. 注册驱动:调用 platform_driver_register(),把驱动挂到内核的驱动链表上
  2. 注册设备:调用 platform_device_register(),把设备信息挂到设备链表上
  3. 匹配:内核遍历两个链表,通过 match() 函数找到名字相同的驱动和设备,然后调用驱动的 probe()

匹配的优先级是这样的:

匹配方式 说明 优先级
设备树 compatible 匹配 设备树节点的 compatible 属性与驱动 of_match_table 匹配 最高
ACPI 匹配 高级配置与电源接口,x86 平台常用 次高
name 字段匹配 驱动 id_table 或 driver.name 与设备 name 匹配 最低

我个人习惯,新项目一律用设备树 compatible 匹配。为什么?因为设备树是硬件描述的事实标准,而且兼容性最好。

3.3 设备树(Device Tree)匹配流程

设备树的匹配流程,我画了一张图,你看完就明白了:

设备树匹配流程 设备树匹配流程 设备树节点 compatible = "vendor,device" 内核解析 DTB 生成 platform_device 驱动注册 of_match_table 匹配 调用 probe() 驱动获取资源并初始化 获取资源 platform_get_resource() 💡 匹配关键点: • 设备树节点中的 compatible 属性必须与驱动 of_match_table 中的某一条完全一致 • 匹配成功后,内核自动调用 probe(),并将设备资源通过 platform_device 传递给驱动

设备树匹配的核心,就是 compatible 字符串。驱动里声明「我能支持哪些设备」,设备树里声明「我是什么设备」,内核当红娘牵线。

我的经验:写 compatible 时,建议遵循「厂商,设备型号」的命名规范。比如 "ti,am335x-uart"、"fsl,imx6q-uart"。这样既避免冲突,又方便维护。

3.4 实战:将杂项设备驱动拆分为 Platform 驱动

光说不练假把式。我们拿一个实际的杂项设备驱动来改造。假设原来有一个简单的 LED 控制驱动,用的是 misc_register() 注册的。

3.4.1 原始杂项设备驱动(改造前)

static struct miscdevice led_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "my_led",
    .fops = &led_fops,
};

static int __init led_init(void)
{
    /* 硬编码 GPIO 号 */
    gpio_request(128, "led");
    gpio_direction_output(128, 0);
    
    misc_register(&led_misc);
    return 0;
}

问题在哪?GPIO 号 128 是写死的。换一块板子,GPIO 号变了,就得改源码。这就是典型的「驱动和设备耦合」。

3.4.2 改造为 Platform 驱动

第一步,定义设备资源结构体:

struct led_platform_data {
    int gpio_num;
    int active_low;
};

第二步,实现 Platform 驱动框架:

static const struct of_device_id led_of_match[] = {
    { .compatible = "myvendor,myled", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, led_of_match);

static int led_probe(struct platform_device *pdev)
{
    struct led_platform_data *pdata;
    struct device *dev = &pdev->dev;
    
    /* 从设备树获取 GPIO 资源 */
    pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL);
    if (!pdata)
        return -ENOMEM;
    
    pdata->gpio_num = of_get_named_gpio(dev->of_node, "led-gpios", 0);
    if (pdata->gpio_num < 0) {
        dev_err(dev, "failed to get gpio\n");
        return pdata->gpio_num;
    }
    
    /* 申请并初始化 GPIO */
    devm_gpio_request(dev, pdata->gpio_num, "led");
    gpio_direction_output(pdata->gpio_num, 0);
    
    platform_set_drvdata(pdev, pdata);
    return 0;
}

static int led_remove(struct platform_device *pdev)
{
    /* 资源由 devm 自动释放 */
    return 0;
}

static struct platform_driver led_driver = {
    .probe  = led_probe,
    .remove = led_remove,
    .driver = {
        .name = "my_led",
        .of_match_table = led_of_match,
    },
};

module_platform_driver(led_driver);

第三步,设备树节点:

myled: myled@0 {
    compatible = "myvendor,myled";
    led-gpios = &gpio1 16 GPIO_ACTIVE_HIGH;
};
注意:改造后,驱动不再硬编码任何硬件资源。GPIO 号、中断号、寄存器地址全部从设备树获取。这就是「驱动分离」的精髓。

3.5 改造前后的对比

对比项 杂项设备驱动(旧) Platform 驱动(新)
硬件资源 硬编码在驱动中 从设备树获取
可移植性 差,换板子必须改驱动 好,只需改设备树
匹配方式 无匹配机制 compatible 自动匹配
资源管理 手动申请/释放 devm_ 自动管理
代码复用 低 高,同一驱动可对应多个实例

我曾经在一个项目里,把 5 个杂项设备驱动改成了 Platform 驱动。改完之后,驱动代码量减少了 30%,但覆盖的板型从 2 种扩展到了 8 种。这就是分离带来的威力。

3.6 避坑指南

我曾经踩过的坑:
  • compatible 字符串不匹配:驱动和设备树的 compatible 必须一字不差,包括大小写。我吃过一次亏,把 "myvendor,myled" 写成了 "myvendor,my-led",结果 probe 死活不调用。
  • 忘记 MODULE_DEVICE_TABLE:这个宏告诉内核模块支持哪些设备。不加的话,设备树匹配可能失效。
  • 资源释放顺序:虽然 devm_ 能自动释放,但如果你在 remove 里手动释放了某些资源,要确保 probe 里对应的申请顺序和 remove 里的释放顺序相反。
我的调试技巧:如果 probe 没被调用,先检查 /sys/bus/platform/devices/ 下有没有对应的设备节点。如果没有,说明设备树没解析成功。如果有,检查驱动的 of_match_table 是否和设备树的 compatible 匹配。

嗯,驱动分离这件事,说白了就是「把变化的部分和不变的部分分开」。设备信息是变化的,驱动逻辑是相对稳定的。Platform 驱动模型帮我们实现了这个分离,而设备树则是描述设备信息的标准语言。掌握了这套机制,你的驱动就能做到「一次编写,到处运行」。