2. 设备树基础

在 Linux 设备树驱动适配工程中,最先要建立起来的不是“怎么写节点”,而是“源码、头文件、编译器、二进制、内核解析”这条链路之间的对应关系。很多初学者把 DTS、DTSI、DTB、DTC 混为一谈,导致在排查“驱动没匹配上”“节点被覆盖”“属性读不到”时无从下手。本节先把这四者的角色讲清楚,再进入设备树语法、节点、属性、标签与引用的核心规则。

一、DTS、DTSI、DTB、DTC 的职责与关系

设备树源码通常以 .dts 和 .dtsi 两种文本形式存在。.dts 是面向某一块具体板子的顶层描述文件,.dtsi 是可被包含的公共片段,类似 C 语言里的头文件。DTC(Device Tree Compiler)是设备树编译器,它把文本源码编译成内核可解析的二进制格式 DTB(Device Tree Blob)。内核启动时由 bootloader 把 DTB 地址传给内核,内核再展开为设备节点树,供 platform driver、i2c driver 等匹配使用。

因此,四者的关系可以概括为:DTS/DTSI 是源,DTC 是工具,DTB 是产物,内核是消费者。理解这条链路,是后续所有适配工作的基础。

设备树源码到内核解析的完整链路 .dts 板级顶层描述 /include/ 引用 dtsi .dtsi SoC 公共片段 可被多板复用 DTC 设备树编译器 dtc -I dts -O dtb .dtb 二进制 Blob bootloader 加载 内核启动阶段 bootloader 传递 DTB 地址 内核解析 展开 device_node 驱动匹配 compatible 匹配 DTS/DTSI → DTC → DTB → 内核 device_node → 驱动 probe

二、设备树语法:节点、属性、标签与引用

设备树文本的语法非常简洁,核心只有两类元素:节点(node)和属性(property)。节点用大括号表示层级,属性用“键 = 值”表示。节点名通常写成 name@address 的形式,其中 @ 后面的地址用于区分同一类设备的多个实例。

/dts-v1/;
#include "soc.dtsi"

/ {
    model = "My Board V1";
    compatible = "vendor,myboard", "vendor,soc";

    uart0: serial@10010000 {
        compatible = "vendor,uart";
        reg = <0x10010000 0x1000>;
        interrupts = <10>;
        status = "okay";
    };
};

&uart0 {
    pinctrl-names = "default";
    pinctrl-0 = <&uart_pins>;
};

上面这段代码里出现了几个关键概念:uart0: 是标签(label),它给节点起了一个可被引用的别名;&uart0 是引用(reference),表示“找到标签为 uart0 的节点,并在此处追加或覆盖属性”;<&uart_pins> 是phandle 引用,用于把另一个节点作为参数传给当前属性。标签和引用是设备树实现“公共片段 + 板级差异”复用的关键机制。

三、属性类型与常见写法

属性值可以是空、字符串、32 位整数数组、字节数组或 phandle 引用。不同属性类型在驱动里读取方式不同,写错类型往往导致 of_property_read_u32() 返回失败。

属性写法 类型 典型用途 驱动读取接口
status = "okay"; 字符串 使能/禁用节点 of_device_is_available()
reg = <0x1000 0x100>; u32 数组 寄存器基址与长度 platform_get_resource()
compatible = "v,uart"; 字符串列表 驱动匹配 of_match_device()
pinctrl-0 = <&uart_pins>; phandle 引用引脚配置节点 of_parse_phandle()
local-mac-address = [00 11 22 33 44 55]; 字节数组 MAC 地址等原始数据 of_property_read_u8_array()

四、典型场景:dtsi 公共定义 + dts 板级覆盖

实际项目中,SoC 厂商会提供一份 soc.dtsi,里面定义所有外设节点、寄存器地址和默认状态。板级 board.dts 只做三件事:包含 dtsi、修改 status、追加板级属性。例如:

/* soc.dtsi */
uart0: serial@10010000 {
    compatible = "vendor,uart";
    reg = <0x10010000 0x1000>;
    status = "disabled";
};

/* board.dts */
#include "soc.dtsi"

&uart0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_pins>;
};

这种“dtsi 定义骨架、dts 覆盖差异”的模式,是设备树工程化的核心。它让同一份 SoC 描述可以服务多块板子,减少重复维护。

提示:标签名在同一设备树中必须唯一。如果两个 dtsi 都定义了同名标签,DTC 会报重复标签错误。建议在标签前加模块前缀,如 uart0、i2c1、spi2,避免冲突。

五、常见误区与复盘

  • 误区一:把 dtsi 当成“必须被编译的文件”。dtsi 本身不直接编译,它通过 #include 被 dts 包含后一起编译。
  • 误区二:认为 DTB 是文本。DTB 是二进制,调试时需要用 dtc -I dtb -O dts 反编译回文本查看。
  • 误区三:节点写了但驱动不 probe。优先检查 compatible 是否与驱动 of_match_table 一致,再检查 status 是否为 okay。
  • 误区四:属性类型写错。例如把 reg 写成字符串,驱动读取会失败,但编译不一定报错。
排障建议:遇到设备树问题时,先确认 DTB 是否真的被更新。很多“改了没生效”的情况,是因为编译产物没有替换到 boot 分区,或者 bootloader 加载了旧 DTB。可以用 fdtdump 或 dtc -I dtb -O dts 检查实际生效的 DTB 内容。

掌握 DTS、DTSI、DTB、DTC 的关系,以及节点、属性、标签、引用的基本语法,是后续编写和调试设备树驱动的前提。只有先看清“源码如何变成内核里的 device_node”,才能在驱动适配时快速定位问题所在。