1、设备树基础:设备树(DTS)的起源与作用、基本语法结构、dts/dtsi/dtb文件关系

1.1 设备树从哪来?为什么要搞这么个东西?

做嵌入式Linux开发的朋友,一定绕不开设备树。我记得刚入行那会儿,Linux内核里还全是板级文件——每个开发板一个board-xxx.c,里面密密麻麻全是硬件寄存器地址、中断号、GPIO配置。那时候改个硬件,得改内核源码,重新编译,麻烦得要命。

后来Linus Torvalds发飙了,说这代码简直是一堆垃圾。于是设备树(Device Tree)被引入,说白了就是:把硬件描述从内核代码里剥离出来,变成一个独立的数据结构。

设备树的作用,我总结成一句话:告诉内核,你的板子上有哪些硬件,它们怎么连接的。你想想看,没有设备树的时候,内核得硬编码知道“I2C控制器在0x1000地址上”,有了设备树,内核启动时解析一下DTS文件,啥都知道了。

核心思想:设备树 = 硬件拓扑的配置文件。内核不再关心具体板子,只关心设备树描述的硬件。

1.2 基本语法结构——其实没那么玄乎

第一次看DTS文件,你可能会觉得像C语言的结构体,又像JSON。其实它有自己的规则,但上手很快。我习惯把它理解成:一个树形结构的键值对集合

来看个最简单的例子:

/dts-v1/;

/ {
    model = "MyCameraBoard";
    compatible = "vendor,camera-board";

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

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

    i2c@1000 {
        compatible = "vendor,i2c";
        reg = <0x1000 0x100>;
        #address-cells = <1>;
        #size-cells = <0>;

        camera@10 {
            compatible = "vendor,camera-sensor";
            reg = <0x10>;
            clocks = <&clkc 15>;
            reset-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
        };
    };
};

嗯,这里要注意几个关键点:

  • 根节点/ 表示根,整个设备树的起点。就像文件系统的根目录。
  • 节点名cpusmemory@80000000i2c@1000。节点名后面可以跟@地址,用来区分同名的节点。
  • 属性compatibleregclocks。属性就是键值对,值可以是字符串、数字、数组、二进制数据。
  • reg属性:描述地址和大小。格式是 <地址 大小>,地址和大小分别由 #address-cells#size-cells 决定。

我的小技巧:写设备树时,先把硬件框图画出来。CPU、内存、总线、外设,一层层嵌套。然后对着框图写节点,不容易漏。

1.3 dts、dtsi、dtb——三兄弟的关系

这三个文件,我刚开始也搞混过。其实很简单:

文件类型 全称 作用 类比
.dts Device Tree Source 板级设备树源文件,描述具体开发板 C源文件 (.c)
.dtsi Device Tree Source Include 公共设备树头文件,描述SoC通用部分 C头文件 (.h)
.dtb Device Tree Blob 编译后的二进制文件,内核直接使用 可执行文件 (.o/.bin)

说白了,.dtsi 是公共部分,.dts 是板级特有部分。我在项目中遇到过,有人把SoC的时钟配置写在了.dts里,结果换了个板子,同样的SoC,还得重新写一遍。正确的做法是:SoC厂商提供soc.dtsi,板子厂商在board.dts#include它,然后只写自己板子特有的东西。

编译过程也很直接:

dtc -I dts -O dtb -o myboard.dtb myboard.dts

这个dtc工具(Device Tree Compiler)会把.dts和它include的所有.dtsi合并,生成一个.dtb二进制文件。内核启动时,bootloader把.dtb加载到内存,内核解析它,就知道硬件长什么样了。

避坑指南:我曾经因为.dtsi里定义了一个节点,.dts里又定义了一个同名节点,结果属性被覆盖了,摄像头死活不工作。后来才发现,设备树里同名节点是覆盖关系,不是合并关系。如果你要追加属性,得用&节点名的引用方式。

1.4 设备树在Camera驱动中的角色

讲到这里,你可能要问:设备树跟Camera驱动到底啥关系?

关系大了去了。一个Camera模组,通常包含:

  • 传感器本身:挂在I2C总线上,需要描述地址、时钟、复位引脚
  • MIPI接口:需要配置数据通道数、时钟频率
  • 电源管理:需要描述AVDD、DVDD、IOVDD的电压和时序
  • 控制信号:比如PWDN、RESET、STANDBY等GPIO

这些信息,全部写在设备树里。驱动代码通过of_系列API(比如of_property_read_u32of_get_named_gpio)去读取设备树里的配置。这样,换一个Camera模组,只需要改设备树,不需要改驱动代码。

我个人习惯,在写Camera驱动前,先把设备树写好。因为设备树就是硬件的“身份证”,驱动只是去读这张身份证。身份证写错了,驱动再怎么写也白搭。

一句话总结:设备树是Camera驱动的“硬件配置清单”,驱动是“按清单干活的人”。清单写清楚,活就干得漂亮。

1.5 本章小结

这一章我们聊了设备树的起源——为了解决内核代码的混乱;基本语法——树形结构、节点、属性;以及dts/dtsi/dtb三兄弟的关系。下一章,我们会深入Camera驱动中具体的设备树节点怎么写,包括时钟、GPIO、电源、MIPI配置等。

嗯,设备树这东西,刚开始觉得麻烦,用习惯了你会发现——真香。