4、内核驱动框架:platform driver模型、设备树匹配机制、probe函数实现要点

好,咱们今天聊点硬核的。Sensor驱动适配,说白了就是让内核认识你的硬件。而platform driver模型,就是Linux内核里最常用的“搭桥”方式。我刚开始接触这块时,也觉得概念挺多,什么platform_device、platform_driver,还有设备树,绕来绕去的。但等你真正调过几个Sensor,就会发现,这套框架其实很清晰。

4.1 platform driver模型:为什么需要它?

你想想看,传统的PCI、USB设备,它们有标准的枚举方式,内核能自动发现。但Sensor这类设备呢?它们大多挂在I2C、SPI总线上,或者直接是SoC内部集成的。内核没法自动“发现”它们,得有人告诉内核:“嘿,这里有个加速度计,地址是0x1D。”

platform driver模型就是干这个的。它把硬件资源(比如中断号、内存地址、GPIO)和驱动代码解耦。驱动只关心怎么操作硬件,而硬件在哪里、用什么资源,由platform_device或设备树来描述。

核心思想:驱动与设备分离。驱动是“万能钥匙”,设备是“锁”,匹配上了才能干活。

我个人习惯把platform driver理解成一个“中介”。它不直接操作硬件地址,而是通过platform_get_resource()这类API去拿资源。这样做的好处是,换一个板子,只要改设备树,驱动代码几乎不用动。

4.2 设备树匹配机制:驱动怎么找到设备?

设备树(Device Tree)是现在ARM Linux的主流做法。它用.dts文件描述硬件,编译成.dtb传给内核。驱动要做的,就是声明自己“能支持哪些设备”。

匹配的核心是 of_match_table。驱动里会定义一个数组,里面放着它支持的设备兼容性字符串。比如:

static const struct of_device_id sensor_of_match[] = {
    { .compatible = "vendor,my-accelerometer" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);

设备树里对应的节点长这样:

my_sensor: sensor@1d {
    compatible = "vendor,my-accelerometer";
    reg = <0x1d>;
    interrupt-parent = <&gpio1>;
    interrupts = <5 IRQ_TYPE_EDGE_RISING>;
};

内核在启动时,会遍历所有platform_device,拿着设备树里的compatible值,去驱动的of_match_table里找。匹配上了,就调用驱动的probe函数。

我的经验:compatible字符串的命名有讲究。一般是“厂商,型号”,比如“bosch,bma250”。我曾经见过有人写成“bma250”不带厂商,结果跟别的驱动冲突了,排查了半天。嗯,这里要注意,命名一定要唯一。

匹配优先级是这样的:

匹配方式 说明 优先级
of_match_table 设备树匹配,最常用 高
id_table 传统platform ID匹配 中
name匹配 直接比较驱动名和设备名 低

说白了,现在新项目基本都用设备树匹配。id_table那种老方式,我建议你了解一下就行,除非你在维护老内核。

4.3 probe函数实现要点:驱动初始化的“主战场”

probe函数是驱动被匹配成功后的入口。所有初始化工作都在这里完成。我见过不少新手,把probe写得又长又乱,出了问题很难调试。

一个标准的probe函数,应该做这几件事:

  1. 解析设备树资源:获取中断号、寄存器地址、GPIO等。
  2. 分配并初始化数据结构:比如i2c_client、私有数据。
  3. 注册中断:如果设备有中断引脚。
  4. 初始化硬件:比如设置Sensor的采样率、量程。
  5. 注册输入设备或IIO设备:让用户空间能访问。
  6. 创建sysfs属性文件(可选):方便调试。

来看一个精简的probe示例:

static int my_sensor_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_sensor_data *data;
    struct resource *res;
    int irq, ret;

    // 1. 分配私有数据
    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    platform_set_drvdata(pdev, data);

    // 2. 获取中断资源
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    // 3. 获取I/O内存或I2C地址(这里以I2C为例)
    // 实际Sensor驱动通常用i2c_client,这里只是演示platform资源获取
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENXIO;

    // 4. 初始化硬件(伪代码)
    ret = my_sensor_hw_init(data);
    if (ret)
        return ret;

    // 5. 注册中断
    ret = devm_request_threaded_irq(dev, irq, NULL,
                    my_sensor_irq_handler,
                    IRQF_TRIGGER_RISING | IRQF_ONESHOT,
                    "my_sensor", data);
    if (ret)
        return ret;

    // 6. 注册IIO设备或输入设备
    // ...

    dev_info(dev, "my_sensor probed successfully\n");
    return 0;
}

避坑指南:我曾经在probe里直接调用msleep()做硬件延时,结果导致内核启动变慢。后来发现,有些资源在probe时还没准备好,应该用devm_系列API或者放到工作队列里。另外,probe失败时一定要清理已分配的资源,devm_系列API能帮你自动做这事,强烈推荐。

还有一点,probe函数里不要做太多耗时操作。如果硬件初始化需要几百毫秒,考虑用异步probe或者拆分成多个阶段。否则系统启动时,你的Sensor驱动可能会成为瓶颈。

4.4 知识体系核心逻辑

为了让你更直观地理解,我画了一张流程图。它展示了从设备树到驱动匹配,再到probe执行的完整路径。

设备树 (.dts) compatible = "vendor,chip" 内核解析设备树 生成 platform_device 匹配 of_match_table 遍历驱动兼容性列表 platform_driver .of_match_table probe 函数执行 1. 获取资源 (irq, mem) 2. 初始化硬件 3. 注册中断/设备 匹配失败,返回 -ENODEV

这张图把整个流程串起来了。从设备树描述硬件,到内核生成platform_device,再到驱动匹配并执行probe。你写驱动时,心里要时刻装着这条线。

我的建议:调试时,可以在probe入口加一句dev_dbg(),打印出pdev->name和匹配到的of_match_table条目。这样能快速确认驱动是否被正确匹配。我曾经靠这招,发现一个设备树里compatible写错了字母,省了大半天时间。

好了,platform driver模型的核心就这些。设备树匹配机制是敲门砖,probe函数是主战场。把这两块吃透,Sensor驱动适配你就掌握了七成功力。剩下的三成,就是具体Sensor的操作细节了。