1. 从一次设备发烫的排查说起:thermal framework 到底在管什么
前阵子帮朋友看一块嵌入式板子,跑视频解码不到十分钟,外壳就烫得没法摸,系统日志里温度读数一路飙到 95 摄氏度,但频率该跑还是跑,风扇该转还是不转。这种"温度失控"的现象,十有八九是 thermal framework 没配好,或者压根没配。Linux 内核功耗子系统里,thermal framework 是专门负责"感知温度、做出决策、执行降温"的一整套通用架构,它把温度传感器、降温设备、策略逻辑抽象成统一的模型,让驱动开发者不用为每块板子重写一套温控代码。
这套框架解决的核心问题很朴素:芯片会发热,发热到一定程度会降频、会死机、甚至烧毁,所以必须有人盯着温度,超了就采取动作。听起来简单,但真实系统里温度传感器可能有好几个(CPU、GPU、电池、外壳),降温手段也五花八门(降频、关核、限充电电流、开风扇),谁先动、动多少、什么时候恢复,这些决策逻辑如果散落在各个驱动里,维护起来就是灾难。thermal framework 的价值就在于把这些东西标准化、可配置化。
这篇文章适合谁看?如果你在做嵌入式 Linux 开发、BSP 移植、功耗调优,或者单纯想搞懂/sys/class/thermal下面那一堆目录到底是什么意思,那这篇梳理就是给你准备的。我会从整体架构讲到关键数据结构,再落到设备树配置和实际调试,尽量把"为什么这么设计"讲透,而不是只丢一堆结构体定义。thermal zone、cooling device、governor 这几个关键词会贯穿全文,它们是理解整套框架的钥匙。
2. thermal framework 的四层抽象:为什么这样分层
2.1 从硬件到策略的职责切分
刚接触 thermal framework 的人容易懵,因为它的代码分布在drivers/thermal/、include/linux/thermal.h以及各个平台的驱动里,结构体之间互相引用,理不清谁管谁。我的经验是,先别急着看代码,先在脑子里建立一个四层模型,从下往上分别是:硬件层、抽象层、策略层、用户接口层。
硬件层就是真实的温度传感器(比如 SoC 内部的 TSADC、外部的 I2C 温度芯片)和降温设备(CPUfreq、devfreq、风扇 PWM、充电器限流)。抽象层是 thermal framework 提供的统一表示,把传感器抽象成 thermal zone,把降温手段抽象成 cooling device。策略层是 governor,它决定温度到了某个点该让哪个 cooling device 出多少力。用户接口层就是 sysfs 和 netlink,让你能在用户态看温度、改策略、收事件。
这样分层的好处是解耦。传感器驱动只需要注册自己,不用关心谁来读;降温设备驱动只需要实现set_cur_state,不用关心什么时候被调用;策略逻辑集中在 governor 里,换策略不用动硬件驱动。我见过一些老代码把温控逻辑直接写在传感器驱动里,结果换块板子就得重写,这就是没有分层的代价。
2.2 thermal zone:温度的"观测站"
thermal zone 是这套框架里最核心的概念,你可以把它理解成一个"温度观测站"。一个 thermal zone 通常对应一个需要监控的热点,比如 CPU 集群是一个 zone,GPU 是一个 zone,电池是一个 zone。每个 zone 里可以挂多个温度传感器(thermal_sensor),框架会按配置的方式(比如取最大值、取平均)算出一个当前温度。
为什么一个 zone 要支持多个传感器?因为同一个热点在不同位置的温度可能不一样。比如 CPU 集群,核心旁边有一个传感器,封装外面还有一个,取哪个更合理取决于你的保护目标。如果保护的是结温,那就取核心的;如果保护的是外壳不烫手,那就取外部的。框架允许你在设备树里配置多个 sensor 并指定聚合方式,这个设计很实用。
zone 还维护着一组 trip point(触发点),这是温度阈值。每个 trip point 有类型(critical、hot、passive、active)和温度值。温度越过某个 trip point,就会触发对应的动作。critical 类型最严重,通常直接触发系统关机或重启,因为再热下去硬件要坏了。passive 类型用于降频,active 类型用于开风扇这类主动散热。
2.3 cooling device:降温手段的统一接口
cooling device 是降温执行者。任何能降低系统温度的东西,只要实现标准接口,就能注册成 cooling device。最常见的几类:CPUfreq 的 cooling device 通过限制 CPU 最大频率来降温;devfreq 的 cooling device 限制 GPU 或内存频率;风扇 cooling device 通过 PWM 调速;还有充电限流、屏幕降亮度等。
每个 cooling device 有一个cur_state和一组states。state 是离散的档位,比如 CPU 有 0 到 N 档,0 是不限制,N 是限制到最低频。框架通过设置cur_state来让设备出力。这里有个容易踩的坑:不同 cooling device 的 state 语义不一样,有的是数字越大越凉快(风扇档位),有的是数字越大越热(CPU 限制档位,因为 state 越大频率被压得越低,但表示的是"限制程度")。写 governor 或者调策略时一定要看清楚语义,我当初就因为搞反了方向,调了半天发现温度越调越高。
2.4 governor:决策的大脑
governor 是策略层,决定"温度多少度时,让哪个 cooling device 出多少力"。内核自带几种 governor,最常用的是step_wise、power_allocator和fair_share。step_wise逻辑简单,温度每越过一个 trip point 就往上加一档降温力度,适合大多数场景。power_allocator更复杂,它基于 PID 控制思想,动态分配每个 cooling device 的功率预算,适合对性能敏感的移动设备。fair_share则是把降温需求平均分给各个 cooling device。
选哪个 governor 不是拍脑袋决定的。step_wise响应直接但容易震荡,温度在阈值附近来回跳;power_allocator平滑但调参复杂,需要配置sustainable_power、k_po、k_pu等参数。我在做手机项目时,初期用step_wise快速验证,后期切到power_allocator做精细调优,这个路径比较稳妥。
3. 关键数据结构与注册流程:驱动开发者必须搞清的事
3.1 thermal_zone_device 的字段含义
struct thermal_zone_device是 zone 的核心结构体,字段不少,但真正需要关注的没几个。type是 zone 的名字,会出现在 sysfs 里;temperature是当前温度;trips是 trip point 数组;governor指向当前使用的策略;ops是 zone 的操作函数集,最关键的是get_temp,框架靠它读温度。
get_temp这个回调是必须实现的,框架会周期性调用它(周期由polling_delay决定)来更新温度。如果你的传感器支持中断上报温度变化,也可以配置passive模式减少轮询。我建议在传感器精度够用的情况下,轮询周期别设太短,1000ms 是个常见起点,设成 100ms 会让 CPU 频繁被唤醒,反而增加功耗,这就本末倒置了。
struct thermal_zone_device_ops里除了get_temp,还有set_trips、get_trend等可选回调。set_trips用于告诉硬件"温度越过这两个值再通知我",能进一步省电。get_trend返回温度变化趋势,power_allocator会用到它做预测。这些可选回调不实现也能跑,但实现了能提升效果。
3.2 cooling device 的注册与 state 管理
注册 cooling device 用thermal_cooling_device_register,需要传入名字、私有数据和操作函数集。操作函数集里get_max_state返回最大档位,get_cur_state返回当前档位,set_cur_state设置档位。这三个是必须的。
set_cur_state的实现要小心,它可能在中断上下文或原子上下文被调用,所以里面不能睡眠。我见过有人在里面调用了会睡眠的函数,结果系统偶发卡死,排查了很久才发现是这里的问题。如果确实需要做耗时操作,用工作队列异步处理。
state 的档位设计也有讲究。档位太少,降温粒度粗,温度控制不精细;档位太多,governor 决策开销大,而且很多档位实际效果差不多。CPUfreq 的 cooling device 通常把档位映射到可用的频率点,有多少个频率点就有多少档,这个粒度一般够用。风扇的话,我一般设 4 到 8 档,太少会有明显的转速跳变噪音,太多则 PWM 调节频繁。
3.3 设备树里的 thermal 配置
现在大多数 ARM 平台都用设备树描述 thermal 硬件。一个典型的配置包含三部分:thermal zone 节点、cooling device 映射、trip point 定义。zone 节点里用thermal-sensors引用传感器,用trips定义触发点,用cooling-maps把 trip point 和 cooling device 关联起来。
cooling-maps是配置的重点,它定义了"哪个 trip 触发时,影响哪些 cooling device"。每个 map 里有trip、cooling-device、contribution等字段。contribution表示这个设备在本次降温中承担多少份额,多个设备时可以按比例分配。我调过一个四核平台,CPU 和 GPU 共享一个 zone,通过 contribution 让 GPU 在轻度发热时先降频,CPU 保持性能,重度发热时两者一起降,这个策略比一刀切体验好很多。
trip point 的温度值设置是门艺术。设太低,设备动不动就降频,性能受影响;设太高,保护不及时,有风险。我的经验是 critical 设在硬件规格的结温上限减 10 到 15 度作为安全余量,passive 设在用户能感知到烫手之前,active 设在 passive 之下留出提前量。具体数值必须查芯片手册,不能拍脑袋。
4. 温度控制的实际运作:从读数到降温的完整链路
4.1 一次温度更新的完整流程
搞懂流程对调试至关重要。框架启动后,会为每个 zone 创建一个延迟工作(delayed work),按polling_delay周期触发。触发时调用 zone 的get_temp拿到当前温度,更新temperature字段,然后交给 governor 处理。governor 遍历 trip point,找出当前温度落在哪个区间,决定是否需要调整 cooling device 的 state。如果需要,就调用对应 cooling device 的set_cur_state。
这个链路里任何一环出问题都会导致温控失效。get_temp返回错误值,governor 就基于错误信息决策;governor 没配对,可能压根不触发降温;set_cur_state实现有 bug,降温指令下发了但没生效。调试时我习惯先看/sys/class/thermal/thermal_zone*/temp确认读数正常,再看trip_point_*_temp确认阈值合理,最后看cooling_device*/cur_state确认降温动作有没有执行。
4.2 sysfs 接口的实用读法
/sys/class/thermal/下面是调试的主战场。每个thermal_zoneN目录里有type(zone 名字)、temp(当前温度,单位是毫摄氏度,注意要除以 1000)、mode(enabled 或 disabled)、policy(当前 governor)、trip_point_N_type和trip_point_N_temp(各触发点信息)。
cooling_deviceN目录里有type(设备类型)、cur_state(当前档位)、max_state(最大档位)。调优时我会写个小脚本,每秒打印一次温度和所有 cooling device 的 state,观察温度上升时 state 是不是按预期变化。这个笨办法比看代码快得多,能直观发现策略是否符合预期。
有个细节要注意:temp的单位是毫摄氏度,我第一次看的时候以为是摄氏度,看到 85000 还纳闷怎么这么高,后来才反应过来是 85 度。这个单位设计是为了避免浮点运算,内核里很常见。
4.3 中断模式与轮询模式的取舍
温度读取有两种模式:轮询和中断。轮询就是前面说的周期性调用get_temp,实现简单但费电。中断模式是传感器在温度越过设定阈值时主动上报,框架收到中断再读温度,省电但需要硬件支持。
设备树里通过polling-delay和polling-delay-passive控制轮询周期。如果传感器支持中断,可以把polling-delay设成 0 表示不用轮询,靠set_trips配置硬件阈值。但要注意,不是所有传感器都支持set_trips,不支持的话设成 0 会导致温度永远不更新,这个坑我踩过,现象是温度读数一直不变,查了半天才发现是配置问题。
实际项目中,我倾向于混合模式:正常运行时用中断省电,进入 passive 降温状态后切回轮询,因为降温过程中需要更及时的温度反馈。框架的polling-delay-passive就是干这个的,进入 passive 后会用这个更短的周期轮询。
5. 调试温控问题的排查链路:几个真实案例
5.1 温度读数正常但从不降温
这是最常见的现象。温度都 90 度了,CPU 频率还是满血。排查第一步看policy,确认 governor 不是user_space或disabled。第二步看 trip point 温度,确认 passive 阈值不是设得比实际能达到的温度还高。第三步看cooling-maps,确认 trip 和 cooling device 的关联没写错。
我遇到过一次,设备树里cooling-maps的trip引用写错了名字,导致映射没建立,governor 找不到该控制的设备,自然不降温。这种错误编译不报,运行时也不报,只能靠看 sysfs 里cooling_device的cur_state一直不变来发现。后来我养成了习惯,新板子第一次跑温控,一定手动加热(跑压力测试)并盯着cur_state看。
5.2 温度在阈值附近反复震荡
温度一到 70 度就降频,降到 68 度就恢复,恢复后又冲到 70 度,如此反复。这是step_wisegovernor 的典型问题,因为它的恢复逻辑比较激进。解决办法有几个:一是加 hysteresis(迟滞),让恢复温度比触发温度低几度,框架里通过 trip point 的hysteresis字段配置;二是换power_allocator,它的 PID 控制天然平滑;三是调整 trip point 间距,别让两个阈值挨太近。
hysteresis 这个字段很多人不知道,它表示温度要低于 trip 温度多少度才认为"脱离"该 trip。比如 trip 是 70 度,hysteresis 是 2 度,那温度要降到 68 度以下才恢复。合理设置能显著减少震荡,我一般设 2 到 5 度,具体看系统热惯性。
5.3 降温动作生效但温度不降
cur_state确实变了,CPU 频率也降了,但温度就是下不来。这种情况通常不是 thermal framework 的问题,而是散热设计或功耗本身的问题。可能散热片没贴好,可能降频幅度不够,也可能发热源不止 CPU 一个。这时候要用功耗分析工具看各部分的实际功耗,别死磕 thermal 配置。
我碰到过一次,GPU 降频了但温度不降,最后发现是内存控制器在满负荷跑,而内存没有对应的 cooling device。加上 devfreq 的 cooling device 后问题解决。这提醒我们,配置 cooling device 时要覆盖所有主要发热源,不能只盯着 CPU。
6. 把 thermal framework 用好的几个经验
6.1 trip point 数值的确定方法
别抄别人的数值,每块板子的散热条件、芯片体质、外壳材质都不一样。正确做法是:先查芯片手册拿到结温上限,critical 设在上限减安全余量;然后做热测试,记录不同负载下的稳态温度,passive 设在用户可接受温度对应的芯片温度;active 设在 passive 之下。整个过程要反复迭代,我一般会做三轮测试才定稿。
测试时要用真实场景的负载,别只用跑分软件。跑分软件的发热模式和实际应用差别很大,我见过跑分时温控正常、玩游戏时却降频的案例,就是因为游戏负载更接近真实场景。用实际应用测出来的数值才靠谱。
6.2 governor 选择的决策依据
简单设备、对成本敏感、开发周期短,选step_wise,够用且好调。移动设备、对续航和性能平衡要求高,选power_allocator,但要做好调参准备。多发热源需要公平分配,选fair_share。没有银弹,关键是理解你的场景最看重什么。
power_allocator的调参是个技术活。sustainable_power是设备能长期稳定散发的功率,设小了会过度降频,设大了保护不足。这个值最好通过实测确定:让设备在目标温度下稳定运行,测出此时的功耗,就是sustainable_power。k_po和k_pu是 PID 参数,一般用默认值起步,根据震荡情况微调。
6.3 用户态干预的边界
框架提供了 sysfs 接口让用户态改policy、改 trip point,甚至直接设cur_state。这给了灵活性,但也带来风险。生产设备上我建议锁死关键配置,只开放必要的只读接口。用户态乱改温控参数可能导致设备过热,这个责任谁都担不起。
如果确实需要用户态参与决策(比如根据使用场景切换性能模式),用user_spacegovernor 配合一个守护进程,让守护进程通过 netlink 接收温度事件并决策。这样逻辑在用户态,但执行还是走框架的标准路径,比直接写 sysfs 规范。
6.4 和功耗子系统的协同
thermal framework 不是孤立的,它和 CPUfreq、devfreq、regulator 等子系统紧密协作。降频靠 CPUfreq,限流靠 regulator,这些子系统本身也有自己的策略。配置时要注意别让它们打架,比如 CPUfreq 的 governor 想升频,thermal 想降频,最终谁说了算取决于调用顺序和优先级。
我的做法是让 thermal 的优先级高于性能策略,因为过热是安全问题,性能是体验问题,安全优先。具体实现上,thermal 的 cooling device 会直接限制 CPUfreq 的max_freq,这个限制会覆盖 governor 的决策。理解这个覆盖关系,能避免很多"为什么频率上不去"的困惑。
7. 写在最后的一点个人体会
thermal framework 这套架构刚看会觉得绕,但一旦理解了"zone 观测、trip 触发、cooling 执行、governor 决策"这条主线,剩下的都是细节填充。我在多个项目里用它,最大的感受是:配置比代码重要。框架本身很成熟,出问题九成是设备树配错了或者 trip point 设得不合理,真正需要改框架代码的情况极少。
如果你正在调一块新板子的温控,我的建议是先别急着优化,先用默认配置跑起来,确认整条链路是通的——温度能读、trip 能触发、cooling 能动作。链路通了之后再谈调优,否则你连问题出在哪一环都不知道。这个"先通后优"的思路,帮我省下了大量排查时间。