4、CPU节点配置:cpus节点、cpu-map、cpu节点属性

好,咱们进入设备树多核配置的核心部分了。说实话,CPU节点的配置是整个设备树里最「敏感」的一块——配错了轻则少核,重则系统起不来。我这些年调试过的板子里,至少有三成启动问题都出在CPU节点上。

先理清一个概念:设备树里描述CPU,不是简单列几个核就完事。你得告诉内核:有多少个核?每个核是什么架构?主频多少?它们之间怎么互联?嗯,这些信息都藏在 cpus 节点和它的子节点里。

4.1 cpus节点:CPU家族的「户口本」

cpus 节点是所有CPU描述的老祖宗。它本身不描述具体CPU,而是定义一些全局属性,比如地址位数、缓存大小等。我习惯把它比作「CPU家族的户口本」——先说明这个家族的基本情况,再逐个介绍每个成员。

看一个典型的 cpus 节点:

cpus {
    #address-cells = <2>;
    #size-cells = <0>;

    cpu@0 {
        device_type = "cpu";
        reg = <0x0 0x0>;
        compatible = "arm,cortex-a72";
        ...
    };

    cpu@1 {
        device_type = "cpu";
        reg = <0x0 0x1>;
        compatible = "arm,cortex-a72";
        ...
    };
};

这里 #address-cells = <2> 是什么意思?说白了,就是告诉解析器:每个CPU的编号(reg属性)要用两个32位整数来表示。为什么是2?因为有些ARM芯片的MPIDR寄存器是64位的,低32位存集群ID和核ID,高32位可能存其他信息。我见过有人图省事写成1,结果四核芯片只认出来两个核——血的教训。

注意: #size-cells 必须设为0。因为CPU节点不需要描述地址范围,它只需要一个编号。设成非0值会导致解析器行为异常,内核启动时可能直接panic。

4.2 cpu-map:多簇CPU的「导航图」

现在很多芯片不止一个CPU簇。比如一个大核簇(Cortex-A76)加一个小核簇(Cortex-A55),这就是所谓的big.LITTLE架构。这时候光靠 cpus 节点就不够了——你得告诉内核:哪些核属于同一个簇?哪个核是主核?

cpu-map 就是干这个的。它像一张导航图,把物理CPU编号映射到逻辑拓扑上。我去年调一个联发科平台时,就靠它解决了大小核调度问题。

看个例子:

cpus {
    #address-cells = <2>;
    #size-cells = <0>;

    cpu-map {
        cluster0 {
            core0 {
                cpu = <&cpu0>;
            };
            core1 {
                cpu = <&cpu1>;
            };
        };

        cluster1 {
            core0 {
                cpu = <&cpu2>;
            };
            core1 {
                cpu = <&cpu3>;
            };
        };
    };

    cpu0: cpu@0 {
        device_type = "cpu";
        reg = <0x0 0x0>;
        ...
    };

    cpu1: cpu@100 {
        device_type = "cpu";
        reg = <0x0 0x100>;
        ...
    };
    // 以此类推
};

这里 cluster0cluster1 代表两个簇。每个簇下面有 core0core1 等子节点,通过 cpu 属性引用具体的CPU节点。为什么要这么麻烦?因为物理上CPU的编号可能不连续——比如大核从0x000开始,小核从0x100开始。没有 cpu-map,内核根本不知道哪个核在哪个簇里。

我的经验: 如果你用的是ARM64架构,cpu-map 不是必须的,但强烈建议加上。为什么?因为Linux内核的调度器(如EAS)依赖这个信息来做功耗优化。不加的话,大小核调度策略可能失效,导致功耗飙升。我曾经在一个项目里漏掉了 cpu-map,结果小核永远跑不满,大核一直满载——电池续航直接砍半。

4.3 cpu节点属性:每个CPU的「身份证」

每个CPU子节点都有自己的属性。这些属性就像身份证上的信息,告诉内核这个CPU是什么、能跑多快、有什么特性。咱们挑几个关键的说说。

4.3.1 reg属性:CPU的唯一编号

reg 是CPU节点的核心属性。它对应硬件上的MPIDR寄存器值,内核用它来区分不同的CPU。注意,这个值必须是唯一的,不能重复。

对于ARM64,reg 通常是64位的:

cpu@0 {
    reg = <0x0 0x0>;  // 低32位: 0x0, 高32位: 0x0
};

cpu@1 {
    reg = <0x0 0x1>;  // 低32位: 0x0, 高32位: 0x1
};

为什么是两段?因为 #address-cells = <2>。如果芯片只有4个核,且MPIDR只用低8位,你可以写成 #address-cells = <1>,然后 reg = <0x0>reg = <0x1> 这样。但我个人建议统一用2,方便以后扩展。

避坑指南: 我曾经遇到一个情况:芯片有8个核,但设备树里只配了4个,而且reg值还写错了。结果内核启动时,只认出来3个核,第4个核的reg值跟第1个重复了。内核直接报错「CPU already registered」,然后panic。所以,reg 值一定要跟硬件手册上的MPIDR一一对应,别想当然。

4.3.2 clock-frequency属性:CPU的主频

clock-frequency 指定CPU的当前运行频率,单位是Hz。这个属性不是必须的,但很多驱动会用它来做频率相关的初始化。

cpu@0 {
    clock-frequency = <2000000000>;  // 2GHz
};

注意,这个值只是「初始频率」。实际运行中,内核的cpufreq子系统会根据负载动态调整频率。所以,别指望设了这个值就能锁频——那是 clocks 属性和时钟控制器的事。

我见过有人把 clock-frequency 写成 2000(单位是MHz),结果内核以为CPU只有2kHz,调度器直接罢工。嗯,单位一定要用Hz,别偷懒。

4.3.3 其他常用属性

除了 regclock-frequency,还有几个属性值得关注:

属性名 说明 示例值
device_type 固定为 "cpu",告诉内核这是个CPU节点 "cpu"
compatible CPU的架构和型号,用于匹配驱动 "arm,cortex-a72"
enable-method CPU启动方式,如 "psci"、"spin-table" "psci"
next-level-cache 指向L2或L3缓存节点 &L2_0
cpu-idle-states 指向CPU idle状态描述节点 &CPU_SLEEP_0

这里重点说一下 enable-method。它决定了CPU怎么从启动状态进入工作状态。对于ARM64,现在主流是用PSCI(Power State Coordination Interface)。你想想看,如果没有这个属性,内核怎么知道该调用哪个接口来唤醒其他核?

核心要点: CPU节点的配置,本质上是把硬件信息「翻译」成内核能理解的格式。每个属性都有其存在的理由——reg 是身份标识,compatible 是驱动匹配,enable-method 是启动协议。少一个,内核就可能「迷路」。

4.4 实战:一个完整的CPU节点配置

最后,咱们看一个完整的例子。这是一个双簇、每簇4核的ARM64芯片配置:

/dts-v1/;

/ {
    model = "MyCustomBoard";
    compatible = "mycompany,myboard";

    cpus {
        #address-cells = <2>;
        #size-cells = <0>;

        cpu-map {
            cluster0 {
                core0 { cpu = <&cpu0>; };
                core1 { cpu = <&cpu1>; };
                core2 { cpu = <&cpu2>; };
                core3 { cpu = <&cpu3>; };
            };

            cluster1 {
                core0 { cpu = <&cpu4>; };
                core1 { cpu = <&cpu5>; };
                core2 { cpu = <&cpu6>; };
                core3 { cpu = <&cpu7>; };
            };
        };

        cpu0: cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a76";
            reg = <0x0 0x0>;
            enable-method = "psci";
            clock-frequency = <2200000000>;
            next-level-cache = <&L2_0>;
        };

        cpu1: cpu@100 {
            device_type = "cpu";
            compatible = "arm,cortex-a76";
            reg = <0x0 0x100>;
            enable-method = "psci";
            clock-frequency = <2200000000>;
            next-level-cache = <&L2_0>;
        };
        // ... 其他CPU类似

        cpu4: cpu@10000 {
            device_type = "cpu";
            compatible = "arm,cortex-a55";
            reg = <0x0 0x10000>;
            enable-method = "psci";
            clock-frequency = <1800000000>;
            next-level-cache = <&L2_1>;
        };
        // ... 小核簇其他CPU
    };

    L2_0: l2-cache0 {
        compatible = "cache";
        cache-level = <2>;
    };

    L2_1: l2-cache1 {
        compatible = "cache";
        cache-level = <2>;
    };
};

看到没?大核簇的reg从0x000开始,小核簇从0x10000开始。这样内核通过 cpu-map 就知道哪些核属于同一个簇,调度器就能做出更智能的决策。

嗯,CPU节点配置就讲到这里。下一节咱们聊聊CPU的启动方式——spin-table和PSCI,这可是多核启动的关键。到时候我会分享一个我调试PSCI时遇到的「诡异」问题,保证让你印象深刻。