3、驱动分离模式:Platform驱动与设备分离机制
驱动分离,说白了就是把「驱动代码」和「设备信息」解耦。我早年做嵌入式时,经常看到驱动里硬编码了寄存器地址、中断号。换一个板子,就得改驱动源码重新编译。这哪叫工程?这叫搬砖。
Linux 内核为了解决这个问题,引入了 Platform 驱动模型。它的核心思想就一句话:驱动只管操作逻辑,设备只管描述硬件资源。两者通过名字(name)来匹配。
3.1 为什么需要驱动分离?
你想想看,一个 I2C 控制器,在 A 板子上挂的是温度传感器,在 B 板子上挂的是加速度计。如果驱动和设备不分离,你得写两个驱动。分离之后呢?驱动只负责控制器的读写时序,设备信息(挂载什么芯片、地址多少)由板级描述决定。
我遇到过最典型的例子:某项目换了 Flash 型号,只改了设备树里的分区表,驱动一行没动。这就是分离带来的好处。
- 可移植性:驱动代码与硬件资源解耦,换板子只需改设备描述
- 可维护性:驱动逻辑和硬件配置可以独立演进
- 代码复用:同一驱动可服务于多个不同硬件实例
- 标准化:设备资源通过统一接口(platform_data / 设备树)传递
3.2 Platform 驱动与设备匹配流程
Platform 驱动模型的工作流程,其实就三步:
- 注册驱动:调用
platform_driver_register(),把驱动挂到内核的驱动链表上 - 注册设备:调用
platform_device_register(),把设备信息挂到设备链表上 - 匹配:内核遍历两个链表,通过
match()函数找到名字相同的驱动和设备,然后调用驱动的probe()
匹配的优先级是这样的:
| 匹配方式 | 说明 | 优先级 |
|---|---|---|
| 设备树 compatible 匹配 | 设备树节点的 compatible 属性与驱动 of_match_table 匹配 | 最高 |
| ACPI 匹配 | 高级配置与电源接口,x86 平台常用 | 次高 |
| name 字段匹配 | 驱动 id_table 或 driver.name 与设备 name 匹配 | 最低 |
我个人习惯,新项目一律用设备树 compatible 匹配。为什么?因为设备树是硬件描述的事实标准,而且兼容性最好。
3.3 设备树(Device Tree)匹配流程
设备树的匹配流程,我画了一张图,你看完就明白了:
设备树匹配的核心,就是 compatible 字符串。驱动里声明「我能支持哪些设备」,设备树里声明「我是什么设备」,内核当红娘牵线。
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;
};
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 里的释放顺序相反。
/sys/bus/platform/devices/ 下有没有对应的设备节点。如果没有,说明设备树没解析成功。如果有,检查驱动的 of_match_table 是否和设备树的 compatible 匹配。
嗯,驱动分离这件事,说白了就是「把变化的部分和不变的部分分开」。设备信息是变化的,驱动逻辑是相对稳定的。Platform 驱动模型帮我们实现了这个分离,而设备树则是描述设备信息的标准语言。掌握了这套机制,你的驱动就能做到「一次编写,到处运行」。