
做功耗和温控的同事应该都有体会一个SoC跑起来容易跑得稳很难。CPU频率刚拉满温度就飙升然后触发降频、限流严重的直接过热关机用户侧表现为卡顿、黑屏、重启锅全在软件。Linux内核里负责这套“温度控制逻辑”的就是thermal framework。这篇梳理一下它的通用架构不讲具体平台驱动怎么写重点是把框架本身的筋骨拆开它怎么感知温度、怎么表达散热能力、怎么把两者联动起来以及你在调试中会遇到哪些坑。1. 内容整体设计与思路拆解1.1 thermal framework要解决什么问题先摆结论thermal framework在内核里的定位是把“温度控制”从“裸奔的硬件寄存器操作”抽象成“系统级的动态平衡”。如果没有这套框架你要在驱动里做温控只能这么干轮询读传感器温度超过阈值就自己降频、关核、限流逻辑全写在某个单独驱动里。短期能用但一旦平台迭代、传感器数量增加、冷却设备变多这套代码就变成一团乱麻。而且不同SoC的温控策略没有统一表达方式驱动层、策略层、硬件层完全耦合。thermal framework的核心理念是把温控拆成三个角色各自独立、只通过标准接口通信温区thermal zone代表“一个受温度影响的区域”比如CPU、GPU、电池它负责采集温度冷却设备cooling device代表“一种能降温的手段”比如CPU降频、GPU限流、风扇调速策略governor把温度数据和冷却能力配对决定“温度高了到底怎么降”。做架构梳理的时候最容易犯的错误是一上来就钻数据结构。我的习惯是先画一张逻辑分层图把问题域搞清楚再去抠代码细节。这套框架如果按层拆大致是硬件层温度传感器thermal sensor、被控硬件CPU、GPU、风扇等框架核心层thermal_sys模块提供数据结构、注册接口、sysfs接口策略层governor绑定温区和冷却设备执行节流用户空间层通过sysfs或netlink读取温度、触发策略调整。我把整篇文章的重点放在第二和第三层因为这是通用架构的核心驱动开发者的绝大部分工作也集中在这一层。1.2 为什么说“通用架构”值得单独梳理很多人觉得thermal framework就是“读温度然后降频”理解太浅了。真正把它的通用架构理清楚你会看到一个非常精巧的设计设备树描述物理拓扑内核核心提供通用机制governor插入控制策略用户空间可以介入策略。举一个实际场景某ARM平台CPU和GPU离得很近共用一片散热区域。在设备树里你可以给CPU和GPU分别定义thermal zone也可以让它们共享一个zone。如果共享那温度采集点怎么选用CPU传感器还是GPU传感器还是取两者最大值这些在裸写驱动时都是“拍脑袋决定”的但在thermal框架里有明确的数据结构去表达这种关系。另外不同产品对温控的需求差异极大。手机要保续航、重手感服务器要保性能、不降频平板可以允许更高的表面温度。thermal framework通过governor和trip point表达这种差异同一套硬件你只需要换策略配置就能得到完全不同的温控行为。通用架构的价值就在这里硬件千差万别但框架提供的“温度采集—阈值判定—策略执行—冷却生效”这条流水线是固定的你只需要沿着这条流水线把自己的硬件接入进来。2. 核心机制thermal zone的注册与温度采集2.1 thermal zone设备的三个关键结构体先看温区侧的三个核心结构体这是理解框架的第一把钥匙。第一个是struct thermal_zone_device_ops定义了温区的基本操作回调struct thermal_zone_device_ops { int (*get_temp) (struct thermal_zone_device *tz, int *temp); int (*get_trend) (struct thermal_zone_device *tz, enum thermal_trend *trend); int (*set_trips) (struct thermal_zone_device *tz, int low, int high); int (*get_mode) (struct thermal_zone_device *tz, enum thermal_device_mode *mode); int (*set_mode) (struct thermal_zone_device *tz, enum thermal_device_mode mode); };最重要的就是get_temp这是框架获取温度的入口。注意温度单位是毫摄氏度不是摄氏度也不是开尔文这个在下游驱动里经常有人拿错。第二个是struct thermal_zone_device_ops的载体——struct thermal_zone_device它就是温区设备在内核里的对象。这里不需要背所有字段只需要知道它包含了三部分信息温度传感器操作接口、trip point信息、zone的sysfs节点。第三个是struct thermal_zone_params这个是温区参数的集合包括轮询间隔polling_delay、被动降温开关passive_delay、governor名称governor_name等。注意这个结构体在注册温区时可以不填默认值会自动补上但代码里如果你不显式指定governor内核会用默认的step_wise。注册温区的核心调用就一行thermal_zone_device_register()但这行代码后面的逻辑值得掰开讲讲。注册过程中内核会完成这些动作分配thermal_zone_device对象填充ops、tzp、trip点等为zone建立sysfs目录生成type、temp、trip_point_*_temp、policy等节点把zone挂到全局链表thermal_tz_list上触发一次初始温度更新然后根据是否被动模式决定是否启动轮询定时器。2.2 温度采集的两种方式轮询与主动上报框架里温度获取分两种模式这个设计很值得展开。轮询模式是thermal zone的默认工作方式。内核启动一个定时器每隔polling_delay毫秒调用一次get_temp取回温度后更新到zone的temp字段同时通知governor做策略判断。这种方式实现简单、兼容性好但缺点也明显温度快速变化时轮询间隔是迟到信息间隔太短又会增加功耗。实际产品里轮询间隔通常设在100ms到2s之间而且要配合trip point来调整。主动上报模式是framworkv5.4以后引入的改进。传感器或硬件可以主动调用thermal_zone_device_update()来触发一次温度处理而不是等轮询定时器到点。关键点在于set_trips回调硬件会在温度跨过高阈值或低阈值时产生中断这时驱动主动上报温度内核只在“有事件”的时候做计算空闲时完全零开销。这个模式特别适合那些支持硬件温度阈值中断的SoC可以大幅降低温控对内核的打扰。但要注意在主动上报模式下polling_delay不能随便设成0因为一旦硬件中断失效至少还有轮询兜底这是我在实际产品中吃过亏的——有个平台的中断在低功耗状态下会丢失结果温度失控全靠最后一道hard limit顶着。2.3 温区注册流程中的判断逻辑注册温区之前内核会做一系列合理性检查。我这个“检查清单”是踩过坑后沉淀下来的确认thermal zone的type字段唯一内核用type做zone的标识重名会影响sysfs创建和属性查找确认trip point数量不为0一个没有任何trip point的zone没有任何实际意义框架拿它做不了任何决策确认get_temp回调必然存在没有温度采集能力的zone框架不知道你当前的温度governor跑去用默认温度50度做决策这是灾难确认get_crit_temp语义正确有些驱动把crit温度设得比hot还低完全反向导致系统动不动直接关机。这些检查并不会全部由内核静态完成有些是运行时才暴露的所以写完驱动一定要做实际的热测试不要光看能注册成功就觉得OK。3. 冷却设备与绑定机制如何知道“该降多少”3.1 cooling device的本质与设计思路散热能力怎么在内核里表示答案是冷却设备。一个struct thermal_cooling_device代表一种可调节的“降温资源”。CPU调频是一个冷却设备GPU限频是一个冷却设备风扇是一个冷却设备甚至一个可以降低功耗的DDR控制接口都能注册成冷却设备。这就是这套框架抽象能力强大的地方你不需要在乎背后是什么硬件只需要暴露“当前状态”和“最大状态”两个维度。核心结构体是struct thermal_cooling_device_opsstruct thermal_cooling_device_ops { int (*get_max_state) (struct thermal_cooling_device *cdev, unsigned long *state); int (*get_cur_state) (struct thermal_cooling_device *cdev, unsigned long *state); int (*set_cur_state) (struct thermal_cooling_device *cdev, unsigned long state); };三个回调分别回答三个问题最大档位是多少当前在哪个档位切到某个档位怎么做这个设计把冷却设备抽象成“档位”的概念很贴合实际CPU调频有频点档位风扇有转速档位GPU有频率档位。注册冷却设备的调用是thermal_cooling_device_register()。它会为cdev创建sysfs节点包括type、max_state、cur_state用户空间可以直接读写这些节点来手动控制冷却设备这在调试阶段非常方便。3.2 绑定关系trip point与cooling map有了zone和cdev还需要两者之间的绑定关系这就是trip point和cooling map干的事。trip point是热区上的温度阈值定义了一组“事件点”温度超过这个点governor要开始动作。比如trip_point_0_temp 55000被动冷却开始trip_point_1_temp 85000性能限制加深trip_point_2_temp 100000强制关机。每个trip point还带一个类型enum thermal_trip_type有THERMAL_TRIP_ACTIVE主动冷却如风扇、THERMAL_TRIP_PASSIVE被动冷却如调频、THERMAL_TRIP_HOT需要更激进的处理、THERMAL_TRIP_CRITICAL无条件关机。而每个trip point通过cooling-maps绑定到具体的冷却设备决定了“在这个trip点触发时冷却设备要到哪一档”。比如CPU zone的trip_point_0绑定了CPU freq的cooling device设定state2意味着温度超过55度时把CPU频率限制到第2档。这一步的绑定关系在设备树里的表达非常典型thermal-zones { cpu-thermal { trips { cpu_alert0: trip0 { temperature 55000; hysteresis 2000; type passive; }; }; cooling-maps { map0 { trip cpu_alert0; cooling-device cpu_cooling_dev 2 4; }; }; }; };这段设备树的实际含义是温度到55度把CPU冷却设备切到2档但最大只能到4档。这里“2 4”两个数字分别是最小state和最大state最小state是切入值时必须达到的档位最大state是允许它升到的最大档位这个约束直接限制了governor的调节空间。3.3 绑定粒度与实际产品选型做产品时“绑定粒度”是个值得认真思考的问题。同一块SoC上CPU可以有多个cluster每个cluster独立调频域GPU是一个独立调频域NPU又是一个。到底把它们注册成几个cooling device我的建议是按调频域freq domain拆不要按IP模块拆。SOC侧同一路PMIC供电且共享频率表的IP合并成一个cooling device更高效供电独立、频率独立的拆开更灵活。否则你会在设备树里写出“一个trip点绑了五六个cooling device”这种复杂到没法维护的配置而且governor计算时多个cdev之间还可能相互打架。4. Governor策略层温控算法的设计与演进4.1 governor在框架中的位置前面说的zone和cdev相当于“硬件抽象层”真正“干活”的是governor。它拿到zone当前的温度、当前的趋势trend、当前的trip状态决定把哪些冷却设备调到什么状态。内核里每个governor对应一个struct thermal_governorstruct thermal_governor { struct list_head governor_list; char name[THERMAL_NAME_LENGTH]; int (*throttle) (struct thermal_zone_device *tz, int trip); int (*manage) (struct thermal_zone_device *tz, int trip); };throttle在进入trip点或退出trip点的时候被调用manage在一些governor实现里负责更细粒度的调节。每个zone在注册时可以指定使用哪个governor也可以通过sysfs的policy节点在运行时切换。这就带来一个非常大的产品红利硬件完全一样的情况下换一个governor就能改变整个温控行为。调试温控“手感”的时候先换governor再调参数往往比硬调trip point快得多。4.2 step_wise最实用也是默认的governorstep_wise是目前内核默认的governor也是我实际项目里用得最多的。它的调节逻辑简单但有效温度超过trip点且温度还在上升就将该trip点绑定的冷却设备提升一档温度超过trip点但温度趋势是下降的保持当前档位不动温度回到trip点以下且低于“退出缓冲”hysteresis则降低一档。每个周期只调整一档这是它的核心特征。这样做的好处是非常平滑不会出现温度震荡。坏处是响应偏慢温度陡升时跟不上节奏。实际项目中step_wise的调参核心在trip point的hysteresis字段。它决定了退出阈值的滞后宽度进点是55度拖尾设5度那要降到50度以下才完全退出。滞后设小了会反复进出trip点频率振荡设大了降温慢性能浪费。我一般从5%的trip点温度开始试比如55度的trip点先试2到3度。4.3 power_allocator从“调档”到“算功率”比step_wise聪明一个量级的是power_allocator。它不再把冷却设备当档位处理而是把调度问题抽象成“给定温度预算如何分配功率”。它的实现基于一个PID控制器加功率预算模型。先把温区的trip点定义出一个“可持续温度”和一个“可持续功率”然后PID控制器根据当前温度和目标的偏差计算出允许消耗的功率再把这个功率分配给绑定的冷却设备。这个governor有两个典型使用场景CPU和GPU争抢热预算的SoCCPU降一点GPU就能多跑一点反之亦然需要动态判断谁当前更重要温度预算紧张的高性能设备比如笔记本和游戏手机需要在性能释放和温控之间做精细化平衡。但power_allocator的调试门槛明显更高。它需要你提供准确的功率模型系数sustainable_power稳态可耗散功率、k_po、k_pu、k_i等PID系数。这些参数调不好系统会震荡表现为频率来回跳、帧率不稳定。我见过不少团队在这上面耗几周最后退回step_wise的场景。我的态度是先用step_wise跑通逻辑再根据实测温度曲线决定要不要换power_allocator。4.4 其他governor和自定义governor内核里还有fair_share按权重分配冷却比例、bang_bang开关式控制适用于风扇这类只有开关的冷却设备等governor。bang_bang值得特别提一句。有些风扇只有开和关两档step_wise那种多档调节逻辑对它毫无意义。bang_bang的逻辑是超过trip点就开低于trip点减滞后就关非常简单但容易触发风扇频繁开关所以trip点的滞后要设得比较大。如果你发现自带governor都不满足需求比如产品要求“温度每高1度CPU频率就严格对应降低一档”你可以写自己的governor注册方式很简单static struct thermal_governor my_governor { .name my_governor, .throttle my_throttle, }; thermal_governor_add(my_governor);然后zone里指定governor_name my_governor即可。但自定义governor要慎重框架的通用逻辑、trip点的语义、冷却设备的档位约束都有默认假设你一旦打破这些假设问题排查会非常痛苦。5. 设备树里的thermal-zones从物理拓扑到软件描述5.1 标准设备树绑定解析设备树是thermal framework的“配置语言”理解这套语言是把框架用好的关键一步。一个典型的thermal-zones节点包括四个子块trips定义所有温度阈值cooling-maps定义每个trip点绑定了哪些冷却设备以及档位范围thermal-sensors指定温度传感器的phandlecoefficients指定Sensor与zone之间的线性换算系数slope和offset。注意这里的换算公式zone温度 (传感器原始温度 * slope) offset单位同样是毫摄氏度。如果SoC的ADC传感器读出来的是原始ADC码你必须在这里做线性换算这是新手最容易忽略的地方。5.2 多传感器与多zone的设计模式实际SoC中一个thermal zone常常需要读取多个传感器取最大值或加权平均。设备树里的写法是thermal-sensors sensor0, sensor1, sensor2;内核默认会取所有传感器的最大值作为zone温度。这种设计非常贴合实际CPU是多核的可能每个核附近一个传感器但我们需要的是“最热的那个核”作为温控依据而不是平均温度。另一种常见模式是不同zone之间共享冷却设备。比如CPU zone和GPU zone都绑定了同一个风扇cdev。这种情况下风扇的调度由两个zone共同驱动。step_wise处理这种共享cdev时逻辑是“谁需要更高档位就听谁的”但如果两个zone的温度都在各自trip点附近振荡风扇转速会来回跳体验很差。我的解决办法是把风扇单独拉成一个zone优先响应温度更高的一端并加大两侧zone的滞后值。5.3 设备树中容易踩的坑设备树配置thermal有一个我反复提醒过的问题trip点温度值和传感器精度不匹配。如果你的传感器精度是±2度你写一个相邻trip点差值只有1度的配置这两个点实际上等于不存在因为噪声会把它们搞乱。另一个坑是**critical温度不能设得比系统可承受上限还高**。有的团队为了“不要过早关机”把crit设到115度结果PCB已经烤变形了内核才发现。我的底线是crit温度必须低于硬件数据手册标定的绝对最大值且至少留5度余量。还有一点是关于type字符串的。trip类型在设备树里可接受的值是active、passive、hot、critical不要自己发明别的类型内核只认这四个字符串对应的枚举值。6. 调试手段与实战教训我从项目中总结出的排查路径6.1 thermal相关的调试接口与工具排查thermal问题最常用的手段是把sysfs节点全部过一遍# 查看所有thermal zone的类型 ls /sys/class/thermal/ # 查看某个zone的温度 cat /sys/class/thermal/thermal_zone0/temp # 查看zone的trip点配置 cat /sys/class/thermal/thermal_zone0/trip_point_0_temp # 查看zone使用的governor cat /sys/class/thermal/thermal_zone0/policy # 手动调整冷却设备档位调试用 echo 3 /sys/class/thermal/cooling_device0/cur_state如果系统里装了lm-sensors这类工具也能直接读到部分thermal zone数据但说实话内核自带sysfs的信息密度已经足够了尤其在嵌入式环境里别依赖额外的用户空间工具。打开内核trace事件是目前排查thermal问题最有效的手段echo 1 /sys/kernel/tracing/events/thermal/thermal_temperature/enable echo 1 /sys/kernel/tracing/events/thermal/thermal_zone_trip/enable echo 1 /sys/kernel/tracing/events/cpufreq/cpufreq_target/enable然后跑cat /sys/kernel/tracing/trace你能看到每个时刻的温度变化轨迹以及频率调整记录配合时间戳对齐能快速判断“温度升高到触发降频之间是否有异常延迟”。6.2 一组实际排查案例我在一个项目上遇到“CPU温度看起来不高但系统频繁降频”的问题。看sysfs里的trip点配置都是正常的55度、65度、85度但热测试发现50度就开始降频了。排查了很久最后发现是设备树里多个zone共用了同一个cooling device另一个zone的温度先到了trip点把CPU频率压住了。这种影子效应在sysfs里看CPU zone本身完全正常只有把所有zone的温度曲线一起拉出来才看得到。另一类问题是温度跳动导致的频率振荡。现象是CPU频率在1.2GHz和1.8GHz之间每隔几百毫秒跳一次帧率曲线全是毛刺。用trace抓下来发现温度在trip点附近反复穿越滞后设得太小进入和退出太频繁。解决方法是把该trip点的hysteresis从2度加大到5度振荡立即消失。第三类问题跟传感器读数有关。某平台传感器在开机时读到的温度是0度导致zone认为系统在极冷状态冷却设备全部处于关闭状态。等SoC真正跑热之后传感器又恢复正常但中间的“信任窗口”已经错过一次真正需要降温的场景。排查后发现是Sensor驱动在固件未就绪时返回0没有返回负错误码。这里要强调一下get_temp回调在传感器异常时务必返回错误码不要返回0或者一个假温度框架拿到错误码后会保留上次有效温度比拿假温度做决策安全得多。6.3 用户空间介入与冷却设备管理内核的thermal框架也支持用户空间作为“最终决策者”。把zone的policy改成user_space后内核不再调用governor做自动调节温度事件的决策权完全交给用户空间守护进程。用户空间通过读temp节点获取温度通过写cdev的cur_state或读trip_point_*_temp节点来调节冷却设备。这种模式在复杂的消费类产品里非常常见。比如手机厂商的温控守护进程会结合前台应用负载、电池温度、表面温度做综合决策而不是单纯看SoC温度。内核框架提供数据、事件、执行通道用户空间负责策略优化各取所长。这种分工其实才是thermal framework最终想达到的形态内核做机制策略层可以灵活替换。7. 避坑清单与快速参考根据这几年的实战项目我把thermal framework最容易出问题的点全部列成一张速查表基本覆盖了我见过的绝大多数线上故障。问题现象可能原因排查方向温度显示异常偏高或偏低get_temp单位错误或设备树coefficients换算错误检查驱动温度单位是否为毫摄氏度换算公式是否与硬件手册一致系统频繁降频但温度不高其他zone挤占了冷却设备档位拉全zone温度曲线检查cooling-maps共享关系频率在相邻档位间振荡trip点滞后太小反复进出阈值加大hysteresis优先保证趋势稳定性温度到了trip点但governor不动作zone指定了错误的governor或cdev的max_state未正确设置检查policy节点检查cdev最大state是否过小风扇反复启动停止bang_bang搭配过小的滞后给开关型风扇使用bang_bang governor并设置较大滞后critical trip触发后直接重启但没留证据用户空间没有及时读取事件用netlink事件接口或增加ramoops保存内核日志热测试时某个传感器读数为0sensor驱动初始化未完成返回0修正get_temp回调未就绪时返回错误码另外一个兜底技巧是把thermal_zone报警作为启动时的自检项。在工程师开发板上启动后直接读取所有zone的初始温度和trip点如果出现温度明显异常比如0度或超过100度就打印警告这样能在功能测试之前就发现配置问题。8. 最后的实操经验闲聊几句我个人的感受。做thermal框架相关的工作最忌讳的事情就是“只调参数不看曲线”。无论你在哪个平台低功耗产品的温控调试一定要先把trace拉出来把温度曲线、频率曲线、trip点三条线放到同一时间轴上对比。不需要什么高级工具一个文本记录足够关键是时间戳要对齐。我见过很多团队调试thermal问题靠猜今天试一个大滞后明天试一个低阈值完全没有数据支撑最后问题复现靠运气、修好也靠运气。另外在实际产品的温控验收中不要只盯着峰值温度。温控的效果要看三个指标温度曲线的平滑度、频率曲线的振荡幅度、以及达到热平衡的时间。一个好的温控策略可以允许温度高一点但频率必须稳定温度曲线振荡说明governor在做无效的反复调节这种“没有功劳也有苦劳”的状态恰恰是最消耗用户体验的。还有一点请务必把thermal这套框架的初始化和调用路径完整走读一遍再动配置。只靠设备树配置和拿来主义你会被一些诡异的现象反复折磨。真正把thermal_zone_device_register到governor调用链这条路走通你才能理解为什么某些trip点配置会导致governor“视而不见”为什么某些cdev会“抢占式”地压低工作频率。这些知识没有捷径就是抓数据、看代码、对比测试一步一个脚印。