Linux thermal framework 温控框架:从架构到设备树配置实战 1. 从一次温控翻车说起thermal framework 到底管什么去年帮一个朋友调一块嵌入式板子跑压力测试不到三分钟就降频跑分直接腰斩。他第一反应是散热器没装好换了硅脂、加了风扇问题照旧。后来抓了一段内核日志发现是 thermal zone 在 85 度就触发了被动降温把 CPU 频率压到了最低档。散热硬件没问题问题出在温控策略的触发点上——这就是 thermal framework 在背后干活。Linux 内核的功耗子系统里thermal framework 是专门负责“温度感知与响应”的那一层。它要干的事情说起来很朴素读温度、判断是否越界、越界了采取动作。但真落到代码里它牵扯到传感器驱动、设备树描述、调频子系统、冷却设备注册、governor 策略选择、中断与轮询机制等一大堆东西。很多人第一次看drivers/thermal/目录会觉得文件不算多但每个文件背后都连着一整套设备模型。这篇内容适合谁看如果你在做嵌入式 Linux 开发、功耗优化、或者单纯想搞明白“为什么我的板子一热就卡”那 thermal framework 是绕不开的一环。我会从整体架构讲起把 thermal zone、thermal governor、cooling device、trip point 这几个核心概念串起来再落到实际的设备树配置和调试方法上。不堆源码但关键结构体和调用链会点到让你看完能自己动手配一个 thermal zone 出来。需要提前说明的是不同内核版本之间 thermal framework 的 API 有过调整比如thermal_zone_device_register的参数在 5.x 之后有变化devfreq_cooling的注册方式也改过。我下面讲的内容以 5.10 到 6.x 这条主线为准遇到版本差异会单独标出来。2. thermal framework 的整体架构与设计思路2.1 为什么内核要单独搞一套温控框架温度这件事硬件层面其实已经有保护了。大多数 SoC 内部都有一个硬件温控模块温度超过临界值直接拉低频率甚至断电。那为什么内核还要再搞一套软件框架原因在于硬件保护是“最后一道防线”它的触发点通常设得很高比如 105 度或 110 度而且动作很粗暴——要么全速要么停。软件框架的价值在于“提前介入、精细调节”。在温度还没到危险区之前就通过降频、限制 GPU、关掉部分核心等手段把温度压住让设备在性能和温度之间找到一个平衡点。这就像开车硬件保护是安全气囊软件温控是提前松油门。thermal framework 的设计目标可以归纳成三条第一把温度传感器抽象成统一的设备不管你是 I2C 温度芯片还是 SoC 内部寄存器对上层的接口一致第二把“降温手段”也抽象成设备CPU 调频、GPU 调频、风扇、甚至主动降亮度都可以注册成 cooling device第三用可配置的策略把“温度”和“动作”关联起来这个策略就是 governor。2.2 四个核心角色zone、sensor、cooling device、governor整个框架围绕四个角色转理解它们的关系架构就通了一半。thermal zone是一个逻辑上的“温控区域”它代表一个需要被监控的温度点。比如“CPU 集群温度”“GPU 温度”“电池温度”都可以是一个 zone。一个 zone 绑定一个温度传感器同时可以绑定多个 cooling device。zone 是框架调度的基本单位。thermal sensor是实际读温度的驱动它通过thermal_zone_device注册进框架。读温度的方式有两种一种是直接读寄存器一种是走通信总线I2C/SPI读外部芯片。框架不关心你怎么读只要求你实现get_temp回调。cooling device是“可以被降温动作影响的设备”。CPU 是一个 cooling device因为你可以通过调频降低它的发热风扇是一个 cooling device因为你可以调它的转速。每个 cooling device 有一个states数组表示它支持的降温档位档位越高降温越狠。governor是策略大脑。它定期检查 zone 的温度对照 trip point决定把哪个 cooling device 调到哪个档位。内核自带几种 governorstep_wise、fair_share、bang_bang、power_allocator。选哪个取决于你的场景。它们之间的数据流是这样的sensor 提供温度 → zone 持有温度和 trip point → governor 读取 zone 状态并计算动作 → 动作作用到 cooling device → cooling device 降低发热 → 温度回落。这是一个闭环。2.3 设备树如何描述温控拓扑在嵌入式场景里thermal zone 和 cooling device 的绑定关系通常写在设备树里。这样驱动代码不用硬编码换一块板子只改 dts 就行。一个典型的 thermal zone 节点长这样thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive 250; 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是进入被动降温后的轮询间隔单位毫秒设成 250 意味着每 250ms 检查一次温度。polling-delay是正常状态下的轮询间隔通常设大一点省电。thermal-sensors指向具体的传感器节点。trips里定义触发点type有三种passive表示触发后走调频降温active表示触发风扇等主动设备critical表示临界触发后直接关机保护。cooling-maps把 trip 和 cooling device 关联起来THERMAL_NO_LIMIT表示不限制档位范围。注意hysteresis是迟滞值防止温度在触发点附近抖动导致频繁开关降温。比如触发点是 70 度迟滞 2 度那么温度要降到 68 度以下才会解除降温。这个值设太小会导致策略震荡设太大会导致降温解除不及时。3. 核心数据结构与注册流程拆解3.1 thermal_zone_device 结构体里有什么thermal_zone_device是框架的核心结构体定义在include/linux/thermal.h。它字段很多但真正需要关注的就那么几个。type是 zone 的名字比如 cpu-thermal在 sysfs 里会体现。temperature是最近一次读到的温度单位是毫摄氏度。trips和num_trips描述触发点数组。governor指向当前使用的策略。ops是驱动实现的回调集合最关键的是get_temp。polling_delay和polling_delay_passive控制轮询节奏。passive标志表示当前是否处于被动降温状态。还有一个容易被忽略的字段是prev_low_trip和prev_high_tripgovernor 用它们来判断温度是往上走还是往下走从而决定是加大降温还是解除降温。这个设计是为了避免“温度刚降一点就立刻解除降温然后马上又超温”的震荡。3.2 传感器驱动怎么注册一个 zone注册流程分两步先实现thermal_zone_device_ops再调用注册函数。static int my_sensor_get_temp(struct thermal_zone_device *tz, int *temp) { u32 val; regmap_read(my_regmap, TEMP_REG, val); *temp (val 0xFFF) * 100; /* 转成毫摄氏度 */ return 0; } static const struct thermal_zone_device_ops my_sensor_ops { .get_temp my_sensor_get_temp, }; static int my_sensor_probe(struct platform_device *pdev) { struct thermal_zone_device *tz; tz devm_thermal_zone_of_sensor_register(pdev-dev, 0, priv, my_sensor_ops); if (IS_ERR(tz)) return PTR_ERR(tz); return 0; }这里用的是devm_thermal_zone_of_sensor_register它是设备树场景下的推荐接口会自动从 dts 里读取 trip point 和 cooling map 配置。如果你的驱动不走设备树可以用thermal_zone_device_register但那样 trip 和 cooling device 都得在代码里手动配。实操心得get_temp回调里不要做耗时操作。这个函数会被 governor 在轮询时频繁调用如果里面走了慢速 I2C 读会拖慢整个温控响应。如果传感器读一次确实慢考虑在驱动里做缓存或者用中断方式更新温度而不是纯轮询。3.3 cooling device 的注册与档位设计cooling device 的注册有两种方式。CPU 调频冷却通常由 cpufreq 子系统自动注册你不需要手动写。风扇、GPU 这类需要自己注册。static int my_fan_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state) { *state 3; /* 支持 0-3 共 4 档 */ return 0; } static int my_fan_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state) { /* state 0 关3 最大转速 */ fan_set_pwm(state * 85); return 0; } static const struct thermal_cooling_device_ops my_fan_ops { .get_max_state my_fan_get_max_state, .set_cur_state my_fan_set_cur_state, }; cdev thermal_cooling_device_register(my-fan, priv, my_fan_ops);档位设计有个原则档位越高降温效果越强但代价也越大。对于风扇0 档是关最高档是全速。对于 CPU档位对应的是频率限制程度。step_wisegovernor 会从低档往高档逐步试探而不是一上来就拉满这样能避免过度降温影响性能。4. governor 策略选择与工作原理解析4.1 step_wise最常用的渐进式策略step_wise是大多数场景的默认选择。它的逻辑很直白温度超过 trip 点就把 cooling device 的档位加一温度降回 trip 点以下就把档位减一。每次只动一档所以叫“渐进式”。它的好处是平滑不会因为温度瞬间波动就大幅调整。坏处是响应慢如果发热很猛一档一档加可能来不及。所以step_wise适合发热变化平缓的场景比如手机日常使用。step_wise内部维护了一个thermal_instance列表每个 instance 对应一个 trip 和一个 cooling device 的绑定关系。它遍历所有 instance根据当前温度和 trip 的关系决定target档位。这里有个细节它区分“趋势”。如果温度在上升它会倾向于加档如果温度在下降它会倾向于减档。这个趋势判断就是靠前面提到的prev_low_trip和prev_high_trip。4.2 power_allocator精细化功耗分配power_allocator是更高级的策略它不满足于“超温就降档”而是试图在温度约束下最大化性能。它需要 cooling device 提供功耗模型也就是“在某个档位下这个设备消耗多少功率、能散多少热”。它的核心是 PID 控制器。根据当前温度和目标温度的偏差计算出应该分配多少功率预算然后按比例分配给各个 cooling device。这个策略适合对性能要求高、又需要精细温控的场景比如高性能 ARM 服务器芯片。用power_allocator的前提是你的 cooling device 实现了get_requested_power和state2power回调。如果没实现governor 会退化成简单模式效果大打折扣。4.3 bang_bang 与 fair_share 的适用边界bang_bang是最简单的策略温度超过 trip 点cooling device 直接拉到最高档温度降下来直接关掉。它适合风扇这种“要么转要么不转”的设备但不适合 CPU 调频因为频繁全开全关会导致性能剧烈抖动。fair_share的思路是“公平分配”。当多个 cooling device 绑定到同一个 trip 时它根据每个设备的当前档位和最大档位按比例分配降温任务。比如两个设备一个已经满档另一个还有余量它会让后者多承担一些。这个策略在多 cooling device 场景下比step_wise更均衡。选择 governor 没有绝对标准我的经验是单设备、发热平缓用step_wise多设备、需要均衡用fair_share有精确功耗模型、追求性能用power_allocator风扇这种二元设备用bang_bang。5. 实操从零配置一个 thermal zone 并验证5.1 确认内核配置项动手之前先确认内核开了相关选项。在.config里检查这几项CONFIG_THERMALy CONFIG_THERMAL_OFy CONFIG_THERMAL_GOV_STEP_WISEy CONFIG_THERMAL_GOV_POWER_ALLOCATORy CONFIG_CPU_THERMALy CONFIG_CPU_FREQ_GOV_THERMALyCONFIG_THERMAL_OF是设备树支持嵌入式场景必开。CONFIG_CPU_THERMAL让 CPU 能注册成 cooling device。CONFIG_CPU_FREQ_GOV_THERMAL是调频子系统和温控的桥梁没它 CPU 降温不生效。5.2 编写设备树节点假设 SoC 内部有一个温度传感器寄存器基地址0x10050000支持一个 trip 点 70 度触发 CPU 降频临界 95 度关机。dts 这样写tsadc: tsadc10050000 { compatible myvendor,tsadc; reg 0x10050000 0x1000; clocks cru SCLK_TSADC; #thermal-sensor-cells 1; }; thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive 200; polling-delay 1000; thermal-sensors tsadc 0; trips { cpu_alert: cpu-alert { temperature 70000; hysteresis 2000; type passive; }; cpu_crit: cpu-crit { temperature 95000; hysteresis 1000; type critical; }; }; cooling-maps { map0 { trip cpu_alert; cooling-device cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT, cpu1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT; }; }; }; };#thermal-sensor-cells 1表示这个传感器节点需要一个参数来区分通道这里用 0 表示第一个通道。cooling-maps里绑定了 cpu0 和 cpu1意味着降温时两个核心一起限频。5.3 验证 zone 是否注册成功系统起来后先看 sysfsls /sys/class/thermal/应该能看到thermal_zone0、cooling_device0这样的目录。进到 zone 目录里cat /sys/class/thermal/thermal_zone0/type cat /sys/class/thermal/thermal_zone0/temp cat /sys/class/thermal/thermal_zone0/policytype显示 zone 名字temp显示当前温度毫摄氏度policy显示当前 governor。如果temp读出来是 0 或者报错说明get_temp回调有问题回去查传感器驱动。再看 cooling devicecat /sys/class/thermal/cooling_device0/type cat /sys/class/thermal/cooling_device0/cur_state cat /sys/class/thermal/cooling_device0/max_statecur_state是当前档位max_state是最大档位。如果 CPU cooling device 的max_state是 0说明 cpufreq 没注册成功检查CONFIG_CPU_THERMAL和 cpufreq 驱动。5.4 用热压测试观察降温行为验证配置是否生效最直接的办法是制造热源。可以用stress-ng跑满 CPUstress-ng --cpu 4 --timeout 300s同时开另一个终端监控温度和档位watch -n 1 cat /sys/class/thermal/thermal_zone0/temp; \ cat /sys/class/thermal/cooling_device0/cur_state正常情况下你会看到温度爬升到 70 度附近cur_state从 0 变成 1然后温度被压住不再往上冲。如果温度继续冲到 95 度触发了 critical 关机说明降温力度不够要么 cooling device 档位太少要么 trip 点设太高。注意测试时确保板子散热条件和你实际使用场景一致。如果你在空调房里裸板测试通过装进密闭外壳后可能完全不是一回事。温控参数一定要在最终形态下验证。6. 常见问题与排查技巧实录6.1 温度读数为 0 或不变这是最常见的问题。先确认传感器驱动 probe 成功dmesg | grep thermal看有没有注册日志。如果驱动没问题检查get_temp回调的返回值。有些传感器需要先启动转换、等一段时间再读如果驱动里没做这个时序读出来就是 0。还有一种情况是寄存器地址或位域搞错了。用devmem直接读寄存器对比一下devmem 0x10050000 32如果寄存器值在变但temp不变那就是驱动解析逻辑的问题。6.2 降温不生效温度继续飙升先看 cooling device 的cur_state有没有变化。如果一直是 0说明 governor 没触发。检查 trip 点的type是不是passive如果是active但你没有注册对应的主动 cooling devicegovernor 不会动作。如果cur_state变了但温度不降说明 cooling device 的降温能力不足。CPU 调频冷却依赖 cpufreq 驱动如果 cpufreq 只支持两档频率降温效果就很有限。这时候要么增加频率档位要么加入更多 cooling device比如限制 GPU、限制内存带宽。6.3 温度在 trip 点附近频繁震荡这是迟滞值设太小导致的。温度刚到 70 度触发降温降了一度到 69 度就解除然后马上又到 70 度如此反复。解决办法是加大hysteresis一般设 2000 到 5000 毫摄氏度比较合适。另一个原因是polling-delay-passive设得太小导致 governor 检查太频繁。250ms 是个比较平衡的值设成 50ms 会增加 CPU 负担设成 1000ms 又响应太慢。6.4 常见问题速查表现象可能原因排查方向temp 读数为 0驱动未 probe 或时序错误dmesg 日志、寄存器直读cur_state 不变trip 类型不匹配或 governor 未加载检查 policy、trip type降温无效cooling device 档位不足查看 max_state、cpufreq 档位温度震荡hysteresis 太小或轮询太快调整迟滞和轮询间隔critical 误触发trip 温度设太低或传感器偏差校准传感器、调整 tripgovernor 切换失败内核未编译对应 governor检查 CONFIG_THERMAL_GOV_*6.5 几个容易踩的坑第一个坑是设备树里thermal-sensors的 phandle 写错。如果指向了一个没有#thermal-sensor-cells的节点注册会失败但日志不一定明显。用dtc反编译 dtb 确认一下。第二个坑是 cooling device 的states数组越界。set_cur_state里如果没做边界检查governor 传进来一个超出范围的档位可能导致未定义行为。一定要在回调里 clamp 一下。第三个坑是多个 zone 共享一个 cooling device 时的竞争。两个 zone 同时想调 CPU 频率governor 之间没有协调机制可能出现一个要降一个要升的情况。这种场景建议用power_allocator它能统一分配。7. 调试手段与性能观测进阶7.1 用 tracepoint 看 governor 决策过程thermal 子系统内置了 tracepoint可以打开看 governor 每次决策的细节echo 1 /sys/kernel/debug/tracing/events/thermal/enable cat /sys/kernel/debug/tracing/trace_pipe你会看到thermal_temperature、thermal_zone_trip、cdev_update等事件。thermal_zone_trip会打印哪个 trip 被触发cdev_update会打印 cooling device 档位变化。调策略的时候这个比看 sysfs 直观得多。7.2 用 thermal debugfs 看 zone 状态挂载 debugfs 后/sys/kernel/debug/thermal/下面有每个 zone 的详细信息cat /sys/kernel/debug/thermal/thermal_zone0/里面会列出所有 trip point、绑定的 cooling device、当前档位、以及 governor 的内部状态。排查“为什么没触发”这类问题时先看这里能省很多时间。7.3 观测降温对性能的实际影响温控生效不代表没有代价。用perf或者简单的time命令对比降温前后的性能time stress-ng --cpu 1 --cpu-method matrixprod --timeout 10s在温度触发前后各跑一次看耗时差异。如果降温后性能掉了一半以上说明 cooling device 档位设计太激进需要调整 trip 点或者增加中间档位。我个人在实际项目里的体会是thermal 配置没有“一次调好”的说法。不同外壳、不同环境温度、不同负载特征都会影响最终表现。比较务实的做法是先把 trip 点设保守一点确保不触发 critical然后根据实测数据逐步放宽找到性能和温度的平衡点。另外如果你用的是国产 SoC 平台厂商 BSP 里通常已经有一套 thermal 配置不要急着推翻重写先读懂它的 trip 点和 cooling map 设计意图很多时候改一两个参数就能解决问题比从头配一套省事得多。