第4章:设备驱动模型——总线-设备-驱动与设备树实战

设备驱动模型,说白了就是Linux内核为了管理硬件而设计的一套“相亲系统”。总线是媒人,设备和驱动是男女双方。媒人负责牵线,双方各自提供自己的“择偶标准”,配对成功就结婚——驱动开始工作。

我刚开始接触这个模型时,觉得它太抽象了。直到有一次在项目中调试一个USB摄像头驱动,死活挂不上,最后发现是设备ID匹配错了。嗯,从那以后我再也不敢小看这套模型了。

4.1 总线-设备-驱动模型的核心逻辑

内核里这套模型,核心就三个角色:总线(Bus)、设备(Device)、驱动(Driver)。它们之间的关系,我用一张图给你讲清楚。

核心思想:驱动不直接操作硬件,设备不直接调用驱动。总线作为中间层,负责匹配和绑定。

总线-设备-驱动模型结构图 总线(Bus) 设备(Device) 驱动(Driver) 匹配过程:match() 函数 匹配成功后,驱动 probe() 被调用,设备正式被驱动接管

你看,总线在中间,设备和驱动在两边。总线负责调用match()函数,看看设备和驱动能不能对上眼。对上了,就调用驱动的probe()函数,驱动开始干活。

4.2 platform总线——嵌入式开发的好帮手

在嵌入式世界里,很多设备不像PCI、USB那样有标准的枚举机制。比如I2C控制器、SPI控制器、GPIO按键——它们直接挂在CPU的内存总线上。这时候,platform总线就派上用场了。

我个人习惯把platform总线叫做“虚拟总线”。它专门用来管理那些“没有标准总线可挂”的设备。你想想看,一个简单的LED灯,它不需要PCI枚举,也不需要USB描述符,但它确实需要一个驱动。platform总线就是为这类设备准备的。

4.2.1 platform设备与驱动的匹配规则

匹配规则其实很简单,就三种方式:

  • 设备树匹配:通过compatible属性。这是目前最主流的方式。
  • ACPI匹配:主要用于x86平台,嵌入式用得少。
  • 名称匹配:通过name字段。老代码里常见,现在不推荐了。

我的建议:新项目一律用设备树匹配。名称匹配容易冲突,我在一个项目里就踩过坑——两个驱动用了同一个名字,结果谁先加载谁说了算,完全不可控。

4.2.2 一个简单的platform驱动示例

下面这个例子,展示了一个platform驱动的基本骨架。代码不长,但包含了核心要素。

#include <linux/module.h>
#include <linux/platform_device.h>

static int my_driver_probe(struct platform_device *pdev)
{
    pr_info("my_driver: probe success!\n");
    // 在这里做硬件初始化
    return 0;
}

static int my_driver_remove(struct platform_device *pdev)
{
    pr_info("my_driver: remove\n");
    // 在这里做清理工作
    return 0;
}

static const struct of_device_id my_of_match[] = {
    { .compatible = "mycompany,mydevice" },
    { /* 结束标记 */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static struct platform_driver my_driver = {
    .probe  = my_driver_probe,
    .remove = my_driver_remove,
    .driver = {
        .name = "my_driver",
        .of_match_table = my_of_match,
    },
};

module_platform_driver(my_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");

这段代码里,of_match_table指向设备树匹配表。当内核发现设备树里有一个compatible = "mycompany,mydevice"的节点时,就会调用这个驱动的probe()函数。

注意:MODULE_DEVICE_TABLE(of, my_of_match)这行不能少。少了它,内核在编译模块时就无法生成设备树别名,导致模块无法自动加载。我曾经因为这个疏忽,浪费了半天时间排查。

4.3 设备树(Device Tree)——硬件描述的艺术

设备树,说白了就是一份“硬件说明书”。它告诉内核:CPU是什么、内存多大、外设挂在哪里、中断号是多少。没有设备树之前,这些信息都硬编码在C代码里,换个板子就得重新编译内核。

我记得第一次接触设备树时,觉得它就是个配置文件。后来发现,它远比配置文件复杂——它有语法、有绑定规则、有覆盖机制。嗯,这里要注意,设备树不是万能药,但它确实解决了硬件描述与内核代码解耦的大问题。

4.3.1 设备树的基本语法

一个简单的设备树节点长这样:

/dts-v1/;

/ {
    model = "MyBoard";
    compatible = "mycompany,myboard";

    chosen {
        bootargs = "console=ttyS0,115200";
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  // 256MB内存
    };

    soc {
        uart0: serial@10000000 {
            compatible = "mycompany,uart";
            reg = <0x10000000 0x1000>;
            interrupts = <0 5 4>;
            clock-frequency = <24000000>;
        };
    };
};

这里的关键字段:

字段 含义 说明
compatible 兼容性字符串 驱动匹配的核心依据,格式为"厂商,设备"
reg 寄存器地址和大小 格式为<地址 长度>,支持多段
interrupts 中断描述 格式依赖中断控制器,通常为<中断类型 中断号 触发方式>
status 设备状态 "okay"表示启用,"disabled"表示禁用

4.3.2 设备树在驱动中的使用

驱动里怎么读取设备树信息?看这个例子:

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    u32 val;
    int ret;

    // 读取reg信息
    struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENXIO;

    // 读取中断号
    int irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    // 读取自定义属性
    ret = of_property_read_u32(np, "clock-frequency", &val);
    if (ret)
        dev_warn(dev, "missing clock-frequency\n");

    return 0;
}

你看,platform_get_resource()和platform_get_irq()是platform总线提供的API,它们自动从设备树里解析reg和interrupts。自定义属性则用of_property_read_*系列函数。

小技巧:调试设备树时,可以在内核启动参数里加of_devtree或者通过/sys/firmware/devicetree/base查看实际解析结果。我经常用dtc -I fs -O dts /sys/firmware/devicetree/base把当前设备树反编译出来看。

4.4 sysfs与udev——用户空间与内核的桥梁

sysfs是一个虚拟文件系统,挂载在/sys目录下。它把内核里的设备、驱动、总线信息以文件形式暴露给用户空间。udev则是一个用户空间守护进程,它监听sysfs的变化,自动创建设备节点、加载固件、执行规则。

我刚开始用Linux时,觉得/sys目录下的东西很神秘。后来理解了,它就是内核的“窗户”——你想看什么,打开对应的文件就行。

4.4.1 sysfs的目录结构

sysfs的核心目录:

  • /sys/bus/:所有总线,每个总线下面有devices/和drivers/子目录。
  • /sys/devices/:所有设备的真实层级结构。
  • /sys/class/:按功能分类的设备视图,比如net/、input/、tty/。
  • /sys/block/:块设备。

举个例子,你想看platform总线下的设备和驱动:

# 查看platform总线下的设备
ls /sys/bus/platform/devices/

# 查看platform总线下的驱动
ls /sys/bus/platform/drivers/

# 查看某个设备的具体属性
cat /sys/bus/platform/devices/serial@10000000/of_node/compatible

4.4.2 udev规则实战

udev规则文件通常放在/etc/udev/rules.d/目录下。规则格式是“键值对”,匹配条件满足后就执行动作。

我曾经遇到一个问题:一个USB转串口设备,每次插入后设备名都不一样,导致脚本无法正常工作。解决方案就是写一条udev规则,给它固定一个符号链接。

# /etc/udev/rules.d/99-usb-serial.rules
# 匹配USB转串口设备,创建固定符号链接
ACTION=="add", SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="my_usb_serial"

这条规则的意思是:当系统添加一个tty子系统设备时,如果它的vendor ID是0403(FTDI),product ID是6001,就创建一个/dev/my_usb_serial符号链接指向它。

避坑指南:udev规则匹配时,ATTRS会遍历设备的父节点,性能开销较大。能用ATTR(只匹配当前设备)就别用ATTRS。我曾经在一个项目里写了20多条ATTRS规则,结果设备插入后要等好几秒才出现节点——后来改成ATTR,瞬间就好了。

4.4.3 sysfs与驱动的交互

驱动也可以主动创建sysfs属性文件,让用户空间直接读写。比如一个LED驱动,可以暴露一个brightness文件:

static ssize_t brightness_show(struct device *dev,
                               struct device_attribute *attr, char *buf)
{
    int val = read_brightness(dev);
    return sprintf(buf, "%d\n", val);
}

static ssize_t brightness_store(struct device *dev,
                                struct device_attribute *attr,
                                const char *buf, size_t count)
{
    unsigned long val;
    if (kstrtoul(buf, 10, &val))
        return -EINVAL;
    set_brightness(dev, val);
    return count;
}

static DEVICE_ATTR_RW(brightness);

static int my_probe(struct platform_device *pdev)
{
    // ... 初始化代码 ...
    device_create_file(&pdev->dev, &dev_attr_brightness);
    return 0;
}

这样,用户空间就可以通过echo 100 > /sys/devices/.../brightness来控制LED亮度了。这种交互方式,说白了就是“内核暴露接口,用户空间决定策略”。

4.5 本章小结

总线-设备-驱动模型是Linux内核设备管理的基石。platform总线解决了嵌入式设备“无标准总线可挂”的问题。设备树让硬件描述与内核代码解耦,换板子不再需要重新编译内核。sysfs和udev则打通了内核与用户空间的通道,让设备管理变得灵活可控。

我个人觉得,理解这套模型的关键在于“分层”和“解耦”这两个词。每一层只关心自己的事,层与层之间通过标准接口通信。你想想看,如果没有这套模型,每个驱动都要自己处理硬件枚举、资源分配、用户空间交互——那代码得乱成什么样?

嗯,这一章的内容就到这里。记住,多动手写代码、多查看/sys和/proc下的文件,比看十遍理论都有用。