☰
Linux内核thermal框架全解析:从传感器到降温闭环的通用架构
2026/10/8 1:03:09 网站建设 项目流程

说到Linux内核的功耗管理,很多人脑子里蹦出来的多半是cpufreq调频、cpuidle调休、suspend/resume休眠唤醒这些大头。但真做过嵌入式整机功耗或者手机续航优化的工程师,心里都清楚:还有一个常年背锅的框架,就是thermal。手机打个游戏发热降频,平板边充电边看视频热得烫手然后亮度掉一半,服务器机柜里跑满负载直接被硬关机,这些场景背后全是thermal framework在管。

这篇文章是Linux内核功耗子系统系列第九篇,我打算把thermal这棵老树的通用架构完整捋一遍:它解决了什么问题、核心对象有哪些、温度从传感器到策略再到冷却设备这条链路是怎么跑的、DTS里那套thermal-zones长啥样、以及遇上异常时怎么顺着sysfs一步步查到根因。对入门内核功耗开发的兄弟、做BSP bringup的BSP工程师、还有做产品级热设计的应用层同学,这篇都应该能帮你在脑子里搭出一张完整地图。

1. thermal framework到底管什么:一个从发热到降温的完整闭环

1.1 一句话概括这个框架的价值

thermal framework在Linux内核里承担的角色,通俗讲就是“感知发热、决策降温、执行动作”三件事。它内部把这三件事拆给了三个不同的角色:传感器(thermal sensor)负责采集温度,governor负责拿温度数据做决策,冷却设备(cooling device)负责执行降频、关核、开风扇等降温动作。

说得更直白一点,这套框架就是给系统装了一个带温度计的空调遥控器:温度计负责告诉房间现在多少度,遥控器上的逻辑决定要不要开制冷、开几档,空调外机负责真正把温度压下来。对内核来说,房间就是芯片,温度计是挂在总线上的温度传感器(NTC热敏电阻或SoC内置tsensor),遥控器逻辑是governor,空调外机就是CPU调频模块、GPU调频模块、风扇PWM控制器这些。

没有这个框架之前,每个SoC厂商都自己写一套定时读温度、到了阈值就往cpufreq里写频率的逻辑,代码风格千奇百怪,放到开源社区互相review起来头大如斗。thermal framework从2008年前后进入内核主线的目标就是把这些乱七八糟的私有实现归一化,让sensor、policy、actuator三层各自实现标准回调,你做得再花哨,接口必须统一。

这套框架解决的核心痛点其实是两个:第一,温度保护不能只靠cpufreq一个维度,GPU、ISP、充电IC、屏幕背光、电池、甚至WIFI模块都可能需要联动保护,必须有一个统一的抽象能把它们都纳进来;第二,策略和硬件要解耦,同一套降温策略代码要能跑在不同厂家的芯片上,否则每一款芯片都重新造轮子,生态根本攒不起来。

1.2 三层解耦:sensor、governor、cooling device为什么要分开

先看一下thermal framework在源码里的布局,这几个目录对应了我在开头说的分层:

  • drivers/thermal/thermal_core.c:框架核心,负责thermal zone生命周期管理、温度更新入口、与sysfs对接
  • drivers/thermal/thermal_sysfs.c:暴露给用户态的全部接口
  • drivers/thermal/thermal_helpers.c:温度获取与冷却设备调用的工具函数
  • drivers/thermal/thermal_governor.c+drivers/thermal/gov_*.c:策略模块
  • drivers/thermal/thermal_mmio.c、thermal-generic-adc.c等:传感器驱动
  • drivers/thermal/cpufreq_cooling.c、cpuidle_cooling.c、devfreq_cooling.c:标准冷却设备实现

设计者把这三层各自做成可替换的模块,为的就是让它们能独立演进。BSP工程师在新芯片上bringup时,最常见的组合是:generic-adc-thermal或者SoC私有sensor驱动采集温度,step_wisegovernor做策略,cpufreq_cooling+ 风扇pwm做执行。硬件换了,sensor驱动换掉;产品策略变了,echo一下换个governor;散热结构升级了,冷却设备多绑一个风扇。三块之间只靠标准接口握手,互不干扰。

用生活类比的写法再说一遍:传感器就是体温计,governor就是你自己判断“低烧还是高烧要不要吃药”,冷却设备就是降温和退烧的手段。如果不把“测体温”和“决定怎么退烧”分开,换个体温计你整套逻辑都要重写,这在工程上显然是愚蠢的。所以内核里温度监测和决策逻辑从来不在同一个模块里,这是读懂thermal代码的第一把钥匙。

1.3 实际场景里它都在哪里发挥作用

常见的落地场景,也是这几年我做功耗优化时反反复复在打交道的几个方向:

  • 手机/平板主芯片散热:CPU的cpufreq_cooling、GPU的devfreq_cooling,温度到了passive阈值就开始阶梯式降频。骁龙、天玑平台上这套几乎是标配,只是各家governor的系数调得不太一样
  • 笔记本整机散热:风扇曲线由thermal驱动,CPU温度到达active trip之后按档位拉升PWM占空比,同时主板上的acpi-fan与int340x_thermal也参与联动
  • 服务器功耗封顶:AI服务器单卡动辄450W,机柜供不上电的时候会触发power_allocatorgovernor,把整机功耗按比例切给多个冷却设备,保证不死机不宕服务,这是thermal和功耗管理结合最紧密的场景
  • 电池与充电保护:电池温度超过45度触发的充电降流,充电IC的cooling state联动,很多国产机型的“边玩边充上限50度”提示就是thermal zone把电池热区报给上层
  • 工业设备稳定性:工控机在无风扇密闭机箱里靠自然散热,thermal负责在临近极限时强行限制负载,防止恶劣环境下整机死机

理解了这些场景,再看后面的代码结构就顺了。不同场景只是同一个闭环的不同实例:sensor不同、governor不同、cooling device不同,但闭环骨架不变。

2. 核心对象与工作链路:先把骨架背下来

2.1 thermal_zone_device:一切热区的根

内核里最核心的对象是struct thermal_zone_device,你可以把它理解成“一个受监控的温度区域”。一块芯片上往往有多个zone:CPU一个、GPU一个、电池一个、环境温度一个。每个zone拥有自己的温度传感器、自己的trip point(温度阈值)、自己的policy(策略)。

结构体里的关键字段我在代码里标注一下:

struct thermal_zone_device { char type[THERMAL_NAME_LENGTH]; /* 名称,如 cpu-thermal */ struct device device; /* 对应 /sys/class/thermal/thermal_zoneX */ int id; struct thermal_zone_device_ops *ops; /* 读温度、设置trip等回调 */ struct thermal_zone_params *tzp; /* governor私有的可调参数,IPA这里塞权重 */ struct list_head thermal_instances; /* 该zone下所有 trip-冷却设备 连接 */ struct thermal_governor *governor; /* 当前绑定的governor */ struct mutex lock; int temperature; /* 当前最新温度,单位毫摄氏度mC */ int last_temperature; /* 上一次的温度 */ int emul_temperature; /* 仿真温度,调试神器 */ int passive_delay; int polling_delay; int num_trips; struct thermal_trip *trips; int state; /* 当前trip状态位图,1bit代表一个trip被触发 */ };

注意这个temperature和last_temperature:所有governor的决策都基于这两个值,温度更新是热区工作的心跳。一个zone的注册入口通常是thermal_zone_device_register(),但现代设备树方式下更常见的入口是devm_thermal_of_zone_register(),它会把设备树里解析好的trip和cooling map全自动挂上。注册成功之后,驱动只需要做一件事:周期性更新温度,剩下的交给core去分发。

温度更新的两条路径值得说透。一条是轮询模式:内核里有一个thermal_zone_device_set_polling机制,当温度变化没有中断触发时,系统按polling_delay周期启动一个delayed_work,到点就调__thermal_zone_device_update。另一条是中断/主动上报模式:传感器驱动自己监测到温度变化后直接调用thermal_zone_update_temp或者更新trip状态,这种方式sensor支持热中断时延迟低很多,适合对温度变化敏感的电池和充电场景。实际工程里大多数SoC内置tsensor走轮询,温度变化不剧烈也没必要上中断。轮询太频费电,太疏保护不及时,所以一般取100ms~500ms之间,被动发热场景下再细调。

2.2 trip point:温度阈值怎么写才合理

thermal_trip是决策的输入拐点,它包含温度值、迟滞(hysteresis)、类型和目标动作。trip类型内核定义了四种,每种语义差异很大:

类型含义典型动作
THERMAL_TRIP_ACTIVE主动级,温度超过阈值后主动引入散热手段拉高风扇转速、开启某种已有冷却路径
THERMAL_TRIP_PASSIVE被动级,需要通过降低性能来抑制发热触发cpufreq降频、devfreq降低GPU频率
THERMAL_TRIP_HOT热提示级,系统仍可运行但需要强烈干预触发hot回调,向用户态发通知弹温度警告
THERMAL_TRIP_CRITICAL临界级,温度再高就会损坏硬件直接执行关机或紧急断电

为什么需要迟滞hysteresis?因为直接拿单一阈值卡温度必然会振荡。想象一下:阈值85度,85.1度降频,降到84.9度恢复满频,结果传感器稍有噪声,系统就在满频和低频之间反复横跳,体验几乎灾难。所以每个trip点都会配一个迟滞窗口,比如temperature = 85000, hysteresis = 5000,意思就是升到85度触发,但要降到80度才恢复,中间留出5度的缓冲区。工程上这个值一般取5~10度,取太大降温太慢,取太小还是会抖。

还有一个实战中容易忽略的地方:trip点排序。内核里trip是按数组存储的,遍历时按索引顺序判断。调参会的人通常会把active放前面、passive居中、critical放最后,逻辑上由低到高排列,这样step_wise的“当前落在哪个温度区间”判断才能逐级推进。如果你把critical放中间,代码里比大小判断就可能出现某个区间莫名提前触发的问题。

2.3 governor和cooling device的标准接口约定

governor本质是一组函数指针,核心是throttle():温度更新后被core调用,governor根据当前zone温度和trip状态,决定每个cooling device要处在什么状态。它的注册通过thermal_governor_add挂到一个全局链表,用户态随时可以通过policy文件切换。governor接口中真正重要的函数如下:

struct thermal_governor { char name[THERMAL_NAME_LENGTH]; int (*throttle)(struct thermal_zone_device *tz, int trip_id); int (*watch)(struct thermal_zone_device *tz, int trip); };

throttle被调用时,governor要遍历tz->thermal_instances找到所有关联的冷却设备实例,然后逐个调整cur_state。cooling device的标准接口是struct thermal_cooling_device_ops,最核心是下面三个:

struct 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); };

max_state代表冷却能力分几档,cur_state是当前档位。cpufreq_cooling在实现set_cur_state时,本质就是计算一个降频后的频率上限,然后调用cpufreq_update_util或者cpufreq_set_boost去限制最高频率。一台支持16档频率的CPU,max_state可能被映射成16档冷却等级。你再仔细想一下:频率是离散的,冷却等级是整数,所以governor实际上是在做“档位→频率上限”的映射,这也是为什么step_wise里state的升降都是一格一格来的。

core层在governor和cooling device之间还塞了一层thermal_instance,这是“trip-id + cooling-device”的连接器,它记录了这条冷却路径的上下限(lower/upper)、权重(weight),以及当前状态。一个trip点可以连多个冷却设备,一个冷却设备也可以被多个trip点共用。用一句话总结这个模型:thermal_zone是整个舞台,trips是门槛,cooling devices是执行者,thermal_instance把门槛和执行者配对并约束了每对之间的“档位范围”。

3. governor决策逻辑逐一点评:谁最适合什么场景

3.1 step_wise:从温度档位到冷却档位的翻译器

step_wise是内核里最常用、也最适合入门的governor。它做的事情非常直观:把zone的当前温度和各个trip点逐个比大小,确定当前处于哪个温度区间,然后据此升高或降低冷却state。决策时还会参考last_temperature判断趋势:如果升温趋势明显,state可以加大步长一次升多档;如果降温趋势明显,保守地逐步回调。

代码里关键是这两个辅助函数,处理的正是“过阈值时要不要跳档”:

static unsigned long get_target_state(struct thermal_instance *instance, int cur_temp, int trip_temp) { /* 温度高于trip且当前state < upper时,继续升高 */ if (cur_temp >= trip_temp && instance->target < instance->upper) instance->target = instance->target + 1; /* 温度低于trip且当前state > lower时,开始回落 */ else if (cur_temp < trip_temp && instance->target > instance->lower) instance->target = instance->target - 1; return instance->target; }

这段逻辑看着简单,工程里我却见过不少人把参数配错导致“降温后回不来”。回不来的根源在于每个instance的lower被配成了一个大于0的值。打个比方:你的冷却映射里写了cooling-device = <&cpu0 1 4>,这表示该冷却设备最低允许state是1,那温度降下来之后频率永远到不了顶,总被限制一档,用户感知就是“手机一直有点卡”。所以写设备树时,lower到底设成0还是1,一定要想清楚要不要允许完全恢复。

step_wise适合大多数嵌入式场景,尤其是有比较明确多级保护需求的产品。它不关心功耗建模,纯粹“看温度换档”,代码短、行为可预测、调试简单。缺点是响应全靠轮询频率,传感器上报慢的平台上降温动作会慢半拍。

3.2 power_allocator:用可控功耗换最大性能

power_allocator(简称IPA)是thermal governor里最复杂也最值得研究的一个,它的核心思路不再是“温度超了就降频”,而是把可用功耗当作资源去分配。它先预估整个热区在不超过目标温度的前提下能承受的“可持续功耗”(sustainable power,单位mW),然后通过PID控制器实时计算当前允许的总功耗,再按权重分给各个冷却设备。

PID三个分量在IPA里的角色分别是:

  • P(比例):当前温度与目标温度之差,偏差越大,允许功耗砍得越狠
  • I(积分):累积的温度超调量,专门对付持续发热——温度一直高于目标,积分项会不断加大功率压缩
  • D(微分):温度变化趋势,升温很快时提前抑制,抑制过冲

公式简化表达就是:allocated_power = k_p * err + k_i * ∫err + k_d * d(err)/dt,这里的k_po、k_pu、k_i是PID系数,weights控制每个冷却设备瓜分多少。实际运行时,IPA要求你给每个参与约束的冷却设备设置权重,权重越大分到的“功耗预算”越足,频率降得越少。

这套方案的最大优势是性能损失可控:同样一个热约束,step_wise可能把CPU从2.8GHz直接挂到1.2GHz,造成明显卡顿;IPA则会精确计算“降到1.8GHz刚好能让温度稳定在85度”,把多余性能留下来。服务器、笔记本这类对持续性能要求高的场景,IPA几乎是首选。而手机平台用的反而不多,原因是IPA调参成本高,sustainable_power需要实测整机热阻去标定,机型又太多,每款都标会累死工程师。

调试IPA时最常翻车的点就是thermal_zone_params里忘记配sustainable_power。这个值不配或配错,PID算出来的功率就是天方夜谭,表现为温度迟迟压不住或者频率下降幅度和预测完全不符。喂给IPA的参数基本都得靠“大功率负载+红外热像仪”实测标定,这也是它工程门槛高的主要原因。

3.3 bang_bang与fair_share:简单场景下的务实选择

bang_bang是个两态控制器,没有中间档位:温度超过trip就全速冷却,低于trip就完全停止。它最适合“要么开要么关”的执行机构,典型就是风扇——很多交流风扇PWM曲线其实就两步,转和不转。如果你用step_wise去控制这类执行器,反而会因为中间档位的无意义抖动损伤机械结构。bang_bang最简单的实现逻辑就是读温度、比较、写state 0或max_state,判断里需要做一次迟滞处理,否则按钮开关一样的精神抖擞。

fair_share则是按贡献比例分配冷却负担的轻量级governor:多个冷却设备共同承担降热任务时,它根据每个设备trip权重把“应该削减的性能”按比例摊派。它不像IPA那么精于功耗计算,也没有PID积分项,胜在极轻量、无调参成本,适合IoT设备或者传感器数量少、性能约束不严的场合。这两者在内核代码里都只占几百行,理解难度很低。

3.4 user_space:把决策权交给应用

user_spacegovernor是个很有意思的“透明模式”。它不产生任何降温决策,只是一个温度事件的窗口:温度进入某个trip区间时,通过netlink或者写入sysfs事件向用户态进程发通知,由系统服务(比如Android的thermal hal)来真正决定要不要降频、要不要关后台。有些SOC厂商甚至会把底层的调频口也交出去,用户态拿到温度直接操作cpufreq或调用私有服务。

我见过不少方案在user_spacemode下自己写一套多级降温表格,上层逻辑确实灵活,但隐患也很明显:用户态进程崩溃或者服务卡死,热保护就失灵了。内核态governor再傻,它至少能在sched线程里稳定执行;用户态再聪明,也扛不住系统锁死这种极端情况。所以我的建议很明确——critical trip的最终保护必须留在内核里,用户态策略只能处理active/passive级别的舒适优化,把命门交给应用层是产品事故高发点。

3.5 常见governor速查对比

Governor核心思路调参难度适合场景主要风险
step_wise温度区间→冷却档位低嵌入式、手机、通用保护档位跳变感知明显,恢复慢
power_allocator功耗预算+PID分配高服务器、笔记本、性能敏感整机sustainable_power标定不准就白给
bang_bang两态开关低风扇等启停设备迟滞配置不当导致的机械抖动
fair_share按权重分摊削弱低IoT、多冷却设备轻量均衡没有温度趋势预测,约束能力弱
user_space事件上抛用户态处理中Android、整机服务联动用户态崩溃风险,critical兜不住

4. devicetree里的thermal打法:从节点到生效

4.1 thermal-zones节点怎么组织

现代主流的SoC都走设备树配置thermal,arch/arm64/boot/dts/...里几乎都能看到thermal-zones根节点。一个zone通常对应一个或一组传感器,基本写法如下:

thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <250>; /* 被动模式(触发passive trip)时轮询间隔250ms */ polling-delay = <1000>; /* 常规模式下轮询间隔1000ms */ thermal-sensors = <&tsens0 0>; /* 传感器引用:tsens0控制器的第0通道 */ trips { cpu_alert0: cpu-alert0 { temperature = <85000>; /* 单位毫摄氏度mC,即85度 */ hysteresis = <5000>; /* 迟滞5度,触发后降到80度才恢复 */ type = "passive"; }; cpu_alert1: cpu-alert1 { temperature = <95000>; hysteresis = <10000>; type = "active"; }; cpu_crit: cpu-crit { temperature = <110000>; hysteresis = <0>; type = "critical"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu0 0 2>, <&cpu1 0 2>; }; map1 { trip = <&cpu_alert1>; cooling-device = <&fan0 0 10>; }; }; }; };

polling-delay-passive和polling-delay是轮询节奏的分水岭:系统在没触发被动trip时按慢节奏跑,省电;一旦进入被动保护区间,就切换到快节奏,提高响应速度。传感器延迟大的平台,这个值要往小了调。

thermal-sensors写法是<&controller channel>,一个控制器有多个通道时可以用多个条目。这里我多说一句:一个zone挂两个传感器时,取的是平均温度还是最大温度,取决于core实现的get_temp策略,这点我很早就踩过坑,代码里默认逻辑往往不是你想当然的“取最大”,而是取决于sensor驱动怎么实现聚合。调试时最好先打印一下thermal_zone0/temp的数值和实际手感温度对比,确认数据语义再放行。

4.2 trips和cooling-maps的绑定原理

trips节点里的每个trip会被解析成thermal_trip数组,cooling-maps里的每个cooling-device三元组会被解析成thermal_instance:第一个数字是lower,第二个是upper,第三个其实不是数字——三元组<&cpu0 0 2>的含义是“允许cpu0冷却state在0到2之间被governor调度”,第三个位置是绑定的冷却设备节点,不是上限值。正确格式是<&phandle lower upper>,其中lower和upper分别表示限定范围。我在日常review时经常看到有人把这种三元组误读为“设备/最低/最高频率”,于是往里面填频率值填了几万,结果编译不过直接在of_thermal解析时报invalid cooling device错误。

cooling map的绑定逻辑典型如下:当zone温度触发cpu_alert0(85度passive)时,governor会遍历所有链接到该trip的instance,把cpu0和cpu1的state控制在0~2之间。如果map0和map1都绑定了同一块CPU,那该CPU的温度保护会有一个“先到先得”的问题:哪个trip先被触发,state就由谁控制;两者同时触发时,state会被推到更高值。绑定时注意不要让多个map的上下限互相覆盖,否则“升上去了降不下来”的经典故障就会排队找上你。

4.3 与cpufreq、cpuidle、devfreq的衔接细节

cooling device的底层实现和对应的子系统怎么握手,值得单独展开。cpufreq_cooling在注册时会创建一个虚拟的冷却设备,max_state等于参与调控CPU的插槽数乘以每个插槽频率档位数。它的set_cur_state实现最终调用cpufreq_update_policy,把policy的max字段临时改低,也就是在cpufreq驱动层面人为封顶。要注意这和cpufreq governor(比如schedutil)不是一回事:schedutil决定的是“负载高时要冲多高”,thermal只是在你头顶加了一个“最大天花板”。负载高但温度低,依然全速跑;温度一到天花板就往下压,实时调度器再急也没用。

cpuidle_cooling相对少见,它通过调整idle state深度来增加散热时间,缺点是生效太温和,大多平台不会作为主力。devfreq_cooling则是给GPU、DDR等非CPU设备用的,原理和cpufreq_cooling完全一致,调devfreq的max_freq。很多做游戏手机优化的兄弟会同时绑CPU、GPU两个冷却设备,再配IPA统一分配功耗权重,这类场景性能调优我最推荐:CPUGPU分开降权,游戏才不会出现“CPU没降但GPU瞬间熄火”的体验断崖。

设备树里还有一个容易被忽略的字段,就是热区的thermal-pressure属性。内核从5.10附近起引入了“thermal pressure”的概念,把“因散热而降频造成的性能损失”折算成一个压力值传给调度器负载均衡。调度器看到某个CPU cluster有thermal pressure,就会把新任务派给温度低的核,避免热核雪上加霜。这个机制能显著改善多核场景下的体感,但需要CONFIG_HAVE_THERMAL_PRESSURE配置,老内核没有,移植时记得检查。

5. 用户态接口与调试验证:sysfs是最后的照妖镜

5.1 顺着/sys/class/thermal/找线索

所有thermal zone注册成功后,都会在/sys/class/thermal/下出现一组目录:thermal_zone0、thermal_zone1、cooling_device0、cooling_device1。这几个文件是排查问题的第一现场:

# 查看当前zone名称与实时温度(毫摄氏度) cat /sys/class/thermal/thermal_zone0/type # 输出 cpu-thermal cat /sys/class/thermal/thermal_zone0/temp # 输出 45123,即45.123度 # 查看当前绑定的governor与可选列表 cat /sys/class/thermal/thermal_zone0/policy # 输出 step_wise cat /sys/class/thermal/thermal_zone0/available_policies # 查看trip点配置 cat /sys/class/thermal/thermal_zone0/trip_point_0_temp cat /sys/class/thermal/thermal_zone0/trip_point_0_type # 手动操作冷却设备(调试时慎用) cat /sys/class/thermal/cooling_device0/type cat /sys/class/thermal/cooling_device0/max_state echo 4 > /sys/class/thermal/cooling_device0/cur_state # 强制拉到第4档

temp是不是毫摄氏度单位、trip_point_*_temp的数值单位,不同内核版本会存在差异。从5.x之后大量驱动统一到mC,但有个别老驱动还要除以1000才能换算出摄氏度。看数据时先对一下type,再拿到温度计实测校核一次,别被单位坑。

链路的验证顺序一般是三句话:先确认温度会随负载变化(sensor正常),再确认policy切到不同governor不会报错(core层正常),最后手动写cur_state确认频率或风扇会跟着动(cooling device正常)。三步全过,framework基本就没有问题;哪一步断了,问题就缩小到哪一层。

5.2 实战排查:governor按温度动作了,但频率没降

这是我在支持团队时遇到最多的Case:负载拉满,温度到了85度,trip状态也变了,cat /sys/class/thermal/cooling_device0/cur_state能看到state在涨,但cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq纹丝不动。这种问题的排查路线特别固定:

  1. 确认cooling device到底是不是cpufreq_cooling:cat cooling_deviceX/type,如果不是cpufreq_cooling而是thermal-cpufreq之类自造驱动,可能它的set_cur_state压根没接真实调频接口,只是更新了一个假state给用户看
  2. 确认绑定关系:cat thermal_zoneX/trip_point_X_cooling_device有值吗?没有就是设备树里cooling-maps没配对
  3. 确认cpufreq驱动是否支持thermal干预:有些老平台没有注册cpufreq_cooling,或者CONFIG_CPU_FREQ_THERMAL没打开,CPU侧即使收到state更新也无处下手
  4. 看dmesg有没有拒绝信息:调频驱动如果锁了scaling_max_freq或者被其他模块约束,thermal降频会被覆盖,dmesg里常有cpufreq: __target_index: Failed to change cpu frequency这样的字眼
  5. 查thermal pressure:新内核里如果CPU已经报告thermal pressure给调度器,虽然频率没降,但调度器已经主动把任务迁移走,实际发热也在下降,这不算bug,是你把“降频”误当成了唯一出路

还有一种隐蔽情况是governor选了user_space但用户态没有对应服务,此时trip信息会在,但冷却设备几乎不动作。因为user_space根本不做决策,所有期望都落在应用层。很多OEM出厂前把policy设成user_space交给厂商服务,服务一旦没起来,后台抓log看,thermal数据全是“正常”的假象,这个问题排查时务必要先确认当前policy是哪个governor。

5.3 常见决策异常速查表

现象大概率原因对策
temp一直显示0sensor驱动没probe,或读取回调返回错误被忽略检查thermal-sensors引用的节点,搜dmesg里sensor注册日志
到阈值不触发trip温度单位搞错,或polling-delay过大确认trip_type与实际判断逻辑,先手动echo temp触发
cur_state到顶但温度压不住cooling map的upper太低,散热能力不够调大upper,或绑更多冷却设备
降温后state不回落instance lower配置大于0,或hysteresis为0导致振荡检查lower,给trip配合理迟滞
critical trip直接关机但没日志内核执行了默认关机流程,没来得及落盘加trace确认触发源,不要指望log
切governor报operation not permitted有热区锁或被thermal_zone_params限制了策略检查thermal_zone_policy是否被冻结,或改dts里governor

这里再披露一个调试技巧:内核的thermal_zone_device为调试提供了emul_temperature仿真接口,echo 85000 > /sys/class/thermal/thermal_zone0/emul_temp可以直接伪造一个85度的温度,完整走一遍“升温→governor决策→cooling device动作”的链路,不用真的拿风扇对着芯片吹或者跑压力测试。第一次bringup thermal时先用emul_temp仿真,能把“硬件温度到达”和“软件逻辑bug”彻底分隔开,排障效率翻倍。但是一定要注意:emul_temp在生产固件里必须关闭,否则被上层误写一个假温度,整机决策全乱。

6. 补充几个从源码里爬出来的细节经验

thermal framework的代码量不小,但吃透骨干之后再看每个驱动的实现,会发现都是在一个固定的“套路”里做文章。我再补充三条实战里被反复验证过的经验,给已经在动手翻代码的兄弟一点线索:

第一,温度更新频率决定整个闭环的响应上限。governor再聪明,如果sensor每2秒才更新一次温度,降温动作落地就是2秒之后的事情,这期间硬件可能已经逼近临界点。所以做高速率发热场景(比如充电+游戏同开)时,一定要把polling-delay-passive压到200ms以内,宁可多耗一点电,也不能让thermal保护滞后。

第二,考察一个热方案的健壮性,重点看“恢复路径”而不是“触发路径”。很多人验收thermal只看“温度高有没有降频”,忽略了“温度低了有没有恢复”。而恢复路径恰恰是各种bug的高发区:hysteresis配错、lower设错、sensor更新频率与hysteresis不匹配导致永远到不了恢复点,这类问题隐蔽且伤体验。我建议验收时专门测“满载关机后迅速降载”的场景,观察cur_state是否能平滑回到0。

第三,多热区联动时留意zone之间的优先级。多个thermal_zone共享同一个cooling device时,每个zone都会去改这个cdev的state,内核默认的thermal_zone_device_update会把它们交织在一起,表现就是“高负载时几个zone轮流争抢风扇”。如果你做多热区产品,建议在设计期就明确好“哪个zone主导哪个cdev”,不要让两个毫无关联的zone同时写同一个风扇的cur_state,否则最终效果一定是振荡加风扇噪音。

代码里的细节还有很多,比如thermal_zone_get_temp的加锁方式、__thermal_zone_set_trips怎么依据窗口决定是否上下限更新、sensor驱动里的set_trips与中断模式配合的正确姿势,这些单拉出来都够写成一篇文章。对刚入手的兄弟,我的建议是顺着本文的路线先把thermal_core.c里__thermal_zone_device_update这个函数读三遍,它把温度更新、trip遍历、governor调用、冷却设备执行全部串在一起,读懂它相当于拿到了一张thermal framework的全局对照表。

嵌入式开发这条路就是不断踩坑填坑,thermal这块尤其难在“平时看不出问题,一发热就是事故”。把框架的通用骨架弄扎实,遇到问题时先分层排查,基本就能避免大半夜被叫起来背锅了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询