3. Linux电源管理框架:CPU idle、CPU freq、OPP与设备树

电源管理在Linux里不是一块铁板,它分了好几个层次。我个人习惯把它们拆成四个核心模块来理解:CPU idle(空闲时怎么省电)、CPU freq(运行时怎么调频)、OPP(性能点怎么定义)、以及设备树(硬件信息怎么描述)。

这四块东西,说白了就是一套组合拳。你想想看,CPU没事干的时候要睡,有事干的时候要跑在合适的频率上,而跑多快、睡多深,都得有个“档位表”——这就是OPP。最后,这些信息怎么告诉内核?靠设备树。

核心观点:电源管理不是单个驱动的事,而是一个从硬件描述到内核调度、再到硬件执行的完整闭环。理解这个闭环,你才能调好功耗。

3.1 CPU idle框架:让CPU学会“偷懒”

CPU idle框架负责一件事:当CPU没有任务可执行时,把它塞进某个休眠状态。不同的休眠状态,功耗不同,唤醒延迟也不同。

我在项目中遇到过一个问题:某个平台默认只用了WFI(Wait For Interrupt),结果待机功耗比竞品高了30%。后来才发现,这个CPU其实支持更深度的状态,只是内核没配好。

3.1.1 状态定义与governor

每个CPU idle状态由struct cpuidle_state描述,包含名称、延迟、功耗、以及进入/退出回调。内核把这些状态组织成一个数组,由governor决定选哪个。

// 一个典型的cpuidle状态定义
static struct cpuidle_state exynos_idle_states[] = {
    {
        .enter          = exynos_enter_wfi,
        .exit_latency   = 1,       // 唤醒延迟 1us
        .target_residency = 1,     // 至少停留 1us
        .flags          = CPUIDLE_FLAG_NONE,
        .name           = "WFI",
        .desc           = "Wait For Interrupt",
    },
    {
        .enter          = exynos_enter_afsr,
        .exit_latency   = 10,      // 唤醒延迟 10us
        .target_residency = 100,   // 至少停留 100us
        .flags          = CPUIDLE_FLAG_TIMER_STOP,
        .name           = "AFSR",
        .desc           = "Active State Retention",
    },
};

这里有个关键参数:target_residency。它告诉governor:如果预计空闲时间小于这个值,就别进这个状态,否则省的电还不够唤醒成本。嗯,这里要注意,很多新手把延迟设得很低,结果频繁进出深度睡眠,功耗反而更高。

3.1.2 常见的governor

Governor 策略 适用场景
menu 基于历史预测空闲时长 大多数通用场景
ladder 逐级尝试更深状态 嵌入式、确定性场景
teo 定时器事件优化 实时性要求高的场景

我个人习惯在嵌入式产品里用menu governor,它的预测算法比较成熟。但如果你做的是实时系统,teo可能更合适——它不会为了省电而错过定时器。

调试技巧:查看当前CPU idle状态使用情况:cat /sys/devices/system/cpu/cpu0/cpuidle/state*/usage。如果某个深度状态usage一直是0,说明governor认为它不值得进。

3.2 CPU freq框架:动态调频的“油门”

CPU freq框架,也叫cpufreq,负责动态调整CPU的运行频率和电压。它的核心逻辑很简单:负载高就升频,负载低就降频。但实现起来,牵扯到硬件、驱动、策略三层的配合。

3.2.1 核心数据结构

每个CPU对应一个cpufreq_policy,里面保存了当前频率范围、governor、以及调频回调。

struct cpufreq_policy {
    struct cpufreq_cpuinfo  cpuinfo;    // 硬件能力
    struct cpufreq_freqs    cur;        // 当前频率
    struct cpufreq_governor *governor;  // 当前策略
    unsigned int            min, max;   // 频率范围
    struct cpufreq_frequency_table *freq_table; // 可用频率表
};

频率表freq_table就是CPU能跑的档位。我曾经在一个项目里发现,厂商给的频率表里有个档位是1.2GHz,但实际跑上去芯片就过热。后来查了OPP表才发现,那个档位的电压没配够——这是后话。

3.2.2 cpufreq governor

governor决定“什么时候调频”。常见的几个:

  • performance:永远跑最高频,功耗不考虑。
  • powersave:永远跑最低频,性能不考虑。
  • ondemand:负载超过阈值就升频,低于阈值就降频。
  • conservative:类似ondemand,但升降更平滑。
  • schedutil:基于调度器负载信息调频,延迟更低。

我个人强烈推荐schedutil。为什么?因为它直接利用调度器的PELT(Per-Entity Load Tracking)信号,比ondemand那种采样方式更精准。我在一个视频编解码项目里,从ondemand切到schedutil,功耗降了8%,帧率反而稳了。

避坑指南:我曾经遇到一个bug——schedutil在某些老内核上会导致CPU stuck。原因是频率切换太快,硬件跟不上。解决方案是在设备树里加一个transition_latency属性,告诉内核硬件切换需要多少时间。

3.3 OPP机制:性能点的“菜单”

OPP(Operating Performance Points)就是一张表,描述了设备在不同频率下需要多少电压。它不只用于CPU,也用于GPU、DDR、总线等。

3.3.1 OPP的结构

每个OPP条目包含频率、电压、以及是否可用。内核用struct dev_pm_opp表示:

struct dev_pm_opp {
    unsigned long       rate;       // 频率,单位Hz
    unsigned long       u_volt;     // 电压,单位uV
    bool                available;  // 是否可用
    struct device_node  *np;        // 对应的设备树节点
};

OPP表通常放在设备树里,像这样:

cpu0_opp_table: opp-table {
    compatible = "operating-points-v2";
    opp-shared;

    opp-200000000 {
        opp-hz = /bits/ 64 <200000000>;
        opp-microvolt = <850000>;
    };
    opp-400000000 {
        opp-hz = /bits/ 64 <400000000>;
        opp-microvolt = <900000>;
    };
    opp-800000000 {
        opp-hz = /bits/ 64 <800000000>;
        opp-microvolt = <1000000>;
    };
    opp-1200000000 {
        opp-hz = /bits/ 64 <1200000000>;
        opp-microvolt = <1100000>;
    };
};

这里有个细节:opp-shared表示这个OPP表是多个CPU共享的。如果是独立调频,就不加这个属性。

3.3.2 OPP与cpufreq的联动

cpufreq驱动在初始化时,会调用dev_pm_opp_of_add_table()从设备树加载OPP表,然后生成频率表。调频时,驱动会根据目标频率查OPP表,找到对应的电压,一并设置。

我在项目中遇到过一个问题:OPP表里写了1.5GHz,但硬件实际只能跑到1.4GHz。结果系统在高温下频繁崩溃。后来我加了一个opp-suspend属性,把休眠时的频率限制在1.0GHz以下,问题才解决。

调试命令:查看当前OPP表:cat /sys/kernel/debug/opp/opp_summary。如果某个OPP显示disabled,说明它被条件性禁用了(比如温度过高)。

3.4 设备树中的电源管理节点

设备树是Linux描述硬件的主要方式。电源管理相关的节点,分散在CPU、PMIC、时钟控制器等多个地方。

3.4.1 常见的电源管理属性

属性 作用 示例
clocks 指定设备的时钟源 clocks = <&clk IMX6QDL_CLK_ARM>
operating-points-v2 引用OPP表 operating-points-v2 = &cpu0_opp_table
cpu-idle-states 引用CPU idle状态 cpu-idle-states = <&WFI &AFSR>
voltage-tolerance 电压容差百分比 voltage-tolerance = <5>
wakeup-source 标记设备可唤醒系统 wakeup-source;

3.4.2 一个完整的CPU电源管理节点示例

&cpu0 {
    clocks = <&clk IMX6QDL_CLK_ARM>;
    clock-names = "cpu";
    operating-points-v2 = &cpu0_opp_table;
    cpu-idle-states = <&WFI &AFSR>;
    nvmem-cells = <&cpu_speed_grade>;
    nvmem-cell-names = "speed_grade";
};

这里nvmem-cells用来读取芯片的speed grade(速度等级),不同等级的芯片能跑的最高频率不同。我在一个项目里忘了配这个,结果所有芯片都按最高等级跑,低等级芯片频繁死机。

避坑指南:设备树里的电源管理节点,一定要和datasheet对清楚。我曾经遇到一个案例:设备树里配了三个OPP,但PMIC只能输出两个电压档位。结果中间那个OPP的电压根本设不上去,系统直接panic。解决方案是只保留PMIC支持的档位。

3.5 知识体系总览

下面这张图,把CPU idle、CPU freq、OPP、设备树之间的关系画清楚了。你可以把它当作本章的“地图”。

Linux电源管理框架核心关系图 设备树(DT) CPU idle框架 cpuidle governor + state CPU freq框架 cpufreq governor + policy OPP机制 频率-电压对应表 设备树描述硬件能力,CPU idle决定何时休眠,CPU freq决定运行频率,OPP提供电压保障 四者协同,构成完整的电源管理闭环

从这张图你能看到:设备树在最上层,它描述了CPU支持哪些idle状态、哪些频率档位、以及对应的电压。CPU idle和CPU freq框架各自独立工作,但都依赖OPP表来确保电压正确。我经常跟团队说一句话:设备树是硬件写给内核的情书,OPP是情书里的价目表,而idle和freq是执行者。

3.6 本章小结

这一章我们拆解了Linux电源管理的四个核心模块:

  • CPU idle框架:让CPU在空闲时进入低功耗状态,governor决定进多深。
  • CPU freq框架:根据负载动态调频,schedutil是我个人最推荐的governor。
  • OPP机制:提供频率-电压的对应关系,是调频和调压的基础。
  • 设备树:把硬件能力告诉内核,配错了整个电源管理都跑不起来。