Appearance
我看我写个文本把硬件交代清楚就行,怎么到你Linux还得编译一遍?
你要是写过 INI、YAML 这类配置文件,这个念头太正常了。描述硬件,听着就是写文档的活,怎么到你 Linux 这边,源码、编译器、二进制产物一套全齐,跟 C 语言一个待遇?因为这份文本的主要读者不是人,是启动阶段的软件,它要的是紧凑又好解析的格式。所以人写的源码得先过一道编译,这道工序里的三个名字,咱们先排开认个脸。
三个名字排开
- DTS(device tree source):人写的文本源码,描述那棵树。
- DTB(device tree blob):DTS 编译出来的二进制产物,给启动阶段的软件读。
- dtc(device tree compiler):干编译这件事的工具,上游实现在 kernel.org。
咱们把管线说成一句话:mini.dts 经 dtc 编译成 mini.dtb,启动阶段的软件读取 mini.dtb。反方向也能走,dtc 能把 DTB 反编译回 DTS。
本仓库的贯穿示例是一份十四行的 mini.dts。本节咱们把它从整体读到细节,再亲手编译一次。
看整体
咱们要读的源文件在仓库的 mini.dts,全文如下:
dts
/dts-v1/;
/ {
compatible = "edts,a_demo"; /* 字符串属性 */
#address-cells = <1>; /* 单个 u32 cell */
#size-cells = <1>;
serial@1000 { /* 带单元地址的子节点 */
compatible = "acme,uart";
reg = <0x1000 0x100>; /* 两个 cell */
clock-frequency = <115200>;
status = "okay";
};
};一眼看不懂很正常,咱们先只看形状:一层大括号套一层大括号,这就是树。/ 是根。根上挂着三个属性,即 compatible、#address-cells 与 #size-cells。serial@1000 是根里面唯一的子节点,它自己又挂着四个属性:compatible、reg、clock-frequency、status。0.2 讲过,父子关系承载连接,属性记录信息,你在 mini.dts 里看到的正是那套规则的第一次实战。
节点下面挂着的一行行“名字 = 值”,咱们叫它属性(property)。
逐行读懂
第 1 行 /dts-v1/; 文件版本声明,给编译器看的,跟硬件无关,你把它读成“本文件按 DTS 语法版本 1 来写”就对了。
第 3 行 / { 到末行 }; 根节点,你把它当作机器本身:一台机器一个根,所有节点都挂在它下面。
compatible = "edts,a_demo"; 名字牌,0.2 细讲过的那块。值是一个字符串,惯例写成“厂商,设备”两段,你拿逗号切开看:前面是谁家的,后面是什么。真实世界里的厂商前缀有一份公开维护的名单,这里的 edts 是虚构的示例。
#address-cells = <1>; 与 #size-cells = <1>; 全文件最不像人话的两行,也是读懂 reg 的钥匙。cell 是设备树里数字的基本计量单位,一个 cell 就是一个 32 位大端整数(大端,高位字节排在前面)。这两行是解读规则,管的是本节点下面所有子节点:子节点的地址用 1 个 cell 写,长度也用 1 个 cell 写。为什么需要规则,你接着看到 reg 就明白。
serial@1000 { 一个子节点。@ 后面的 1000 叫单元地址(unit address),声明这个设备住在父节点地址空间的 0x1000 处。@ 前面的 serial 你读作“串口”就行,它才是名字本身。
reg = <0x1000 0x100>; 尖括号包着两个裸数字。它说的是:寄存器区域从 0x1000 开始,长度 0x100。为什么恰好两个数,为什么每个数按 32 位理解?你回头核对上面那两行,地址 1 个 cell,长度 1 个 cell,1 加 1,正好两个数。换一块把 #address-cells 和 #size-cells 都声明成 2 的板子,同样的 reg 就要写四个数。规则变,读法跟着变。
clock-frequency = <115200>; 一个数,串口的时钟频率。115200 你大概眼熟,它是常见的波特率数字。波特率,指串口每秒传输的位数。它在源码里写作十进制,进 DTB 后就是一个 32 位大端整数。
status = "okay"; 一个字符串,标准属性,okay 表示设备启用。你想让内核当它不存在,就改成 disabled。
十四行到此读完。合上文章,上面每条解释你都能对着原文件自己说出来,这一节就成立了。
动手:亲手编译一次
动手环节要用你电脑上的 dtc 和 xxd(用十六进制查看文件字节的工具),本仓验证的版本是 dtc v1.8.1(2026-10-11)。你在仓库的 exercises/lab/00-bootstrap 目录执行,输入文件的唯一来源在 edtc 工程里(本仓自研的工具工程,文末细说),实验不另立副本:
shell
dtc -I dts -O dtb -o mini.dtb ../../../edtc/example/00_bootstrap/mini.dts
ls -l mini.dtb
xxd mini.dtb | head -3下面是咱们在本机真跑的输出(dtc v1.8.1,2026-10-11)。ls -l 的显示格式随系统不同,你要认的数字只有一个,大小:293 字节。
output
00000000: d00d feed 0000 0125 0000 0038 0000 00e4 .......%...8....
00000010: 0000 0028 0000 0011 0000 0010 0000 0000 ...(............
00000020: 0000 0041 0000 00ac 0000 0000 0000 0000 ...A............有两处值得咱们停下细看。开头四个字节 d00d feed 是 DTB 的魔数,机器靠它认格式,第二卷的第 4 章讲 header 时还会见到它。中段和尾段,咱们肉眼就能认出源码里的字符串:
output
00000070: 0000 001a 0000 0001 0000 0001 7365 7269 ............seri
00000080: 616c 4031 3030 3000 0000 0003 0000 000a al@1000.........
00000090: 0000 0000 6163 6d65 2c75 6172 7400 0000 ....acme,uart...output
000000d0: 6f6b 6179 0000 0000 0000 0002 0000 0002 okay............
000000e0: 0000 0009 636f 6d70 6174 6962 6c65 0023 ....compatible.#
000000f0: 6164 6472 6573 732d 6365 6c6c 7300 2373 address-cells.#s中段能看到节点名 serial@1000 和属性值 acme,uart。尾段能认出属性值 okay,以及一串属性名 compatible、#address-cells。你写下的源码文本以字节的形式住进了产物各处,但整个文件已经是二进制,不再是文本。这些字符串分别住在哪个区块、为什么这样安排,第二卷的第 4 章揭晓。
反方向咱们也能走:
shell
dtc -I dtb -O dts mini.dtb咱们拿到的输出:
dts
/dts-v1/;
/ {
compatible = "edts,a_demo";
#address-cells = <0x01>;
#size-cells = <0x01>;
serial@1000 {
compatible = "acme,uart";
reg = <0x1000 0x100>;
clock-frequency = <0x1c200>;
status = "okay";
};
};咱们对照着看,结构全都回来了,注释没有回来。还有个细节,源码里的十进制 115200 回来变成了十六进制 0x1c200,<1> 变成了 <0x01>。值没变,写法变了。所以这里要把边界立好:反编译还原结构与值,不还原注释和源码写法。拿它检查“树长什么样、值是多少”很可靠,把它当源码还原就不对了。
顺带认一个 .dtsi
.dtsi 文件是公共片段,.dts 可以引入它。典型用法:芯片厂商提供一份 SoC(片上系统,一颗芯片打包 CPU 与外设)级描述 .dtsi,板卡厂商在 .dts 里引入它,再补上板上的设备。细节第一卷的第 2 章展开,这里咱们跟它混个脸熟。
三个容易混的名字
- dtc:上游参考编译器,C 语言实现,本仓库拿它当基准。
- edtc:本仓自研的 C++ 玩具工程,目标是逐步实现自己的编译器并与上游对拍。
- cpp:构建链里的 C 预处理器,与 C++ 没有直接关系,内核构建拿它来预处理 DTS 源文件,第一卷的第 3 章会看到。
三个名字在后续章节都会反复出现,咱们在这里立此存照。
本节自检
和 0.1 一样,合上文章问自己三个问题。都答得上来,你已经能独立读懂一份最小的设备树源码了。
- DTS、DTB、dtc 各是什么,谁变成谁?想想那条管线图。
reg = <0x1000 0x100>恰好是两个数,每个数按 32 位理解。#address-cells与#size-cells是怎样决定了这个读法的?- 咱们编译又反编译了一轮。哪些东西回来了,哪些没有?
咱们的术语依据如下:cell 的 32 位大端定义、单元地址与厂商前缀惯例,出自 Devicetree Specification v0.4 的本仓缓存副本。命令输出是咱们本机真实运行的,验证环境为 dtc v1.8.1。