1. 先从链路说起:RK3588 上谁的 CPU 温度在“被监控”
我先说结论:RK3588 的 CPU 温度监控,从来不是某一个外设驱动单独干完的事,而是TSADC 温度传感器、thermal 框架、cpufreq 冷却设备三者配合的结果。如果你只盯着drivers/thermal/rockchip_thermal.c看,最多能弄明白温度是怎么读出来的;真正让 CPU 在大核满载 90 度时自动降频、让系统不崩的,是 DeviceTree 里thermal-zones那几段配置,以及 thermal core 调度 governor 的整套机制。
我最早做 RK3588 平台调优时,遇到过一个很典型的现象:跑 8 核压力测试,A76 大核频率从 2.4GHz 一路掉到 1.2GHz,但dmesg里没有任何错误,系统也没死。当时第一反应是 cpufreq 配置坏了,查了半天才发现是温度监控链路过早触发,把频率钳住了。从那以后我对“温度监控”这四字的理解就变了——它不是读个温度上报给用户看,而是整个 SoC 热保护策略的输入端。
如果你现在接手一个 RK3588 的板子,想搞清楚 CPU 温度到底怎么被监控、怎么决定降频、怎么调阈值,建议先建立下面这条链路直觉:
- 芯片内部的 TSADC 传感器测量 PN 结电压,把温度变成 ADC 码值;
- rockchip_thermal 驱动读取码值,转换成毫摄氏度;
- thermal core 按照 DeviceTree 中
thermal-zones的轮询策略,周期性读取温度; - 温度超过
passive阈值后,governor 开始调度 cooling device; - cpufreq cooling device 根据冷却状态限制最大频率,CPU 降频发热减少;
- 如果温度继续冲高,触发
critical,直接优雅关机;再不行,TSADC 硬件 TSHUT 强制复位。
后面所有章节,都是围绕这条链路的某一环展开。
1.1 驱动开发者最容易忽略的“边界”
很多人觉得温度监控是“外设驱动”里的一个小功能,写个驱动把温度读出来就完事了。但在 RK3588 这种多簇异构平台上,CPU 不是只有一个温度传感器。大核 A76 簇、小核 A55 簇、GPU、NPU、DDR 控制器在 SoC 的不同位置,各自有独立的测温点,热特性完全不一样。
实际调试中,我见过有人在thermal-zones里把 GPU 传感器通道配到了 CPU zone 上,结果满载跑 CPU,温度就是不涨,因为读的是 GPU 的温度。这种问题用软件怎么查都查不出,最后得对着芯片手册的寄存器偏移和 sensor 编号一个一个对。
所以这篇文章里,我会把 Kernel 5.10 / BSP 中常见的 RK3588 温度监控实现拆开讲,包括硬件传感器原理、驱动 probe 流程、DeviceTree 配置语义,以及我在实测中用来验证“降频真的生效”的调试方法。
2. TSADC 传感器的工作方式:PN结压降、ADC 码值与 TSHUT 硬闸
2.1 芯片内部测温点与测量原理
RK3588 内部集成的是 TSADC(Temperature Sensor ADC),不是外挂热敏电阻,也不是像 I2C 温度芯片那样直接通过总线读寄存器。TSADC 的本质是把芯片内部 PN 结的物理特性转换为可量化的电压,再做模拟到数字的转换。
PN 结正向压降随温度升高而下降,温度系数大约在 -2mV/°C 左右。这个物理特性非常稳定,所以 SoC 厂商习惯在关键热源旁边放几个二极管测温点,再通过一个多路选择器接到 ADC 前端。RK3588 的 TSADC 模块就有多个通道,分别覆盖大核簇、小核簇、GPU 核心等区域。通道号对应到 DeviceTree 里就是thermal-sensors = <&tsadc 0>这种写法里的第二个 cell。
从驱动代码角度看,RK3588 的传感器通道和寄存器偏移是写死在rockchip_thermal_data所匹配的 chip 数据里的。不同 SoC 的传感器数量、名字、寄存器都不一样。比如老平台SENSOR_CPU对应一个固定寄存器位,到 RK3588 上可能就重排了。所以当你看到一份 BSP 里get_temp回调对不同 sensor 做不同换算时,不要觉得是随意写的,那都是按芯片手册标定过的。
2.2 寄存器控制的采样、阈值比较与中断
TSADC 正常工作后并不是一直在转换,而是按采样周期工作。控制器里有一组寄存器负责控制启动、采样周期、复位逻辑。驱动在初始化时会把模块从复位状态拉出来,使能模拟部分,然后按预设周期触发采样。
温度读出来之后,芯片会和一个高温阈值、一个低温阈值做比较。这两个阈值寄存器是关键:温度高于高温阈值时,产生一个中断信号,通知 thermal core“超温了,赶紧限频”;温度回落到低温阈值以下,再产生中断,告诉系统“可以解除限制了”。这种硬件阈值比较最大的好处是,不用 CPU 不停轮询。Kernel thermal core 本身支持轮询模式,但有了阈值中断后,polling-delay可以设得很小甚至为 0,让 CPU 在温度正常时彻底休息。
驱动中常见的一组寄存器定义包括:
#define TSADCR_TSHUT_EN BIT(0) #define TSADCR_TSHUT_POLARITY BIT(1) #define TSADCR_TSHUT_MODE_CRU BIT(2)这些位决定了超出阈值之后硬件怎么动作,以及有效电平方向。配置不对,会出现“温度超了但中断没触发”或者“没超温但频繁误触发”的情况,这类问题在调试中非常隐蔽。
2.3 硬件级别的 TSHUT:Linux 失效时的最后防线
除了软件 thermal 管理,RK3588 的 TSADC 还支持硬件热关断(Thermal Shutdown)。如果芯片温度达到 TSHUT 阈值,TSADC 可以不经过 CPU,直接触发硬件动作:要么让 CRU 复位整个系统,要么拉一个 GPIO/信号给 PMIC,把电源切断。这个机制是纯硬件链路,即使内核 panic、中断卡死、驱动跑飞,它也能在微秒级别把芯片从危险温度里救回来。
驱动代码里对应的接口是set_tshut_temp和set_tshut_mode。probe 阶段会把 TSHUT 保护温度写入寄存器,并选择复位模式。
这就引出一个非常实际的坑:如果 TSHUT 阈值设置得比 DeviceTree 里的critical温度还低,那么温度冲到临界值时,硬件会先断电或者复位,根本轮不到 Linux 的 critical 处理逻辑优雅关机,你会在日志里什么都看不到,只能用示波器或者检查 reset 原因寄存器判断是过热复位。我的建议是:TSHUT 阈值要比软件critical高 10 到 15 度,给软件留出处理时间,同时留足安全裕量。
3. rockchip_thermal 驱动:从 probe 开始读懂每条分支
3.1 probe 流程里的“先决条件”
rockchip_thermal是一个平台驱动,probe 函数做的事情可以归为四步:
- 从 device tree 拿到 IO 地址、中断号、时钟;
- 使能模块时钟,把 TSADC 从复位状态释放;
- 初始化寄存器,设置 TSHUT 温度、采样周期、中断阈值;
- 逐一注册 thermal sensor 到 thermal core,绑定
get_temp回调。
其中时钟这一步特别容易忽略。RK3588 的 TSADC 模块需要外部时钟才能工作,如果 DeviceTree 里没有配clocks和clock-names,clk_prepare_enable()会失败,驱动 probe 直接返回错误。然而平台设备驱动有时不会打印特别明显的错误,只是 thermal zone 注册不上,系统里thermal_zone0都看不到。所以拿到一个新板子,第一件事是确认dmesg里 rockchip_thermal 有没有 probe 成功。
下面是一段和 RK3588 BSP 逻辑基本一致的关键 probe 片段,我用伪代码展示,方便你对整体结构有个印象:
static int rockchip_thermal_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct rockchip_thermal_data *thermal; struct resource *res; int irq; thermal->chip = device_get_match_data(dev); res = platform_get_resource(pdev, IORESOURCE_MEM, 0); thermal->regs = devm_ioremap_resource(dev, res); irq = platform_get_irq(pdev, 0); ret = devm_request_irq(dev, irq, rockchip_thermal_irq_handler, 0, "rockchip-thermal", thermal); ret = clk_prepare_enable(thermal->clk); thermal->chip->initialize_registers(thermal); /* 设置硬件保护阈值,比如 115 度 */ thermal->chip->set_tshut_temp(thermal, 115000); thermal->chip->set_tshut_mode(thermal, TSHUT_MODE_CRU); for (i = 0; i < thermal->chip->num_sensors; i++) { devm_thermal_zone_of_sensor_register(dev, i, &thermal->sensors[i]); } }devm_thermal_zone_of_sensor_register这一句是驱动和 thermal 框架绑定的关键:它会自动去 DeviceTree 中找thermal-sensors指向这个设备的 thermal zone,并把get_temp回调挂上去。也就是说,驱动本身根本不知道有多少个 thermal zone 在等它,它只管把 sensor 注册出去,剩下的交给框架。
3.2 温度读取与转换细节
温度读取入口是驱动实现的一个回调,签名类似:
static int rk_tsadc_get_temp(struct rockchip_thermal_sensor *sensor, int *temp)实际读数时,驱动会先判断当前 ADC 是否还在转换,等转换完成后从数据寄存器读出原始码值。这个原始码值不是温度,需要芯片手册里给的拟合公式转换。RK3588 这类芯片的转换通常是一个线性关系:
*temp = (raw - offset) / scale;最终输出单位是毫摄氏度。也就是说,temp返回 45000 表示 45 度。你在/sys/class/thermal/thermal_zone0/temp里看到的数字,也是这个毫摄氏度值。
如果读出来的温度明显异常,比如固定是 0 或者 40000 出头怎么都不变,大概率是换算参数和当前通道不匹配,或者寄存器读写掩码不对。这种问题需要对着 TRM 的寄存器描述逐位核对。
3.3 中断处理与 thermal_zone 联动
RK3588 的 TSADC 中断处理函数里,会检查哪些 sensor 的阈值条件被触发。如果触发的是高温,就会调用类似thermal_zone_device_update()的函数,让 thermal core 立刻重新计算冷却策略。其实质是打断当前 governor 的轮询节奏,在硬件中断上下文里把“超温”事件快速传达给系统。
这个设计很巧妙:它不是靠 thermal core 定时轮询发现温度超标,而是温度一越过硬件阈值线,中断就顶上来,减少了几百毫秒的响应延迟。对于 RK3588 这种高功耗 SoC,这几百毫秒很关键。之前有平台就是因为只靠轮询,温度冲高时频率限制慢了一拍,A76 在满频阶段多跑了两个周期,局部热点直接蹿上去。
3.4 初始化代码中的关键配置点
初始化寄存器时,驱动还要配置采样周期、传感器通道使能、以及阈值比较的消抖逻辑。消抖(Debounce)在温度监控里很重要:芯片温度会有高频纹波,如果阈值比较不做滤波,温度在临界点附近抖动,会导致频繁上下触发中断,CPU 被反复限频解频,性能忽高忽低,体验非常糟。所以驱动里往往会让同一个超温状态持续若干次采样周期后,才真正触发中断。
这一点在实际调测时的表现是:cat /sys/class/thermal/cooling_device0/cur_state一会儿 1 一会儿 0,频率来回跳。如果遇到这种情况,先不要调 trip 温度,检查消抖设计才是正路。
4. thermal-zones 与 cooling-maps:温度策略的真正发源地
4.1 一组典型的 RK3588 温度策略配置
从调试者的角度看,温度监控的“策略”不在 C 代码里,而在 DeviceTree 的thermal-zones节点里。RK3588 BSP 的 dtsi 里通常会定义多个 zone,其中和你 CPU 温度直接相关的大概长这样:
thermal_zones: thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <0>; polling-delay = <1000>; thermal-sensors = <&tsadc 0>; trips { cpu_alert0: cpu-alert0 { temperature = <75000>; hysteresis = <2000>; type = "passive"; }; cpu_alert1: cpu-alert1 { temperature = <85000>; hysteresis = <2000>; type = "hot"; }; cpu_crit: cpu-crit { temperature = <105000>; hysteresis = <2000>; type = "critical"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; }; map1 { trip = <&cpu_alert1>; cooling-device = <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; }; }; }; soc_thermal: soc-thermal { polling-delay-passive = <0>; polling-delay = <1000>; thermal-sensors = <&tsadc 1>; /* trips 和 cooling-maps 与上面类似 */ }; };thermal-sensors = <&tsadc 0>里的第二个 cell 是 sensor 通道号。不同 SDK 可能把 CPU 通道设成 0,也可能设成 1,务必以 dtsi 和芯片手册为准。我之前就被这个坑过:某版本 SDK 里cpu-thermal用的是通道 0,换了新 SDK 变成通道 1,我没注意,结果所有调频行为都基于 GPU 温度,CPU 快烧到临界了还在满频跑。
4.2 trip 节点类型:passive、hot、critical 的分工
trips 节点是温度策略的“事件表”,每个 trip 定义了一个温度阈值和后处理类型:
| type | 含义 | 实际用途 |
|---|---|---|
| passive | 进入被动冷却阶段 | governor 开始调用 cooling device,最典型的就是 CPU 降频 |
| hot | 热事件通知 | 一般用来触发上层策略,比如风扇提速、性能模式切换 |
| critical | 临界温度 | thermal core 执行orderly_poweroff(),软件层最后的保护 |
hysteresis是迟滞温度,单位毫摄氏度。它防止温度在阈值附近来回穿越:假设cpu_alert0是 75000,hysteresis为 2000,那么温度降到 73000 才解除这一档的冷却。合理的迟滞能避免频率抖动。
很多刚从单片机转过来的朋友会问:直接设一个临界温度不就行了?不行。RK3588 大核跑满到 85 度可能只需要几十秒,如果直接一步到降频档位最低,性能损失过大,用户体验断崖式下跌。所以真正产品化的配置往往是多级 trip:75 度先降一点,80 度再降一档,90 度直接锁低频,105 度关机。每一级对应一个冷却状态,调起来才有弹性。
4.3 cooling-maps:把温度事件解码成降频动作
cooling-maps把 trip 和冷却设备绑定起来,语法是:
cooling-device = <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>;cpu0在这里不是指单个核,而是指向一个 cpufreq policy 对应的冷却设备。RK3588 的大小核是独立的两个 cpufreq policy,所以你会看到 dtsi 里分别有&cpu0和&cpu4的冷却映射:cpu0 代表小核簇,cpu4 代表大核簇。冷却时只降大核、只降小核、或者两个一起降,完全由 cooling-maps 怎么配决定。
THERMAL_NO_LIMIT表示上下限不限制,也就是从最低档到最高档都可以由 governor 控制。如果你想让某个 cooling device 只允许在特定档位之间运动,可以把这两个 cell 改成具体的冷却状态编号。
CPU 的 cooling device 不是手工注册的,而是 cpufreq 驱动自动创建的。cpufreq-cpu-cooling模块会把 CPU 的 OPP 频率表映射成冷却档位:频率档越多,冷却分级越细。RK3588 大核有几档频率,冷却设备就有几个状态。这也是为什么建议在 dts 里把 CPU 的 OPP 表配完整,否则max_state很小,温度控制粗糙。
4.4 governor 决定“以多快的方式降频”
温度超了、冷却设备也绑好了,具体怎么调整冷却状态,由 thermal governor 负责。RK3588 平台上最常见的两个 governor 是step_wise和power_allocator。
step_wise的逻辑很直白:当前温度位于哪个 trip 区间,就逐步调整冷却设备状态。温度越限就升档,温度回落就降档,每次调整一步,防止频率突变。它性能开销极小,适合多数场景。
power_allocator则是用 PID 控制器估算当前功耗,计算一个“功率预算”,再分配到各个 cooling device。它更智能,能在性能和温度之间找平衡,但调参也更复杂,不适合上来就用。
在 dtsi 中你可以给 zone 指定 governor:
cpu_thermal: cpu-thermal { thermal-governor = "step_wise"; ... };如果没指定,就用 Kernel 编译时的默认 governor。我个人的经验是:初期调试先用step_wise,因为它的每一步行为都可预测、可解释,方便验证链路;真正优化用户体验时再考虑power_allocator。
5. 实测指南:把温度监控从黑盒变成白盒
5.1 sysfs 里先摸清现状
RK3588 启动后,温度监控相关信息几乎都能从 sysfs 里看到。把下面几条命令跑一遍,你就能快速确认整个链路是否健康:
# 查看 thermal zone 名称 cat /sys/class/thermal/thermal_zone*/type # 查看当前温度,单位毫摄氏度 cat /sys/class/thermal/thermal_zone0/temp # 查看 trip 阈值 cat /sys/class/thermal/thermal_zone0/trip_point_0_temp cat /sys/class/thermal/thermal_zone0/trip_point_1_temp cat /sys/class/thermal/thermal_zone0/trip_point_2_temp # 查看冷却设备类型与当前状态 cat /sys/class/thermal/cooling_device*/type cat /sys/class/thermal/cooling_device*/max_state cat /sys/class/thermal/cooling_device*/cur_state正常情况下,thermal_zone0/type应该显示 dtsi 里定义的 zone 名,temp是一个几万的数,比如 45000。cooling_device*里会有名字类似cpufreq-cpu0的节点,这就是 CPU 频率冷却设备。
5.2 用压力负载验证 CPU 降频生效
链路有没有真的干活,只有靠负载压测才能看出来。我常用的验证流程是这样的:
第一步,启动压力负载:
stress --cpu 8 --timeout 120如果板子上没有stress,用yes > /dev/null &多拉几个后台进程也能制造 CPU 负载,效果差一点但够用。
第二步,同时观察温度、冷却状态和频率:
for i in 1 2 3 4 5 6 7 8 9 10; do echo "--- $i ---" cat /sys/class/thermal/thermal_zone0/temp cat /sys/class/thermal/cooling_device*/cur_state cat /sys/devices/system/cpu/cpu4/cpufreq/scaling_cur_freq sleep 2 done如果你看到温度超过cpu_alert0后,cur_state从 0 变成 1、2,而 A76 大核的scaling_cur_freq在下降,说明整条温度监控链路是通的。如果温度已经超过阈值但cur_state始终为 0,说明 cooling-maps 配置没生效,或者 cooling device 没注册成功,优先回 dtsi 检查cooling-device指向。
5.3 常见问题清单与排查思路
实测中我遇到过的温度监控问题,基本逃不出下面这几类:
| 现象 | 根因方向 | 排查建议 |
|---|---|---|
| 负载跑满,温度不变或异常 | 传感器通道映射错误,读到的是 GPU/NPU 温度 | 对照 TRM 检查thermal-sensors通道号,看驱动是否打印了 sensor 类型 |
| 温度正常上升,但频率不掉 | cooling-maps 没有绑定 cpufreq 冷却设备 | 查看cooling_device*/type,确认有cpufreq-cpu0且max_state > 0 |
| cur_state 跳变剧烈 | 阈值消抖不足或迟滞偏小 | 加大 trip 的hysteresis,检查驱动消抖配置 |
| 到达 critical 后直接断电,无优雅关机日志 | 硬件 TSHUT 阈值低于软件 critical | 检查set_tshut_temp配置,将 TSHUT 阈值抬高到 critical 之上 |
| 降温后频率迟迟不恢复 | polling-delay-passive过大或 governor 状态卡住 | 减小polling-delay-passive,确认低温恢复中断正常触发 |
5.4 两个很有用的验证技巧
第一个技巧:临时调低 trip 验证链路。比如把cpu_alert0的temperature从 75000 改成 50000,重新编译烧录后,随便跑点负载温度就超过阈值,观察cur_state变化和频率下降。验证完再改回去。这样能在几分钟内确认整条链路没有问题,而不必等温度真的冲到 75 度。开发调试阶段我经常用这招,比反复吹风枪靠谱得多。
第二个技巧:手动写cur_state。sysfs 里的cur_state是可以手动改的,调试时可以绕过 governor,直接验证冷却设备本身是否正常:
echo 0 > /sys/class/thermal/cooling_device0/cur_state cat /sys/devices/system/cpu/cpu4/cpufreq/scaling_cur_freq echo 1 > /sys/class/thermal/cooling_device0/cur_state cat /sys/devices/system/cpu/cpu4/cpufreq/scaling_cur_freq echo 0 > /sys/class/thermal/cooling_device0/cur_state如果手动写状态频率也不变,那就是冷却设备注册有问题,和 thermal governor 无关,排查方向可以马上转到 cpufreq 和 OPP 表上。这个技巧能帮你把问题快速切分成“温度采集环节”和“频率控制环节”,省掉大量猜疑时间。
我调 RK3588 温度策略时还有个习惯:验证完一定要把cur_state手动归零。否则冷却设备停在某个限频档位上,后面测性能数据全部偏慢,而且非常难发现。这算是最便宜的教训之一了。