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、设备树之间的关系画清楚了。你可以把它当作本章的“地图”。
从这张图你能看到:设备树在最上层,它描述了CPU支持哪些idle状态、哪些频率档位、以及对应的电压。CPU idle和CPU freq框架各自独立工作,但都依赖OPP表来确保电压正确。我经常跟团队说一句话:设备树是硬件写给内核的情书,OPP是情书里的价目表,而idle和freq是执行者。
3.6 本章小结
这一章我们拆解了Linux电源管理的四个核心模块:
- CPU idle框架:让CPU在空闲时进入低功耗状态,governor决定进多深。
- CPU freq框架:根据负载动态调频,schedutil是我个人最推荐的governor。
- OPP机制:提供频率-电压的对应关系,是调频和调压的基础。
- 设备树:把硬件能力告诉内核,配错了整个电源管理都跑不起来。