3. 设备树节点与属性:根节点、CPU节点、内存节点、chosen节点详解
设备树里最重要的几个节点,说白了就是根节点、CPU节点、内存节点和chosen节点。它们构成了系统启动的骨架。我刚开始接触设备树时,总觉得这些节点就是填几个数字,后来踩了不少坑才明白——每个属性背后都有它的设计逻辑。
核心认知:设备树的根节点是整棵树的起点,CPU节点告诉内核“我有几个核、什么架构”,内存节点描述物理内存布局,chosen节点则传递启动参数。这四个节点缺一不可。
3.1 根节点:整棵树的起点
每个设备树文件都必须有一个根节点。它用 / 表示,是所有其他节点的父节点。根节点里通常放一些系统级的属性,比如 model、compatible、#address-cells 和 #size-cells。
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
#address-cells = <1>;
#size-cells = <1>;
// 其他节点...
};
model 就是板子的名字,纯描述性,内核基本不用它做匹配。compatible 才是关键——内核通过它来匹配对应的平台驱动。我见过有人把 compatible 写反了顺序,结果内核加载了错误的驱动,系统直接挂掉。
#address-cells 和 #size-cells 决定了子节点中 reg 属性的地址和长度字段占几个32位整数。比如 #address-cells = <1> 表示地址用1个32位整数表示,#size-cells = <1> 表示长度也用1个32位整数。如果是64位地址,就得设成 <2>。
我的习惯:根节点的 #address-cells 和 #size-cells 一定要和实际硬件地址宽度一致。我曾经在一个64位ARM平台上忘了改这两个值,结果内存节点解析出来的地址全是错的,内核直接 panic。
3.2 CPU节点:告诉内核你有几个核
CPU节点描述了处理器的架构和数量。它通常放在根节点下,节点名是 cpus,里面包含一个或多个 cpu 子节点。
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a8";
reg = <0x0>;
clock-frequency = <1000000000>;
};
};
注意 #size-cells = <0>,因为CPU的 reg 属性只表示CPU编号,不需要长度字段。每个 cpu 节点必须有 device_type = "cpu",这是设备树规范要求的。
compatible 属性告诉内核这是什么架构的CPU。比如 "arm,cortex-a8" 对应ARM Cortex-A8。内核会根据这个值选择对应的CPU操作函数集。
多核系统怎么写?很简单,加多个 cpu 子节点就行:
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a72", "arm,armv8";
reg = <0x0>;
enable-method = "psci";
};
cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a72", "arm,armv8";
reg = <0x1>;
enable-method = "psci";
};
};
注意:多核系统中,enable-method 属性很关键。它告诉内核如何启动从核。常见值有 "psci"(ARM标准)、"spin-table"(老式方法)。选错了,从核就起不来。
3.3 内存节点:物理内存的布局图
内存节点告诉内核物理内存的起始地址和大小。节点名通常是 memory,必须包含 device_type = "memory" 属性。
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
这个例子表示物理内存从 0x80000000 开始,大小为 0x20000000(512MB)。reg 属性的格式由根节点的 #address-cells 和 #size-cells 决定。
如果内存不连续,可以写多个 reg 段:
memory {
device_type = "memory";
reg = <0x80000000 0x20000000
0xA0000000 0x20000000>;
};
这表示两段内存:第一段从 0x80000000 开始512MB,第二段从 0xA0000000 开始512MB。
避坑指南:我曾经在一个项目里把内存大小写错了,少写了一个0,结果系统只识别了64MB内存,跑个根文件系统都费劲。排查了半天才发现是 reg 里的数值写错了。所以,写内存节点时一定要对照硬件手册,确认物理地址和内存大小。
3.4 chosen节点:启动参数的传递通道
chosen 节点不描述硬件,它传递的是系统启动时的运行时参数。比如内核启动参数、initrd 的位置、启动设备等。
chosen {
bootargs = "console=ttyO0,115200 root=/dev/mmcblk0p2 rw";
stdout-path = &uart0;
};
bootargs 是最常用的属性,它把内核启动参数传给内核。比如控制台设备、根文件系统位置、调试选项等。stdout-path 指定系统日志输出到哪个串口。
为什么叫 chosen?因为它是 bootloader 在启动时“选择”填入的节点。设备树源文件里可以写默认值,但 bootloader 通常会覆盖它。比如 U-Boot 会修改 bootargs 来传递自己的启动参数。
除了 bootargs,chosen 节点还可以包含:
initrd-start和initrd-end:指定 initramfs 在内存中的位置linux,initrd-start和linux,initrd-end:同上,但更规范bootloader-version:记录 bootloader 版本,方便调试
关键点:chosen 节点是 bootloader 和内核之间的“传话筒”。内核启动时最先解析的就是它。如果 bootargs 写错了,内核可能连控制台都打不开,你就只能看黑屏了。
3.5 四个节点的关系图
下面这张图展示了这四个节点在设备树中的位置和关系:
3.6 节点属性的通用规则
除了上面提到的专用属性,设备树节点还有一些通用属性,我简单列一下:
| 属性名 | 含义 | 示例 |
|---|---|---|
compatible |
设备兼容性字符串,用于驱动匹配 | "ti,am335x-uart", "ns16550" |
reg |
设备地址和长度 | <0x48020000 0x1000> |
interrupts |
中断号、触发类型 | <0 29 4> |
status |
设备状态:okay 或 disabled |
"okay" |
label |
设备标签,方便引用 | "uart0" |
status 属性我特别想提一下。调试时可以把某个设备设成 "disabled",内核就不会去初始化它。这在排查硬件冲突时非常有用。
注意:compatible 属性的值是有优先级的。内核会从左到右依次尝试匹配驱动。所以,越具体的值越靠左,越通用的越靠右。比如 "ti,am335x-uart", "ns16550",内核先尝试匹配 TI 的驱动,不行再回退到通用 16550 驱动。
3.7 实战:一个完整的设备树片段
最后,我把这四个节点拼在一起,给你看一个完整的例子:
/dts-v1/;
/ {
model = "TI AM335x BeagleBone Black";
compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx";
#address-cells = <1>;
#size-cells = <1>;
chosen {
bootargs = "console=ttyO0,115200 root=/dev/mmcblk0p2 rw";
stdout-path = &uart0;
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a8";
reg = <0x0>;
clock-frequency = <1000000000>;
};
};
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x20000000>;
};
// 其他外设节点...
};
这个片段就是 BeagleBone Black 设备树的骨架。你把它编译成 .dtb,内核就能正确识别CPU、内存和启动参数。嗯,这就是设备树最核心的部分。