2、Linux内核电源管理基础:CPU Freq与CPU Idle框架、Clock框架与Gate操作、Regulator框架与电压控制
好,咱们进入正题。这一章讲的是Linux内核里电源管理的几个核心框架。说实话,这些框架是MTK平台低功耗优化的地基。地基不稳,上面盖的楼再漂亮也得塌。我个人习惯是,拿到一个新平台,先不看上层应用怎么调,先把这几个框架摸清楚。
2.1 CPU Freq与CPU Idle框架
这两个东西,一个管“跑多快”,一个管“歇多久”。你想想看,CPU就像个运动员,CPU Freq决定他冲刺的速度,CPU Idle决定他休息的深度。
2.1.1 CPU Freq:动态调频
CPU Freq框架,说白了就是根据系统负载动态调整CPU的主频和电压。负载高了,频率拉上去;负载低了,频率降下来。这个机制在MTK平台上特别重要,因为手机不像服务器,电池就那么点容量。
框架里有个核心概念叫 governor(调频策略)。常见的几个:
- performance:一直跑最高频,性能拉满,功耗也拉满。调试阶段偶尔用用。
- powersave:一直跑最低频,省电但卡顿。基本没人用。
- ondemand:负载高了立刻升频,负载降了慢慢降。老牌策略,响应快但不够平滑。
- interactive:Google当年推的,对触控响应优化过。MTK早期平台常用。
- schedutil:现在的主流。直接跟调度器挂钩,用调度器的负载信息来做调频决策。更精准,延迟更低。
我在项目中遇到过一个问题:某个MTK平板,播放视频时偶尔卡顿。查了半天,发现是schedutil的调频响应太慢,CPU没及时升频。后来我调整了 up_rate_limit_us 参数,把升频延迟从10ms降到2ms,问题就解决了。嗯,这里要注意,调频延迟不是越小越好,太激进反而会增加功耗抖动。
/sys/devices/system/cpu/cpufreq/ 下的节点查看当前策略和频率。比如 cat scaling_governor 看当前策略,cat scaling_cur_freq 看当前频率。
2.1.2 CPU Idle:深度睡眠
CPU Idle框架管的是CPU没事干的时候怎么省电。ARM架构的CPU通常有多个 idle state(也叫C-state),从浅到深:
| State | 名称 | 功耗 | 唤醒延迟 | 说明 |
|---|---|---|---|---|
| C0 | 运行态 | 高 | 0 | CPU正在干活 |
| C1 | WFI | 较低 | 几微秒 | 关闭时钟,保持电压 |
| C2 | Retention | 低 | 几十微秒 | 保持寄存器状态 |
| C3 | Power Down | 极低 | 几百微秒 | 关闭电源域,丢失状态 |
我曾经踩过一个坑:某个项目待机电流偏大,怎么都降不下去。后来发现是某个驱动在CPU进入C3之前,没有释放一个锁,导致CPU只能停在C1。改完之后,待机电流直接降了5mA。所以,驱动里对idle state的约束一定要检查清楚。
2.2 Clock框架与Gate操作
Clock框架,管的是芯片里各个模块的时钟。你想想,芯片里那么多模块,不是每个模块时时刻刻都需要时钟的。比如音频模块,你不放歌的时候,它的时钟完全可以关掉。这就是Gate操作——把时钟“门控”掉。
2.2.1 Clock框架的基本结构
Linux的Clock框架用 struct clk 来表示一个时钟。每个时钟可以有自己的父时钟,形成一棵时钟树。MTK的时钟树特别复杂,一个SoC里可能有几百个时钟节点。
常用的API:
clk_get(dev, "name"):获取时钟句柄clk_prepare_enable():准备并使能时钟clk_disable_unprepare():禁能并清理时钟clk_set_rate():设置时钟频率clk_get_rate():获取当前频率
代码示例:
/* 获取时钟 */
struct clk *clk = clk_get(dev, "uart_clk");
if (IS_ERR(clk)) {
dev_err(dev, "Failed to get uart clock\n");
return PTR_ERR(clk);
}
/* 使能时钟 */
clk_prepare_enable(clk);
/* 设置频率 */
clk_set_rate(clk, 115200 * 16);
/* 使用完后关闭 */
clk_disable_unprepare(clk);
clk_put(clk);
我个人习惯是,在驱动的probe函数里就把所有需要的时钟都获取好,然后在suspend/resume里做enable/disable。这样逻辑清晰,也方便调试。
2.2.2 Gate操作:精准控钟
Gate操作,说白了就是“用的时候开,不用的时候关”。但这里有个讲究:不是所有时钟都能随便关。比如某个模块的时钟关了,但它的子模块还在工作,那就会出问题。
MTK平台在Clock框架里做了很多硬件级别的Gate控制。比如 clk_gate 结构体,可以直接操作硬件寄存器来开关时钟。这样做的好处是延迟极低,适合频繁开关的场景。
clk_prepare_enable() 才能工作。如果某个外设不工作,第一件事就是检查它的时钟有没有开。
我曾经遇到一个案例:I2C总线在某个频率下通信不稳定。查了半天,发现是I2C模块的时钟源被其他模块抢占了,导致实际频率跟设置的不一致。后来用 clk_get_rate() 一查,果然差了20%。所以,调完时钟一定要确认实际频率。
2.3 Regulator框架与电压控制
Regulator框架,管的是电压。CPU要降频,通常也得跟着降压,不然功耗降不下去。这就是DVFS(动态电压频率调整)的核心思想。
2.3.1 Regulator的基本概念
Regulator分为两种:
- LDO(低压差线性稳压器):输出纹波小,但效率低。适合给模拟电路供电。
- DC-DC(开关电源):效率高,但纹波大。适合给数字电路供电。
在MTK平台上,CPU核心电压通常由DC-DC提供,而一些外设(比如摄像头、音频)的电压由LDO提供。
常用的API:
regulator_get(dev, "name"):获取regulator句柄regulator_enable():使能输出regulator_disable():禁能输出regulator_set_voltage():设置电压值regulator_get_voltage():获取当前电压
代码示例:
/* 获取CPU核心电压的regulator */
struct regulator *cpu_reg = regulator_get(cpu_dev, "cpu-vcore");
if (IS_ERR(cpu_reg)) {
dev_err(cpu_dev, "Failed to get CPU regulator\n");
return PTR_ERR(cpu_reg);
}
/* 使能输出 */
regulator_enable(cpu_reg);
/* 设置电压为1.0V */
regulator_set_voltage(cpu_reg, 1000000, 1000000);
/* 使用完后关闭 */
regulator_disable(cpu_reg);
regulator_put(cpu_reg);
2.3.2 电压控制中的坑
电压控制看起来简单,但实际项目中坑不少。我总结几个常见的:
- 电压裕量不足:有些芯片在低温下需要更高的电压才能稳定工作。如果只按常温的电压设置,低温下可能会死机。
- 电压切换速度:regulator的电压切换不是瞬间完成的。如果切换太快,可能导致电压过冲或下冲。MTK平台通常有硬件自动控制,但驱动里也要留够时间。
- 依赖关系:有些模块的电压依赖于其他模块。比如某个外设需要先有核心电压,才能有IO电压。顺序搞反了,芯片可能烧掉。
2.4 三个框架的协同工作
CPU Freq、Clock、Regulator这三个框架不是孤立的。它们之间通过 OPP(Operating Performance Point) 来协同。OPP定义了一组“频率-电压”对,比如:
| OPP | 频率 | 电压 |
|---|---|---|
| 0 | 500 MHz | 0.8 V |
| 1 | 1.0 GHz | 0.9 V |
| 2 | 1.5 GHz | 1.0 V |
| 3 | 2.0 GHz | 1.1 V |
当CPU Freq决定切换到某个频率时,它会通过OPP找到对应的电压,然后通知Regulator框架去调整电压。同时,Clock框架也会确保时钟频率的切换是平滑的。
在MTK平台上,这个协同过程通常由 MTK CPUFreq 驱动来完成。它会注册一个 cpufreq_driver,然后在 target() 回调里依次执行:
- 设置新的时钟频率
- 等待时钟稳定
- 设置新的电压
- 等待电压稳定
- 更新状态
嗯,这里要注意,步骤的顺序不能搞反。如果先降压再降频,可能会导致CPU在低电压下跑高频,直接死机。
好了,这一章就到这里。下一章我们讲MTK平台特有的电源管理硬件,比如PMIC、PWM、EINT这些。到时候我会结合具体的项目案例,讲讲怎么用这些硬件来优化功耗。