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 四个节点的关系图

下面这张图展示了这四个节点在设备树中的位置和关系:

设备树核心节点关系图 设备树核心节点关系 根节点 / cpus 节点 memory 节点 chosen 节点 cpu@0 cpu@1 cpu@2 说明: • 根节点是整棵树的起点,所有其他节点都是它的子节点或后代节点 • cpus 节点包含一个或多个 cpu 子节点,描述处理器架构和数量 • memory 节点描述物理内存的起始地址和大小 • chosen 节点传递启动参数,是 bootloader 和内核的通信桥梁

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、内存和启动参数。嗯,这就是设备树最核心的部分。