4. 设备树基础:设备树语法、节点与属性、compatible匹配机制

好,咱们进入设备树这个环节。说实话,我刚接触Zephyr那会儿,最头疼的就是设备树。它不像普通代码那样直观,但你又绕不开它。我习惯把它理解成「硬件的户口本」——你的板子上有什么外设、挂在哪条总线上、地址是多少,全得写清楚。

4.1 设备树到底是什么?

设备树(Device Tree)是一种描述硬件的数据结构。它用树形结构把CPU、内存、外设这些硬件资源组织起来。Zephyr用它来替代传统的硬编码头文件。

你想想看,以前换一个传感器,得改好几处代码。现在呢?改一下设备树文件就行。驱动代码不用动。这就是设备树的价值所在。

核心思想:硬件描述与驱动代码分离。驱动只关心「怎么操作」,设备树告诉驱动「操作谁」。

4.2 设备树语法入门

设备树的语法其实不复杂。它用的是文本格式,扩展名通常是.dts.dtsi。我刚开始看的时候觉得像JSON,后来发现它更像C语言的结构体定义。

一个最基本的节点长这样:

/dts-v1/;

/ {
    model = "My Custom Board";
    compatible = "mycompany,myboard";

    chosen {
        zephyr,console = &uart0;
    };

    soc {
        uart0: uart@40001000 {
            compatible = "mycompany,uart";
            reg = <0x40001000 0x1000>;
            interrupts = <10 0>;
            status = "okay";
        };
    };
};

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

  • 根节点:用/表示,整棵树的起点
  • 节点名称:比如uart@40001000,@后面是地址
  • 标签:比如uart0:,方便其他地方引用
  • 属性compatibleregstatus这些都是属性

我的小技巧:写设备树时,先把芯片厂商提供的.dtsi文件include进来。你只需要关注自己板子特有的部分。别从头写,那是给自己找麻烦。

4.3 节点与属性详解

节点是设备树的基本单元。每个节点代表一个硬件设备或总线。属性就是描述这个设备的各种参数。

4.3.1 常用属性

属性名 含义 示例
compatible 设备兼容性标识 "mycompany,mydevice"
reg 寄存器地址和大小 <0x40001000 0x1000>
interrupts 中断号与触发方式 <10 0>
status 设备状态 "okay" 或 "disabled"
label 设备名称(已逐渐弃用) "UART_0"

我在项目中遇到过一个问题:某个外设死活不工作,查了半天发现是status写成了"ok"而不是"okay"。Zephyr只认"okay",少一个字母都不行。这种坑,踩过一次就记住了。

4.3.2 节点引用与覆盖

设备树支持引用和覆盖。这个功能非常实用。比如芯片厂商的.dtsi文件里把某个UART的status设成了"disabled",你可以在自己的板级文件里把它改成"okay"

&uart0 {
    status = "okay";
    current-speed = <115200>;
};

&符号引用节点,然后在大括号里覆盖属性。这样就不用把整个节点重新写一遍了。

4.4 compatible匹配机制

这是设备树最核心的机制。说白了,就是驱动和设备之间「对暗号」的过程。

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

#define DT_DRV_COMPAT mycompany_uart

static const struct device *uart_mycompany_init(const struct device *dev)
{
    // 初始化代码
    return dev;
}

#define UART_MYCOMPANY_INIT(n) \
    DEVICE_DT_INST_DEFINE(n, uart_mycompany_init, NULL, NULL, NULL, \
                          POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \
                          NULL);

DT_INST_FOREACH_STATUS_OKAY(UART_MYCOMPANY_INIT)

设备树里写:

uart0: uart@40001000 {
    compatible = "mycompany,uart";
    // ...
};

Zephyr启动时,会遍历所有状态为"okay"的设备节点。对每个节点,它去驱动列表里找compatible字符串匹配的驱动。匹配上了,就调用驱动的初始化函数。

匹配规则:Zephyr把设备树中的compatible字符串里的逗号替换成下划线,然后和DT_DRV_COMPAT宏定义的值做比较。比如"mycompany,uart"变成mycompany_uart

4.5 避坑指南

我曾经踩过的坑:

  • compatible字符串大小写敏感:设备树里写"MyCompany,UART",驱动里写mycompany_uart,匹配不上。全小写是约定俗成的规矩。
  • reg属性地址对齐:有些MCU要求外设基地址必须按某种边界对齐。写错了编译不报错,但运行就挂。
  • 中断号不要搞混:ARM Cortex-M的中断号和芯片手册里的IRQ号可能差16(因为前16个是系统异常)。我有一回就是没注意这个,中断一直不触发。

4.6 实战:写一个简单的设备树节点

假设我们有一颗LED接在GPIO引脚上。设备树可以这样描述:

/ {
    leds {
        compatible = "gpio-leds";
        led0: led_0 {
            gpios = &gpio0 13 GPIO_ACTIVE_LOW;
            label = "User LED";
        };
    };
};

然后在驱动里:

#define DT_DRV_COMPAT gpio_leds

// 获取LED对应的GPIO引脚
#define LED0_NODE DT_NODELABEL(led0)
#define LED0_GPIO_SPEC DT_GPIO_SPEC(LED0_NODE, gpios)

// 初始化时配置GPIO
gpio_pin_configure(DEVICE_DT_GET(LED0_GPIO_SPEC.port),
                   LED0_GPIO_SPEC.pin,
                   GPIO_OUTPUT | LED0_GPIO_SPEC.dt_flags);

你看,驱动里完全没出现具体的引脚号。换一块板子,只要改设备树,驱动代码不用动。这就是设备树的魅力。

我的建议:刚开始学设备树,别急着写复杂的。先拿一个GPIO LED练手。把节点写对,编译通过,灯能亮。然后再去碰I2C、SPI这些复杂外设。一步一步来,稳得很。

好了,设备树的基础就这些。下一节咱们聊聊怎么把设备树和驱动代码真正串起来,写一个完整的自定义驱动。到时候你会看到,设备树只是第一步,真正的魔法在驱动框架里。