第三章:Zephyr设备树文件结构

设备树这玩意儿,刚接触Zephyr的时候确实容易懵。我刚开始也搞不清楚,为什么一个简单的LED闪烁要牵扯这么多文件?后来做项目做多了,才慢慢摸清门道。说白了,设备树就是一套描述硬件的“说明书”,告诉系统你的板子上有什么外设、接在哪个引脚上、怎么配置。

今天咱们就聊聊Zephyr设备树的文件结构。嗯,这部分很重要,你想想看,搞不清文件结构,后面写驱动、调外设都会很痛苦。

3.1 设备树文件的“四层结构”

Zephyr的设备树文件,我习惯把它分成四个层次。就像盖房子,从地基到装修,一层层往上搭。

层次 文件后缀 作用范围 谁在维护
芯片级 .dtsi 整个芯片系列 芯片厂商
默认配置 .dtsi 某系列开发板 Zephyr社区
板级 .dts 具体某块板子 板卡厂商
overlay .overlay 用户自定义

这四层文件通过 #include 机制层层叠加。最终编译时,它们会合并成一个完整的设备树。我在项目中遇到过好几次,明明改了overlay文件,但编译没生效——后来发现是文件路径写错了。

3.2 芯片级文件(.dtsi)—— 地基

芯片级文件,通常以芯片型号命名,比如 stm32f4.dtsinrf52840.dtsi。它定义了芯片内部的所有资源:CPU核心、中断控制器、GPIO控制器、SPI/I2C/UART外设等等。

举个例子,这是STM32F4系列芯片级文件的一部分:

/ {
    soc {
        spi1: spi@40013000 {
            compatible = "st,stm32-spi";
            reg = <0x40013000 0x400>;
            interrupts = <35 5>;
            status = "disabled";
        };
    };
};

注意看,这里 status = "disabled"。为什么默认是禁用的?因为芯片厂商不知道你会用哪个外设,干脆全部默认关闭,等你需要时再打开。我个人习惯是,在板级文件里按需使能,而不是一股脑全开——省电又省资源。

小提示:芯片级文件一般不要去改。它是芯片厂商提供的“硬件说明书”,改错了整个芯片的外设映射都会乱套。我曾经有个同事,为了省事直接在芯片级文件里改引脚配置,结果换了个板子就全废了。

3.3 默认配置(.dtsi)—— 标准户型

默认配置文件,通常以开发板系列命名,比如 stm32f4_disco.dtsi。它定义了这个系列开发板的通用配置:板载LED、按键、外部晶振、调试接口等。

看这个例子:

/ {
    leds {
        compatible = "gpio-leds";
        led0: led_0 {
            gpios = <&gpioc 13 GPIO_ACTIVE_LOW>;
            label = "User LD1";
        };
    };
};

这里定义了板载LED,引脚是PC13,低电平有效。你想想看,如果每块板子都要重新写LED配置,那得多麻烦?默认配置文件就是帮你省掉这些重复劳动。

不过要注意,默认配置文件和芯片级文件都是 .dtsi 后缀。怎么区分?看路径:芯片级文件在 dts/arm/ 下,默认配置在 boards/ 目录里。

3.4 板级文件(.dts)—— 精装修

板级文件,以具体板卡型号命名,比如 stm32f4_disco.dts。它通过 #include 引入芯片级和默认配置,然后做三件事:

  • 使能外设:把 statusdisabled 改成 okay
  • 配置引脚:指定具体用哪个GPIO、哪个复用功能
  • 调整参数:比如修改SPI速率、UART波特率

来看一个完整的板级文件:

#include <stm32f4.dtsi>
#include <stm32f4_disco.dtsi>

/ {
    model = "STMicroelectronics STM32F4 Discovery";
    compatible = "st,stm32f4_disco";
};

&spi1 {
    status = "okay";
    cs-gpios = <&gpiob 6 GPIO_ACTIVE_LOW>;
};

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

注意这里的 &spi1 语法。这是设备树的“引用”写法,相当于说“我要修改之前定义的spi1节点”。我刚开始总写成 &spi1,少了个分号编译报错,找了半天才发现。

避坑指南:板级文件里如果忘了 #include 芯片级文件,编译会报“未定义节点”的错误。我曾经在移植一个新板子时,漏掉了芯片级文件的引用,结果折腾了一下午才发现。

3.5 Overlay文件—— 软装改造

Overlay文件是Zephyr最灵活的设计之一。它允许你在不修改原始设备树文件的前提下,覆盖或添加配置。后缀是 .overlay

什么时候用overlay?我总结了几种场景:

  • 外接模块:比如给开发板外接一个OLED屏幕
  • 引脚重映射:板子默认的I2C引脚被占用了,换一组
  • 调试配置:临时打开某个外设的调试模式

举个例子,假设你想给STM32F4 Discovery外接一个I2C温湿度传感器:

/ {
    i2c_sensor: sht30@44 {
        compatible = "sensirion,sht3xd";
        reg = <0x44>;
        label = "SHT30";
    };
};

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

把这个文件保存为 sht30.overlay,编译时加上 -DSHIELD=sht30 或者直接放在 app/boards/ 目录下,系统就会自动加载。

Overlay文件的优先级最高。如果板级文件里某个外设是 disabled,你在overlay里改成 okay,最终生效的是overlay的配置。嗯,这里要注意,overlay不是万能的,它不能删除节点,只能添加或修改。

核心要点:设备树的四层结构,从芯片级到overlay,优先级逐层升高。越上层的文件,越能覆盖下层的配置。这个设计理念,说白了就是“约定大于配置”——芯片厂商定好基础,板卡厂商做适配,用户做定制,各司其职。

3.6 文件加载顺序与编译机制

Zephyr在编译时,会按照特定顺序加载这些文件。我画了个简化的流程:

  1. 先加载芯片级 .dtsi(基础硬件定义)
  2. 再加载默认配置 .dtsi(板级通用配置)
  3. 然后加载板级 .dts(具体板卡配置)
  4. 最后加载overlay文件(用户自定义)

每一步都是“增量修改”。如果同一个属性在多个文件中出现,后加载的会覆盖先加载的。这就是为什么overlay能“篡改”板级配置的原因。

我记得有一次调试一个SPI设备,怎么配置都不对。后来用 west build -t guiconfig 查看最终生成的设备树,才发现是overlay文件和板级文件里定义了同一个引脚,overlay覆盖了板级的配置。从那以后,我每次改设备树都会先检查最终生成的 build/zephyr/zephyr.dts 文件。

调试技巧:编译完成后,在build目录下找到 zephyr.dts 文件,这就是最终合并后的设备树。如果外设不工作,先看这个文件里的配置对不对。我90%的设备树问题,都是通过看这个文件找到原因的。

好了,设备树的文件结构就聊到这儿。下一章咱们会深入设备树的语法和属性,到时候你会看到更多实际案例。记住,搞懂这四层结构,后面写驱动、调外设就会顺手很多。