做 Linux 功耗优化的同学应该都有体会,功耗和温度永远是一对拆不开的冤家。CPU 跑得越猛,功耗越大,SoC 温度爬升越快,快到阈值就不得不降频,性能又掉下来。这个“检测温度、做出决策、执行降温动作”的闭环,在内核里就是 thermal framework。这篇是 Linux 内核功耗子系统系列的第九篇,我按自己的理解,把这套框架的通用架构完整梳理一遍。内容会偏 BSP 和驱动开发视角,从核心组件、数据结构、设备树配置到 governor 选型、调试手段都会过一遍,适合正在做温控接入、功耗限制或者被“莫名降频”问题折磨的嵌入式开发者。
1. thermal framework 到底是什么,它管得住什么问题
1.1 功耗与温度的“最后一公里”
很多刚接触功耗子系统的同事会有一个误解:温控就是“温度高了降频”,逻辑很简单。但真正落到内核里,这一件事被拆成了三个层次的问题。
第一层是感知:你得知道当前芯片内部、外壳、电池附近的温度是多少,这靠温度传感器驱动来完成。第二层是决策:拿到温度之后,是降一级频率就够了,还是需要直接关掉某个耗电模块?这需要一套策略逻辑。第三层是执行:决策出来了,然后去调整 CPU 频率、打开风扇、限制 GPU 带宽,甚至触发系统关机,这靠的是各种 cooling device。
thermal framework 就是把这三层串起来的骨架。它本身不做具体的降温动作,也不负责温度采样,它只定义标准和机制,让传感器和冷却设备能够对接、让策略能统一调度。所以看 thermal 代码时,你会发现核心目录 drivers/thermal 下真正复杂的不是某个硬件驱动,而是 thermal_core.c、thermal_helpers.c、governor 相关的这套通用逻辑。
1.2 三大核心组件
从架构上看,thermal framework 里面有三个绕不开的概念:thermal zone、governor、cooling device。
thermal zone 是热区,可以理解成一个“温度管理域”。一块芯片上可能有 CPU、GPU、DDR 多个发热源,你可以给它们建一个或多个热区。每个热区至少绑一个温度传感器,并定义若干温度阈值,也就是 trip points。
governor 是温控策略,相当于热区的“大脑”。当热区温度超过某个 trip 点后,governor 决定要不要动、动多少。常见的策略有 step_wise、power_allocator、user_space、bang_bang,后面我会分开聊。
cooling device 是冷却设备,是真正干活的执行者。它抽象了所有可以“降温”的手段,比如 CPU 调频、DDR 降频、风扇调速。每个 cooling device 对外只露出一个状态接口,叫 cur_state,范围从 0 到 max_state。0 表示不限制,数值越大限制越狠。
这三者的关系,我用一句话概括:传感器把温度喂给 thermal zone,thermal zone 把温度变化告诉 governor,governor 根据 trip 点决策出目标状态,最终通过绑定关系去改变 cooling device 的 cur_state。
1.3 一次完整的温度到降温动作的数据流
顺着一次真实的温控流程去看会更清楚。假设 CPU 热区配置了 85 度的 passive trip,传感器每 100ms 上报一次温度。
第一步,传感器驱动通过中断或者轮询拿到温度,调 thermal_zone_device_update 通知 framework。第二步,framework 遍历这个热区的所有 trip,逐个判断温度是否越过阈值。第三步,如果超过了某个 trip,就调用当前 governor 的 throttle 回调。第四步,governor 根据当前温度趋势,决定把绑在这个 trip 上的 cooling device 的 cur_state 加一还是减一。第五步,cooling device 真正去调 cpufreq 接口限频,或者给风扇驱动发指令。
注意这个流程里没有“全局统筹”。每个 trip 各自绑定一组 cooling device,温度越高的 trip,绑定的冷却设备一般越激进。比如 85 度只允许 CPU 频率降到第二档,95 度的 trip 可能就直接把 CPU 压到最低频,同时还限制 GPU。这套分级设计的好处是简单可控,坏处是缺少全局最优解,这也是为什么后来有了 power_allocator 这样的 governor,从功率预算的角度去做分配。
2. 核心数据结构与内核 API 细节
2.1 thermal_zone_device:热区的“本体”
在内核里,一个热区就是一个 struct thermal_zone_device。我建议大家直接去阅读 include/linux/thermal.h,那里的注释还算友好。这个结构体里几个关键字段得记牢。
type 是热区名字,字符串形式,比如 "cpu-thermal"。id 是热区编号,注册时由内核分配,对应 sysfs 里 thermal_zone0、thermal_zone1 的后缀。ops 是回调集合,传感器驱动的核心能力都挂在里面。governor 指向当前生效的温控策略。trips 是温度阈值点的数组,每个元素包含 trip 类型、温度值、hysteresis 迟滞值。passive_delay 和 polling_delay 是轮询间隔,前者是进入被动散热后的轮询周期,后者是常规状态下的周期。
还有一个容易被忽略的字段是 forced_passive。有些平台在温度冲到一定值后,即使没有配置 passive trip,内核也会用这个字段表示“已经处于被动散热状态”,从而频繁轮询。它和你 DTS 里的 polling-delay-passive 是联动的。
2.2 thermal_zone_device_ops:回调函数才是灵魂
传感器驱动不会直接操作 thermal_zone_device 内部数据,它只需要实现一组回调。老版本内核里最核心的是 get_temp,framework 调它来获取当前温度,单位是毫摄氏度,也就是 85000 代表 85 度。别小看这个单位,很多人写驱动时在这里翻车:把传感器读到的摄氏度直接填进去,结果温度永远是 85 度,冷却设备疯狂动作。
还有几个回调值得说。set_trips 是给硬件设置中断阈值的,支持硬件中断的传感器会用到;framework 会在温度接近 trip 点时调用它,让传感器在越过阈值时主动产生中断,而不是靠 CPU 轮询。get_trend 是拿温度变化趋势的,step_wise 这类 governor 依赖趋势来判断是升温还是降温。另外还有 set_mode、set_trip_temp,分别用来开关热区和动态修改 trip 温度。
手动注册热区的老接口格式大致是这样:
static const struct thermal_zone_device_ops my_ops = { .get_temp = my_get_temp, .get_trend = my_get_trend, }; static const struct thermal_zone_params my_tzp = { .governor_name = "step_wise", .no_hwmon = false, }; tz = thermal_zone_device_register("my-zone", 3, 0, priv, &my_ops, &my_tzp, 0, 1000); if (IS_ERR(tz)) return PTR_ERR(tz);注意新版内核的 thermal_zone_device_register 签名已经改了,第二个参数从 trip 数量变成了 struct thermal_trip 数组指针。写驱动前一定先查你内核版本的 thermal.h,不要拿网上老代码直接抄。
2.3 cooling device 与绑定的实现
cooling device 的注册方式类似,核心是 struct thermal_cooling_device_ops。需要实现三个回调:get_max_state 返回最大状态值,get_cur_state 返回当前限制状态,set_cur_state 设置限制状态。
cpufreq 冷却设备是最典型的例子。它的 max_state 一般对应 CPU 频率档位数减一,cur_state 越高,允许的最高频率越低。当 cur_state 切到某个档位,它内部会通过 cpufreq_update_policy 把 CPU 频率上限压下去。
热区和冷却设备之间的绑定有两种途径。一种是手动调 thermal_zone_bind_cooling_device,把某个 trip 和某个 cooling device 绑起来:
thermal_zone_bind_cooling_device(tz, trip_id, cdev, THERMAL_NO_LIMIT, THERMAL_NO_LIMIT);另一种是设备树自动绑定,解析 cooling-maps 后由 of-thermal 帮你完成绑定。大多数现代平台用的都是后者,手动绑定多见于老平台或者纯代码初始化场景。
2.4 trip point 的形式与内核版本差异
trip point 描述的是“什么温度下做什么事”。它有一个类型枚举,常见四种:
| 类型 | 含义 | 典型动作 |
|---|---|---|
| active | 主动散热 | 开风扇、增强通风 |
| passive | 被动散热 | 降频、降功耗 |
| hot | 热警告 | 更激进限频,通知系统 |
| critical | 临界 | 强制关机或硬件保护 |
critical 是最后一道防线。内核检测到温度超过 critical trip 后,会尝试 orderly_poweroff,如果系统卡死还会走更底层的 emergency poweroff。所以真实产品里,critical 一般同时依赖硬件 PMIC 或者 EC 的保护电路,不会完全信任软件。
每个 trip 还有一个 hysteresis 迟滞值。这个值非常重要,它表示从多少度开始恢复。比如 trip 温度 85、hysteresis 5,那么温度升到 85 触发降温,要降到 80 才解除限制。没有 hysteresis 的话,温度在 85 附近轻微波动就会导致频率反复跳变,实际体验会非常差。
3. 从设备树到驱动:怎么把一个传感器接入 framework
3.1 thermal-zones 节点与 thermal-sensor 属性
现代 ARM 平台基本都用设备树描述热区,由 of-thermal 模块在启动时统一解析并注册。这意味着传感器驱动本身不需要调用 thermal_zone_device_register,只要正确实现 thermal_zone_of_device_ops 并把自己注册成“温度传感器”就行。
顶层 thermal-zones 节点下,每个子节点就是一个热区。核心属性是 thermal-sensors,格式是一个 phandle 加 sensor_id,比如 thermal-sensors = <&board_sensor 0>;。sensor_id 对应传感器驱动注册时传入的 sensor_id 参数,主要是为了支持一个芯片里有多个通道的场景。
热区节点还支持 polling-delay 和 polling-delay-passive 两个毫秒单位属性。前者是正常状态下的轮询间隔,后者是进入被动散热后的轮询间隔。我见过很多默认配置把两个都写成 1000ms,温度已经过了 trip 点还慢慢悠悠一秒才查一次,这会导致降温响应慢半拍。
3.2 trips 和 cooling-maps 的写法与注意事项
一个典型的热区节点长这样:
thermal-zones { cpu-thermal { polling-delay-passive = <100>; polling-delay = <1000>; thermal-sensors = <&board_sensor 0>; trips { cpu_alert0: cpu-alert0 { temperature = <75000>; hysteresis = <5000>; type = "passive"; }; cpu_alert1: cpu-alert1 { temperature = <85000>; hysteresis = <5000>; type = "hot"; }; cpu_crit: cpu-crit { temperature = <100000>; hysteresis = <2000>; type = "critical"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu0 0 2>; }; map1 { trip = <&cpu_alert1>; cooling-device = <&cpu0 0 THERMAL_NO_LIMIT>; }; }; }; };注意 cooling-device 里三个 cell 的含义:第一个是起始状态,第二个是最大状态。它限定了这个 trip 对这个 cooling device 的操作范围。map0 里写 0 2,意思是降到第二档就够了;map1 里写 THERMAL_NO_LIMIT,就是完全不设上限,可以一路压到最低频。
contribution 属性是给 power_allocator 用的,表示该冷却设备的功耗占比权重,其他 governor 基本忽略它。如果你切到 IPA 后频率分配不符合预期,先检查每个 map 的 contribution 是否合理。
3.3 传感器驱动的常见实现
传感器驱动接入 framework 的标准姿势是调用 devm_thermal_zone_of_sensor_register。这个接口会返回对应热区的 thermal_zone_device 指针,如果设备树里找不到匹配的热区会返回错误。
一个最简单的温度传感器驱动骨架:
#include <linux/thermal.h> #include <linux/platform_device.h> struct board_thermal_priv { struct device *dev; struct thermal_zone_device *tz; }; static int board_get_temp(void *data, int *temp) { struct board_thermal_priv *priv = data; int raw; raw = board_sensor_read_raw(priv->dev); if (raw < 0) return raw; /* 原始值换算成毫摄氏度 */ *temp = board_sensor_raw_to_millicelsius(raw); return 0; } static const struct thermal_zone_of_device_ops board_ops = { .get_temp = board_get_temp, }; static int board_thermal_probe(struct platform_device *pdev) { struct board_thermal_priv *priv; int ret; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->dev = &pdev->dev; platform_set_drvdata(pdev, priv); priv->tz = devm_thermal_zone_of_sensor_register(&pdev->dev, 0, priv, &board_ops); if (IS_ERR(priv->tz)) return PTR_ERR(priv->tz); return 0; }这个驱动本身不创建热区,它只负责提供 get_temp。热区的完整定义在 DTS 里。如果你的热区里有多个 trip,框架会自己处理,驱动不需要关心。这也是 of-thermal 最舒服的地方:驱动只管上报温度,策略和绑定都在设备树里配。
3.4 注册成功后 sysfs 里长什么样
系统起来后,热区信息会暴露在 /sys/class/thermal/thermal_zone0 下面。平时调试最常用的几个入口:
- temp:当前温度,毫摄氏度。
- type:热区名字。
- policy:当前 governor 名字,可以直接 echo 切换。
- trip_point_0_temp / trip_point_0_type:每个 trip 的阈值和类型。
- emul_temp:温度模拟入口,写一个毫摄氏度值就能欺骗温度读值,测试温控逻辑非常方便。
对应地,冷却设备在 /sys/class/thermal/cooling_device0 下面,cur_state 可读可写。手动写 cur_state 是测试冷却设备是否正常的好办法,比如散热风扇平台,echo 1 > cur_state 看风扇会不会转。
4. governor 温控策略:选型与原理
4.1 step_wise:最常用的保守派
step_wise 是大多数默认配置的选择,也是入门最容易理解的一个。它的核心思路是“走台阶”:温度超过 trip 点后,每次 update 最多把 cur_state 加一;温度回落后,再逐步减一,而不是一下从 0 跳到 max_state。
这个策略依赖 get_trend 或者内部计算的趋势方向。趋势判断为升温时,它会加大限制;趋势为降温时,它倾向于保持当前状态,等稳定一段时间再降。这种设计是为了避免频率大幅震荡。
step_wise 的优点是简单、稳定、可预期。缺点是它只管“动一步算一步”,没有全局的功耗意识。比如 CPU 和 GPU 同时超温,step_wise 会各自按各自热区往下降,不会考虑“降 GPU 比降 CPU 性价比更高”这类优化。
4.2 power_allocator(IPA):按功率预算分配
power_allocator 是 thermal framework 里最有“科技感”的 governor,它也常被称为 IPA。它把整个热区看成一个功率分配问题,目标是让系统功耗维持在一个可持续的水平附近,从而把温度控制在 trip 内。
IPA 的工作前提是冷却设备必须支持功率模型。cpufreq 冷却设备在注册时会通过 CPU 的 OPP 表推导出每个频率档位对应的功率,这样 governor 才能把“需要降低多少功耗”换算成“频率降到哪一档”。
控制逻辑上,IPA 内部是一个 PID 控制器。核心参数有这些:
| 参数 | 作用 |
|---|---|
| sustainable_power | 系统可持续承受的功耗,单位 mW,是校准基准 |
| k_po | 超温时的比例系数,控制响应速度 |
| k_pu | 低于温度目标时的比例系数 |
| k_i | 积分系数,消除稳态误差 |
调试 IPA 时最常改的是 sustainable_power。这个值如果设得太小,系统会过早降频,性能损失明显;设得太大,温度压不住。我自己的经验是先按产品外壳散热能力估一个值,然后做真实负载测试,观察温度曲线和频率曲线来反向修正。
4.3 其余策略与触发机制
bang_bang 是开关式控制,适合风扇这类只有“开/关”两种状态的设备。它配合 hysteresis 使用,温度超过打开阈值就开,低于关闭阈值就关,实现简单,但没有中间状态,风扇会频繁启停。
fair_share 是加权分配策略,根据每个冷却设备的权重去分摊负载,使用场景不太广。user_space 则干脆不做内核决策,只通过 uevent 把温度变化通知用户态,由用户态程序决定怎么做。调试阶段我很喜欢临时切到 user_space,这样内核不会干预上限频率,方便单独验证 CPU 本身能不能跑上去。
所有 governor 的触发入口都是 thermal_zone_device_update。这个函数会在三种情况下被调用:轮询超时、温度传感器硬件中断、强制手动调。进入 update 后,framework 会重新读温度,遍历 trip,然后调当前 governor 的 throttle 回调。所以你在驱动里频繁调 thermal_zone_device_update 会非常影响性能,硬件能支持中断就尽量用中断。
4.4 如何切换和编译 governor
编译时,governor 由 CONFIG_THERMAL_GOV_* 系列开关控制。比如 CONFIG_THERMAL_GOV_STEP_WISE、CONFIG_THERMAL_GOV_POWER_ALLOCATOR。默认至少要编一个 step_wise 进去,否则热区注册会因为没有可用 governor 而失败。
运行时可以直接改 sysfs 里的 policy 文件来切换,不需要重新编译。这个特性在调参阶段非常有用。我经常在测试脚本里按顺序切 user_space、step_wise、power_allocator,让同一款设备跑同一款压力测试,对比温度曲线,很快就能看出哪个策略适合当前产品形态。
5. 实操:给一块板子加上 CPU 降频温控
5.1 确认内核 config
动手之前,先确认内核打开了这些选项。需要 CONFIG_THERMAL、CONFIG_THERMAL_OF、CONFIG_THERMAL_GOV_STEP_WISE,如果要用 IPA 还需要 CONFIG_THERMAL_GOV_POWER_ALLOCATOR。CPU 调频冷却需要 CONFIG_CPU_FREQ,以及对应的 cpufreq cooling 支持,一般是 CONFIG_CPU_THERMAL。
老平台还有 CONFIG_THERMAL_EMULATION,用来使能 sysfs 里的 emul_temp 节点。这个开关测试温控逻辑特别重要,否则你只能靠热风枪吹板子验证。
5.2 写 DTS
以第五章那个 DTS 为基础,你需要额外确认两点:一是板子上确实有温度传感器设备树节点,compatible 对应你的驱动;二是在 cpus 节点里给 CPU 加 #cooling-cells = <2>。后者是 cpufreq cooling device 能被 cooling-maps 引用的前提。
cpus { cpu0: cpu@0 { compatible = "arm,cortex-a55"; reg = <0x0 0x0>; #cooling-cells = <2>; ... }; };如果漏了 #cooling-cells,系统启动时 dts 校验会报错,冷却设备绑定失败,但你 sysfs 里未必看得到明显异常,可能只是温度一直压不住。这类问题排查起来最头疼,我建议 probe 完成日志里主动打印绑定的 cooling device 名字。
5.3 验证流程
编译烧录后,第一步确认热区注册成功:
ls /sys/class/thermal/ cat /sys/class/thermal/thermal_zone0/type cat /sys/class/thermal/thermal_zone0/temp cat /sys/class/thermal/thermal_zone0/policy正常能看到 cpu-thermal、真实温度、默认 governor 名字。确认 cooling device 存在:
cat /sys/class/thermal/cooling_device0/type cat /sys/class/thermal/cooling_device0/max_statetype 一般是 cpufreq,max_state 表示频率档位数。然后手动测冷却设备是否正常,直接把 cur_state 拉到最大值再读 CPU 频率上限:
echo 2 > /sys/class/thermal/cooling_device0/cur_state cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq如果频率确实降下来了,说明冷却链路没问题。最后用 emul_temp 模拟高温,验证 governor 联动:
echo 90000 > /sys/class/thermal/thermal_zone0/emul_temp cat /sys/class/thermal/cooling_device0/cur_state正常几秒内 cur_state 会从 0 往上走。测试完记得 echo 0 恢复,否则下一次真实温控会被模拟温度干扰。
5.4 压力测试与参数调整
验证完基本流程,还要做负载压力测试。比如多线程跑 dhrystone、循环做内存拷贝,同时用 thermal 事件记录温度曲线。压力测试至少跑 30 分钟,重点看三件事:温度是否稳定在 passive trip 附近、频率是否存在周期性大幅摆动、有没有因为临界保护直接断电重启。
参数调整时我优先看 hysteresis。如果实测波动在 2 到 3 度之间,hysteresis 给 5 度以上比较安全。其次是 polling-delay-passive,不需要追求过高的轮询频率,100ms 到 250ms 对多数嵌入式平台足够。太快的轮询只会增加系统唤醒次数,对功耗反而有害。最后才是 governor 的配置,比如 IPA 的 PID 参数,这部分建议依赖 trace 数据而不是拍脑袋调。
6. 常见问题、踩坑记录与调试工具
6.1 热区不注册、温度读不到
热区没注册,第一步查 /sys/class/thermal/ 下面有没有对应 type 的目录。如果没有,多半是设备树节点没被解析到,或者 compatible 不匹配。确认 CONFIG_THERMAL_OF 已打开,并且驱动的 of_match_table 写正确。
传感器设备 probe 成功但温度节点读出来是 0 或者错误码,问题基本在 get_temp 实现。最常见的坑是温度单位搞错。传感器寄存器里读出来的原始值往往需要加 offset、乘系数,最终换算成毫摄氏度。你可以在驱动里临时用 dev_info 直接打点,确认返回值合理后再接 framework。
还有一种情况:热区注册成功了,但 temp 永远显示一个固定值。这通常是传感器挂在 I2C 上,而 I2C 读取加入了 cache 或者被电源管理关掉了,导致每次读回的都是最后一次的缓存值。检查传感器的 power management 回调,确保 get_temp 调用期间设备处于可读状态。
6.2 governor 不触发或者疯狂触发
governor 完全不触发,先看 policy 文件里是不是当前想要的策略。如果默认是 fair_share,而且你没配权重,它可能根本不动。直接 echo step_wise 试试。
疯狂触发通常是 hysteresis 太小。温度在 trip 点附近抖动,每次抖动都跨越阈值,governor 就不停地加状态减状态。解决办法是调大 hysteresis,让恢复阈值比触发阈值低 3 到 5 度。还有一个隐藏因素:多个传感器绑在一个热区时,框架取的是最高温度还是平均温度取决于实现,如果你的两个传感器温差大,会导致温度值来回跳。
触发太快还有一种可能:polling-delay-passive 设置太激进,比如 10ms,CPU 大部分时间都在做 thermal update。用 trace 看一下 update 频率,如果每秒超过几十次而且温度变化不大,就该把 polling 调大。
6.3 cooling device 生效但降温幅度不对
这个问题我碰过好几次。表现是 cur_state 确实在涨,但 CPU 频率上限没怎么降。原因大概率是 cpufreq cooling device 的 max_state 和实际频率档位不匹配。比如 CPU 有 5 个档位,但 max_state 只有 2,相当于每档跳了很大幅度。
另一个原因是 cooling-maps 限制了最大状态。你 DTS 里 cooling-device = <&cpu0 0 2>,那就永远只能在 0 到 2 之间动,后面的 trip 如果不覆盖更激进的 map,即使温度到了 critical 也不会降到最低频。建议在 hot 和 critical trip 的 map 里把上限放开。
6.4 用 trace 抓热事件
调试 thermal 问题,最有效的工具是内核 trace。相关事件在 /sys/kernel/tracing/events/thermal/ 下面,包括 thermal_temperature、thermal_zone_trip、cdev_update 等。用 trace-cmd 抓一段很直观:
trace-cmd record -e thermal_temperature -e cdev_update -e thermal_zone_trip然后施加负载,结束后看 report。你能清楚看到温度从多少度开始跳、哪个 trip 被触发、冷却设备的 cur_state 每个时刻是多少。这比反复 cat sysfs 准确得多,因为 sysfs 是采样,trace 是全程记录。
如果内核没开 CONFIG_TRACEPOINTS,也可以用 ftrace 的 hwlat 或者直接把 dev_info 临时加进 thermal_zone_device_update 打点。但 trace 方案最干净,不改代码,线上也能用。
6.5 与调度器的 thermal pressure 联动
做功耗优化的工程师可能还听说过 thermal pressure,它和 thermal framework 关系密切。当 CPU 因为温控被降频后,调度器的 CPU 容量计算需要知道这件事,否则它会把任务继续往这个“看起来很空闲”的 CPU 上放,结果导致任务排队更长。
内核通过 arch_set_thermal_pressure 把 thermal 造成的频率限制转换成容量损失。编译选项是 CONFIG_SCHED_THERMAL_PRESSURE。在 ARM 平台上,这会影响调度器的负载均衡和 EAS 的功耗计算。如果你发现温控限频后系统整体延迟反而更高,先检查这个配置是否打开。
这个设计也解释了一个现象:单纯看 cpufreq 频率上限觉得“还行”,但调度器已经认为这个 CPU 快废了,从而把任务切走。温控的影响不只是降频本身,还会传导到任务调度和功耗分配。所以调温控参数时,不能只看温度曲线,还要看调度器负载分布。
我自己调 thermal 参数时有个习惯:先把 critical trip 加到产品规格允许的上限,然后切到 user_space,用真实负载测出 CPU 热极限;再切回 step_wise,逐步收紧 passive trip 和 hysteresis,观察频率曲线是否平滑。这样每一步变量都单一,出了问题也知道改的是哪里。thermal framework 看着组件多,真正吃透之后,它本质就是一个带策略的传感器聚合器和限频执行器。你只要记住“zone 感知、governor 决策、cdev 执行”这条主线,遇到问题就不会被卷进细节里出不来。