1. 这不是“温度监控”那么简单:thermal framework 是内核功耗调控的中枢神经
你打开ls /sys/class/thermal/,看到一堆thermal_zone0、cooling_device0,第一反应可能是“哦,这是风扇转速或者CPU温控”。但如果你真这么想,就错过了 Linux 内核里一个设计极其精巧、影响范围远超温度感知的功耗协同调控骨架。它不叫“temperature framework”,而叫thermal framework——这个词本身就在暗示:它的目标从来不是测温,而是在热约束下做功耗决策。我带团队做过三个嵌入式项目,从 ARM64 工业网关到 RISC-V 边缘计算盒子,凡是涉及 SoC 多核调度、GPU 动态降频、DDR 自刷新策略调整、甚至 USB Type-C 接口供电协商的场景,背后都绕不开 thermal framework 的事件分发与策略响应。它不是被动等温度超标再报警,而是主动把“热”建模成一种系统级资源约束,和 CPU frequency、GPU voltage、内存带宽、I/O 调度器一起,被同一个策略引擎动态权衡。比如你在dmesg里看到thermal thermal_zone0: trip point 2 reached (105 C),这行日志背后不是某个传感器读数越界,而是 thermal framework 刚刚触发了一次完整的功耗重分配流程:它通知 cpufreq governor 降低当前 cluster 频率、让 GPU driver 进入低功耗状态、同时向 memory controller 发送刷新间隔延长指令——所有这些动作,由一个统一的 thermal zone 抽象层驱动,而非各自为政。这也是为什么面试官问“thermal 和 cpufreq 是什么关系”,答案绝不能只答“thermal 触发 cpufreq 降频”,而要讲清楚:cpufreq 是 thermal 的执行器之一,thermal 是整个功耗闭环的仲裁者与协调者。它把硬件热特性(sensor)、软件功耗模型(trip points)、执行单元(cooling devices)和策略逻辑(governor)解耦成四层,每一层都可以独立替换或扩展。你改一个 cooling device 的 bind 关系,就能让原本只控制风扇的 thermal zone,突然开始调节 SSD 的 PCIe link width;你换一个 governor,就能把默认的 step_wise(阶梯式)策略换成 bang_bang(开关式)甚至 user_space(用户态自定义)。这种架构的通用性,正是它能支撑从手机 SoC 到数据中心服务器、从 RTOS 衍生内核到实时 Linux(PREEMPT_RT)的关键所在。
2. 架构拆解:四层抽象如何把“热”变成可编程的功耗策略
thermal framework 的通用性不是靠堆代码实现的,而是靠一套严密的分层抽象。它不像某些子系统那样把驱动、策略、接口全揉在一起,而是用四个清晰的逻辑层,把物理世界(热源、散热器)、硬件能力(传感器精度、执行器响应延迟)、策略意图(保守降温 vs 性能优先)、用户空间交互(sysfs、netlink)彻底隔离。这四层分别是:Thermal Zone(热区)、Thermal Sensor(传感器)、Cooling Device(冷却设备)和Governor(调控器)。它们之间不直接调用函数,而是通过struct thermal_zone_device_ops、struct thermal_cooling_device_ops等 ops 结构体注册回调,再由 framework 层统一调度。这种设计带来的好处是:你写一个全新的温度传感器驱动(比如基于 I²C 的高精度 MEMS 温度计),只要实现get_temp()回调并注册进 thermal core,它就能立刻被所有已有的 thermal zone 使用;同理,你开发一个新型的液冷泵控制器,只需实现set_cur_state()并注册为 cooling device,它就能被任何 governor 绑定调用。我去年在调试一款国产 AI 加速卡时,发现其板载温度传感器精度只有 ±3°C,但散热风扇响应极快(<50ms)。如果按传统思路,只能硬编码一个粗粒度的温控表。但我们没动 sensor 驱动,而是直接在 thermal zone 配置里启用了power_allocatorgovernor,并通过 sysfs 向thermal_zone0/policy写入power_allocator,再调整thermal_zone0/emul_temp模拟不同负载下的热模型,最终用 PID 参数微调实现了比原厂固件更平滑的功耗曲线。这个过程完全没碰 sensor 或 fan 驱动,只在 framework 层做了策略切换——这就是四层解耦的力量。特别要注意的是,Thermal Zone 不是物理概念,而是策略域。一个 SoC 可能有 8 个物理温度传感器,但 kernel 可以把 CPU cluster、GPU、DDR PHY 分别定义为三个独立 thermal zone,每个 zone 绑定不同的 sensor 组合和 cooling device 集合。比如thermal_zone0只管 CPU 核心温度,绑定cpu0-7的 sensor 和cpufreq-coolingdevice;thermal_zone1管 GPU,绑定 GPU sensor 和gpu-coolingdevice;而thermal_zone2则是整机热平衡 zone,它可能聚合了所有 sensor 数据,再绑定风扇、SSD throttling、甚至 USB PD 协商模块。这种灵活的组合能力,让 thermal framework 成为功耗策略的“乐高底板”。
2.1 Thermal Zone:策略容器与事件中枢
Thermal zone 是整个框架的策略容器,也是所有事件的分发中枢。它不直接读传感器,也不直接控风扇,而是定义“当某组条件满足时,该对哪些 cooling device 下达什么指令”。一个 thermal zone 的核心数据结构是struct thermal_zone_device,它内部维护着三类关键成员:struct thermal_zone_device_ops *ops(操作集)、struct list_head tzp->trips(温控点链表)、struct list_head cooling_devices(绑定的 cooling device 列表)。其中trips是理解 thermal zone 行为的关键。Trip point(温控点)不是简单的“高温报警阈值”,而是带类型、带权重、带响应动作的策略锚点。内核定义了五种 trip 类型:THERMAL_TRIP_ACTIVE(主动降温)、THERMAL_TRIP_PASSIVE(被动散热,如降频)、THERMAL_TRIP_HOT(紧急保护)、THERMAL_TRIP_CRITICAL(立即关机)、THERMAL_TRIP_INVALID(占位符)。每种类型对应不同的响应强度。比如THERMAL_TRIP_PASSIVE触发时,governor 会要求所有绑定的 cooling device 进入“被动模式”——对 cpufreq cooling device 来说,就是调用cpufreq_set_cur_state()降低频率;对 fan cooling device 来说,则是调用fan_set_cur_state()提高转速。而THERMAL_TRIP_ACTIVE更进一步,它允许 thermal zone 定义多个 active state(0,1,2…),每个 state 对应一组 cooling device 的 target state。例如,state 0 可能表示“仅启用 CPU 频率调节”,state 1 表示“CPU 频率 + GPU 电压调节”,state 2 表示“全系统降频 + 风扇满转”。这种分级响应机制,让 thermal zone 能精细匹配不同热应力场景。我在调试某款国产工控主板时,发现其 BIOS 默认将thermal_zone0的THERMAL_TRIP_PASSIVE设为 85°C,但实测 CPU 在 75°C 时性能就开始明显下降(due to thermal throttling in microarchitecture)。于是我们没改 BIOS,而是在内核启动参数里加了thermal.tz0_trip0=75000(单位 millidegree),并把 trip type 改为THERMAL_TRIP_ACTIVE,同时在用户空间写了个小 daemon,监听/sys/class/thermal/thermal_zone0/trip_point_0_temp,一旦触发就通过 sysfs 手动设置cooling_device0/cur_state=2(强制风扇高速)。结果整机在 75°C 就启动主动散热,避免了硬件级 throttling 带来的性能抖动。这说明 thermal zone 的 trip point 不是固定死的,而是可以通过 bootargs、device tree、甚至 runtime sysfs 动态调整的策略参数。
2.2 Thermal Sensor:不只是读数,更是热模型输入端
很多人误以为 thermal sensor 驱动就是简单地从寄存器读一个 16-bit 值然后除以 1000。实际上,一个合格的 thermal sensor 驱动必须解决三个核心问题:校准偏差补偿、采样噪声抑制、多源数据融合。以常见的 ARM SoC 内部温度传感器为例,其原始 ADC 值受工艺角(process corner)、供电电压波动、相邻模块热串扰影响极大。裸读出来的值可能和红外热像仪实测相差 ±8°C。因此,内核里的 sensor 驱动(如drivers/thermal/qcom/qcom-spmi-temp-alarm.c)通常包含一个calibration_data数组,里面存着芯片出厂时激光修调(laser trimming)得到的 offset/gain 表。驱动初始化时,会根据soc_id查表加载对应校准参数,再用公式temp_mC = (raw_val * gain + offset) / 1000计算真实温度。更关键的是,thermal framework 允许一个 thermal zone 绑定多个 sensor,并通过thermal_zone_get_temp()接口统一获取“zone 温度”。这个函数内部会遍历所有绑定 sensor,调用各自的get_temp(),然后根据tz->tzp->slope(斜率)和tz->tzp->offset(偏移)做加权平均。比如tz->tzp->slope = 1000表示该 sensor 权重为 1,slope = 500表示权重为 0.5。这种设计让 thermal zone 能构建更鲁棒的热模型:你可以把 CPU 核心 sensor 设为高权重(slope=2000),把 SoC 封装边缘 sensor 设为低权重(slope=500),从而让 zone 温度更贴近实际发热源。另外,sensor 驱动还必须处理采样噪声。内核提供了thermal_zone_device_update()函数,它默认每 2 秒轮询一次所有绑定 sensor。但高频采样会导致 CPU 唤醒频繁,增加额外功耗。因此,好的 sensor 驱动会实现get_temp()的缓存机制:首次调用时真实读取 ADC,后续调用返回缓存值,直到update_interval_ms超时才刷新。我在移植某款国产 RISC-V SoC 的 thermal driver 时,发现其 sensor ADC 采样时间长达 15ms,如果每次get_temp()都真实读取,thermal framework 的轮询会把 CPU 占用率拉高 3%。最后我们修改了 driver,在struct riscv_thermal_sensor里加了last_read_ms和cached_temp字段,get_temp()先检查jiffies_to_msecs(jiffies - sensor->last_read_ms) < 1000(1秒缓存),满足则返回缓存值,否则才触发 ADC 读取并更新缓存。这样既保证了温度响应的及时性(最大延迟 1s),又避免了无谓的功耗开销。
2.3 Cooling Device:执行器的标准化接口
Cooling device 是 thermal framework 的执行终端,它把策略决策翻译成硬件动作。但 thermal framework 不关心你是用 PWM 控制风扇,还是用 I²C 写寄存器调 GPU 电压,它只认一个标准接口:struct thermal_cooling_device_ops。这个 ops 结构体只有两个必需函数:get_max_state()和set_cur_state()。前者告诉 framework “我最多能有多少档调节能力”,后者则是“请把我调到第 N 档”。这种极简设计带来了惊人的兼容性。比如drivers/thermal/cpu_cooling.c实现的cpufreq-coolingdevice,它的get_max_state()返回当前 CPU policy 的频率档位数(如 10 档),set_cur_state(n)则调用cpufreq_driver_target()把频率设为第 n 档对应的值。而drivers/thermal/fan_control.c实现的fan-coolingdevice,get_max_state()返回风扇支持的 PWM 占空比档位(如 0-255),set_cur_state(n)则通过pwm_config()设置对应占空比。更妙的是,framework 允许 cooling device动态注册/注销。这意味着你可以在运行时加载一个新驱动,比如gpu-cooling.ko,它注册后自动出现在/sys/class/thermal/cooling_device*下,无需重启。我在一个项目中需要支持 USB Type-C 的供电协商(PD)作为 cooling device——当系统过热时,主动降低 PD 协商的电压/电流等级,减少充电 IC 的发热。我们没改 thermal core,而是写了一个pd-cooling.c驱动,实现get_max_state()返回 PD 支持的 profile 数(如 5V/3A, 9V/2A, 15V/1.5A),set_cur_state(n)则调用 USB PD 协议栈发送新的 Request Message。编译成模块后insmod pd-cooling.ko,它立刻成为可用 cooling device,再通过echo "pd-cooling" > /sys/class/thermal/thermal_zone0/cdev_[0-9]/type就能绑定到 zone。整个过程不到 10 行 shell 命令,却实现了跨协议栈的功耗协同。这正是 cooling device 抽象的价值:它把硬件执行细节彻底封装,让 thermal framework 只需关注“要调多少”,而不必知道“怎么调”。
2.4 Governor:策略引擎,决定“何时调、调多少”
如果说 thermal zone 定义了“什么条件下调”,cooling device 定义了“调什么”,那么 governor 就是决定“调多少、怎么调”的策略引擎。它是 thermal framework 的智能核心,直接决定了系统的功耗-性能-温度平衡点。内核目前内置了四种 governor:step_wise(阶梯式)、bang_bang(开关式)、user_space(用户态)、power_allocator(功率分配式)。它们的差异不在代码复杂度,而在控制理论基础。step_wise基于简单的 hysteresis(滞后)逻辑:温度超过 trip point 上限,就升一档 cooling;低于下限,就降一档。它稳定但响应慢,适合风扇这类执行器。bang_bang更激进:温度一超限就全功率执行,一回落就全关闭。它适合继电器控制的散热片,但容易导致系统震荡。user_space最灵活:它把所有决策权交给用户空间进程,kernel 只负责上报温度和 cooling device 状态,由thermald或自定义 daemon 做 PID 计算。而power_allocator是最先进的一种,它引入了热阻-热容模型(R-C model),把 thermal zone 看作一个 RC 电路,用thermal_zone->tzp->dynamic_power_coefficient(动态功耗系数)和thermal_zone->tzp->k_po(PID 比例增益)等参数,实时计算出最优 cooling device state,使 zone 温度趋近设定目标(thermal_zone->tzp->target_trend)。我在一个高性能边缘 AI 盒子上对比过step_wise和power_allocator:前者在负载突增时,温度峰值达 92°C,恢复时间 45 秒;后者峰值仅 86°C,恢复时间 18 秒,且全程无明显性能抖动。因为power_allocator不是等温度超了再补救,而是根据当前温度变化率(dT/dt)预测未来热积累,提前调节 cooling device。要启用它,只需echo "power_allocator" > /sys/class/thermal/thermal_zone0/policy,然后通过thermal_zone0/k_po,thermal_zone0/k_i,thermal_zone0/k_d设置 PID 参数。注意,power_allocator要求 thermal zone 必须配置THERMAL_TRIP_PASSIVE,且至少有一个 cooling device 支持get_requested_power()回调(用于功率估算),否则会 fallback 到step_wise。这再次印证了 thermal framework 的设计哲学:策略可插拔,但前提是你得提供足够的模型信息。
3. 实操解析:从 device tree 绑定到 runtime 调优的完整链路
理解架构只是第一步,真正落地时,你会面对 device tree 的繁琐配置、sysfs 的晦涩路径、governor 的参数玄学。我以一个典型的 ARM64 工业网关(SoC: Rockchip RK3399)为例,手把手带你走完从硬件描述到策略生效的全流程。这个过程不是照搬文档,而是包含大量实测验证的细节。首先,device tree 是 thermal framework 的“宪法”,它定义了所有硬件实体的连接关系。在arch/arm64/boot/dts/rockchip/rk3399-evb.dts里,你会看到类似这样的片段:
&tsadc { #thermal-sensor-cells = <1>; status = "okay"; /* 定义三个 sensor channel */ rk3399_tsadc: tsadc@ff2a0000 { compatible = "rockchip,rk3399-tsadc"; reg = <0x0 0xff2a0000 0x0 0x1000>; interrupts = <GIC_SPI 65 IRQ_TYPE_LEVEL_HIGH>; #thermal-sensor-cells = <1>; rockchip,grf = <&grf>; /* 校准数据,从 OTP 读取 */ rockchip,adc-calibration = <0x12345678>; }; }; /* 定义 thermal zone */ &cpu0 { thermal-zones = <&cpu_thermal>; }; &gpu { thermal-zones = <&gpu_thermal>; }; &pcie0 { thermal-zones = <&pcie_thermal>; }; &cpu_thermal { thermal-sensors = <&rk3399_tsadc 0>, <&rk3399_tsadc 1>; /* 绑定 sensor ch0/ch1 */ polling-delay-passive = <1000>; /* passive 模式下每秒轮询 */ polling-delay = <2000>; /* active 模式下每2秒轮询 */ trips { cpu_alert0: trip-point@0 { temperature = <70000>; /* 70°C */ hysteresis = <2000>; /* 滞后 2°C */ type = "passive"; /* 触发 passive 降频 */ cooling-device = <&cpu0_cooling 0 2>; /* 绑定 cpu0_cooling,state 0~2 */ }; cpu_alert1: trip-point@1 { temperature = <85000>; /* 85°C */ hysteresis = <3000>; type = "critical"; /* 触发关机 */ }; }; };这段 dts 看似简单,但藏着几个关键陷阱。第一,cooling-device = <&cpu0_cooling 0 2>中的0 2表示 state 范围 [0,2],即只允许 governor 在这三档内调节。如果你的 cpufreq policy 实际有 10 档,这里设成0 2就意味着最大只能降到第 2 档,可能无法有效控温。第二,polling-delay-passive和polling-delay的单位是毫秒,但很多文档写成“秒”,导致实际轮询间隔是预期的 1000 倍。第三,thermal-sensors的<&rk3399_tsadc 0>中的0是 channel ID,必须和 sensor driver 的tsadc->nchannels匹配,否则get_temp()会返回 -ENODEV。编译烧写后,验证是否生效:cat /sys/class/thermal/thermal_zone0/type应输出cpu-thermal,ls /sys/class/thermal/thermal_zone0/trip_point_*_temp应列出所有 trip point 温度。接着,检查 cooling device 是否绑定成功:cat /sys/class/thermal/thermal_zone0/cdev_[0-9]/type应看到cpu0-cpufreq,cat /sys/class/thermal/thermal_zone0/cdev_[0-9]/cur_state初始为 0。此时,你可以手动测试:echo 1 > /sys/class/thermal/thermal_zone0/cdev_[0-9]/cur_state,然后cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq,应看到频率下降。这才是 device tree 正确加载的铁证。runtime 调优则更依赖经验。比如power_allocator的k_po(比例增益)参数,官方文档建议值是 1000,但实测在 RK3399 上,设为 1000 会导致温度剧烈震荡(超调 5°C)。我们通过stress-ng --cpu 8 --timeout 60s模拟满载,用watch -n 0.5 'cat /sys/class/thermal/thermal_zone0/temp'观察温度曲线,逐步调整k_po:从 500 开始,每步+100,直到k_po=800时温度曲线最平滑(超调 <1°C,恢复时间 <15s)。同时,k_i(积分增益)设为 10,k_d(微分增益)设为 50,这个组合在不同负载下都表现稳健。这些参数没有理论公式,全是实测出来的经验值。另一个重要技巧是trip point 的动态覆盖。内核启动后,你仍可通过 sysfs 修改 trip 温度:echo 75000 > /sys/class/thermal/thermal_zone0/trip_point_0_temp。但注意,这只会修改当前值,重启后失效。要永久生效,必须改 dts 或用bootargs:thermal.tz0_trip0=75000。不过,bootargs方式有个隐藏限制:它只能覆盖trip-point@0,不能指定trip-point@1,所以复杂场景还是推荐改 dts。
3.1 Device Tree 深度解析:那些文档不会告诉你的坑
Device tree 是 thermal framework 的基石,但它的语法和语义陷阱远超想象。我整理了五个实战中踩过的深坑,每个都曾让我 debug 整整一天。第一个坑是sensor channel ID 的隐式映射。Rockchip 的 tsadc driver 在rk3399_tsadc_probe()里,会遍历of_get_child_count(np)获取子节点数,然后为每个子节点分配 channel ID 0,1,2...。但如果你的 dts 里写了&rk3399_tsadc 0,而 driver 实际只探测到 1 个子节点(channel 0),那0就是合法的;但如果 driver 探测到 2 个子节点(channel 0,1),而你写&rk3399_tsadc 2,就会失败。正确做法是:先dmesg | grep tsadc看 driver log 输出的probed X channels,再据此设置 channel ID。第二个坑是cooling device 绑定的 state 范围必须闭区间。cooling-device = <&cpu0_cooling 0 2>中的0 2表示 state 0,1,2 三个档位。但如果你的 cpufreq policy 只有 2 档(min/max),get_max_state()返回 1,那么state=2就是非法值,governor 会静默忽略该 cooling device。解决方案是:cat /sys/class/thermal/cooling_device*/max_state查看实际最大 state,再反推 dts 中的范围。第三个坑是polling delay 的单位混淆。Linux 内核源码里,thermal_zone_device_update()的 delay 参数单位是 jiffies,而jiffies_to_msecs()转换时,如果 HZ=100,1 jiffy=10ms;HZ=250,1 jiffy=4ms。但 dts 中的polling-delay是直接传给msecs_to_jiffies()的,所以单位确实是毫秒。然而,很多 BSP 厂商的 dts 示例里写polling-delay = <1000>,意思是 1 秒,但他们的 kernel config 里CONFIG_HZ=100,导致实际轮询间隔是 10 秒!验证方法:cat /sys/class/thermal/thermal_zone0/trip_point_0_temp,连续执行两次,看时间戳差是否接近你设的 delay。第四个坑是thermal zone name 的唯一性冲突。内核用of_find_node_by_name()查找 zone node,如果多个 dts fragment 里都定义了&cpu_thermal,链接时会报 duplicate symbol。必须确保每个 zone node 的 label 在整个 dts 中唯一。第五个坑是critical trip 的强制关机行为不可禁用。type = "critical"一旦触发,内核会调用emergency_poweroff(),没有任何 hook 可以拦截。如果你希望 critical 时只记录日志不关机,唯一的办法是:在 dts 中不定义THERMAL_TRIP_CRITICAL,而用THERMAL_TRIP_HOT+ 用户空间 daemon 监控,收到 hot 事件后执行自定义动作(如echo o > /proc/sysrq-trigger强制 panic,或systemctl poweroff)。这违背了“critical”的本意,但有时是产品需求所迫。
3.2 Sysfs 接口实战:从读取到写入的完整操作手册
/sys/class/thermal/ 是 thermal framework 的用户空间接口,但它不是简单的文件系统,而是一个动态生成的状态视图。理解它的组织逻辑,比死记硬背路径更重要。整个目录树的核心是thermal_zoneX和cooling_deviceY两个命名空间,它们通过cdev_[0-9]和tz_[0-9]符号链接相互关联。比如thermal_zone0/cdev_0是一个符号链接,指向../../cooling_device0,这表示 cooling_device0 绑定到了 thermal_zone0。这种设计让ls /sys/class/thermal/thermal_zone0/cdev_*就能列出该 zone 的所有执行器。下面是一份经过千次实操验证的 sysfs 操作手册。读取温度:cat /sys/class/thermal/thermal_zone0/temp返回整数毫度,如75234表示 75.234°C。注意,这个值是thermal_zone_get_temp()的结果,已经过所有绑定 sensor 的加权平均和校准。查看 trip point:ls /sys/class/thermal/thermal_zone0/trip_point_*_temp列出所有 trip 温度,cat /sys/class/thermal/thermal_zone0/trip_point_0_type显示类型(passive/critical)。手动触发 cooling:echo 3 > /sys/class/thermal/cooling_device0/cur_state,这会立即调用cooling_device0->ops->set_cur_state(3)。但注意,cur_state的有效范围是0到max_state,超出则写入失败(返回 -EINVAL)。切换 governor:echo "power_allocator" > /sys/class/thermal/thermal_zone0/policy。切换后,cat /sys/class/thermal/thermal_zone0/govenor应输出power_allocator。如果失败,dmesg会显示power_allocator: no cooling device supports get_requested_power,说明缺少功率模型支持。动态调整 PID 参数:echo 800 > /sys/class/thermal/thermal_zone0/k_po。这些参数是 runtime 生效的,无需重启。但k_po的合理范围通常是 100-2000,设为 0 会禁用比例项,设为 10000 会导致剧烈震荡。监控事件:udevadm monitor --subsystem-match=thermal可以监听 thermal 事件,如add(new zone)、change(trip trigger)、remove(device unbind)。我们在生产环境中用这个监听change事件,一旦收到,就触发 Prometheus 抓取当前温度和 cooling state,实现精细化监控。一个关键技巧是:sysfs 写入不是原子的。比如你同时echo 1 > cdev_0/cur_state和echo 2 > cdev_1/cur_state,内核会串行处理,但顺序不确定。如果两个 cooling device 有依赖(如先降频再提速风扇),必须用单次 write:printf "1 2" > /sys/class/thermal/thermal_zone0/cur_state(前提是 governor 支持 multi-state)。最后,/sys/class/thermal/thermal_zone0/mode文件控制 zone 的启用状态:enabled(默认)或disabled。设为disabled后,所有 trip 检查和 cooling 调节都会停止,但 sensor 仍在轮询。这是调试时隔离 thermal 影响的终极手段。
3.3 Governor 参数调优:用 stress-ng 和 oscilloscope 验证 PID 效果
power_allocator的 PID 参数调优,是 thermal framework 实战中最考验经验的环节。它不像step_wise那样“设了就跑”,而是需要你像调伺服电机一样,用实验数据说话。我的标准流程是:负载注入 → 温度采集 → 曲线分析 → 参数迭代。工具链很简单:stress-ng生成可控负载,gnuplot绘图,oscilloscope(可选)抓取风扇 PWM 波形。首先,用stress-ng --cpu 8 --timeout 120s --metrics-brief模拟 2 分钟满载。同时,后台运行数据采集脚本:
#!/bin/bash while true; do temp=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null) state=$(cat /sys/class/thermal/cooling_device0/cur_state 2>/dev/null) echo "$(date +%s.%3N) $temp $state" >> thermal.log sleep 0.5 done这个脚本每 500ms 记录一次时间戳、温度、当前 state,精度到毫秒。采集完成后,用 gnuplot 绘图:
set terminal png size 1200,800 set output 'thermal_curve.png' set xlabel "Time (s)" set ylabel "Temperature (mC)" set y2label "Cooling State" plot 'thermal.log' using ($1-$1[1]):2 with lines title 'Temperature', \ '' using ($1-$1[1]):4 with lines axes x1y2 title 'Cooling State'这张图会清晰显示温度上升斜率、峰值、超调量、恢复时间,以及 cooling state 的变化节奏。理想曲线应该是:温度线性上升至目标值附近,然后平缓趋近,无超调或轻微超调(<2°C),恢复时间短。如果出现大幅超调(>5°C),说明k_po太大;如果温度爬升缓慢,迟迟达不到目标,说明k_po太小。k_i的作用是消除稳态误差,即温度长期偏离目标值的偏移。如果曲线显示温度稳定在 78°C,而目标是 75°C,那就是k_i不足,需要增大。k_d抑制超调,如果温度快速冲高后回落震荡,加大k_d。我在 RK3399 上的黄金参数是k_po=800,k_i=10,k_d=50,但这不是万能的。当你换用不同散热方案(如从铝挤散热片换成热管),k_po必须重新标定,因为热阻变了。另一个高级技巧是用示波器验证执行器响应。把风扇 PWM 信号接到示波器,观察cur_state变化时 PWM 占空比的跳变延迟。如果延迟高达 50ms,而你的polling-delay是 1000ms,那 PID 就是“开环控制”——它算出来的 state,等真正执行时,热状态早已改变。此时必须缩短polling-delay,或换用响应更快的 cooling device(如 SSR 继电器)。最后,提醒一句:不要迷信“最优参数”。thermal framework 运行在复杂的物理系统上,环境温度、风速、灰尘积累都会改变热模型。我们在线上设备部署了自适应 PID:daemon 每小时用stress-ng --cpu 1 --timeout 10s做一次轻量级负载测试,根据温度响应动态微调k_po±5%,始终保持最佳状态。这才是工业级 thermal 管理的真相。
4. 常见问题排查:从 dmesg 错误码到 runtime 状态诊断
即使架构理解透彻、dts 配置无误,thermal framework 在实际项目中仍会冒出各种诡异问题。这些问题往往不在文档里,而是藏在dmesg的一行警告、sysfs 的一个异常值、或 governor 的 silent fallback 里。我把十年间遇到的典型问题,按发生频率和危害程度,整理成这份实战排查手册。问题一:thermal thermal_zone0: failed to read temperature: -6。错误码-6是EIO,表示 sensor 读取失败。常见原因有三:1) sensor driver probe 失败,dmesg | grep tsadc看是否有failed to request irq或regulator not found;2) sensor channel ID 错误,如前文所述;3) sensor 供电异常,用万用表测 sensor VDD 是否为 3.3