1. 一个真实项目现场的CPU降频困局日志全无频率上不去接手这个项目的时候客户反馈很直接机器跑分比竞品低一截游戏场景帧率稳不住打开CPU频率监控一看大核最高只能到1.8GHz左右明明芯片规格里写着最高2.8GHz。第一反应是内核cpufreq策略没调好但查了一圈设备树、内核cmdline、功耗策略都没发现明显异常。后来翻到高通平台特有的thermal-engine日志才意识到问题是出在温控降频上。可当时的日志几乎等于没有只有零散几行带thermal标签的logcat根本看不出是哪个传感器触发、降到了哪个频率、持续了多久。这次调试经历让我觉得值得单独写一篇完整记录。原因很简单很多人遇到CPU频率被神秘压低第一反应是改cpufreq、甚至怀疑scheduler负载均衡方向就歪了。在Android 12的高通平台上默认温控链路是由用户空间的thermal-engine配合内核thermal框架、LMHLimits Management Hardware一起工作的调试方法和老平台有挺多不一样的地方。这篇文章会从框架认知、日志开关、根因定位、参数调整四个维度把完整的实战过程展开适合正在做高通平台系统开发、性能调优或者被CPU莫名降频折磨过的嵌入式工程师参考。先说结论绝大多数频率上不去的案子根子在thermal配置文件里定义的限制规则而不是硬件坏了。你需要的不是玄学优化而是把debug日志开起来看着数据一步步找到是谁在下发限频命令。2. 先摸透thermal-engine在Android 12上的工作边界它到底管了什么2.1 user space thermal-engine与内核thermal框架的分工高通平台的温控体系从骁龙845之后基本稳定成了内核用户态两层协作模式。内核侧负责收集温度传感器数据tsensTemperature Sensor驱动把芯片内部各个热敏点的温度转换成可读的sysfs节点thermal框架根据设备树配置的trip point在温度越过阈值时触发thermal zone的冷却机制。用户态侧则由thermal-engine这个守护进程负责更精细的算法决策它读取/sys/class/thermal底下的实时温度结合/vendor/etc/thermal-engine/目录下的.conf配置文件计算出每个CPU cluster、GPU、modem、充电模块需要执行的mitigation动作再通过写sysfs节点的方式下发限频指令。Android 12上新引入的ThermalHAL进一步把系统状态暴露给了上层APP比如ThermalStatus可以告诉应用当前是温和还是严重状态。但要注意ThermalHAL更多是通知性质的真正兜底温控、强制限制硬件频率的还是底层这套thermal-engine加LMH的组合。所以排查降频问题时logcat里搜thermal只是第一步更关键的日志在/data/vendor/thermal/目录和内核dmesg里。2.2 配置文件的角色不是改两行参数那么简单高通在/vendor/etc/thermal-engine/下放置了多个conf文件命名通常带有平台代号比如thermal-engine-sa8775p.conf或者thermal-engine.conf。里面定义了几类核心对象sensor声明使用哪些温度传感器比如tsens_tz_sensor1、xo_therm、pa_therm0同时可以给传感器设置权重和偏移。algorithm定义PID算法参数主要是pid_dt、pid_i、pid_k以及采样周期。thermal-engine默认不是简单的超过温度就一刀切降频而是通过PID输出一个百分比折算成可用的最高频率。mitigation真正执行降温动作的逻辑都在这里。每个mitigation会指定触发的trip温度以及到达某个温度段之后的动作比如cpufreq_limits限制大核最高频率、bcl_mitigation限制电池电流、cdev调用内核冷却设备。配置项还区分了sssteady state、p0/p1/p2等不同档位。常见的一个坑是你改了.conf文件里某个sensor的温度阈值但没改对应algorithm的参与条件结果PID算法仍然在更低的温度就提前介入改完看不出任何效果。2.3 LMH与内核补充机制千万别忽略除了thermal-engine高通平台还有一套独立的LMH硬件模块专门负责快速硬限频保护。它的配置文件通常是/vendor/etc/lmh/LMH_config.xml或类似的XML触发逻辑比thermal-engine更直接当检测到电流过大或温度过冲时会绕过用户态算法直接把CPU最高频率钳制到一个硬件级上限。这意味着你可能看到thermal-engine开心的报着未触发限制但CPU频率还是上不去——这时候八成是LMH在背后动手了。内核侧还要注意msm-thermal这个模块虽然在高通较新的内核里它默认是disabled状态由用户态thermal-engine接管但如果内核config里没有完全关闭它仍可能抢占部分sensor的控制权和用户态配置打架。排查降频时我习惯先跑一条命令把所有thermal zone和当前冷却状态打出来看看到底是哪些实体正在限频再决定追哪条日志。for zone in /sys/class/thermal/thermal_zone*; do echo $zone: type$(cat $zone/type) temp$(cat $zone/temp) for cdev in $zone/cdev*; do [ -e $cdev ] echo $(basename $cdev) cur$(cat $cdev/cur_state) max$(cat $cdev/max_state) done done这条命令能快速暴露谁在限频。后续所有debug日志的解读都要回到这张表上。3. 快速开启debug日志的三种姿势配置文件、内核trace与sysfs直查3.1 姿势一改thermald的log_level立竿见影很多人在这一步就卡住了因为厂商的userdebug版本里/data/vendor/thermal/目录可能是空的或者只有几KB的压缩log。默认情况下thermal-engine的日志级别是ERROR很多信息根本没写进文件。开启方法藏在/vendor/etc/thermal-engine/thermal-engine.conf或thermald.conf里不同平台略有差异但都在配置文件的全局参数区域有类似这样的字段logging { log_level: DEBUG; log_file: /data/vendor/thermal/thermal-engine.log; log_size: 1024; } log_level DEBUG;注意有些平台用的是mlog.properties来控制日志系统里面会有一行log.tag.thermalV这样的开关。改完配置文件之后重启thermal-engine进程才生效adb root adb remount adb shell stop thermal-engine start thermal-engine # 或者直接kill -9init会自动拉起 adb shell killall thermal-engine拉起的瞬间日志就会开始写入。实测下来DEBUG级别会刷出每个采样周期的所有sensor温度、每个algorithm的计算输出、每个mitigation的动作信息量非常大。所以不建议长期开着DEBUG跑抓到问题现场后及时切回ERROR或INFO否则几百MB的日志文件很快把/data撑爆。3.2 姿势二内核trace路线追到ms级粒度如果用户态日志显示一切正常但频率还是被压低就要怀疑内核侧的限频动作了。Android 12的高通内核一般使能了CONFIG_THERMAL_TRACE可以借助tracefs记录thermal相关的内核事件。抓trace比看logcat更精确能把温度越过trip点→thermal governor触发冷却设备→CPUfreq被更新的完整时序记录到微秒级。# root后先挂载tracefs adb shell mount -t tracefs tracefs /sys/kernel/tracing # 使能thermal相关事件 adb shell echo thermal:* /sys/kernel/tracing/set_event # 也可以更精细一点只要温度变化和trip相关 adb shell echo thermal:thermal_temperature* thermal:thermal_zone_trip* /sys/kernel/tracing/set_event # 清空缓存开始抓取 adb shell echo /sys/kernel/tracing/trace # 复现问题然后导出trace adb shell cat /sys/kernel/tracing/trace /data/local/tmp/thermal_trace.txt adb pull /data/local/tmp/thermal_trace.txt内核trace的解读重点是thermal_zone_trip事件里的字段尤其是trip数值和temperature。你经常能看到trip阈值明明没超过但governor提前介入的情况那是因为设备树定义里有个hysteresis滞后值或者trigger温度本身就包含偏移。这种细节在用户态日志里根本体现不出来只有内核trace能看到真实比较逻辑。3.3 姿势三sysfs直查法不依赖任何守护进程有些极端情况连thermal-engine都被系统砍掉了或者init进程起不来服务这时候还有最后一招直接读sysfs。内核thermal框架本身会维护每个thermal zone的trip点、温度、冷却设备状态这些节点不依赖用户态进程。前面第2节那段for循环脚本就是典型用法。除了thermal zone目录cpufreq也有自己的调频状态节点# 大核policy当前允许的最高频率 cat /sys/devices/system/cpu/cpu4/cpufreq/scaling_max_freq # 当前实际频率 cat /sys/devices/system/cpu/cpu4/cpufreq/scaling_cur_freq # 如果scaling_max_freq明显低于cpuinfo_max_freq就是被某种策略限频了 cat /sys/devices/system/cpu/cpu4/cpufreq/cpuinfo_max_freq额外提一个很多老工程师习惯用的老接口/sys/module/msm_thermal/parameters/enabled。在Android 12的新内核里这个节点大概率已经不存在或者变成只读的了如果发现没有权限不用纠结直接走上面两条新路线。sysfs直查法的优点是与软件栈解耦就算thermal-engine代码改得面目全非只要内核thermal框架在就一定有一份最原始的限频状态。3.4 抓log前的准备工作比抓log本身更重要开DEBUG日志之前我强烈建议先想清楚几个问题复现条件是什么——是亮屏打游戏还是充电时高负载还是待机突然降频需要多长日志才能覆盖完整场景日志存到哪个分区不会被覆盖我踩过的一次坑是随便开个DEBUG然后跑游戏跑了二十分钟回来发现/data/vendor/thermal下日志文件有500多MB而且因为log_size配置了循环覆盖最关键的崩溃前几分钟反而被冲掉了。正确做法是先小范围复现确认动作序列后在现场前5秒开始抓取及时stop。如果需要长时间观察就配合logrotate的配置或定期手动拷贝。还有一个容易忽略的点userdebug和eng版本对日志权限、SELinux策略的处理不同。eng版本通常permissive日志随便写userdebug版本在user域下仍然会被SELinux挡住一部分文件访问可能出现thermal-engine侧写不了log的情况。遇到这种问题先adb shell su 0 setenforce 0把SELinux暂时关掉定位完再恢复别一上来就改动vendor的file_contexts生产验证时容易出幺蛾子。4. 从日志到根因一次完整的降频排查链路4.1 拿到DEBUG日志后的第一件事归类而不是瞎看打开thermal-engine.log你会看到一堆形如sensor tsens_tz_sensor10 temp73000、algorithm pid_standard output45、mitigation cpufreq_limits freq1800000这样的记录。如果没做过归类大概率看三分钟就晕了。我习惯先按时间轴把日志切成三个片段温度爬升段看哪些sensor的温度在快速逼近阈值。触发接入段看哪个algorithm先计算输出了什么样的mitigation百分比。频率变化段看cpufreq_limits里的freq值怎么一步步往下走最后停在哪个数值。这三段用Excel或者grep都能快速切出来。核心问题是哪个sensor的温度数据和最终触发限频的mitigation之间存在强关联。日志里sensor非常多电池有bcl机身有skin屏幕有xo_therm芯片内部有tsens_tz_sensor1到sensor15每个都可能触发不同动作。只盯最后输出频率没有意义要找到温度变化曲线和限频动作时间点高度吻合的那一个。4.2 案例拆解一台设备在游戏场景下大核锁到1.7GHz当时项目上的具体现象是跑某大型游戏5分钟后大核cpu4的scaling_cur_freq锁定在1.7GHz左右小核也降到1.2GHz游戏帧率从50fps掉到35fps。抓thermal-engine日志后关键行呈这样[DEBUG] tsens_tz_sensor7 temp86500 trip85000 [DEBUG] algorithm cpu_algorithm total_load12 sensor_margin3000 output48 [DEBUG] mitigation cpufreq_limits cluster0 freq1700000 [DEBUG] mitigation cpufreq_limits cluster1 freq1700000sensor7温度冲到86.5°C越过了85°C的trip点PID算法输出48%直接导致两个cluster最高频率被限制到1.7GHz。到这里根因已经清楚了不是系统调度问题不是cpufreq策略问题是tsens_tz_sensor7这个点温度过高。那为什么这个点的温度这么高再看内核trace发现这个sensor在设备树里的位置映射的是CPU的hotspot附近导热设计上散热路径比较差。也就是说游戏场景的持续高负载很快就把这个点的温度堆上去了而散热方案没有及时把热量导走。排查链路到这一层需要回答一个关键问题是传感器本身误报还是真实温度高做法是拿红外测温枪或者热像仪测芯片表面温度和tsens读数对比。如果差异超过10-15°C那要考虑sensor校准或硬件贴装问题如果读数基本相符那就是散热余量确实不够要从配置或结构散热上找答案。4.3 对比测试法验证Limit来源的唯一正确姿势为了确认限制动作完全来自thermal-engine而非LMH或者内核其他线程我做了三组对比测试测试项操作结果基线原版配置跑游戏10分钟5分钟时限频到1.7GHz温度稳定在86°C关闭thermal-enginestop thermal-engine后跑游戏频率冲到2.8GHz温度继续攀升至95°C仅改trip温度把sensor7的trip从85°C改为95°C10分钟内不再触发限频温度到91°C封顶第一组给了根因第二组验证了thermal-engine确实是原始执行者而不是LMH或内核msm-thermal第三组验证了调整配置后系统行为能按预期变化。注意第二组的做法很危险温度冲到95°C以上已经接近极限只在工程机上调试点到即止量完温度赶紧start回去。这种对比测试法比翻代码猜逻辑高效得多。4.4 复现与验证日志开关不是改完就完事定位出根因后还需要严格复现和数据存档。建议在跑完整测试流程时同步记录三路数据thermal-engine log、内核trace、cpuinfo_cur_freq的变化曲线。这三路数据能相互印证也方便你后续跟硬件团队、温控算法团队对峙减少你改的配置没效果之类的扯皮。复现时还要注意环境变量的一致性。环境温度25°C和35°C下的thermal表现差异非常大同一台机器在空调房和户外测试触发降频的时间可能相差3到5分钟。我习惯在测试报告里固定记录室温、是否充电、屏幕亮度、游戏画质档位条件不一致的复现数据不算数。没有这个意识后续做配置回归测试时会浪费大量时间。5. 解决降频问题的实战策略与安全边界不要为跑分阉割保护5.1 对症下药的几个调整方向排查出是tsens某个sensor触发后不等于一定要改trip温度。根据不同症状有几个层次的对策优先级从高到低排列方案A优化导热与结构散热。这个方案最治本但周期长通常要走结构改版。如果发现sensor读数比实际芯片表面温度高很多却一直触发要考虑sensor贴装位置、导热硅脂厚度、VC均热板覆盖范围。软件上做得再好散热底子不行也是白搭。方案B微调trip点与算法参数。适合温度实打实偏高但只是偶尔瞬时突破阈值的场景。把trip温度适当上调同时配合pid_k和pid_i的调整让thermal-engine不要一碰到阈值就立刻深降频。例如algorithm { name: cpu_algorithm; sampling_period: 1000; pid_params { pid_i: 1000; pid_dt: 400; pid_k: 8; } }这个公式的逻辑要理解PID算法输出的是限制权重不是直接输出频率。pid_k越大温度越靠近目标值时输出越激进pid_i负责稳态消除余差积分累积可以防止温度在阈值附近反复横跳。调参时一次只动一个变量否则降频曲线变了也说不清是哪个参数引起的。方案C修改mitigation的频率步进。如果产品定位允许可以在mitigation里把最低限制档位调的温和些比如把1.7GHz档改成2.1GHz档让游戏体验不太难看。但要留意电池电流和充电温度是否同步失控需要同时看bcl_mitigation配置。方案D使用cdev绑定替代直接cpufreq_limits。高通thermal-engine除了直接写cpufreq上限还可以绑定内核冷却设备比如利用cpufreq_cooling的功率限制模式通过限制功耗而非死限频率来降温。这种方式对性能体验更友好但对内核config有要求需要确认CONFIG_CPU_FREQ_GOV_POWERALLOC是否开启。5.2 改配置后必须同步验证的四件事改配置不是改完重启就结束至少要验证四件事频率恢复速率触发降频后温度回落到安全值频率是否能在合理时间内恢复到峰值。实测中PID积分参数调得太大会导致频率恢复特别慢温度都降到60°C了还在限频用户体感就是手机持续卡顿很久。充放电电流如果同时改了bcl相关配置必须盯着/sys/class/power_supply/battery/current_now防止大电流场景下电池温度失控。表面温度手机机身可接触表面温度有安规要求通常要求不超过45-48°C。为了保跑分把skin sensor的阈值调太高用户握着烫手的机器投诉比跑分低更严重。稳定性压测stress -c 8一小时配合充电场景确认thermal-engine不会出现死锁、不响应或者反复restart的情况。这些验证项我一般做成一张清单表格每次改完配置逐项打勾。只有全部通过才敢把改动合入基线。5.3 关于直接关掉thermal的劝退网上常见各种root后关闭温控删掉thermal-engine的教程我强烈不建议在生产环境下这么做。不是怕温度太高烧掉芯片这么简单的理由更重要的是高通平台的温控链路不仅仅保护CPU还保护充电IC、PMIC、UFS闪存、屏幕背光驱动。你只关了thermal-engine内核thermal框架仍然会在芯片温度突破criticaltrip时强制shutdown这个shutdown非常突然没有任何优雅的保存过程用户数据损坏的风险极大。即便你连内核thermal也一起关掉先不说设备寿命问题就一条就够喝一壶手机在高温环境下连续录4K视频或者导航充电十几分钟内就可能因为电池聚合物膨胀导致外壳变形或者电池鼓包这个责任不是一句测试没问题能担得起的。跑分党只看分数系统工程师要看全生命周期和用户的物理安全。我个人的底线是软件调试阶段可以在工程机上临时禁用但release版本一定保留完整的温控能力最多是把策略调得更激进一点。这条边界入行越久体会越深。6. 调试后的经验沉淀与常见坑6.1 日志文件过大与循环覆盖问题DEBUG级别的thermal-engine日志一个10分钟游戏场景就能轻松到100MB以上。如果配置文件里设定了log_size它会按行数或大小做环形覆盖看起来日志文件体积稳定但真正想找的关键片段可能已经被覆盖了。解决思路有两个一是临时把log_size调大比如调到4096KB以上或者直接注释掉该字段让日志不覆盖二是准备一个后台脚本周期性把日志文件拷贝到独立路径比如/data/local/tmp/thermal_log_$(date %s).log做切片保存。实测中还有一个更隐蔽的问题/data/vendor/thermal/目录所在的data分区可能在长期高压写入后出现IO性能瓶颈拖慢thermal-engine的处理反而影响温控响应。所以抓完日志及时清理不要留着一堆几百MB的log在data分区里吃空间。6.2 权限与SELinux策略的坑越早踩越省事用adb remount改/vendor/etc/thermal-engine/thermal-engine.conf的情况userdebug版本上经常遇到Read-only file system。别硬刚正确姿势是先把配置文件拉到本地修改再push回去检查权限一致并确认SELinux标签没变adb pull /vendor/etc/thermal-engine/thermal-engine.conf # 本地编辑... adb push thermal-engine.conf /vendor/etc/thermal-engine/thermal-engine.conf adb shell chmod 644 /vendor/etc/thermal-engine/thermal-engine.conf adb shell chown root:root /vendor/etc/thermal-engine/thermal-engine.conf adb shell restorecon /vendor/etc/thermal-engine/thermal-engine.confrestorecon这一步经常被忽略但如果不做SELinux可能会给这个文件打上错误标签导致thermal-engine启动时无法正常读取配置。有几次改完配置重启服务日志里没有任何错误但所有修改都不生效最后发现就是SELinux标签异常进程读的是旧配置。6.3 高通平台与MTK平台在调试思路上的差异调试过程中处理过MTK平台同事的咨询电话能明显感受到两家的设计哲学差异。MTK的温控通常在内核态mtk_ts_cpu里直接实现用户态进程更多是状态上报和策略配置改的是设备树里的mitigation参数和thermal_zone的trip点。高通则把大量计算放在用户态thermal-engine里配置灵活度更高但调试时要多开一层日志、多验证一层进程是否真的读到了新配置。所以如果你以前主要调试MTK平台转到高通平台时很容易犯一个错误只改内核设备树不动user space的conf文件结果发现内核侧接受到了新trip点但thermal-engine用自己的算法在更低温度就介入限频了。调试前先确认当前平台是哪家的温控主导能少走很多弯路。6.4 调试工具组合推荐一份我的固定作业流经过这几个项目我形成了一套固定的调试作业流分享出来供参考# 第一步确认版本和配置文件 adb shell getprop ro.board.platform adb shell cat /vendor/etc/thermal-engine/thermal-engine.conf | head -30 # 第二步开启DEBUG日志 adb shell sed -i s/log_level: INFO/log_level: DEBUG/ /vendor/etc/thermal-engine/thermal-engine.conf adb shell restorecon /vendor/etc/thermal-engine/thermal-engine.conf adb shell killall thermal-engine # 第三步同步准备内核trace adb shell mount -t tracefs tracefs /sys/kernel/tracing adb shell echo thermal:* /sys/kernel/tracing/set_event adb shell echo /sys/kernel/tracing/trace # 第四步复现问题结束后统一收数据 adb pull /data/vendor/thermal/thermal-engine.log adb shell cat /sys/kernel/tracing/trace /data/local/tmp/trace.txt adb pull /data/local/tmp/trace.txt adb shell cat /proc/interrupts | grep -i thermal # 看thermal中断触发次数/proc/interrupts这一条是我后来才加的因为thermal中断的触发频率能直接反映内核侧thermal governor是否在频繁介入和用户态日志结合看能快速判断是用户态算法问题还是内核trip配置问题。6.5 从这次调试里总结出的三点心得第一日志开关是最便宜的基础设施别等出问题再搭。很多团队上线前明明可以开DEBUG跑一轮压力测试却因为怕日志占空间而用ERROR级别跑完整轮测试出了问题再回来补抓数据已经不完整了。正确做法是release前的验证阶段至少在关键工况下开INFO级别把每一次温控介入都记录下来。第二温控问题大概率是系统工程问题不是单个模块的锅。同一个sensor温度过高背后可能牵扯PCB走线、屏蔽罩、导热凝胶、风扇如果有的话、结构通风设计、CPU功耗调优每一环都要有人认领。单纯靠软件压频率是最无奈的下策它解决了体验数字没有解决物理热量。第三解决问题的判断标准不是不降频而是合理降频。高负载场景下CPU温度飙升后降低频率保护硬件是符合客观物理规律的。方案做得好不好看的是降频曲线是不是平滑、恢复速度是不是够快、用户在交互层面能不能感知到。完全不降频、顶着100°C跑的产品不是调教功力强而是定时炸弹。回到文章开头那个项目的结局最终没有大幅放宽trip阈值而是通过调整PID参数把降频后的最低档从1.7GHz提到了2.1GHz同时让频率恢复更积极游戏帧率从35fps回到了45fps左右表面温度控制在43°C以内。方案不是最激进的但用户体感和安全之间找到了一个能交付的平衡点。这个思路比任何一组降频参数都更重要。