第一章:设备树入门——什么是设备树?为什么需要设备树?设备树在Linux内核中的角色

大家好,我是老李。做嵌入式Linux驱动开发有些年头了。今天咱们聊聊设备树。

说实话,我刚入行那会儿,还没有设备树这东西。那时候写驱动,板子信息都是硬编码在C文件里的。每次换一个板子,就得改一堆代码,重新编译内核。那叫一个痛苦。

后来设备树出现了。我第一次接触它时,心里想:这不就是个配置文件吗?有什么了不起的?

嗯,后来我发现,我错了。它远不止是个配置文件那么简单。

1.1 什么是设备树?

设备树,英文叫Device Tree,简称DT。它是一种描述硬件资源的数据结构。

说白了,它就是一张硬件清单。告诉内核:你的CPU是什么?内存多大?I2C控制器挂在哪里?GPIO引脚怎么分配的?

设备树的源文件通常以.dts结尾。编译后生成.dtb文件,这就是内核真正读取的二进制格式。

核心概念:设备树将硬件描述从内核代码中剥离出来,变成独立的数据文件。内核启动时解析这个文件,就知道自己跑在什么硬件上了。

来看一个最简单的例子:

/dts-v1/;

/ {
    compatible = "acme,coyote";
    
    #address-cells = <1>;
    #size-cells = <1>;

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

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

这段代码描述了一个叫"coyote"的板子,内存从0x80000000开始,大小512MB。启动参数也写好了。

你可能会问:就这么简单?对,就是这么简单。但它的威力在后面。

1.2 为什么需要设备树?

这个问题,我当年也问过自己。为什么非要搞个设备树出来?

原因有三个:

  1. 解耦硬件描述和驱动代码——以前改个引脚配置就得重新编译内核,现在改个dts文件就行。
  2. 支持多平台——同一个内核镜像,换不同的dtb文件,就能跑在不同的板子上。
  3. 社区标准化——ARM架构之前乱得很,每家厂商都有自己的板级文件。设备树统一了描述方式。

我记得有一次,客户临时换了一个Flash芯片。引脚完全不一样。要是以前,我得改驱动代码,重新编译,测试,至少折腾两天。有了设备树,我只改了dts文件里的引脚配置,半小时搞定。

我的经验:设备树最大的价值不是技术本身,而是它改变了开发流程。硬件工程师改完原理图,直接更新dts文件。软件工程师拿到新dts,重新编译dtb,就能验证。两边不用互相等。

1.3 设备树在Linux内核中的角色

设备树在内核里扮演什么角色?我打个比方你就明白了。

想象一下,内核是一个操作系统,设备树就是它的"硬件说明书"。内核启动时,先读说明书,知道硬件长什么样,然后按图索骥,加载对应的驱动。

具体流程是这样的:

  • Bootloader(比如U-Boot)加载内核镜像和dtb文件到内存
  • 内核启动时,解析dtb,构建设备树结构
  • 驱动通过OF(Open Firmware)API查询设备树,获取硬件信息
  • 驱动根据这些信息初始化硬件

你看,设备树成了内核和硬件之间的桥梁。没有它,内核就不知道该怎么跟硬件打交道。

注意:设备树不是万能的。它只描述硬件"有什么",不描述硬件"怎么用"。怎么用,那是驱动代码的事。我见过不少新手把设备树当成驱动来写,结果越写越乱。

举个例子,设备树可以告诉你:这里有一个UART控制器,地址在0x1C090000,中断号是37。但它不会告诉你,这个UART怎么初始化,怎么收发数据。那些逻辑,都在驱动代码里。

1.4 设备树的基本语法

设备树的语法其实很简单。核心就几个概念:

概念 说明 示例
节点 用花括号包裹的硬件单元 uart0: serial@1c090000 { ... };
属性 节点的特征描述 compatible = "ns16550";
属性的具体内容 reg = <0x1c090000 0x1000>;

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

&i2c0 {
    status = "okay";
    
    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
    };
    
    rtc@51 {
        compatible = "ds1307";
        reg = <0x51>;
    };
};

这里&i2c0是引用之前定义好的I2C控制器节点。我们在它下面添加了两个子节点:一个EEPROM,一个RTC。

避坑指南:我曾经犯过一个低级错误——忘了写status = "okay";。结果设备树解析没问题,但驱动就是加载不了。查了两天才发现,默认status是"disabled"。嗯,从那以后,我每次写设备树都会检查status属性。

1.5 设备树与驱动的绑定

设备树怎么跟驱动关联起来?靠的是compatible属性。

驱动里会声明自己支持哪些设备:

static const struct of_device_id my_driver_of_match[] = {
    { .compatible = "acme,my-device" },
    { /* sentinel */ }
};

MODULE_DEVICE_TABLE(of, my_driver_of_match);

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

内核启动时,遍历设备树中的所有节点。如果某个节点的compatible属性跟驱动声明的匹配,内核就调用这个驱动的probe函数。

说白了,这就是一个"对暗号"的过程。设备树说"我是acme,my-device",驱动说"我支持acme,my-device",好,你们配对成功。

1.6 小结

设备树不是什么高深的技术。它就是一套描述硬件的规则。但它的出现,让嵌入式Linux开发变得灵活了很多。

我个人觉得,学习设备树最好的方法就是动手。找一个现成的开发板,看看它的dts文件,改几个参数,编译,烧录,看看效果。比看十遍文档都管用。

下一章,我们会深入设备树的语法细节,包括节点、属性、引用、覆盖等高级用法。到时候我会分享一些我在项目中踩过的坑,保证让你少走弯路。

今天就到这里。有问题欢迎交流。