第一章:车规级驱动模型总览

各位同学,咱们今天聊聊Linux内核里的设备驱动模型。说实话,我刚入行那会儿,也被kobject、kset这些东西搞得晕头转向。后来做了几个车规项目,才慢慢摸到门道。

你想想看,一个车载系统里,可能有几十个外设——摄像头、雷达、CAN控制器、以太网交换机……每个设备都得有个驱动。如果没有一套统一的管理机制,那代码得乱成什么样?

内核的驱动模型,说白了就是干这个的。它用一套抽象框架,把设备、驱动、总线这三者串起来。我习惯把它理解成「相亲平台」——设备是姑娘,驱动是小伙,总线就是那个媒人。

1.1 kobject / kset / sysfs:内核里的对象管理

先说说kobject。这是内核里最基础的对象结构。你看代码:

struct kobject {
    const char      *name;
    struct list_head    entry;
    struct kobject      *parent;
    struct kset     *kset;
    struct kobj_type    *ktype;
    struct kernfs_node  *sd;
    struct kref     kref;
    unsigned int        state_initialized:1;
    unsigned int        state_in_sysfs:1;
    unsigned int        state_add_uevent_sent:1;
    unsigned int        state_remove_uevent_sent:1;
    unsigned int        uevent_suppress:1;
};

每个kobject都对应sysfs里的一个目录。我做过一个项目,调试时发现某个设备节点没出现,查了半天,原来是kobject的parent没设对。嗯,这种坑踩过一次就记住了。

kset呢?它是一组kobject的集合。你可以把它理解成一个「群组」。比如所有块设备驱动,可以放在同一个kset下。sysfs里看到的/sys/bus/、/sys/class/,背后都是kset在支撑。

sysfs是内核暴露给用户空间的窗口。你cat /sys/class/xxx/uevent,就能看到设备的热插拔事件。我个人习惯在调试驱动时,先看看sysfs下有没有对应的目录——如果目录都没生成,那probe肯定没走通。

核心要点:kobject是内核对象的最小单元,kset管理一组kobject,sysfs是它们的用户空间映射。三者配合,构成了内核的设备管理骨架。

1.2 驱动与设备的匹配机制

设备来了,驱动怎么找到它?内核有一套匹配机制。最常见的是基于设备树(Device Tree)的匹配。

设备树里描述硬件:

i2c@ff160000 {
    compatible = "snps,designware-i2c";
    reg = <0xff160000 0x1000>;
    interrupts = <0 20 4>;
    clocks = <&i2c_clk>;
    #address-cells = <1>;
    #size-cells = <0>;

    temperature-sensor@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
    };
};

驱动里声明:

static const struct of_device_id tmp102_of_match[] = {
    { .compatible = "ti,tmp102" },
    { }
};
MODULE_DEVICE_TABLE(of, tmp102_of_match);

匹配时,内核会遍历设备树节点,拿compatible属性和驱动里的of_device_id表比对。一旦匹配上,就调用驱动的probe函数。

除了设备树,还有ACPI匹配、I2C设备ID表匹配、SPI设备ID表匹配等等。车规场景下,设备树用得最多。我记得有一次,客户说某个传感器驱动加载不上,我一看设备树,compatible写成了"ti,tmp102a"——少了个字母。这种低级错误,排查起来最费时间。

避坑指南:我曾经在调试时发现驱动匹配不上,查了两天才发现是设备树里reg属性写错了。I2C地址是7位还是8位?设备树里用的是7位地址,左移一位后的值。这个细节,新手特别容易搞混。

1.3 probe流程详解

匹配成功后,内核会调用驱动的probe函数。这是驱动初始化的核心入口。咱们看看典型的probe流程:

static int tmp102_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct tmp102 *tmp102;
    int ret;

    // 1. 分配私有数据结构
    tmp102 = devm_kzalloc(&client->dev, sizeof(*tmp102), GFP_KERNEL);
    if (!tmp102)
        return -ENOMEM;

    // 2. 初始化硬件
    ret = tmp102_init_client(client);
    if (ret < 0)
        return ret;

    // 3. 注册中断
    ret = devm_request_threaded_irq(&client->dev, client->irq,
                                    NULL, tmp102_interrupt,
                                    IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
                                    "tmp102", tmp102);
    if (ret < 0)
        return ret;

    // 4. 注册到内核框架(如hwmon、input等)
    tmp102->hwmon_dev = devm_hwmon_device_register_with_info(
                            &client->dev, client->name, tmp102,
                            &tmp102_chip_info, NULL);

    // 5. 保存私有数据
    i2c_set_clientdata(client, tmp102);

    return 0;
}

probe里一般做这几件事:

  • 分配资源——内存、GPIO、中断、时钟等
  • 初始化硬件——写寄存器、复位、校准
  • 注册子系统——比如hwmon、input、net等
  • 保存上下文——把私有数据挂到设备上

我建议你在probe函数开头加一句dev_info,打印设备信息。这样调试时,看内核日志就知道probe有没有走到。我曾经遇到一个情况,probe返回了0,但设备就是不工作——后来发现是中断号没配对,probe虽然成功了,但中断触发不了。

注意:probe函数里不要做耗时操作。车规系统对启动时间有严格要求,有些ECU要求在几百毫秒内完成所有驱动初始化。如果probe里做了msleep(100),那整个启动时序就崩了。我见过一个项目,就因为probe里加了1秒延时,导致系统无法通过启动时间测试。

probe失败怎么办?内核会返回错误码,驱动不会绑定到设备上。常见的失败原因有:

错误码 含义 常见场景
-ENODEV 设备不存在 I2C地址不对,设备没上电
-ENOMEM 内存不足 分配DMA缓冲区失败
-EINVAL 参数无效 设备树属性解析错误
-EPROBE_DEFER 延迟probe 依赖的资源还没准备好

说到-EPROBE_DEFER,这个很有意思。车规系统里,设备初始化有先后顺序。比如某个传感器依赖I2C控制器,但I2C控制器驱动还没加载。这时候传感器驱动probe时发现找不到父设备,就返回-EPROBE_DEFER。内核会把该驱动放到延迟队列,等依赖设备就绪后再重新尝试probe。

我做过一个项目,有7个设备互相依赖,probe顺序乱成一团。最后靠-EPROBE_DEFER机制,内核自动排好了顺序。你想想看,要是没有这个机制,我们得手动写一堆初始化顺序表,那得多痛苦。

总结一下:设备驱动模型是内核的基石。kobject/kset/sysfs提供了对象管理框架,匹配机制把设备和驱动牵上线,probe函数则是驱动初始化的主战场。搞懂这三块,车规级驱动开发就算入门了。

下一章,咱们深入聊聊设备树——车规场景下最常用的硬件描述语言。到时候我会分享一些实际项目中的设备树调试技巧,保证让你少走弯路。