1. 从一次温控翻车说起:thermal framework 到底管什么
前阵子帮朋友排查一块嵌入式板子的问题,现象很典型:设备跑高负载任务不到三分钟,CPU 频率就被压到最低档,性能直接腰斩,但手摸散热片温度并不算烫。第一反应是散热没做好,加了风扇、换了导热硅脂,问题依旧。后来把内核日志打开,盯着 thermal 相关的打印看,才发现是温控子系统在"过度保护"——某个温度传感器的读数比实际偏高十几度,触发了降频阈值。
这个案例几乎是我接触 Linux 功耗子系统以来遇到的最常见的一类问题:thermal framework 本身没坏,坏的是它和传感器、设备树、调频策略之间的配合。很多人一看到降频就怀疑 CPU 或者散热,其实真正该先看的是 thermal zone 的配置和温度来源。
thermal framework 是 Linux 内核功耗子系统里负责"温度感知 + 温控动作"的那一层。它要做的事情说起来简单:读温度、比阈值、触发冷却。但真正落到代码和配置上,它牵扯的东西非常多——传感器驱动、设备树描述、governor 策略、cooling device 注册、trip point 定义、hysteresis 防抖,每一环出问题都会表现为"温控不按预期工作"。
这篇文章我打算把 thermal framework 的通用架构从头到尾梳理一遍。不是照搬内核文档的翻译,而是按照一个实际做嵌入式、做功耗优化的人会关心的顺序来讲:它由哪些部分组成、各部分怎么协作、设备树怎么写、governor 怎么选、出问题怎么定位。适合已经能看懂基本内核代码、正在做功耗或温控相关工作的读者,也适合刚接触这块、想建立整体认知的朋友。
关键词里提到的 Linux、内核、thermal framework、功耗子系统、通用架构,基本就是本文的主线。下面我会尽量把每个概念都落到"它在实际系统里对应什么、出问题时长什么样"这个层面上。
2. thermal framework 的四大件:zone、sensor、governor、cooling device
要理解 thermal framework,最有效的办法不是先看代码,而是先建立一张"角色关系图"。整个框架可以拆成四个核心角色,它们之间的关系决定了温控行为的一切。
2.1 thermal zone:温控的逻辑单元
thermal zone 是整个框架的组织中心。你可以把它理解成"一个需要被监控和保护的温区"。比如一颗 SoC 上,CPU 集群是一个 zone,GPU 是一个 zone,DDR 可能又是一个 zone。每个 zone 独立定义自己的温度阈值和冷却策略。
一个 thermal zone 在内核里对应struct thermal_zone_device。它内部维护着几个关键信息:
- 这个 zone 绑定哪个温度传感器(或者哪几个)
- 定义了哪些 trip point(触发点)
- 每个 trip point 关联哪些 cooling device
- 当前使用哪个 governor
- 温度轮询的延迟(polling delay)
我见过不少配置把整颗芯片只做成一个 zone,所有传感器混在一起取最大值。这种做法在简单场景下能用,但一旦某个局部热点和整体温度差异大,就会出现"局部过热但整体没到阈值"或者"整体到了阈值但热点早就超了"的问题。合理的做法是按热耦合关系划分 zone,让每个 zone 的温度来源尽量反映它真正要保护的那部分电路。
2.2 thermal sensor:温度数据的来源
sensor 是数据的源头。内核里 sensor 驱动通过thermal_zone_device_register或者较新的devm_thermal_of_zone_register注册自己,向框架提供读温度的回调。
这里有个很容易被忽略的点:sensor 的精度和采样频率直接决定温控的响应质量。有些片上温度传感器(比如 SoC 内部的 TSADC)精度只有 ±5℃,而且采样有延迟。如果你把 trip point 设得离正常工作温度很近,就可能因为读数抖动频繁触发降频。我在实际项目里一般会留出至少 10℃ 的余量,除非散热设计非常确定。
另外,sensor 的读数是否需要校准、是否有多个 sensor 需要加权,这些都属于 zone 层面的策略。框架本身提供了把多个 sensor 绑定到一个 zone 的能力,取最大值或者做平均都可以通过配置实现。
2.3 governor:温度到动作的决策逻辑
governor 是 thermal framework 的"大脑"。它决定当温度达到某个 trip point 时,具体怎么调节 cooling device。内核里常见的 governor 有几种:
| governor | 行为特点 | 适用场景 |
|---|---|---|
| step_wise | 每达到一个 trip 就升一档冷却 | 通用,最常用 |
| power_allocator | 基于功率预算动态分配 | 移动端、需要精细功耗控制 |
| fair_share | 多个 cooling device 按权重分摊 | 多设备协同降温 |
| bang_bang | 到阈值就开、低于阈值就关 | 简单风扇控制 |
| user_space | 把决策交给用户空间 | 需要自定义策略时 |
step_wise是默认选项,行为最直观:温度每跨过一个 trip point,就把对应的 cooling device 状态往上调一级。power_allocator则复杂得多,它需要 cooling device 提供功率模型,然后根据当前温度和目标温度反推该分配多少功率。移动端做续航优化时这个 governor 很关键,但配置门槛也高。
选 governor 的核心判断标准是:你的冷却手段是离散档位还是连续可调。风扇、降频档位这种离散的,step_wise 就够;需要连续调节 CPU 频率和电压的,power_allocator 更合适。
2.4 cooling device:真正执行降温的部件
cooling device 是"手"。CPU 降频、GPU 限频、风扇调速、甚至主动限制充电电流,都可以注册成 cooling device。它通过struct thermal_cooling_device注册,核心是set_cur_state回调。
这里的关键概念是 ** cooling state**:每个 cooling device 有一组离散的状态,从 0(不冷却)到 max_state(最大冷却)。比如一个 CPU 调频 cooling device,state 0 可能是最高频,state N 是最低频。governor 决定把 state 设成几,cooling device 就执行对应的动作。
我踩过的一个坑是:cooling device 的 state 语义在不同驱动里方向可能相反。有的驱动 state 越大越"冷"(降频越多),有的则相反。如果搞反了,就会出现温度越高反而性能越好的诡异现象。注册 cooling device 时一定要确认get_max_state返回的语义,并在set_cur_state里做正确的映射。
把这四个角色串起来,一次完整的温控流程是这样的:sensor 提供温度 → zone 拿温度跟 trip point 比较 → 触发后交给 governor → governor 计算 cooling device 的目标 state → cooling device 执行。任何一环配置错误,最终表现都是"温控不工作"或"温控过度"。
3. 设备树里的 thermal 描述:trip point 与 cooling map 怎么写
对嵌入式开发者来说,thermal framework 打交道最多的地方其实是设备树。内核代码基本不用改,配置全在 DTS 里。这一节我把设备树里 thermal 相关的节点结构拆开讲,重点说清楚每个字段的含义和常见写法。
3.1 thermal zone 节点的基本结构
一个典型的 thermal zone 节点长这样:
thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <100>; polling-delay = <1000>; thermal-sensors = <&tsadc 0>; trips { cpu_alert0: cpu-alert0 { temperature = <70000>; hysteresis = <2000>; type = "passive"; }; cpu_crit: cpu-crit { temperature = <95000>; hysteresis = <2000>; type = "critical"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; }; }; }; };逐字段解释一下。polling-delay-passive是进入被动冷却状态后的轮询间隔,单位毫秒;polling-delay是正常状态下的轮询间隔。这两个值直接影响响应速度和功耗——轮询越频繁,响应越快,但 CPU 被唤醒的次数也越多,反而增加功耗。我一般把 passive 设成 100ms 左右,正常状态设成 1000ms,兼顾响应和开销。
thermal-sensors指向具体的传感器。注意这里可以写多个,框架会按注册顺序处理。
3.2 trip point 的类型与选择
trip point 的type字段决定了触发后的行为,这是最容易配错的地方。内核支持的类型主要有:
- passive:触发被动冷却,通常关联 CPU 降频。这是最常用的类型。
- active:触发主动冷却,通常关联风扇。需要 cooling device 支持主动散热。
- critical:达到此温度直接触发系统关机或重启,是最后一道保护。
- hot:仅上报事件,不强制动作,供用户空间感知。
- emergency:比 critical 更紧急,用于硬件级保护。
hysteresis是回差,防止温度在阈值附近抖动导致反复触发。比如 alert0 设 70℃、hysteresis 设 2℃,那么温度升到 70℃ 触发降频,要降到 68℃ 以下才解除。这个值设太小会导致频繁抖动,设太大会导致降温不及时。经验值是 2℃ 到 5℃ 之间,具体看传感器精度和散热惯性。
注意:critical 类型的 trip point 一定要设,而且要和硬件手册里的绝对最大温度留出余量。我见过有配置把 critical 设得比芯片规格还高,等于这道保护形同虚设。
3.3 cooling map:把 trip 和 cooling device 绑起来
cooling-maps 节点是连接 trip point 和 cooling device 的桥梁。每个 map 里trip指向一个 trip point,cooling-device指向一个 cooling device,后面两个参数是 state 的最小值和最大值。
THERMAL_NO_LIMIT是个宏,表示不限制。如果写成<&cpu0 0 3>,意思是这个 cooling device 的 state 范围是 0 到 3。governor 会在这个范围内调节。
一个 trip point 可以关联多个 cooling device,一个 cooling device 也可以被多个 trip 引用。这种多对多关系让温控策略非常灵活。比如你可以让 alert0 只降 CPU 频率,alert1 同时降 CPU 和 GPU,crit 直接关机。
我实际项目里常用的一个模式是分级降温:低温阈值先降 GPU(因为 GPU 对性能不敏感),中温阈值降 CPU 大核,高温阈值降 CPU 小核并限内存带宽。这样能在保证体验的前提下把温度压住。
3.4 设备树配置的常见错误
配 thermal 设备树时,我总结了几类高频错误:
第一类是sensor 引用错误。thermal-sensors里的 phandle 和参数必须和 sensor 驱动的of_xlate实现匹配。有些驱动第一个参数是传感器 ID,有些是通道号,写错了要么注册失败,要么读到错误的温度。
第二类是trip 温度单位错误。设备树里温度单位是毫摄氏度,70000 表示 70℃。写成 70 就变成 0.07℃,温控永远不会触发。这个错误非常隐蔽,因为编译不会报错。
第三类是cooling device 未注册。如果 cooling-maps 里引用的 cooling device 驱动没加载或者注册失败,整个 zone 的注册会失败或者该 map 被忽略。排查时先看dmesg | grep thermal有没有相关报错。
第四类是polling delay 设成 0。有些配置为了"实时"把轮询延迟设成 0,结果框架用默认值或者行为异常。轮询延迟有最小值限制,设太小没有意义还增加开销。
4. governor 的工作机制:step_wise 与 power_allocator 的取舍
governor 是 thermal framework 里最"聪明"的部分,也是最值得深入理解的部分。这一节我把两个最常用的 governor 拆开讲,说清楚它们各自的决策逻辑和适用边界。
4.1 step_wise 的逐级调节逻辑
step_wise 的核心思想是"每跨一个 trip,动一档"。它的工作流程大致是:
- 每次温度更新时,遍历所有 trip point,找出当前温度所处的区间。
- 如果温度上升跨过了某个 trip,就把该 trip 关联的 cooling device state 加一。
- 如果温度下降跨过了某个 trip 的回差,就把 state 减一。
- 每个 governor 实例维护一个
target状态,记录上一次的决策。
这个逻辑简单直接,但有个细节需要注意:step_wise 是"每 tick 动一档",不是"一步到位"。如果温度上升很快,它需要多个轮询周期才能把 cooling state 调到足够高。所以 polling delay 设得太大,step_wise 的响应就会滞后。
我在实际使用中总结的经验是:对于散热惯性大的系统(比如有大块金属散热片的工控板),step_wise 配合 100ms 到 200ms 的 passive 轮询延迟比较合适;对于散热惯性小的系统(比如手机这种紧凑结构),可能需要更快的轮询或者换用响应更激进的 governor。
step_wise 还有一个变体行为:当温度超过 critical 时,它会直接把所有关联的 cooling device 拉到最大 state,不再逐级调节。这是硬保护逻辑,不受 governor 常规流程控制。
4.2 power_allocator 的功率预算模型
power_allocator 是移动端和服务器端做精细功耗控制时的首选。它的核心思想不是"温度到了就降频",而是"根据当前温度和目标温度,反推系统还能用多少功率"。
它的工作依赖几个关键输入:
- sustainable power:可持续功率,即系统能长期稳定运行而不升温的功率上限。
- cooling device 的功率模型:每个 cooling device 需要提供
get_requested_power和state2power等回调,告诉框架"当前 state 对应多少功率"。 - PID 控制器参数:power_allocator 内部用一个 PID 控制器来平滑调节,避免功率剧烈波动。
工作流程是:框架读取当前温度,和目标温度比较,通过 PID 算出应该分配的总功率,然后按各 cooling device 的功率模型分配下去。如果某个设备已经到最大冷却状态还不够,就继续压其他设备。
这个 governor 的优势是平滑。step_wise 是阶梯式的,温度到了就跳一档,容易造成性能忽高忽低。power_allocator 是连续调节,性能曲线更平滑,用户体验更好。
但它的门槛也高:需要 cooling device 驱动实现功率模型回调,需要正确配置 sustainable power,PID 参数也需要调。如果这些没配好,power_allocator 的表现可能还不如 step_wise。
4.3 两种 governor 的实测对比
我在一块四核 ARM 板子上做过对比测试,场景是持续跑满 CPU 的编译任务,环境温度 25℃,无风扇。
| 指标 | step_wise | power_allocator |
|---|---|---|
| 稳定后 CPU 频率 | 在 800MHz 和 1.2GHz 之间跳 | 稳定在 1.0GHz 左右 |
| 温度波动 | ±3℃ | ±1℃ |
| 编译完成时间 | 较长,因为频繁降频 | 较短,频率更稳定 |
| 配置复杂度 | 低 | 高 |
结论很明确:如果追求稳定性和能效,power_allocator 更好;如果只是要一个能用的温控,step_wise 足够。选择哪个,取决于你的产品定位和团队对这块的投入意愿。
4.4 governor 切换与调试
运行时可以通过 sysfs 切换 governor:
# 查看当前 governor cat /sys/class/thermal/thermal_zone0/policy # 切换 governor echo power_allocator > /sys/class/thermal/thermal_zone0/policy调试时我常用的几个 sysfs 节点:
/sys/class/thermal/thermal_zone*/temp:当前温度/sys/class/thermal/thermal_zone*/mode:enabled 或 disabled/sys/class/thermal/cooling_device*/cur_state:当前冷却状态/sys/class/thermal/thermal_zone*/trip_point_*_temp:各 trip 温度
把 mode 设成 disabled 可以临时关闭温控,用于确认问题是否由温控引起。但生产环境千万别这么干,除非你有硬件级保护兜底。
5. 温控不生效的排查链路:从日志到寄存器
thermal 相关的问题,表现往往很"玄学":有时候降频,有时候不降;有时候温度读数是 0,有时候是负数。这一节我把实际排查中走过的完整链路整理出来,你可以按这个顺序一步步缩小范围。
5.1 第一步:确认 zone 是否注册成功
一切从注册开始。如果 zone 没注册上,后面全是空谈。
ls /sys/class/thermal/正常情况下应该能看到thermal_zone0、thermal_zone1等目录,以及cooling_device0等。如果 thermal_zone 目录不存在,说明 zone 注册失败。
接着看内核日志:
dmesg | grep -i thermal常见的注册失败原因有:sensor 驱动没加载、设备树节点格式错误、cooling device 引用无效。日志里通常会有明确的错误信息,比如failed to register thermal zone或者invalid sensor。
5.2 第二步:确认温度读数是否合理
zone 注册成功后,第一件事是看温度读数:
cat /sys/class/thermal/thermal_zone0/temp返回值是毫摄氏度,比如 45000 表示 45℃。如果读到 0、-273000 或者明显偏离实际的值,说明 sensor 有问题。
温度读数异常的常见原因:
- sensor 未校准:有些传感器的原始值需要经过公式转换,驱动里如果转换系数写错,读数就会偏。
- 读到了错误的通道:多通道 ADC 如果通道号配错,读的是别的传感器的值。
- sensor 未上电或时钟未开:这种情况下读数通常是 0 或者固定值。
我遇到过一次读数是 -273℃ 的情况,排查后发现是 sensor 驱动在读取时 I2C 通信失败,返回了默认的错误值。修复方法是检查 I2C 总线的上拉电阻和时序配置。
5.3 第三步:确认 trip point 是否被触发
温度正常但温控不动作,就要看 trip point 了。
cat /sys/class/thermal/thermal_zone0/trip_point_0_temp cat /sys/class/thermal/thermal_zone0/trip_point_0_type确认 trip 温度设置合理,类型正确。然后观察温度上升时,cooling device 的 state 有没有变化:
watch -n 1 'cat /sys/class/thermal/cooling_device0/cur_state'如果温度已经超过 trip 温度但 state 一直是 0,可能的原因有:
- governor 没正确加载或者被设成了 user_space 但用户空间没程序在管
- cooling map 没建立,trip 和 cooling device 没关联上
- cooling device 的 max_state 是 0,等于没有冷却能力
5.4 第四步:确认 cooling device 是否真的在降温
state 变了但温度不降,问题就在 cooling device 这一侧。
对于 CPU 降频类的 cooling device,要确认它真的改了频率:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq如果 state 变了但频率没变,说明 cooling device 的set_cur_state回调没有正确操作 cpufreq,或者 cpufreq 的 governor 和 thermal 的 cooling device 冲突了。
这里有个经典的坑:cpufreq 的 governor(比如 schedutil)和 thermal cooling device 可能同时想控制频率。thermal 把频率压下去,cpufreq governor 又根据负载把它拉上来,两者打架。解决办法通常是让 thermal cooling device 通过cpufreq_set_policy设置频率上限,而不是直接设频率,这样 cpufreq governor 会在上限内工作。
5.5 第五步:看硬件寄存器确认根因
如果软件层面都正常,温度还是压不住,就要怀疑硬件了。这时候需要读温度传感器的原始寄存器,确认软件读到的值和硬件实际值是否一致。
这一步需要芯片手册和调试工具(比如 devmem 或者厂商的调试接口)。我一般会对比三个值:硬件寄存器原始值、驱动转换后的值、实际用温度计测的值。三者对不上,就能定位是驱动转换错误还是传感器本身有问题。
排查链路走完,基本能覆盖 90% 以上的 thermal 问题。剩下的疑难杂症,往往和具体芯片的硬件特性有关,需要结合手册深入分析。
6. 实战中的几个经验与容易忽略的细节
前面讲的都是框架层面的东西,这一节我分享几个实际项目中踩过的坑和总结的技巧,这些内容在文档里基本找不到,但非常实用。
6.1 多 zone 之间的相互影响
一颗 SoC 上通常有多个 thermal zone,它们之间不是孤立的。CPU 降频后,GPU 的负载可能上升(因为任务转移了),导致 GPU zone 温度升高。如果两个 zone 的 cooling device 有重叠(比如共享同一个调频域),还会互相干扰。
我的处理经验是:划分 zone 时尽量让它们的 cooling device 不重叠。如果实在无法避免,就要在 governor 层面做协调,或者用 power_allocator 这种全局视角的 governor。另外,zone 之间的 polling delay 最好错开,避免所有 zone 在同一时刻唤醒,造成 CPU 负载尖峰。
6.2 温控与调度的配合
thermal 降频和 CPU 调度器之间有个微妙的互动。当 thermal 把某个 CPU 的频率压下去后,调度器如果还把重负载任务往这个 CPU 上放,就会导致任务执行时间变长,整体功耗反而可能上升。
比较理想的做法是让调度器感知到 CPU 的"容量"变化。内核里的 EAS(Energy Aware Scheduling)就是干这个的,它会根据 CPU 的当前频率和电压估算容量,把任务分配到合适的核上。如果你的系统开了 EAS,thermal 降频后调度会自动调整;如果没开,可能需要手动干预。
6.3 温度传感器的布局与热耦合
这是个硬件层面的问题,但直接影响 thermal 配置。传感器放在芯片的哪个位置,决定了它测到的是哪个区域的温度。如果传感器离热点很远,测到的温度会偏低,温控就会滞后。
我在一个项目里遇到过传感器放在芯片边缘、而热点在中心的情况,导致温控总是慢半拍。后来通过调整 trip point 温度(把阈值调低来补偿传感器偏差)缓解了问题。更好的做法当然是在硬件设计阶段就让传感器靠近热点,但这往往不是软件工程师能决定的。
6.4 调试时的临时手段
调试 thermal 时,有几个临时手段很有用,但切记不要带到生产环境:
- 把 zone 的 mode 设成 disabled,确认问题是否由温控引起。
- 临时把 trip 温度调低,快速验证 cooling 路径是否通畅。
- 用
echo直接写 cooling device 的 cur_state,测试 cooling device 本身是否工作。
这些手段能快速定位问题范围,但验证完一定要恢复原配置。
6.5 关于内核版本差异
thermal framework 在不同内核版本之间有不小的变化。比如较新的内核引入了thermal_of_zone_register这套 API,设备树绑定也有更新。如果你在移植驱动或者参考网上的配置,一定要注意内核版本匹配。
我一般会先看Documentation/devicetree/bindings/thermal/下的文档,确认当前内核支持的绑定格式,再动手写设备树。直接抄旧版本的配置,很可能因为 API 变化而注册失败。
7. 写在最后的一点个人体会
thermal framework 这块东西,刚接触时容易被它繁杂的节点和回调吓到,但真正理解了 zone、sensor、governor、cooling device 这四个角色的关系后,会发现它的设计其实很清晰。大部分问题都能归结到"某个角色的配置或实现不对"上。
我个人在实际操作中的体会是:温控问题的排查,七分靠日志和 sysfs,三分靠硬件手册。先把软件层面的注册、读数、触发、执行这条链路走通,确认每一环都正常,再去怀疑硬件。这样能避免一上来就钻到寄存器里,浪费大量时间。
另外,thermal 配置没有"万能模板"。同一套配置在这块板子上好用,换一块板子可能就出问题,因为散热结构、传感器精度、芯片特性都不一样。所以每次新项目,我都会花时间做一轮温控的实测标定,把 trip 温度和 polling delay 调到适合这块板子的值。这个投入是值得的,能省掉后期大量的调试时间。
如果你正在做功耗优化,建议把 thermal 和 cpufreq、cpuidle 放在一起看,它们三者是联动的。单独调其中一个,往往达不到最优效果。后续如果有机会,我可以再写一篇讲这三者如何协同调优的内容。