刚开始接触 RK3588 的时候,我最先翻的不是 CPU 手册,而是散热相关的那几页。这颗8核 SoC 跑起来是真的“热情”,四颗 A76 大核加上 NPU 和 GPU 一起拉满,热密度一点不比台式机小。很多人以为温度监控就是读个 /sys/class/thermal/thermal_zone0/temp,但真正做驱动、做 BSP 的人都知道,这条链路从芯片内部温度传感器一直到用户态那条 sysfs 路径,中间隔着硬件采样、内核驱动、thermal 框架、策略决策、冷却设备执行好几层。这篇文章就顺着这条链路,把 RK3588 的 CPU 温度监控机制从硬件到驱动到应用层一层层拆开,中间会贴上我实际调试时用过的设备树配置、驱动关键代码和排查经验。适合正在搞 RK3588 板卡 Bring-Up、要做散热策略定制,或者想搞懂 thermal 框架怎么工作的朋友阅读。
1. 先搞清楚监控链路:从传感器到用户态到底走了多少环
1.1 RK3588 的发热压力与片上温度传感器布局
RK3588 是 8nm 制程,8 核 CPU 采用 4+4 的大小核架构(4×Cortex-A76 + 4×Cortex-A55),最高主频能跑到 2.4GHz 左右。同时还集成了 Mali-G610 GPU 和 6 TOPS 算力的 NPU,以及 8K 视频编解码模块。这几个大功耗模块在同一个 Die 上,满负载时整颗芯片的功耗能冲到 10W 以上甚至更高,如果不做温度监控和降频保护,结温很容易撞到 100℃ 以上的危险线。
芯片级温度监控靠的是内置的 TSADC(Temperature Sensor ADC),这是一颗片上模数转换器,专门用来采集芯片内部温度。RK3588 内部不止一个温度传感器,CPU、GPU、NPU、SOC 等关键模块都有独立的传感器通道,对应内核里多个 thermal_zone。这样设计的好处是,不同模块的发热节奏不一样,CPU 可能瞬间飙到高温,而 NPU 可能是在跑推理一段时间后才热起来。如果只用一个全局传感器,那就没法针对具体模块做精细的降频策略。
需要注意一点,TSADC 不是外部热敏电阻接进 GPIO 那种方案,它是完整的片上模拟前端。驱动要做的事情,本质上是配置好采样参数、启动转换、读取转换结果,再把 AD 码值换算成真实的温度值。至于温度阈值、降频动作、风扇控制,这些是在 thermal 框架上层完成的,驱动本身只负责“把温度读准”和“提供硬件保护机制”。
1.2 整条监控链路的一句话版
如果要用一句话把 RK3588 的 CPU 温度监控链路说清楚,那就是:TSADC 硬件持续采样 Die 温度,tsadc 驱动把采样码值换算成毫摄氏度,thermal 核心通过轮询或中断拿到这个温度,触发 governor 决策,最终调用 cpufreq、风扇等 cooling device 执行降温动作,同时把温度暴露在 /sys/class/thermal/ 下供用户态读取。
这条链路上参与的角色很多,我列个表,方便对照后面的内容:
| 链路角色 | 对应实体 | 职责 |
|---|---|---|
| 温度采样硬件 | 片内 TSADC + 多通道传感器 | 把温度转成 AD 码值 |
| 底层驱动 | rockchip tsadc 驱动 | 配置寄存器、启动采样、读码值、换算温度 |
| 内核框架 | thermal core + thermal_zone | 管理传感器设备、汇总温度、执行策略 |
| 策略决策 | governor(step_wise / power_allocator 等) | 根据 trip point 决定要不要降频、降多少 |
| 执行机构 | cpufreq cooling device、fan PWM | 执行降频、开风扇等动作 |
| 用户态接口 | sysfs / thermal netlink | 让监控程序和应用拿到温度数据 |
搞懂这张表,再看驱动代码和设备树节点就不会晕了。很多人调试温度问题,一上来就盯着 cat /sys/class/thermal/thermal_zone0/temp 的值波动,却没意识到读数只是整条链路最后呈现出来的结果。如果某个环节配置错了,读数可能“看起来正常”但实际策略完全没生效,这种问题最坑。
2. 驱动之外的软件框架,必须先弄明白 thermal 子系统
2.1 内核 thermal 核心的角色划分
Linux 内核的 thermal 子系统不是为 RK3588 专门设计的,它是一个通用的电源与温度管理框架。理解这个框架,比单独看驱动代码重要得多。核心概念有三个:thermal_zone_device、thermal_cooling_device、governor。
thermal_zone_device 代表一个温度区域,比如 CPU 温度、GPU 温度、主板温度。它内部维护当前温度值、trip point 列表、轮询周期等状态。tsadc 驱动注册的就是这种设备,内核会为它创建一个 /sys/class/thermal/thermal_zoneN 目录。thermal_cooling_device 代表一种降温手段,可以是调节 CPU 频率、控制风扇转速、降低 GPU 频率等,对应 /sys/class/thermal/cooling_deviceN。governor 则是策略大脑,它拿到当前温度和 trip 点数据后,决定要不要让 cooling device 动作。
RK3588 平台上最常见的 governor 是 step_wise。这个策略的特点是“阶梯式”调节,温度超过一个 trip 点就上调一档冷却力度,低于 trip 点并越过迟滞区间就下调一档,避免在阈值附近来回抖。跑功耗敏感的服务器类产品时,也有人用 power_allocator,它会结合功耗预算和温度余量做更平滑的分配,但配置门槛也更高,需要在设备树里给每个 zone 配 sustainable-power 参数。
sysfs 里可以直接看到当前用的是哪个 governor:
cat /sys/class/thermal/thermal_zone0/policy如果系统编译时选择了多个 governor,也可以在运行时切换。我调试时经常临时切到 user_space 模式,这样内核不做自动调节,方便复现高温场景,专心想驱动程序的问题。
2.2 设备树里的温度策略:trip 点和 cooling-map
温度策略的灵魂不在 C 代码里,而在设备树里。trips 定义“温度到了多少度要干什么”,cooling-map 把“温度事件”和“执行动作”关联起来。RK3588 平台默认的 CPU 温度 zone 大致长这样:
thermal_zones: thermal-zones { cpu_thermal: cpu-thermal { polling-delay = <1000>; polling-delay-passive = <100>; sustainable-power = <5000>; trips { cpu_alert0: cpu_alert0 { temperature = <55000>; type = "active"; hysteresis = <2000>; }; cpu_alert1: cpu_alert1 { temperature = <75000>; type = "passive"; }; cpu_crit: cpu_crit { temperature = <95000>; type = "critical"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&fan0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; }; map1 { trip = <&cpu_alert1>; cooling-device = <&cpufreq_cooling0 0 4>; }; }; }; };这里有几个关键点。polling-delay 是主动轮询周期,单位是毫秒,指内核在没有中断触发时每隔多久重新读一次温度。polling-delay-passive 是当温度超过 passive trip 之后进入被动模式时的轮询周期,一般要设得更短,比如 100ms,这样温度爬升阶段响应更快。如果把这两个值设成 0,表示完全靠硬件中断触发,减少 CPU 开销。
trip 的 type 决定行为。active 意味着需要主动散热设备介入,比如开风扇;passive 意味着系统自身开始被动降温,典型动作就是降频;critical 是硬件级别的最后防线,内核收到后会触发系统紧急关机或者重启。hysteresis 是迟滞,单位也是毫摄氏度,设 2000 表示 2℃。它的作用前面提过,避免温度在阈值附近反复触发和释放。假设阈值是 55℃,没有迟滞,温度从 54.9 升到 55.1℃ 触发风扇,然后降到 54.9℃ 又关风扇,再升上去又开,这种抖动很伤风扇寿命。
2.3 冷却设备:cpufreq 与风扇怎么挂进来
冷却设备是 thermal 框架里的“执行者”。RK3588 平台上最常见的两个 cooling device 是 cpufreq cooling device 和风扇 PWM。
cpufreq cooling device 的注册一般由内核的 cpufreq_cooling 模块完成,设备树里通过类似 cpu-cooling-maps 的机制把 CPU 节点和 cooling 设备关联起来。它的原理不是直接改 cpufreq 的策略,而是通过注入 idle 周期来限制 CPU 的实际运行频率。内核会计算一个折算后的频率上限,然后通过调度器让 CPU 在时间片上“偷懒”,等效降低功耗和发热。这种方式的好处是不需要真的切换频率档位,响应快、粒度细。设备树 cooling-device = <&cpufreq_cooling0 0 4> 后面的两个数字是冷却状态的最小和最大档位,0 基本等于不限制,4 表示最多限制到比较低的档位,具体档位对应多少频率要看驱动里的映射表。
风扇是通过 PWM 或 GPIO 控制的。在 RK3588 参考设计中,风扇通常挂在 PWM 控制器上,驱动注册为一个 fan cooling device。active trip 一旦触发,governor 就会让风扇从低转速往高转速调。这里容易踩坑的地方是 PWM 频率和占空比映射关系没调好,导致风扇要么不转,要么一上来就全速。后面实操章节我会给一个排查思路。
3. TSADC 驱动的核心实现,逐段拆给你看
3.1 驱动如何找到并启动 TSADC
RK3588 的 TSADC 在芯片内部有一套独立的寄存器块,基地址一般在 0xFEC00000 附近(具体以芯片手册为准)。驱动要正常工作,首先得在设备树里找到对应的节点。节点的大致结构是这个样子:
tsadc: tsadc@fec00000 { compatible = "rockchip,rk3588-tsadc"; reg = <0x0 0xfec00000 0x0 0x100>; interrupts = <GIC_SPI 148 IRQ_TYPE_LEVEL_HIGH>; clocks = <&cru CLK_TSADC>, <&cru PCLK_TSADC>; clock-names = "tsadc", "apb_pclk"; resets = <&cru SRST_TSADC>; reset-names = "tsadc-apb"; pinctrl-names = "default"; pinctrl-0 = <&tsadc_shut>; #thermal-sensor-cells = <1>; status = "disabled"; };compatible 字符串决定了驱动和节点的匹配。驱动侧用 platform_driver 注册,match 表里写上 "rockchip,rk3588-tsadc" 就能配对。reg 是寄存器物理地址和长度,interrupts 是中断号。clocks 和 resets 节点配置了工作时钟和复位信号,TSADC 这种模拟前端必须有时钟才能启动采样,APB 总线时钟负责寄存器读写。
驱动 probe 函数做的事很固定:从设备树解析资源、获取并使能时钟、复位模块、申请中断、初始化寄存器、最后把 thermal_zone_device 注册进 thermal 框架。这里有个细节,probe 阶段一定要检查时钟是否真的起来了。我遇到过一次时钟频率没配对,TSADC 采样出来的码值整体偏移,温度误差超过 10℃。时钟是模拟 IP 的“心跳”,频率不对整个采样都是错的。
status = "disabled" 的节点,如果板级设备树里没有重新 status = "okay",驱动不会被加载。这是 RK3588 平台上最常见的设备树问题。检查方法很直接:
cat /sys/firmware/devicetree/base/tsadc@fec00000/status读到 disabled 就说明节点没使能。
3.2 温度采样与 AD 码转换
TSADC 内部完成一次温度采样后,把结果放在数据寄存器里。驱动要拿到温度,核心逻辑就是从数据寄存器读出原始码值,再转换成真实的毫摄氏度温度。
转换方式取决于 TSADC 的传感器类型和模拟前端设计。RK3588 的 TSADC 采样结果和温度不是绝对线性关系,原厂驱动里通常采用分段线性近似或者查表法。简化后的数据结构大致是:
static const struct tsadc_conv_table rk3588_conv_table[] = { { .code = 810, .temp = 25000 }, /* code 810 对应 25℃ */ { .code = 770, .temp = 40000 }, /* code 770 对应 40℃ */ { .code = 720, .temp = 65000 }, /* code 720 对应 65℃ */ { .code = 650, .temp = 100000 }, /* code 650 对应 100℃ */ };驱动在运行时会根据采样到的 code 落在哪两个标定点之间,做线性插值,得出对应的温度。这种做法的好处是标定点可以按芯片实测特性来调整。很多 BSP 后期做量产标定时,就是改这张表。如果你在调驱动,温度偏差过大,先怀疑这张表跟自己板子上的芯片特性是否匹配,不要急着改上层策略。
代码上的一次温度读取回调,核心内容大致长这样:
static int rk3588_tsadc_get_temp(void *devp, int *temp) { struct rk3588_tsadc *tsadc = devp; u32 code; int i; /* 读取采样结果寄存器,屏蔽无用的高位 */ code = readl_relaxed(tsadc->base + TSADC_DATA); code &= TSADC_DATA_MASK; /* 先查表找分段区间,再做线性插值 */ for (i = 0; i < TABLE_SIZE - 1; i++) { if (code <= table[i].code) { int frac = (table[i].temp - table[i + 1].temp); int range = (table[i].code - table[i + 1].code); *temp = table[i].temp - frac * (table[i].code - code) / range; return 0; } } /* 超出表范围时,按边界值处理 */ *temp = table[TABLE_SIZE - 1].temp; return 0; }注意这里读寄存器用了 readl_relaxed,没有加内存屏障。这是有意为之,因为温度寄存器是终端寄存器,不涉及 DMA 一致性等问题,普通读操作足够,加屏障反而增加无谓开销。
换算出的温度单位是毫摄氏度,25℃ 就是 25000。用户态读到的 /sys/class/thermal/thermal_zone0/temp 也是这个单位,转成摄氏度除以 1000 就行。
3.3 温度阈值中断与硬件保护机制
轮询模式适合调试,但不适合做紧急保护,因为软件进程可能被调度延迟,中断才是硬件级响应的正道。RK3588 的 TSADC 支持设置高低温阈值,一旦采样值越过阈值,硬件直接触发中断,驱动在中断回调里上报 thermal 框架。
中断处理的简化逻辑:
static irqreturn_t rk3588_tsadc_irq(int irq, void *data) { struct rk3588_tsadc *tsadc = data; u32 isr; isr = readl_relaxed(tsadc->base + TSADC_ISR); if (isr & TSADC_HIGH_IRQ) { /* 高温中断:上报 thermal core,立刻进行降温决策 */ thermal_zone_device_update(tsadc->tz, THERMAL_EVENT_UNSPECIFIED); writel_relaxed(TSADC_HIGH_IRQ, tsadc->base + TSADC_ICR); } if (isr & TSADC_LOW_IRQ) { /* 低温中断:同理,清理中断状态 */ writel_relaxed(TSADC_LOW_IRQ, tsadc->base + TSADC_ICR); } return IRQ_HANDLED; }这段代码里最容易被忽略的是末尾的清中断操作。ISR 寄存器读到的状态位,必须通过写 ICR(中断清除寄存器)来清零,如果漏了这一步,中断标志一直挂着,中断处理函数就会被反复触发,造成所谓的中断风暴。我见过不止一个工程师在这里翻车,现象是系统负载正常情况下,CPU 却一直被 IRQ 打断,整体性能下降。
更硬核的保护机制是 TSADC 的 shutdown 模式。设备树里 pinctrl-0 = <&tsadc_shut> 配的就是这个功能。当温度超过硬件编程的极限值后,TSADC 会直接控制芯片的关机引脚,硬件层面触发 SoC 掉电保护。这个动作不经过 CPU 执行软件,所以即使内核已经 hang 住,它依然能保护芯片不被烧毁。量产产品的散热兜底基本都靠这一层,软件降频只是第一道防线。
4. 实操:让监控真正跑起来,并读对温度
4.1 确认工作在哪个 thermal zone
RK3588 有多个温度传感器,内核会为每个传感器创建一个 thermal_zone。系统起来之后,先看一下到底注册了哪些 zone:
ls /sys/class/thermal/ for z in /sys/class/thermal/thermal_zone*; do echo -n "$z: " cat $z/type donetype 文件会告诉你这块 zone 对应哪个传感器。常见的名字是 cpu-thermal、gpu-thermal、soc-thermal,但也有平台把名字起得比较含糊,比如叫 tsadc-thermal。这时候不要靠猜,可以用 dmesg 看驱动注册日志:
dmesg | grep -i tsadc日志里会把每个 zone 的注册顺序、传感器通道、温度类型打出来。确认了 zone 编号后,再读温度:
cat /sys/class/thermal/thermal_zone0/temp返回的是毫摄氏度。比如输出 45781,意思就是 45.781℃。
4.2 命令行快查及各频率、负载联动观察
只读一个静态温度没意义,温度监控的价值在于看它随负载变化的动态过程。我会这么测:先用 stress-ng 拉满 CPU,一边观察温度一边观察频率,看 thermal 策略有没有正确介入。
stress-ng --cpu 8 --cpu-method matrix --timeout 60 & while true; do t=$(cat /sys/class/thermal/thermal_zone0/temp) f=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq) echo "$(date +%T) temp=${t} freq=${f}" sleep 1 doneCPU 频率单位是 kHz,比如 1800000 就是 1.8GHz。跑起来之后观察几点:温度有没有快速爬升到 passive trip;温度到了之后频率有没有被 clamp 住;温度稳定时曲线有没有在某个值附近形成平台。如果温度一直涨不停、频率纹丝不动,问题多半出在 cooling map 没有生效。
如果需要记录温度曲线做进一步分析,可以写一个简单脚本,输出 CSV 格式到文件:
#!/bin/bash echo "time,temp_cpu,temp_soc,freq0" > /tmp/thermal_log.csv while true; do t0=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null) t1=$(cat /sys/class/thermal/thermal_zone1/temp 2>/dev/null) f0=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null) echo "$(date +%s),$t0,$t1,$f0" >> /tmp/thermal_log.csv sleep 1 done跑完 stress 之后 Ctrl-C 停掉,把 CSV 拖到 Excel 或者画图工具里,曲线一眼就能看出问题。这个习惯我强烈建议养成,温度问题靠肉眼看终端输出的数字,很难发现采样噪声和策略振荡。
4.3 调策略:轮询间隔、被动冷却与风扇联动
调试时最常改的两个参数是轮询间隔和 trip 阈值。轮询间隔可以直接在 sysfs 里调:
cat /sys/class/thermal/thermal_zone0/policy echo step_wise > /sys/class/thermal/thermal_zone0/policypolicy 是 governor 选择。默认平台可能跑的是 step_wise,如果你想验证风扇动作,临时切到 user_space 就是把自动降温逻辑关掉,只保留手动控制。
trip 点的查看和修改在 sysfs 里对应一组文件:
cat /sys/class/thermal/thermal_zone0/trip_point_0_temp cat /sys/class/thermal/thermal_zone0/trip_point_0_type不过要注意,sysfs 里这个值不一定能直接改,很多时候它是只读的,真正要改阈值还是得走设备树。修改流程是:改 dts 文件里的 trip 温度值,重新编译 dtb,然后刷到板子上。对 RK3588 来说,编译 dtb 通常走 Buildroot 或 SDK 的编译命令:
make dtbs改完设备树后,先确认新 dtb 真的加载了。有的引导加载程序会有缓存,或者烧录工具没把新 dtb 刷进对应分区,导致你改了半天的 trip 温度根本没生效。检查方法是重新读:
cat /sys/firmware/devicetree/base/thermal-zones/cpu-thermal/trips/cpu_alert0/temperature注意这里读出来是字节数组,数值要自己换算一下。如果值和设备树里一致,说明新配置生效了。
5. 常见问题与排查技巧实录
5.1 问题速查表
这一路调试下来,我遇到过的问题基本可以归成下面这几类,直接整理成速查表:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| thermal_zone 没出现或数量不对 | 设备树节点 status 为 disabled | 检查 tsadc 节点使能状态 |
| 温度读出来总是固定值 | 采样使能没写正确,或时钟没起 | 检查寄存器 TSADC_CR 中的 ADC 使能位 |
| 温度比实际高很多 | 换算表系数和芯片不匹配 | 对比多块板子的输出,做标定校正 |
| 温度跳变剧烈 | 轮询周期过短或传感器噪声 | 增大轮询间隔,或者检查采样滤波配置 |
| 频率到了阈值却不降 | cooling map 没绑定 cpufreq_cooling | 检查 cooling-device 的 phandle 指向 |
| 风扇不转或转速失控 | cooling map、PWM 极性或频率问题 | 先用 sysfs 手动控制风扇确认 PWM 通路 |
| 温度临近临界值直接关机 | critical trip 触发或硬件 shutdown | 确认是软件策略还是 TSADC shut 引脚动作 |
| 中断风暴导致 CPU 负载高 | 中断状态未清除 | 检查 ISR/ICR 清除逻辑 |
| 修改 DTS 后行为不变 | dtb 没重新编译或没刷入 | 检查 dtb 分区和实际加载地址 |
这张表基本覆盖了 90% 的 RK3588 温度调试问题。遇到具体现象时,先判断问题出在链路哪一段,别一上来就怀疑芯片本身。绝大多数温度异常都是配置问题或者换算问题,真正的硬件损坏概率很低。
5.2 调试心得:不要盲信传感器
最后分享几条硬体会。第一,刚拿到板子时不要直接相信温度传感器的绝对精度。TSADC 出厂特性有离散性,同一型号芯片不同个体在同样工况下,读数可能差 3℃ 到 5℃。所以调试初期我更关注温度曲线的相对变化趋势,判断策略有没有生效,而不是死磕绝对值。等到产品的散热方案定型,再考虑用外部热电偶做一次标定,修正换算表。
第二,高温保护阈值不要只依赖 critical trip。软件链路再快也有延迟,真正到了临界温度再处理已经晚了。我会保留 TSADC 的硬件 shutdown 机制,同时在 passive trip 和 critical trip 之间留足余量。比如 passive 设 75℃,critical 设 95℃,中间 20℃ 的缓冲足够完成降频动作。
第三,电源和散热是一体两面。有时降低处理器电压比降频更有效,但这属于更深的调优范围,不在温度驱动层面。驱动能把温度读准、策略能正常执行,已经解决了 80% 的问题。
我最近调试的一块 RK3588 核心板,最头疼的反而不是驱动本身,而是风扇策略。温度已经超过 active trip 了,风扇却迟迟不启动,后来发现是 cooling map 里 fan 设备的 phandle 指错了节点。这个问题的隐蔽之处在于,sysfs 里 fan 的 cooling_device 节点已经存在,看起来一切正常,但 cooling map 绑定的是另一颗 PWM 控制器,风扇自然不响应。所以排查顺序一定是从下往上:先确认传感器读数真实,再确认冷却设备能手动控制,最后才检查策略映射。