第1章:设备树基础回顾——语法、节点与属性、reg与compatible

各位同学,咱们今天聊聊设备树的基础。说实话,我刚开始接触设备树时,也觉得这东西有点玄乎。不就是一堆文本描述硬件吗?后来踩了几个坑才明白——设备树是Linux内核与硬件之间的“翻译官”,写不好它,驱动根本跑不起来。

1.1 设备树是什么?

设备树(Device Tree),说白了就是一个描述硬件拓扑结构的数据结构。它用文本文件(.dts)描述CPU、内存、外设、中断控制器、GPIO等所有硬件资源。内核启动时解析这个文件,动态构建硬件信息,而不是把硬件信息硬编码在代码里。

我个人习惯把设备树比作“硬件的身份证”。你想想看,一个SoC可能有几十个外设,每个外设的地址、中断号、时钟频率都不一样。如果没有设备树,内核就得为每块板子单独写一份板级文件,维护成本极高。设备树解决了这个问题——同一份内核镜像,换一块板子只需要换一个.dtb文件。

核心要点:设备树是硬件描述语言,不是驱动代码。它告诉内核“我有什么硬件”,而不是“怎么驱动硬件”。

1.2 设备树语法速览

设备树的语法其实很简单,就是一棵树。根节点是“/”,下面挂各种子节点。每个节点代表一个硬件设备或总线。来看一个最基本的例子:

/dts-v1/;

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

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

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

    uart0: serial@10000000 {
        compatible = "vendor,uart";
        reg = <0x10000000 0x1000>;
        interrupts = <0 33 4>;
    };
};

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

  • 节点名称:格式是“名称@地址”,比如cpu@0、serial@10000000。地址部分通常对应设备的基地址。
  • 属性:键值对形式,值可以是字符串、整数、数组等。比如compatible、reg、interrupts都是属性。
  • 标签(label):比如uart0:,方便在其他地方引用这个节点。

1.3 节点与属性——硬件的“自我介绍”

每个节点都有一堆属性,这些属性就是硬件在向内核做自我介绍。我挑几个最常用的说说:

属性名 作用 示例
compatible 设备兼容性标识,驱动匹配的关键 "vendor,device"
reg 描述设备地址空间 <基地址 长度>
interrupts 中断号与触发方式 <0 33 4>
clocks 时钟源引用 <&clkc 15>
status 设备状态(okay/disabled) "okay"

我在项目中遇到过最坑的事:某次调试I2C设备,驱动死活不probe。查了两天,最后发现是compatible字符串写错了一个字母。内核匹配驱动时是严格字符串比较,多一个空格都不行。从那以后,我每次写compatible都会反复核对数据手册。

1.4 reg属性——地址空间的“地图”

reg属性是设备树里最重要的属性之一。它告诉内核:这个设备占用了哪段地址空间。格式是:

reg = <address1 length1 address2 length2 ...>;

address和length的位数由父节点的#address-cells和#size-cells决定。比如:

/ {
    #address-cells = <1>;  // 地址用32位表示
    #size-cells = <1>;     // 长度用32位表示

    uart@10000000 {
        reg = <0x10000000 0x1000>;  // 基址0x10000000,长度0x1000
    };
};

如果#address-cells = <2>,那地址就要写成两个32位数,比如<0x0 0x10000000>。64位地址空间常见这种写法。

避坑指南:我曾经在移植一个老驱动时,发现reg长度写错了。外设寄存器范围是0x1000,我写成了0x100。结果内核只映射了256字节,访问后面的寄存器直接触发异常。记住:reg长度一定要覆盖所有需要用到的寄存器。

1.5 compatible属性——驱动匹配的“钥匙”

compatible属性是设备树与驱动之间的桥梁。内核通过它找到对应的驱动程序。格式通常是"厂商,设备型号",比如:

compatible = "ti,omap3-uart", "ns16550";

这里有两个字符串,内核会按顺序尝试匹配。第一个是精确匹配,第二个是兼容匹配。为什么这么设计?因为有些IP核是通用的,比如16550 UART,很多厂商都在用。写"ns16550"可以让内核用通用驱动,写"ti,omap3-uart"则可以用TI的定制驱动。

驱动代码里怎么匹配?看这个:

static const struct of_device_id my_uart_of_match[] = {
    { .compatible = "ti,omap3-uart", },
    { .compatible = "ns16550", },
    { /* sentinel */ }
};

static struct platform_driver my_uart_driver = {
    .driver = {
        .name = "my_uart",
        .of_match_table = my_uart_of_match,
    },
    .probe = my_uart_probe,
};

内核解析设备树时,会遍历所有节点,找到compatible匹配的节点,然后调用对应的probe函数。说白了,compatible就是一把钥匙,设备树是锁,驱动是钥匙坯子,匹配上了才能开门。

1.6 知识体系总览

为了让大家更直观地理解设备树的核心逻辑,我画了一张图:

设备树核心知识体系 根节点 "/" CPU节点 内存节点 外设节点 compatible reg interrupts status 驱动匹配 地址映射 中断路由 设备使能 节点包含属性,属性决定硬件行为,驱动通过属性匹配工作

1.7 小结

这一章我们回顾了设备树的基础知识。说白了,设备树就是一套描述硬件的“语言”,节点是名词,属性是形容词。compatible是设备的名片,reg是设备的地址地图。掌握了这些,你就能看懂大部分设备树文件了。

嗯,这里要提醒一句:设备树语法虽然简单,但细节很多。比如#address-cells和#size-cells的继承关系、中断映射的复杂规则,这些后面章节会慢慢展开。别急,咱们一步步来。

重要提醒:设备树文件(.dts)编译成二进制(.dtb)后,大小通常只有几KB到几十KB。但一个语法错误就能让内核启动失败。我建议每次修改后都用dtc工具检查一下:dtc -I dts -O dtb -o test.dtb test.dts,养成习惯。


公众号:蓝海资料掘金营,微信deep3321