做ARM平台系统的人,多半都经历过这种场景:产品经理甩过来一张功耗测试报告,说整机待机电流比竞品高了20mA,让你赶紧排查。你打开powertop一看,CPU明明在idle里待了90%的时间,可耗电就是下不去。问题往往不在某一个点,而在整条电源管理链路上——状态没真正进去、进去了被频繁唤醒、某个外设没关联到正确的电源域,任何一个环节出问题,功耗账都算不平。到了ARMv9时代,安全架构变更之后,电源管理的边界又多了一层,很多以前靠经验能定位的问题,现在变得更容易踩坑。
这篇文章我把ARMv9/v8的电源管理系统架构完整梳理一遍,从底层的WFI指令、DVFS调频调压、Power Gating机制,到设备树里的idle states配置、PSCI与ATF固件、以及ARMv9新增的CCA/RME对功耗的影响,最后再给出一套可落地的调试方法。适合做系统软件、BSP移植、功耗调优的工程师,也适合刚接触ARM底层、想搞清楚“CPU睡眠到底发生了什么”的嵌入式开发者。
1. 先把账算清:电源管理到底在管哪些“电”
1.1 动态功耗、静态功耗和系统清单
搞电源管理,首先得知道功耗是怎么产生的。SoC内部主要有两类功耗:动态功耗和静态功耗。
动态功耗的公式是 P = C·V²·f。C是电容负载,V是工作电压,f是时钟频率。这个公式说明三件事:电压的影响是平方级的,所以降电压的收益远大于降频率;频率降低能省电,但省得有限;真正想要“大幅度省电”,核心思路是尽快把电压降下来或者直接把电源关掉。这也是为什么现代处理器都强调“Race to Idle”——活儿快点干完,然后赶紧进入低功耗状态,而不是慢悠悠地干活。
静态功耗则来自漏电流。在28nm时代,漏电占比还不算突出,但到7nm、5nm以及更先进工艺下,晶体管尺寸越来越小,阈值电压越来越低,漏电流占比明显上升。静态功耗的特点是:只要芯片还通着电,它就在耗电,不管CPU有没有在跑。所以对于先进工艺的SoC,光靠降频降电压还不够,必须把不用的模块直接断电,才能真正压住待机功耗。
把视角拉到整机层面,功耗来源通常是这样的:
- AP侧CPU/GPU,动态和静态都有
- DDR内存,自刷新和读写状态差距很大
- 显示链路,屏幕一亮功耗就上去
- 基带、WiFi、蓝牙等通信模块
- 传感器Hub、Always-on Processor(常开处理器)
- 各种PMIC自身的静态损耗、LDO空载损耗
做功耗调优时,我习惯先把整机的功耗拆成“开关状态”和“运行负载”两张表。开关状态决定哪些模块还在供电,运行负载决定正在运行的模块跑多快。电源管理系统架构要解决的核心问题,就是这两张表的自动编排和切换。
1.2 电源域、时钟域、电压域的划分逻辑
理解了功耗来源,再看硬件侧的划分。一个SoC内部不是一整块铁板,而是被切分成很多个电源域(Power Domain)、电压域(Voltage Domain)和时钟域(Clock Domain)。这三者经常被混在一起说,实际是完全不同的概念。
时钟域(Clock Domain)关注的是逻辑是否有时钟在跳。一个模块的时钟可以被关掉(Clock Gating),关掉之后该模块不再翻转信号,动态功耗归零,但静态功耗还在,寄存器的值也能保持。
电压域(Voltage Domain)是同一路电源轨覆盖的模块集合。做DVFS时,一个电压域内的模块必须一起升降压,不能一部分1.0V、一部分0.8V。不同电压域之间交互需要电平转换器(Level Shifter),电压域被断电时需要隔离单元(Isolation Cell)来防止输出信号倒灌。
电源域(Power Domain)是可以独立切断电源的区域。电源域断电后,内部所有逻辑状态全部丢失,静态功耗也归零,但代价是恢复时需要重新上电、复位、加载固件或恢复上下文,延迟很大。
举个例子,一个典型的移动SoC会把CPU cluster A、CPU cluster B、GPU、NPU、ISP、Modem分别划成不同的电源域。其中CPU cluster内部还可能再细分:每个core一个电源域、cluster公共的L2缓存一个电源域。这就是为什么CPUIdle状态可以有那么多层——WFI时可能只关core的时钟,再深一层可以断掉core的电源,再深一层可以把整个cluster的电源都断掉。
设备树里的power-domains属性就是在描述这种归属关系。比如某个外设要挂到某个电源域下,内核的PM Runtime框架才能通过genpd(Generic Power Domain)驱动去控制它的开关。这块配置一旦出错,就会出现“模块没在跑但功耗很高,因为它的电源域根本没关过”这种诡异问题。
2. 三大底层机制:WFI、DVFS与电源闸控
2.1 WFI/WFE与CPU挂起的第一道门
ARM架构里,CPU进入低功耗最基础的入口是WFI(Wait For Interrupt)和WFE(Wait For Event)两条指令。
WFI会让CPU停止执行指令,进入一种“等待中断”的状态。任何使能的中断到达后,CPU会恢复执行。注意,WFI本身并不承诺一定会关闭时钟或电源——它更像是一个“暂停”指令,更深的省电效果取决于硬件如何响应WFI事件。在ARMv8/v9的Linux实现里,CPU进入idle的路径大致是cpuidle框架选择state -> arch_cpu_idle -> cpu_do_idle -> wfi,最终落在这条指令上。
WFE则是等待事件(Event),语义上和WFI有微妙区别。WFE被唤醒后不会自动重新检查条件,通常用于自旋锁场景。WFE的进入退出延迟比WFI更小,但省电效果也相对弱一些,所以在Linux里,WFE一般出现在同步原语中,而不是CPUIdle的主路径上。
这里要说一个常见误区:很多人以为执行WFI就是“睡眠”了,其实远远不够。硬件上,WFI只是一个触发条件,SoC的电源管理单元(PMU)接收到WFI事件后,还要结合当前的状态配置决定要不要关时钟、要不要断电源、要不要先隔离输出。这套动作由硬件状态机完成,软件能控制的,只是事先设置好允许进入哪个idle state。
2.2 DVFS:调频调压的物理逻辑
DVFS(Dynamic Voltage and Frequency Scaling)是目前最常用的在线省电手段。它的核心思想很简单:任务量大的时候提高频率和电压,任务量小的时候降下来。
但为什么要“频率和电压一起调”?这里有一个物理约束。CMOS电路要正常工作,晶体管的开关时间必须满足时钟周期的要求。频率越高,意味着一个时钟周期越短,晶体管必须在更短时间内完成充电放电,这就需要更高的电压来驱动。举个具体的例子,某款CPU运行在2.4GHz时可能需要1.0V电压,运行在1.8GHz时0.85V就够用。如果你在2.4GHz频率下只给0.85V,CPU计算会直接出错,甚至在某些情况下不明不白地死机。
所以DVFS的软件框架里,频率和电压必须作为一个组合来管理,这个组合在Linux里叫做OPP(Operating Performance Point)。一组OPP就是一个合法的电压频率配对,SoC厂商会把所有经过验证的OPP写进设备树或者固件表里,系统运行时只在这些点之间切换,不允许随意组合。
Linux里的cpufreq框架负责这件事。用户态或内核态的governor根据CPU负载决定目标频率,底层驱动再通过clk framework改时钟频率、通过regulator framework改供电电压。调频调压的顺序也有讲究:升频时必须先升压、再升频,否则高频率低电压的过渡态可能出错;降频时要反过来,先降频、再降压,这样能保证中间时刻有足够的电压余量。这个顺序在开发板上可以直接用示波器抓电压轨看,顺序搞反了会在电压波形上看到明显的毛刺。
governor的选择也很重要。老牌的ondemand按采样周期看负载,处理突发任务时来回跳跃比较明显。ARM平台现在更推荐schedutil,它直接利用调度器的PELT负载信号来调频,响应更快,配合EAS(Energy Aware Scheduling)可以在大小核之间做功耗感知的任务调度。我实测过很多场景,schedutil在交互类负载上的功耗比ondemand能省5%-10%,这在小电池设备上是很可观的。
2.3 Power Gating与Clock Gating:两种节电方式的区别
如果说DVFS是“调节阀”,那么Clock Gating和Power Gating就是“关阀门”和“关总闸”。
Clock Gating只是把模块的时钟停掉。模块内部的状态、寄存器内容全部保留,恢复时只要重新打开时钟就能立刻继续工作,几乎没开销。代价是静态漏电依然存在,电压也依然施加在电路上。在CPUIdle状态设计里,浅睡眠(例如WFI + Clock Gating)就是这一类,它的优势是进出快,适合处理器只在短时间内没事做的场景。
Power Gating则是真正把电源切断。省电效果最彻底,但代价也非常大:断电后内部状态全部丢失,重新上电后需要复位、重新加载固件或恢复上下文,退出延迟可能在几十微秒到几毫秒不等。Power Gating的工程难点在于“断”和“复”这两个瞬间。断电前,需要把模块的输出隔离(Isolation),防止断电模块输出悬空信号干扰还在工作的模块;需要把关键上下文保存到不会掉电的地方,比如DDR或者专用的retention寄存器。上电后,还需要按正确的时序解除隔离、释放复位、恢复上下文。
CPU Hotplug就是Power Gating理念的一种体现。当一个CPU core执行offline流程时,内核通过PSCI通知固件把这个core的电源断开;online时再上电。很多人不理解为什么offline一个CPU要那么久(有时候几百毫秒),就是因为背后是完整的电源域通断流程,而不是一条指令能搞定的事。
这里有个容易忽略的验证点:你怎么确认一个电源域真的断了?软件上不能只看日志说“powered off”,最好接一个电流探头看该电源轨的电流曲线。我之前排查过一个“宣称进入了idle但整机电流居高不下”的问题,最后就是靠测量发现某个GPU的电源域根本没断,原因是最早加载GPU驱动的进程没有调用Runtime PM的put接口,导致genpd的引用计数一直没归零。
3. CPUIdle与系统挂起:状态机怎么设计和验证
3.1 设备树里的cpu-idle-states到底怎么填
CPUIdle框架是Linux内核中管理CPU低功耗状态的核心组件。系统里每个CPU可以有多个idle state,按深度排序:state0通常是WFI,state1可能加了Clock Gating,state2可能把CPU core的电源域直接断掉,state3可能连cluster的L2都一起断电。每次CPU进入idle时,governor会根据预测的空闲时长、唤醒延迟要求,选择一个合适的state进入。
设备树中的cpu-idle-states节点负责把这套状态配置告诉内核。常见的配置差不多是下面这个样子:
cpus { cpu@0 { compatible = "arm,cortex-a78"; reg = <0x0>; enable-method = "psci"; cpu-idle-states = <&cpu_sleep_0>, <&cpu_sleep_1>; capacity-dmips-mhz = <1024>; }; cpu-idle-states { entry-method = "psci"; cpu_sleep_0: cpu-sleep-0 { compatible = "arm,idle-state"; local-timer-stop; arm,psci-suspend-param = <0x00010000>; entry-latency-us = <40>; exit-latency-us = <80>; min-residency-us = <200>; status = "okay"; }; cpu_sleep_1: cpu-sleep-1 { compatible = "arm,idle-state"; local-timer-stop; arm,psci-suspend-param = <0x00110000>; entry-latency-us = <100>; exit-latency-us = <200>; min-residency-us = <600>; status = "okay"; }; }; };这些字段的含义,我在实际调试中总结过一套理解方式。entry-latency-us是进入这个状态需要的时间,包括保存上下文、隔离输出等;exit-latency-us是从状态唤醒到CPU能跑代码的时间;min-residency-us是进入这个状态至少要待多久才划算。governor的判断逻辑就是:如果预计空闲时间小于entry+exit的开销,还不如不睡;如果预计空闲时间略大于exit,但小于min-residency,那睡浅一点更合适。
arm,psci-suspend-param的值是最容易搞错的。它其实是一个按位编码的power state参数,低4位或低8位代表state type(比如standby还是power down),高位可能包含affinity level(CPU层面还是cluster层面)、上下文是否保留等信息。这个编码没有统一标准,必须和固件(TF-A或其他EL3实现)里的解析逻辑严格对应。Linux只负责把这个值原样传给PSCI_CPU_SUSPEND接口,解释权在固件侧。
local-timer-stop这个属性也很关键。它表示进入这个idle state后,CPU的local timer会停止计时。如果不设这个属性,内核会认为local timer依然能唤醒CPU,那么在处理器睡眠时定时器中断来不了,整个内核的时钟管理就乱了。
3.2 从CPUIdle到Suspend-to-RAM的完整链路
CPUIdle解决的是“CPU没事干”的场景,系统挂起(Suspend-to-RAM / Suspend-to-Idle)则解决“整机想睡觉”的场景。两者的区别是,系统挂起时不只是CPU进idle,而是所有设备、总线、中断控制器都要配合进入低功耗状态。
在Linux下执行suspend的流程,可以简单概括成下面几条路径:
- 用户写入
echo mem > /sys/power/state,内核进入suspend路径 - 冻结用户态进程,把线程都停住
- 逐个调用设备的suspend回调,设备驱动把硬件设置成低功耗状态
- 关闭非boot CPU,让它们走CPUHotplug流程
- 最后剩下的boot CPU执行PSCI_SYSTEM_SUSPEND,把整个系统交给固件
- 固件和硬件进入最终的睡眠状态,DDR进入自刷新模式
- 等待唤醒源(RTC、按键、网络唤醒包等)触发中断
这条链路单看每一步都不复杂,难在牵一发动全身。任何一个设备的suspend回调如果阻塞了、或者某个设备没注册到suspend链路上,系统挂起就可能失败或者发虚。我调试中最常遇见的场景是:系统“看似”挂了,电流也降了,但唤醒后发现网络模块还在运行,因为它注册的是runtime PM而不是system suspend。
唤醒路径同样重要。系统挂起后,GIC(通用中断控制器)必须有独立的always-on电源域,否则无法接收中断并唤醒boot CPU。各种唤醒源的中断要使能好,wakeup_source要在设备驱动里正确注册,否则内核可能会问“这个设备没被标记为唤醒源,你为什么不让我睡”。
唤醒的延迟也是一个关键指标。手机从按下电源键到屏幕亮起,理想情况应该在100ms-200ms左右。这个时间不仅包括硬件上电时序,还包括固件恢复上下文、内核恢复设备、显示链路重新初始化的时间。ARMv9时代,如果还牵扯到Realm环境的恢复,这个时间预算还需要重新评估。
4. ARMv8到ARMv9:PSCI、ATF与新增的安全边界
4.1 PSCI接口:电源管理在EL3的“操作系统”
CPU的电源管理不能只靠Linux内核自己操作寄存器。ARM在标准里定义了一个协议叫PSCI(Power State Coordination Interface),它规范了操作系统和固件之间的电源管理调用接口。为什么需要这么一层?因为CPU的电源状态、集群的电源状态、整个系统的电源状态,本质上控制在EL3固件手里。Linux内核运行在EL1,没有权限直接摆弄这些电源寄存器,必须通过SMC指令陷入EL3去请求固件执行。
PSCI主要接口我用下面的表整理一下:
| 接口名 | 作用 | 典型调用时机 |
|---|---|---|
| PSCI_VERSION | 查询PSCI版本 | 内核启动早期 |
| PSCI_CPU_SUSPEND | 让指定CPU进入低功耗状态 | CPUIdle或CPU hotplug |
| PSCI_CPU_OFF | 关闭指定CPU | 关核、系统挂起 |
| PSCI_CPU_ON | 启动指定CPU | 开核、唤醒 |
| PSCI_AFFINITY_INFO | 查询CPU/cluster状态 | 某些固件调试 |
| PSCI_SYSTEM_SUSPEND | 整机挂起 | Suspend-to-RAM |
| PSCI_SYSTEM_OFF | 系统关机 | 关机流程 |
| PSCI_SYSTEM_RESET | 系统复位 | 重启流程 |
Linux内核通过psci_ops结构体封装这些调用,代码路径在drivers/firmware/psci/和arch/arm64/kernel/psci.c。实际工作中,我们不需要经常去改PSCI层代码,但排查问题时必须看得懂固件和内核之间的交互。比如一个CPU无法进入深度idle,我会先在TF-A侧打开PSCI日志,看内核发来的suspend参数是什么,再对比固件的解析逻辑,很多疑难杂症最后都出在参数编码不一致上。
PSCI的实现通常放在ATF(ARM Trusted Firmware,即TF-A)里。TF-A运行在EL3,是ARMv8之后安全启动和运行时固件的地基。它负责启动后续的BL32(如OP-TEE)、BL33(UEFI或内核),也负责处理PSCI。如果你使用的平台是自定义固件而不是标准TF-A,那PSCI的power state编码解释方式完全可以不一样,这也是很多Soc内部资料里需要专门文档说明的部分。
4.2 ARMv9的CCA/RME给电源管理带来的新问题
ARMv9引入的CCA(Confidential Compute Architecture)是这几年ARM在架构层面最大的一次变化。CCA的核心是RME(Realm Management Extension),它新增了一个被称为Realm的安全执行环境。简单理解,原来ARMv8的世界划分是EL0/1/2下的Normal World和Secure World,CCA在Normal World里又划出一块隔离区域,称为Realm,用来跑机密计算负载,连Hypervisor和OS都看不到Realm内部的内存和状态。
这个新世界对电源管理的影响,主要体现在三个方面。
第一,多了一层固件。RME引入了RMM(Realm Management Monitor),运行在R-EL2。正常世界和Realm世界之间的切换需要经过RMM,这意味着原来从内核到EL3的一次PSCI调用,在某些场景下可能会涉及RMM的参与和状态保存。多一次固件切换,就多一份时间和功耗开销。
第二,Realm状态的保存恢复变得更加复杂。CPU进入深度idle或系统挂起时,如果有Realm正在运行,它的寄存器上下文、内存加密状态、Granule状态都需要考虑。系统恢复时,RMM需要重新建立Realm的运行环境。这个过程的延迟如果设计得不好,会直接影响唤醒速度和待机功耗。
第三,安全边界扩大了,调试变得困难。Realm内存对Normal World是隔离的,这就意味着以前可以在内核里直接读内存看状态的调试方法不再适用。排查Realm相关功耗问题时,只能借助RMM和TF-A提供的接口,诊断链路长了不是一点半点。
我在一个启用了RME的平台上做过对比测试:在完全相同的负载下,开启RME之后系统空载电流比关闭RME时高了20mA-30mA,唤醒延迟增加了大概几十微秒。这个损耗对于大核平台也许无所谓,但对于电池供电的移动设备,20mA可不是一个小数字。所以做ARMv9平台功耗设计时,CCA带来的额外功耗预算必须在方案早期就考虑进去,否则后面调优非常被动。
5. 实操演示:配置、调参与功耗排查
5.1 一份可复现的设备树电源管理配置
下面给出一份更完整的设备树配置片段,涵盖CPU idle states、CPU频率的OPP表、以及一个外设关联到电源域的例子。注意,频率和电压数值只是示例,必须按你的实际SoC手册替换。
/ { cpus { #address-cells = <1>; #size-cells = <0>; cpu0: cpu@0 { compatible = "arm,cortex-a78"; reg = <0x0>; device_type = "cpu"; enable-method = "psci"; operating-points-v2 = <&cpu0_opp_table>; cpu-idle-states = <&cpu_sleep_0>, <&cpu_sleep_1>; power-domains = <&cpu_cluster_pd>; }; cpu0_opp_table: opp-table-0 { compatible = "operating-points-v2"; opp-shared; opp-300000000 { opp-hz = /bits/ 64 <300000000>; opp-microvolt = <750000>; }; opp-1800000000 { opp-hz = /bits/ 64 <1800000000>; opp-microvolt = <850000>; }; opp-2400000000 { opp-hz = /bits/ 64 <2400000000>; opp-microvolt = <1000000>; }; }; }; cpu-idle-states { entry-method = "psci"; cpu_sleep_0: cpu-sleep-0 { compatible = "arm,idle-state"; local-timer-stop; arm,psci-suspend-param = <0x00000001>; entry-latency-us = <30>; exit-latency-us = <50>; min-residency-us = <150>; }; cpu_sleep_1: cpu-sleep-1 { compatible = "arm,idle-state"; local-timer-stop; arm,psci-suspend-param = <0x00100001>; entry-latency-us = <80>; exit-latency-us = <200>; min-residency-us = <500>; }; }; /* 外设挂到某个电源域下 */ soc { ethernet@... { compatible = "..."; reg = <...>; power-domains = <ð_pd>; wakeup-source; }; }; power-domains { cpu_cluster_pd: cpu-cluster-pd { #power-domain-cells = <0>; domain-idle-states = <&cluster_sleep_0>; }; eth_pd: eth-pd { #power-domain-cells = <0>; }; }; };有几个细节要特别留意。
第一个,OPP表里voltage-v2的字段是opp-microvolt,一个OPP可以配置三个电压,分别对应normal、min、max,我在这里只写了一个,实际要按需求补全。第二个,如果多个CPU core共享同一个频率域,需要标记opp-shared,否则调度器可能误以为每个core可以独立变频,导致选频逻辑错乱。第三个,power-domains引用关系必须和固件侧的电源域编号一致,否则内核调用genpd开关时会报错,或者固件收到一个根本不认识的电源域ID。
配置完设备树,可以用下面的命令验证当前生效的完整配置:
# 查看CPU支持的idle state cat /sys/devices/system/cpu/cpu0/cpuidle/state*/name cat /sys/devices/system/cpu/cpu0/cpuidle/state*/usage cat /sys/devices/system/cpu/cpu0/cpuidle/state*/time # 查看CPU频率和OPP cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq # 查看genpd状态 ls /sys/kernel/debug/pm_genpd/如果state0的usage始终为0,说明CPU根本没有进入idle,通常是被某段内核代码持锁或者被实时任务抢占了;如果state2的usage始终为0,多半是该状态的latency配置过高,governor评估后认为不划算,所以从来不选它。
5.2 用powertop、ftrace和电流探头定位功耗问题
软件调参和配置只是第一步,真正要验证功耗效果,必须把工具链配齐。我常用的调试组合是:powertop做粗筛,ftrace做细查,电流探头/示波器做最终确认。
powertop是最容易上手的工具。运行powertop --debug会扫描系统中的设备、中断和内核线程,给出每个组件的大致功耗估算和唤醒次数排名。它还能一键开启所有设备的自动省电策略,也就是powertop --auto-tune。这个命令适合快速定位“哪个外设在频繁唤醒系统”这类问题。注意,powertop的功耗估算只是一个粗糙的模型,不能替代真实电流测量,但它对排序查找问题是足够高效的。
ftrace在电源管理调试中的价值被很多人低估。比如要抓CPUIdle的进入退出时间,可以这样:
# 查看CPUIdle相关事件 trace-cmd record -e 'cpuidle:*' -e 'cpu_frequency:*' -e 'power:*' sleep 10 trace-cmd report | head -50这段命令会记录10秒内CPUIdle状态切换、CPU频率变化、电源域开关的完整事件流。从report里能清楚看到每个CPU在什么时间进入了哪个idle state、停留了多久、被什么中断唤醒。如果发现某CPU在极短时间内反复进出深度idle,那就是“睡不沉”的典型表现,通常和中断风暴或者governor配置有关。
电流探头是功耗调优的裁判。建议至少准备一个能测到mA级别精度的电流探头,卡在整机供电的主电源轨上。测量时先把系统稳定在桌面状态,记录基准电流;然后分别测CPU满载、浅idle、深idle、suspend几种状态的电流。对比设备树配置修改前后的电流差异,能非常直观地判断改动是否有效。有一次我为了定位“某外设空载泄漏1mA”的问题,就是通过电流探头加逐项断电,最后发现是PMIC上某根GPIO默认状态配错,导致一路LDO始终处于使能状态。这种问题用软件日志几乎是看不出来的,必须靠硬件测量才能收口。
6. 常见坑与排查技巧汇总
6.1 睡不醒、反复醒、醒不来:三类典型故障
电源管理的问题,最终都会表现为三类症状:睡不醒、反复醒、醒不来。下面这张表是我这几年调试经验的浓缩,遇到问题可以对号入座。
| 症状 | 可能原因 | 排查命令/手段 |
|---|---|---|
| 系统无法进入suspend | 有设备suspend回调返回错误;有进程持有wakelock | cat /sys/kernel/debug/wakeup_sources,cat /sys/power/wake_lock |
| 系统suspend后很快自动唤醒 | 某设备中断使能且没被屏蔽;wakeup_source频繁触发 | 查看/sys/kernel/debug/wakeup_sources的active_count;抓ftrace的irq事件 |
| CPU进入深度idle后无法被唤醒 | local-timer-stop属性配置不当;唤醒中断没有路由到该CPU;电源域isolate没做好 | 检查GIC中断路由配置;抓PSCI日志;测量唤醒中断是否到达 |
| 进入idle但电流没降 | 电源域引用计数不为0;时钟门控没生效;外设还挂在电源域上 | 检查/sys/kernel/debug/pm_genpd;逐项卸载外设驱动测电流 |
| 唤醒延迟过高 | 退出idle后固件恢复上下文耗时;DDR频率从低到高恢复慢;Realm上下文恢复 | 用ftrace的power:事件和irq:事件标定时间线;对比关闭RME的延迟 |
6.2 几个只有做底层才容易踩的坑
再分享几个我踩过的坑,都是网上文档很少写、但实际开发中很常见的问题。
第一个坑是内核和固件对PSCI power state编码的理解不一致。不同SoC的TF-A实现里,power state的位域划分可能完全不同。有的固件用低4位表示power down层级,有的用低8位;有的把affinity level放在bit16,有的放在bit24。内核设备树里填的arm,psci-suspend-param必须和固件对齐。我遇到过开发者从别家平台抄了一份设备树,idle states配得漂漂亮亮,但CPU就是不进深度睡眠,查了三天,最后发现是固件只认识0x100,内核一直在传0x10000。
第二个坑是runtime PM的autosuspend超时时间设置不合理。设备不再工作时,驱动通过pm_runtime_put_sync让设备进入idle,但genpd会等待一段延迟再真正关电源。如果autosuspend设得太长,比如10秒,而设备每5秒就用一次,那电源域永远等不到关闭的时机,功耗就白白浪费了。这个值要根据设备的实际使用节奏来调,不是越大越好,也不是设成0最好。
第三个坑是不同idle state之间的“连贯性”验证。CPUIdle的state层级设计是有依赖顺序的,比如进入state2之前可能必须先进入state1完成某个保存动作,但设备树只是平铺地描述每个state的延迟参数,不负责描述依赖关系。这个依赖关系必须在固件的PSCI实现里保证。所以改idle states时,至少要把state0到最深state都实际跑一遍,记录每层的电流波形,验证退出唤醒是否每次都成功。不能只看state2配置好看就认为它会生效,中间哪个环节断了,最深处那个state会变成一个永远进不去的摆设。
还有一个经常忽略的是FIQ和Secure中断对电源管理的影响。在ARMv8/v9体系里,EL3和安全世界的定时器、中断如果处理不当,会频繁唤醒正在idle的CPU。而且这类唤醒很多时候在内核日志里看不到,因为中断被固件直接处理了。排查这类问题,需要在TF-A里打开FIQ日志,或者用硬件的performance counter统计secure中断次数,否则你根本猜不到是哪里来的唤醒源。
调试ARM电源管理系统,最大的体会是不要只盯着软件框架看。pinctrl配置、电源轨的电容大小、PMIC的默认上电状态、固件的PSCI实现,任何一个环节不匹配,都会在前面说的某个症状里爆发出来。我个人的建议是,做功耗优化的朋友,至少先把SoC的TRM里Power Domain章节完整读一遍,然后在你手里那块开发板上,用电流探头把idle、suspend、resume这几个典型场景的电流波形全部抓出来存档。后续每次改动,只需对比波形,很多争论不清的问题就能一眼看出来。这套方法我带过好几个团队,真的是最直接也最可靠的一条路。