3. 设备驱动模型与设备树:从混乱到有序

说实话,我刚入行那会儿,Linux驱动开发还没设备树这东西。那时候板级文件里全是硬编码的寄存器地址、中断号,换个引脚就得改代码重新编译。嗯,那叫一个痛苦。后来设备树来了,整个开发方式都变了。今天我就跟你聊聊这个转变背后的核心——设备驱动模型和设备树。

3.1 Linux设备驱动模型:bus、device、driver

先说说驱动模型。你想想看,一个嵌入式系统里有多少设备?I2C、SPI、USB、GPIO……如果每个驱动都自己管自己的资源,那代码会乱成一锅粥。Linux内核为了解决这个问题,搞了一套抽象模型。

这套模型的核心就三个概念:总线(bus)、设备(device)、驱动(driver)。说白了,总线是桥梁,设备是硬件实体,驱动是操作逻辑。它们仨怎么配合呢?

  • 总线:负责匹配设备和驱动。比如platform总线,它不关心具体硬件,只管把设备和驱动牵线搭桥。
  • 设备:描述硬件资源。比如寄存器基地址、中断号、时钟频率等。
  • 驱动:实现操作逻辑。比如怎么初始化、怎么读写、怎么处理中断。

我在项目中遇到过一个问题:一个I2C触摸屏驱动死活加载不上。查了半天,发现是设备树里i2c节点的地址写错了,导致驱动匹配不到设备。你看,模型再漂亮,数据不对也白搭。

3.2 设备树(Device Tree)基本语法

设备树是什么?它就是一个描述硬件信息的文本文件。内核启动时解析它,然后动态创建设备。这样,同一份内核镜像可以适配不同硬件,只要换设备树文件就行。

设备树的文件后缀是.dts,编译后变成.dtb。基本语法很简单,我直接给你看个例子:

/dts-v1/;

/ {
    compatible = "vendor,board";
    #address-cells = <1>;
    #size-cells = <1>;

    cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0x0>;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;
    };
};

注意几个关键点:

  • /dts-v1/; 是版本声明,必须写在第一行。
  • 根节点/ 包含所有设备。
  • compatible 属性用于匹配驱动,格式是"厂商,型号"。
  • reg 描述地址和大小,#address-cells#size-cells 决定它的格式。
我的习惯:写设备树时,先把compatible属性想清楚。它就像身份证,驱动靠它找到你。我曾经因为compatible写了个空格,导致驱动匹配失败,排查了整整一下午。

3.3 设备树节点与属性

设备树的节点就是硬件设备,属性就是设备的参数。常见的属性有:

属性名 作用 示例
compatible 驱动匹配标识 "ti,am335x-gpio"
reg 地址和大小 <0x4804c000 0x1000>
interrupts 中断号 <0 29 4>
clocks 时钟源 <&clk_uart0>
status 设备状态 "okay" 或 "disabled"

节点可以嵌套。比如一个I2C控制器下面挂多个从设备:

&i2c0 {
    status = "okay";
    clock-frequency = <100000>;

    touch@38 {
        compatible = "goodix,gt911";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <15 2>;
    };

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
    };
};

这里&i2c0是引用,表示在别处定义好的节点上追加内容。这种写法很常见,我建议你多用。

避坑指南:我曾经在reg属性里把地址写成了十进制,结果驱动读到的地址完全不对。记住,reg里的地址默认是十六进制,但如果你用<0x38>这种写法,它还是十六进制。别搞混了。

3.4 在驱动中解析设备树

设备树写好了,驱动怎么读呢?内核提供了一套API。我拿一个简单的platform驱动举例:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    struct resource *res;
    void __iomem *base;
    int irq;
    u32 val;

    // 获取寄存器地址
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

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

    // 读取自定义属性
    if (of_property_read_u32(np, "my-custom-value", &val) == 0)
        dev_info(dev, "custom value: %d\n", val);

    return 0;
}

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

static struct platform_driver my_driver = {
    .probe = my_probe,
    .driver = {
        .name = "my_device",
        .of_match_table = my_of_match,
    },
};
module_platform_driver(my_driver);

这段代码里,of_match_table是关键。它告诉内核:这个驱动支持哪些compatible的设备。当设备树里有匹配的节点时,内核就会调用probe函数。

常用的解析函数还有:

  • of_property_read_string():读字符串属性
  • of_get_named_gpio():读GPIO引脚
  • of_parse_phandle():解析引用(比如时钟、中断控制器)

重点提醒:解析设备树时,一定要检查返回值。比如of_property_read_u32返回非0,说明属性不存在。我见过有人不检查,直接拿未初始化的变量去配置硬件,结果设备工作异常。

嗯,设备树和驱动模型就这些核心内容。说白了,设备树是硬件的「身份证」,驱动模型是内核的「婚姻介绍所」。两者配合,让驱动开发变得灵活又规范。你写驱动时,先把设备树理清楚,再写代码,会顺畅很多。

下一章我们聊聊字符设备驱动,那是驱动开发的基础,也是你第一个真正动手写的驱动。