2、设备树基础语法:节点、属性、值的定义与规则

好,咱们正式开始啃设备树的语法。说实话,很多初学者一看到.dts文件里那些花括号、分号、数字和字符串混在一起,就觉得头大。我当年第一次接触时也差不多——这玩意儿到底是描述硬件还是写配置文件?

其实你把它想象成一本「硬件说明书」就行了。设备树就是用一种树形结构,把CPU、内存、外设这些硬件资源,清清楚楚地告诉内核。今天我就带你把这本说明书从头到尾翻一遍。

2.1 节点:设备树的基本单元

设备树里的一切,都是从节点开始的。每个节点代表一个硬件设备或总线。节点可以嵌套,就像文件夹套文件夹一样。

一个最简单的节点长这样:

/ {
    model = "MyBoard";
    compatible = "vendor,myboard";
};

最外层的/是根节点,代表整个板子。里面可以挂CPU、内存、串口、I2C控制器等等。每个节点都有自己的名字,格式是node-name@unit-address

节点命名规则:

  • node-name:通用名称,比如uart、i2c、gpio
  • @unit-address:可选,通常是设备基地址,比如uart@fe001000

我个人习惯把节点名写得尽量通用。比如串口就叫serial,而不是my_uart_1。为什么?因为内核匹配驱动时,优先看compatible属性,节点名只是给人看的。你写得太花哨,同事接手时反而懵。

2.2 属性:描述节点的「形容词」

节点本身只是个空壳,真正描述硬件的是属性。每个属性都有一个名字和一个值。属性名是字符串,值可以是多种类型。

常见的属性类型有:

属性类型 示例 说明
字符串 model = "MyBoard" 用双引号括起来
32位整数 reg = <0x10000000 0x1000> 尖括号括起来,空格分隔
64位整数 reg = <0x00000000 0x10000000> 用两个32位拼一个64位
字节数组 local-mac-address = [00 11 22 33 44 55] 方括号括起来,十六进制
字符串列表 compatible = "vendor,device", "generic-device" 逗号分隔多个字符串
混合类型 foo = <0x1234>, "string" 用逗号分隔不同类型

嗯,这里要注意:compatible属性是最重要的,没有之一。内核驱动就是靠它来匹配设备的。格式通常是"厂商,设备型号",比如"ti,am3359-uart"。我在项目中遇到过有人把厂商名写反了,结果驱动死活加载不上,查了两天才发现是"uart,ti"写成了"ti,uart"——顺序反了!

2.3 值的编码规则:大端还是小端?

设备树里所有整数都是大端(Big-Endian)编码。这一点坑过不少人。你想想看,x86和ARM默认都是小端,但设备树偏偏用大端。为什么?因为设备树最初来自PowerPC,PowerPC就是大端。

举个例子:

reg = <0x12345678>;

在内存里实际存的是0x12 0x34 0x56 0x78(从低地址到高地址)。如果你用小端思维去读,就会得到0x78563412,完全错了。

避坑指南:我曾经在调试一个FPGA设备时,发现读出来的寄存器地址总是对不上。查了半天,原来是设备树里写的地址是大端,而驱动里用of_property_read_u32读出来自动转了CPU字节序。但那个FPGA的寄存器本身也是大端,结果转了两次,地址就乱了。所以记住:设备树里的整数是大端,但内核API会自动帮你转换,别自己再转一次。

2.4 特殊属性:标准化的「潜规则」

除了自定义属性,设备树规范定义了一些标准属性,每个设备节点都应该遵守。我挑几个最常用的说说:

  • reg:描述设备的地址空间。格式是<地址 长度>,可以有多个。
  • #address-cells#size-cells:告诉解析器,reg里的地址和长度各占几个32位整数。比如#address-cells = <1>表示地址是32位,#address-cells = <2>表示64位。
  • interrupts:描述中断号。具体格式取决于中断控制器。
  • status:设备状态,常用值是"okay""disabled"。调试时很有用。

举个例子,一个典型的UART节点:

uart0: serial@fe001000 {
    compatible = "vendor,uart";
    reg = <0xfe001000 0x1000>;
    interrupts = <0 33 4>;
    status = "okay";
};

这里reg表示UART的寄存器基地址是0xfe001000,长度0x1000字节。interrupts的三个数字分别表示中断类型、中断号、触发方式。具体含义要看中断控制器的绑定文档。

2.5 引用与标签:让设备树更灵活

设备树支持用标签(label)来引用节点。标签写在节点名前,用冒号分隔:

uart0: serial@fe001000 {
    // ...
};

然后其他地方可以用&uart0来引用这个节点。这在覆盖或修改节点时特别有用。比如你想在板级设备树里使能某个UART:

&uart0 {
    status = "okay";
};

这样就不用把整个节点重新写一遍。我习惯把所有外设节点都加上标签,方便后续扩展。你想想看,如果芯片有6个UART,每个都要写一遍status = "okay",没有标签的话得复制粘贴6次,万一改个名字还得改6处——太容易出错了。

2.6 包含文件:复用与模块化

设备树支持#include预处理指令,跟C语言一样。通常SoC厂商会提供一个.dtsi文件,定义芯片内部的所有外设。板级.dts文件再包含它,然后只修改需要改的部分。

// 芯片级定义:soc.dtsi
#include "soc.dtsi"

/ {
    model = "MyBoard";
    compatible = "vendor,myboard";
};

// 板级定义:myboard.dts
#include "soc.dtsi"

&uart0 {
    status = "okay";
    clock-frequency = <115200>;
};

这种分层设计,说白了就是「不要重复造轮子」。芯片厂商把通用部分写好,板级开发者只需要关注自己板子特有的东西。我在做项目时,经常看到有人把整个设备树从头写到尾,结果芯片升级时所有板子都得改——这就是没用好.dtsi的后果。

2.7 知识体系总览

说了这么多,咱们用一张图把设备树语法的核心脉络理清楚:

根节点 / 子节点:cpu@0, serial@fe001000, i2c@... 属性:compatible, reg, interrupts, status, model... 值类型:字符串、32位整数、64位整数、字节数组、字符串列表、混合 特殊机制:标签(&)引用、#include包含、#address-cells/#size-cells 最终生成 .dtb 二进制文件 设备树语法核心结构:节点 → 属性 → 值 → 特殊机制 → 编译

我的小建议:刚开始学设备树,别急着背语法。先找一块现成的开发板,打开它的.dts文件,对照着原理图看。看到某个外设节点,就去查它的compatible属性,然后在内核源码里搜对应的驱动。这样来回几次,语法自然就熟了。我当年就是这么干的,比死啃文档快得多。

好了,设备树的基础语法就这些。节点、属性、值,再加上引用和包含,你已经能看懂大部分.dts文件了。下一节咱们会深入reg属性和地址映射,那才是真正考验理解能力的地方。


专注资料整理