第3章 CPU频率与电压调节(CPUFreq)

CPUFreq 是嵌入式 Linux 功耗管理的核心模块。说白了,它就是根据系统负载动态调整 CPU 的运行频率和电压。我做了这么多年低功耗优化,可以负责任地告诉你——调好 CPUFreq,往往能省下 30% 以上的功耗。

3.1 CPUFreq 框架三层架构

CPUFreq 框架分为三层:governor(策略层)、core(核心层)、driver(驱动层)。我习惯把这三层比作「大脑、骨架、手脚」。

核心要点:三层分工明确,上层策略决定「调多少」,下层驱动决定「怎么调」。

CPUFreq 三层架构 Governor(策略层) ondemand / schedutil / userspace / powersave Core(核心层) sysfs 接口 / 策略调度 / 通知链 Driver(驱动层) acpi-cpufreq / cpufreq-dt / intel_pstate 💡 数据流向:用户态 → sysfs → Governor → Core → Driver → 硬件寄存器 调频请求从上层逐级下发,最终由 Driver 写入硬件 P-State 寄存器

3.2 Governor 策略深度解析

Governor 是 CPUFreq 的「大脑」。它决定什么时候升频、什么时候降频。我见过不少项目,选错 governor 导致功耗翻倍。

Governor 策略特点 适用场景 功耗表现
performance 始终最高频 性能优先,不考虑功耗 最差
powersave 始终最低频 极低负载、待机 最好
ondemand 负载突变时立即升频 传统嵌入式设备 中等
conservative 频率渐变,平滑调整 对响应不敏感的场景 较好
schedutil 基于调度器负载信号 新内核、实时性要求高 最优
userspace 用户程序手动控制 定制化策略 取决于实现

3.3 ondemand vs schedutil 实战对比

这两个 governor 是实际项目中最常用的。我分别说说它们的坑和优势。

ondemand 的工作机制

ondemand 通过采样周期检测 CPU 负载。当负载超过 up_threshold(默认 95%)时,立即跳到最高频。低于 down_threshold 时逐步降频。

我曾经踩过的坑:在某个 IoT 项目中,ondemand 的采样周期设成了 10ms,结果 CPU 在 200ms 内反复升降频 20 次,功耗反而比固定高频还高。后来我把 sampling_rate 调到 50ms,问题解决。

# 查看当前 governor
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

# 切换为 ondemand
echo ondemand > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

# 调整采样周期(单位:微秒)
echo 50000 > /sys/devices/system/cpu/cpufreq/ondemand/sampling_rate

# 调整升频阈值
echo 85 > /sys/devices/system/cpu/cpufreq/ondemand/up_threshold

schedutil 的优势

schedutil 是 4.7 内核引入的「新秀」。它直接利用调度器的 PELT(Per-Entity Load Tracking)信号,不需要额外采样。说白了,调度器已经知道每个任务需要多少算力,schedutil 直接拿来用。

我的建议:如果你的内核版本 ≥ 4.7,优先用 schedutil。它在响应速度和功耗之间取得了更好的平衡。我在一个视频编解码项目上对比过,schedutil 比 ondemand 省电约 12%。

# 启用 schedutil
echo schedutil > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

# 查看 schedutil 参数(内核 5.4+)
cat /sys/devices/system/cpu/cpufreq/schedutil/rate_limit_us

# 设置速率限制(默认 1000us)
echo 2000 > /sys/devices/system/cpu/cpufreq/schedutil/rate_limit_us

3.4 用户空间策略定制

有时候现成的 governor 满足不了需求。比如你的设备有特殊的功耗曲线,或者需要根据温度、电池电量做联合调频。这时候就要自己写策略了。

方法一:userspace governor + 用户态脚本

最简单的方式。用户程序通过 sysfs 直接写频率。

#!/bin/sh
# 简易温度感知调频脚本
while true; do
    temp=$(cat /sys/class/thermal/thermal_zone0/temp)
    if [ "$temp" -gt 65000 ]; then
        # 温度超过 65°C,降频到 800MHz
        echo 800000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
    elif [ "$temp" -lt 45000 ]; then
        # 温度低于 45°C,升频到 1.2GHz
        echo 1200000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
    fi
    sleep 2
done

小技巧:userspace 模式下,记得先查看 scaling_available_frequencies,确保你写的频率是硬件支持的。我见过有人写了个不存在的频率,结果驱动直接忽略请求。

方法二:编写自定义 governor 内核模块

这是更高级的做法。你需要实现 cpufreq_governor 结构体中的回调函数。

#include <linux/cpufreq.h>

static struct cpufreq_governor my_gov = {
    .name = "my_custom_gov",
    .owner = THIS_MODULE,
    .init = my_gov_init,
    .exit = my_gov_exit,
    .start = my_gov_start,
    .stop = my_gov_stop,
    .limits = my_gov_limits,
};

// 核心调频决策函数
static void my_gov_work(struct work_struct *work)
{
    unsigned int freq;
    unsigned long load = get_cpu_load();  // 获取当前负载

    if (load > 80)
        freq = 1400000;  // 高频
    else if (load > 40)
        freq = 1000000;  // 中频
    else
        freq = 600000;   // 低频

    __cpufreq_driver_target(policy, freq, CPUFREQ_RELATION_L);
}

module_init(my_gov_init);
module_exit(my_gov_exit);

注意:编写自定义 governor 时,一定要处理好并发问题。调频回调可能被多个 CPU 同时调用。我早期写的模块就因为这个导致死锁,整机挂死。

3.5 DVFS 配置实战

DVFS(Dynamic Voltage and Frequency Scaling)是 CPUFreq 的底层支撑。调频的同时必须调压,否则芯片会不稳定。

频率 (MHz) 电压 (mV) 功耗 (mW) 说明
1400 1200 850 最高性能
1000 1050 520 均衡模式
600 900 210 省电模式
200 750 45 深度睡眠

从表格可以看出,频率降低 50%,功耗能降 75%。这就是 DVFS 的价值所在。

# 查看支持的频率和电压
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies

# 查看当前频率
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq

# 查看可用 governor
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors

# 设置频率范围(最小值 600MHz,最大值 1.2GHz)
echo 600000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq
echo 1200000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq

关键经验:DVFS 的电压表(OPP Table)通常由设备树或固件提供。我在调试一个平板项目时,发现某颗芯片的 1.4GHz 档位电压偏低,导致高负载下随机重启。后来在设备树中微调了 OPP 的电压值才稳定。

3.6 调试与验证

配置完 CPUFreq 后,一定要验证效果。我常用的方法:

  1. 查看调频日志:开启内核的 CPUFreq 调试选项 CONFIG_CPU_FREQ_DEBUG
  2. 实时监控频率:用 watch -n 1 "cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq"
  3. 功耗测量:用电流计或板载 ADC 读取实际功耗
  4. 压力测试:运行 stress --cpu 4 观察频率响应和温度变化

我的习惯:每次改完 CPUFreq 配置,先用 trace-cmd 抓一段 cpufreq 事件,看看调频是否过于频繁。如果 1 秒内调频超过 10 次,说明参数需要优化。

CPUFreq 调优没有银弹。不同的硬件、不同的 workload,最优配置都不一样。多试、多测、多分析 trace,慢慢就能找到感觉。