Skip to content

第 6 章 DTS:把机器的话译成人话

有一类问题,表面上看是语法问题,实际上是设计哲学问题。我们在这一章要处理的正是这样一个问题。在此之前,你可能觉得内核配置就是一堆宏定义和寄存器地址的堆砌——只要数字对上了,机器就该跑起来。但当你真正深入到底层,试图描述一个复杂的 SoC(片上系统)时,你会发现「硬编码」这条路很快就走不通了。

硬件描述需要一种结构,一种既能被人类阅读(以便维护),又能被机器精确解析(以便启动)的结构。这就是 Devicetree 存在的意义。而这一章,我们要剥开它的外衣,直接看它的源码形态——DTS (Devicetree Source)。别被它那类似 C 语言的长相骗了,它的脾气和 C 语言完全不同。如果你用写 C 代码的直觉去写 DTS,你会踩到意想不到的坑。这一章的任务就是帮你建立起对 DTS 语法的直觉,从文件结构到每一个符号的精确含义。


6.1 编译指令与文件结构:拼图的规则

在深入细节之前,我们先解决一个最现实的问题:复用。没有任何一个理智的工程师会为每一款开发板都从头写一份全新的设备树。现实情况是,一块 SoC(比如 STM32 或 i.MX)会被用在几十种不同的板子上,它们共享 CPU 核心架构,只在 peripherals(外设)和引脚复用上有细微差别。这就引出了 DTS 的第一个核心机制:文件包含

你可以把 .dtsi 文件理解为 C 语言的 .h 头文件——它通常包含通用的 SoC 定义,不被直接编译,而是被具体的板级描述文件引用。但有一点不同:.dtsi 里装的往往不是宏定义,而是大段的节点结构。假设你在写一块名为 myboard 的板子,它基于 vender-chip。你的 myboard.dts 可能只有几行,剩下的全部「继承」自通用定义。

引用的语法非常直观:/include/ "vender-chip.dtsi"。这里有一个细微但重要的点:/include/ 是编译器指令不是节点。它没有分号结尾(虽然某些旧版本编译器可能容忍分号,但规范里没有)。这行代码的作用是把 vender-chip.dtsi 的内容原封不动地「粘贴」到当前位置。被包含的文件(.dtsi)当然还可以继续包含其他文件,形成一棵包含树。

千万别漏掉 /include/ 前面的斜杠。如果你写成了 include "...",编译器 dtc 会把它当成一个普通的节点名处理,然后报一堆莫名其妙的语法错。这一点真的坑了我半天,看日志看了半天才发现是少了个斜杠。


6.2 标签:给节点系上名字牌

现在文件已经包含进来摆在你面前的是成百上千个节点。如果你想修改其中一个节点(比如把某个 GPIO 的默认电平拉高),难道要复制整段节点定义只是为了改一个属性?当然不。这就是 Label(标签) 存在的理由。

你可以把标签想象成给节点贴上的一个「便利贴」。**便利贴(标签)**只存在于源代码里,是给人看的,也是给编译器用的。节点本身最终会被编译进 DTB(二进制)。一旦编译完成,便利贴就被扔掉了,但在编译阶段它是我们引用节点的捷径。

定义标签的语法是在标识符后面加个冒号:cpu0: cpu@0 { ... };。这里 cpu0 就是标签。如果你想引用这个节点,不需要写那一长串路径 &{/cpus/cpu@0},只需要用 &cpu0。这里的 & 符号是关键,它告诉编译器:「嘿,别把这当成字符串,这后面跟着的是个引用,去把那个节点的 phandle 或者路径找出来填在这里。」

标签的命名规则很严格。长度只能是 1 到 31 个字符,只能由数字(0-9)、大小写字母(a-z, A-Z)和下划线(_)组成,绝对不能以数字开头。这最后一条是重灾区,如果你习惯性地给某个节点贴个标签叫 2core,编译器会直接甩脸子报错。


6.3 节点与属性:构建树的砖块

好了,现在我们有了文件有了标签,接下来就是构建树本身了。DTS 的世界里只有两种东西:节点属性

节点是一切的基础。它的标准定义格式长这样:[label:] node-name[@unit-address] { [properties definitions] [child nodes] };label 是我们刚讲过的便利贴,可选。node-name 是节点的名字,比如 ethernetserialgpio@unit-address 是单元地址,这个部分是关键——它对应着该设备在总线上的地址,比如 serial@40011000

这里有一个坑点预警:如果你的节点没有 reg 属性,通常就不应该写 @unit-address。但如果你写了,或者地址填错了,内核解析时虽然可能不会 panic,但驱动匹配会失败,因为 Linux 是通过 compatible + address 来唯一标识设备的。

这里有一个容易混淆的地方:节点名compatible 属性里的名字。节点名是你给这个设备起的「绰号」(比如 uart0),compatible 是它的「身份证号」(比如 vendor,xyz-uart)。驱动程序通常只看身份证号,不在意绰号。

有时候通用的 .dtsi 文件里定义了你的板子上不存在的硬件,你需要把它删掉。语法如下:/delete-node/ node-name; 或者 /delete-node/ &label;。你可以通过节点名删也可以通过标签删,如果两个名字都一样标签引用通常更精准,避免误删。

节点里装的是属性,属性就是键值对。有值的属性[label:] property-name = value;空值的属性[label:] property-name;,这表示属性存在但数据长度为 0,通常用来做一个开关。就像节点一样属性也能删:/delete-property/ property-name;。这在覆盖板级配置时非常有用——先把原来的属性删了再赋一个新的值,而不是试图去「修改」它(因为 DTS 不支持修改只能覆盖)。

属性值的定义有严格的七种数据形态,这是 DTS 语法里最繁琐但也最重要的部分。32位整数数组 是最常见的类型,比如中断号、时钟 ID,用尖括号 < > 包裹,里面的值可以是 C 风格的整数(支持进制前缀)。DTS 编译器竟然是支持算术运算的,这有点反直觉但确实很方便。你可以用括号包裹表达式,支持的运算符非常全:算术(+, -, *, /, %)、位运算(&, |, ^, ~, <<, >>)、逻辑(&&, ||, !)、关系(<, >, <=, >=, ==, !=)、三元(? :)。

freq = <(33000000 / 33)>; 会在编译时就算出结果,内核里直接存的是 1000000。DTS 本质上是按 32 位单元处理的,如果要表示一个 64 位整数(比如 64 位的物理地址),你需要用两个 32 位单元来拼。clock-frequency = <0x00000001 0x00000000>; 意味着这是一个 0x100000000 的数值。

字符串用双引号包裹,注意结束符 NULL 也是隐式包含在数据里的字节串用方括号 [ ] 包裹,直接写 16 进制字节,中间的空格是可选的但为了可读性通常加上。混合与拼接:一个属性可以包含逗号分隔的多个部分,编译器会把它们粘在一起。compatible = "ns16550", "ns8250"; 实际上是一个字符串数组。

引用是最强大的功能。在单元格数组中,你可以通过 &label 引用另一个节点,编译器会把它自动展开为那个节点的 phandle(一个唯一的整数 ID)。interrupt-parent = < &mpic >;。你也可以直接用路径引用(虽然不推荐,因为太脆弱):interrupt-parent = < &{/soc/interrupt-controller@40000} >;。如果在数组外面直接引用,展开的就是节点的全路径ethernet0 = &EMAC0; 展开后可能是 "/soc/ethernet@4000"。

标签甚至可以插在属性值的中间,给某个特定的位置做个记号,这在某些需要计算偏移量的场景下很有用。虽然不常见但在处理复杂的 reg 属性时,能极大地提高可读性。


6.4 文件布局:一张图纸的轮廓

最后,我们把所有的碎片拼起来,看一张完整的 DTS 文件长什么样。文件必须以版本声明开头,这是为了避免歧义(旧版 DTS v0 和 v1 格式不兼容):/dts-v1/;

接下来是内存保留区:/memreserve/ <address> <length>;。这告诉内核:「这块内存区域你别动,可能被 BootLoader 或者某些安全监控程序占用了,千万别划给系统拿去当堆栈用。」例如:/memreserve/ 0x10000000 0x4000; 保留 memory region 0x10000000..0x10003fff。

然后就是根节点了:/ { [property definitions] [child nodes] };。所有的一切——CPU、内存、总线、外设——都必须挂在根节点这棵大树下面。就像宇宙大爆炸之后,所有的物质都在这个奇点之内展开。最后别忘了注释:/* 这是 C 风格的块注释 */// 这是 C++ 风格的行注释。善用注释是给别人留条活路,也是给三个月后健忘的自己留条活路。


本章回响

现在,你应该能看懂那一大坨 .dts 文件在说什么了。它不再是一堆乱七八糟的符号,而是一张精密的地图。label 是路标,node 是地标,property 是具体的经纬度。 我们之所以要遵守这些繁琐的规则——从严格的字符集到复杂的引用展开——是因为在另一端,Linux 内核正在像一只精密的钟表一样,按照这些信息去初始化硬件。

还记得开头提到的那个问题吗?为什么不能直接用 C 代码硬编码?因为 DTS 把「数据」从「代码」中剥离了出来。这使得我们可以在不重新编译内核的情况下,通过修改 DTS 文件来适配不同的硬件板子。这就是 Devicetree 作为一个独立子系统的核心价值。

下一章,我们将把这些文本编译成二进制 DTB,并亲手把它们塞进内核里,看看内核醒来后是如何解读这张图纸的。届时,今天我们在这里定义的每一个 < >&,都会变成内存里真实的数据结构。

Built with VitePress