ARM电源管理链路解析:PSCI、cpuidle与cpufreq
发布时间:2026/9/17 6:08:19
分类:文化教育
浏览:1234

1. 待机功耗降不下来才意识到电源管理是一整条链路先讲一个我自己的故事。前几年调一块板子芯片是ARMv8架构的企业级SoC软件上该关的外设都关了CPU也进了WFIWait For Interrupt但整板功耗还是在十几瓦上下不去。最初我以为是某个传感器或DDR没有进自刷新于是一个外设一个外设排查折腾了两天没结果。最后是硬件同事提醒了一句你确认EL3那边把cluster真正下电了吗我去翻了Trusted Firmware-AATF的日志才发现核心确实停了但cluster电源域因为一个调试组件的引用计数没清零一直维持在保留模式。那一刻我才真正理解所谓电源管理系统架构不是一个驱动、一份设备树、一个函数的事而是从硬件电源域设计、固件接口到内核调度框架的完整链路。任何一个环节脱节功耗都下不去。这篇文章我就把这个链路讲透。内容基于ARMv8和ARMv9架构覆盖从硬件组件SCP、GIC、Generic Timer、标准化接口PSCI、SCMI到Linux侧落地cpuidle、cpufreq、DTPM的完整视图最后还会有排查系统睡不下去的实操思路和教训。适合三类人看正在调低功耗的BSP/内核工程师、做SoC电源域设计的硬件工程师以及想系统理解ARM电源管理架构的学生或转行开发者。2. 功耗从哪来架构要管的是什么2.1 一个公式看懂功耗来源要理解电源管理架构先得理解功耗的物理来源。CMOS电路里功耗可以粗略拆成动态功耗和静态功耗两部分动态功耗P α × C × V² × f。这个公式里α是翻转率activity factorC是负载电容V是核心电压f是时钟频率。这一项来自晶体管开关时对电容充放电。可以看到电压对功耗的影响是平方关系也就是说把电压从1.0V降到0.8V理论上动态功耗能降36%。这解释了为什么所有DVFSDynamic Voltage and Frequency Scaling方案都把调压看得比调频更重——频率降10%功耗降10%电压降10%功耗降19%。静态功耗P I_leakage × V。这是晶体管在非完全关断状态下泄漏电流造成的功耗。工艺节点越小泄漏电流占比越高。到7nm以下某些低负载场景中静态功耗甚至能超过动态功耗。要压制静态功耗只能靠关电源域Power Gating——把整个区域断电或者靠降低电压到接近阈值。这两个公式决定了电源管理的基本策略负载高的时候用DVFS在性能和功耗之间取平衡负载低的时候把核心往浅睡眠、深睡眠、掉电状态逐级推同时尽可能关掉不需要的电源域整机空闲时再进入system suspend这类全局低功耗状态。这也意味着架构层面必须有一个机制让裸机上的WFI指令能翻译成电源域关断让Linux里的调频率请求能翻译成PMIC输出电压变化。ARM做的事情就是把这个机制标准化。2.2 为什么需要一套架构而不是各自操作寄存器可能有人会问不就是操作几个寄存器吗关CPU就是置位一个power gate控制位调频就是改两个分频器和PMIC I2C为什么要上升到架构原因有三。第一ARM架构下运行着多个安全级别的软件——非安全世界有Linux/Android安全世界有OP-TEE固件里还有EL3的ATF和EL0的SCP固件。如果每个软件都直接操作硬件寄存器谁来保证互斥谁有权限关掉别人正在用的电源域必须有一个人口统一管理。第二CPU的电源状态和系统的电源状态不是一回事。一个CPU可以独立掉电一个cluster可以一起掉电整个系统还能进入suspend或off。这些不同级别的状态切换之间还有依赖关系必须用一个带层级的模型管理。第三电源管理经常涉及谁唤醒谁的问题。CPU睡眠后是谁来唤醒它是GIC收到中断后发出唤醒信号还是SPMSystem Power Management单元定时唤醒这一整条唤醒路径如果不预先设计好睡眠就是一睡不醒。所以ARM架构的答案很明确用异常级别Exception Level做权限分层用标准化接口做软件契约把谁来管、管到什么级别、怎么唤醒全部定清楚。这也是硬件厂商能够各自实现不同SoC但操作系统只需要写一套驱动的原因——接口稳定实现可以百花齐放。3. 硬件侧的核心角色SCP、GIC、Generic Timer和唤醒路径3.1 SCPSoC里的大管家在ARMv8/v9的典型SoC里除了主核Application ProcessorAP通常还有一颗小处理器叫SCPSystem Control Processor。SCP运行独立的固件在AP掉电甚至系统深度睡眠时它自己保持运行负责掌管电源域开关、时钟分配、电压调节、系统热管理等杂务。它和AP之间的通信协议在较新的平台上是SCMISystem Control and Management Interface走共享内存加门铃中断的机制。为什么要单独放一颗SCP这其实是工程上的无奈AP核心为了性能和生态追求的是通用计算能力而电源状态切换需要极其精准的时序控制比如上电时要等供电稳定、要给特定单元发复位序列这些操作在硬件上往往有几百微秒到几毫秒的时延要求。如果让Linux直接做调度延迟和中断延迟根本不可控。SCP用独立固件、独立时钟域来做这些硬实时操作AP只需发一个请把这个cluster下电的请求然后等着结果就行。可以把它类比成物业公司——你AP只需要报修什么时候修、用什么工具箱、走什么流程物业SCP自己安排。3.2 GIC睡眠时谁负责敲门一个CPU进入WFI或WFE之后只有中断或事件能把它唤醒。中断控制器GICGeneric Interrupt Controller在这里扮演关键角色。GIC分为两个主要部分GIC Distributor通常位于always-on电源域和GIC CPU Interface通常位于对应的CPU电源域内。对于单核唤醒流程很直观外设产生中断GIC Distributor检测到之后向目标CPU的CPU Interface发送唤醒信号CPU从WFI中醒来进入中断处理流程。但有个细节需要注意如果整个cluster都掉电了CPU Interface本身都没电了GIC Distributor还能不能唤醒这时GIC可以通过唤醒请求信号Wake Request通知电源控制器为对应CPU或cluster重新上电。也就是说唤醒请求不是直接打到CPU上而是先打到电源管理逻辑上等供电稳定了再把中断送进CPU。这个顺序如果搞反或者GIC的唤醒信号没有正确布线就会出现中断已经发生了CPU却醒不过来的经典问题。在ARMv8/v9的服务器场景里GICv3/v4的LPILocality-specific Peripheral Interrupt和v4/v4.1的直接虚拟中断注入进一步优化了虚拟化场景下的唤醒路径这个我在后面ARMv9部分单独说。3.3 Generic Timer与系统计数器低功耗下的北京时间很多人忽略Timer在电源管理中的作用。ARM架构里有一个系统计数器System Counter它通常由always-on电源域供电提供一个全局统一的时间基准。Linux的时钟事件clockevent和时钟源clocksource都依赖它。当CPU进入深度睡眠时per-CPU的generic timer会停止或丢失状态这时系统必须依赖系统计数器来校正时间。就功耗管理而言这里有两个直接相关的设计点一是深度睡眠状态的退出延迟和重新同步系统计数器的时间要匹配。如果系统计数器在睡眠期间有漂移或者恢复时间长调度器的时间片、网络超时等都会受到严重影响。二是arm的architected timer在CPU suspend时通常要求固件保存和恢复timer的状态这部分在PSCI规范里也有明确划分——CPU私有状态下固件负责保存系统状态下则由唤醒固件处理。你在排查睡醒之后系统时钟跳变这类问题时首先要怀疑的就是这个环节。4. PSCICPU生命周期管理的标准契约4.1 PSCI是什么为什么用SMC调用PSCI全称是Power State Coordination Interface是ARM定义的电源管理标准接口。从ARMv8开始它成为标配。前面提到的谁来管、管到什么级别、怎么唤醒在PSCI里就用一组系统调用定义好了。PSCI调用不是普通的函数调用而是通过SMCSecure Monitor Call指令进行的。SMC指令会让CPU从EL1/EL2陷入EL3EL3的固件通常是ATF收到调用号和参数后在安全的世界里完成电源状态切换。为什么必须绕这么一圈因为电源状态的切换涉及全局硬件资源——比如关掉一个cluster时要检查cluster里每个CPU是不是都处于可关闭状态再比如系统suspend时需要协调所有cluster和大量外设。这些操作需要比普通内核更高的权限而且不能被非安全世界的任何软件随意触发。EL3在这里就像是一个看门人所有电源状态变更都从它这里走。4.2 核心调用逐个拆解CPU_SUSPEND、CPU_ON、CPU_OFF、SYSTEM_SUSPENDPSCI的函数很多但日常打交道最频繁的底下这几个就够了。我列个表后面逐个展开。调用名调用号SMC64功能PSCI_VERSION0x84000000查询PSCI版本CPU_SUSPEND0xC4000001让指定的CPU进入电源状态CPU_OFF0x84000002把指定CPU完全关闭CPU_ON0xC4000003把一个关闭的CPU重新启动AFFINITY_INFO0xC4000004查询CPU/cluster/系统状态SYSTEM_SUSPEND0xC400000E整个系统进入睡眠SYSTEM_OFF0x84000008系统关机SYSTEM_RESET0x84000009系统复位CPU_SUSPEND是最核心的调用参数里最关键的是power_state。这个字段按PSCI规范分了几段低4位是状态类型standby还是power down第4到7位是affinity level亲和级别0代表单个CPU1代表cluster2代表整个系统更高位则是厂商自定义的电源状态ID用于区分同一级别下的不同睡眠深度。Linux的cpuidle驱动会把设备树里的每个cpu-idle-states节点翻译成一次CPU_SUSPEND调用。比如cpu sleep状态对应affinity level 0的power downcluster sleep就对应affinity level 1。你可以在内核日志里看到类似PSCI: CPU_SUSPEND power_state0x...的信息拆开来看就知道它进入了哪个级别的状态。CPU_ON则负责把之前关闭或从未启用的CPU拉起来。系统启动时Linux的SMP初始化就依赖它给每个secondary CPU发一个CPU_ON请求CPU在EL3复位入口醒来然后再返回到内核的secondary entry。CPU_OFF则相反把当前CPU整个关掉一般用于热插拔和最终idle状态。SYSTEM_SUSPEND是整机级别的睡眠相当于Linux的suspend-to-RAM。它要求所有CPU都先进入低功耗状态然后由最后一个活跃的CPU发起SYSTEM_SUSPEND调用把整个系统交给固件。这时候SCP会把DDR切到自刷新、关闭大部分电源域只保留唤醒源相关的部分。4.3 ATF与SCP的配合工作方式在软件层面EL3固件ATF是PSCI的直接实现者。但ATF并不会真的去操作每个电源域的寄存器——那通常是SCP固件的职责。ATF和SCP之间通过共享内存和门铃中断通信。ATF收到CPU_SUSPEND后做一层简单的处理如果目标状态涉及cluster或系统级关电ATF会调用SCP固件的接口让SCP去执行真正的电源序列如果只是单个CPU的power downATF自己直接写寄存器就完成了。这种分工保证了ATF保持精简同时把硬实时任务留给专用控制器。这个过程中有一个很容易被忽略的时效性问题CPU_SUSPEND调用返回时CPU可能已经处于正在掉电的临界状态。PSCI规范要求CPU_SUSPEND调用对于浅睡眠standby是调用返回后CPU继续运行对于深度睡眠power down调用根本不会返回因为CPU已经断电了唤醒后固件会从复位入口重新执行再像CPU_ON那样跳回内核。理解这一点非常关键——很多开发者试图在CPU_SUSPEND调用返回后继续执行一段代码来保存寄存器结果发现代码怎么都不执行其实就是这个机制在起作用。5. Linux侧如何消费这套架构从cpuidle到cpufreq5.1 PSCI驱动如何在内核里落地Linux内核把PSCI封装成了一个firmware driver路径在drivers/firmware/psci/。它在启动早期通过设备树节点psci完成探测内核会检查这个节点的compatible、method是smc还是hvc以及各个函数的ID。method字段很直观地告诉内核这个平台是通过SMC还是HVC陷入EL3/EL2执行调用。这个驱动的存在让内核上层的cpuidle、cpufreq、CPU热插拔代码都变成了一组与硬件无关的抽象调用。比如内核要热插拔一个CPU时内核的cpu_ops回调函数最终会调用psci_ops.cpu_off()。架构的好处在这里体现得非常直观同是一套Linux内核源码既可以跑在高通手机上、也可以跑在华为的服务器芯片上、还可以跑在QEMU模拟的virt平台上上层代码一行都不用改差异全部被设备树和psci驱动吸收掉了。5.2 cpuidle每个idle状态都是一次PSCI调用Linux的cpuidle子系统负责在CPU空闲时选择进入哪个睡眠状态。ARM平台上通常使用generic cpuidle driver或psci cpuidle driver。它做的事情很简单把设备树中cpu-idle-states节点描述的各种睡眠状态映射到PSCI CPU_SUSPEND的不同power_state。具体地看设备树里一个典型的idle状态节点大概长这样cpu_sleep_0: cpu-sleep-0 { compatible arm,idle-state; local-timer-stop; entry-latency-us 200; exit-latency-us 400; min-residency-us 1000; arm,psci-suspend-param 0x00000001; };这里的arm,psci-suspend-param字段就是最终传给CPU_SUSPEND调用的power_state值。你在上面可以看到entry-latency、exit-latency、min-residency这几个参数——它们不是随便填的而是cpuidle governor做状态选择的依据。如果一个CPU空闲的时间还不到min-residency进入深度睡眠反而会亏因为唤醒成本比一直浅睡还高governor就会选择浅状态或干脆idle不睡。所以perf场景下调优低功耗很多时候不是在改代码而是在调整设备树里这几个延迟参数。5.3 cpufreq/DVFS调频调压的完整路径cpuidle管的是CPU没事做时的功耗而cpufreq管的是CPU有事做但不需要全速时的功耗。两种策略合在一起才构成完整的功耗管理闭环。ARM平台上常见的cpufreq后端有两个一个是cpufreq-dt敲设备树里的operating-points-v2表来得到各个频率和对应的电压另一个是scmi-cpufreq通过SCMI协议向SCP请求调频调压。cpufreq的governor比如schedutil会根据CPU的利用率动态选择频率。选择好目标频率后驱动会做先调压还是先调频的顺序处理。细节是升频时一般先升压再升频保证高频下电压足够降频时先降频再降压防止电压过高浪费。这个顺序如果反了轻则频率上不去、性能低重则直接死机。如果是通过SCMI调压这个顺序通常由SCP固件自己保证但如果是直接操作PMIC的I2C接口那就必须在驱动里严格执行。这块踩坑的人很多我建议所有做DVFS调优的工程师都看一眼调压调频的时序日志。5.4 DTPM与其他扩展内核级的功耗治理除了CPU本身的功耗一个完整系统里还有GPU、NPU、DDR、各种外设。ARM系统架构里有一个叫做DTPMDynamic Thermal Power Management的框架它在powercap子系统下抽象出一个功耗控制视图允许用户态通过sysfs给不同设备设置功耗上限内核再把上限转换成实际频率和电压限制。它和传统的thermal框架配合使用已成为ARM服务器和移动设备上做功耗治理的标准路径之一。我在实际项目中用它解决过整机功耗超预算的问题系统负载本身不高但GPU和NPU各自为政功率峰值叠加导致整机超过热设计功耗。用DTPM给每个设备设一个比理论峰值略低的功耗cap整机功耗立刻被压住而且性能损失非常小。这类问题不是单靠CPU电源管理能解决的必须有一个跨设备的功耗视角DTPM就是ARM体系里补上这块的东西。6. ARMv9带来的变化不是推倒重来而是补齐场景6.1 ARMv9在电源管理框架上是继承而非革命先说结论ARMv9并没有推出一个PSCI 2.0或SCMI 4.0之类的颠覆性方案电源管理的基础框架和ARMv8保持一致。你手上的PSCI知识、cpuidle/cpufreq经验在v9平台上完全适用。v9的变化更多体现在两个方向一是对v8.x时代引入的FEATFeature扩展的选择性继承二是针对机密计算、虚拟化和更大规模系统管理的新需求在系统架构层面做了补齐和增强。这一点常被高估。很多人在ARMv9发布后问电源管理是不是有什么全新的东西要学答案是没有太多。ARMv9更多是把之前v8身上这是一次渐进式演进而不是架构革命。但正因为如此理解v8的电源管理就等于理解v9的八成功力剩下两成才是新场景的增量。6.2 GICv4/v4.1虚拟化场景下的唤醒路径优化ARMv9平台普遍搭载GICv4/v4.1中断控制器它在电源管理上的最大价值是优化了虚拟化场景的唤醒路径。在GICv3时代当虚拟机里的一个虚拟CPUvCPU对应的物理CPU睡眠时如果有虚拟中断要送达hypervisor必须被中断唤醒、处理虚拟中断注入、再判断是否需要唤醒对应的物理CPU。这个过程涉及多次异常陷入和上下文切换延迟和功耗都不理想。GICv4引入了一个重要能力直接向vCPU注入虚拟中断vLPI。也就是说GIC硬件可以绕过hypervisor的大部分处理直接把虚拟中断投递到正确的物理CPU上并通过硬件机制唤醒它。GICv4.1又做了一些修正让vPEvirtual Processing Element的调度和抢占更高效。在MECMobile Edge Computing和云游戏这类对虚拟化中断延迟敏感的场景里这个改进对能效的贡献非常显著。6.3 RME与多安全世界下的电源管理考量ARMv9的RMERealm Management Extension引入了第四种安全状态——Realm状态由RMMRealm Management Monitor管理。这本身不是电源管理扩展但它给电源管理带来了一个新的约束矩阵不同状态的软件Root、Realm、Secure、Non-Secure都可能在同一个CPU上运行而电源状态切换需要确保任何状态下都没有关键任务被意外打断。举个例子当Realm世界里正在处理机密数据时非安全世界触发了一个CPU_SUSPEND请求固件需要判断这个CPU当前是否承载了Realm工作负载不能简单粗暴地断电。这类判断逻辑通常落在EL3固件和RMM之间的协调上。实际做BSP的时候我见过因为RME相关配置不当导致Realm世界任务被电源管理误杀的情况排查起来非常隐蔽——因为表面上看所有的PSCI调用都是成功的。所以如果你在ARMv9 RME平台上调低功耗一定不要只看PSCI返回值还要把RMM的日志打开对照。6.4 在QEMU/Foundation Model上观察PSCI行为对于没有真实开发板、或者想在早期阶段验证电源管理逻辑的开发者QEMU的virt平台是一个很好的观察窗口。QEMU会模拟一个PSCI实现你可以通过trace或gdb打断SMC调用直接看到内核发起CPU_SUSPEND、CPU_ON等调用的时机和参数。Foundation ModelARM官方提供也能模拟ARMv8/v9的很多行为但它的主要价值是跑固件验证而不是功耗本身。需要提醒的是在QEMU上看到PSCI调用成功绝不等于在真实SoC上功耗就正常。因为真实的功耗行为大量依赖SCP固件、PMIC和硬件电源域设计这些在模拟器里基本是空壳。模拟器适合调试协议交互和驱动逻辑不适合评估是否真的省电。我在项目早期用QEMU迭代PSCI驱动逻辑但最终验收还是得上板子上用功率分析仪测数据。这个边界要先想清楚可以省掉很多无效劳动。7. 实战排查系统睡不下去的完整链路7.1 从功耗高到谁在捣乱的排查框架做低功耗调试最大的痛点是问题现象很模糊功耗好像高了10%、有时候能睡有时候不能睡。面对这类问题我一般按照下面的框架来排查顺序很重要不要一上来就盯寄存器。先确认内核到底有没有尝试进入睡眠状态。在板子上执行echo mem /sys/power/state同时用串口抓内核日志看有没有走到suspend流程的后期。如果日志停在某个驱动上直接用pm_test分步定位。再确认CPU是不是真的进了idle状态。看/sys/kernel/debug/tracing里有没有cpu_idle事件以及cpuidle的state usage统计。然后确认PSCI调用是否发出、固件侧是否执行了真正的断电动作。这需要EL3日志或SCP日志。最后才是测量验证——用功率分析仪或板载PMU确认整板功耗曲线。这个自上而下的顺序能保证不会被底层细节带偏。很多人一上来就拿着示波器去量电源轨结果看到一堆毛刺根本分不清是哪一层的问题。而如果从内核的日志和trace一层层往下走定位往往很快。7.2 用pm_test分级定位suspend流程Linux内核提供了一个非常实用的调试接口/sys/power/pm_test。它允许你把suspend流程切断在不同的阶段只测试前半段。比说echo devices /sys/power/pm_test之后再执行suspend内核会在所有设备的suspend回调执行完之后立刻唤醒不进真正的睡眠。这样你就能逐步确认问题出在设备层、还是出在PSCI调用后的固件层。常见的用法是从core开始测再逐步升级到platform和processors每次都能排除掉一大片区域。如果到了processors阶段流程可以走到最后的CPU hotplug且成功唤醒那问题多半在外设或DMA如果连core阶段都过不去那就要先查中断、时钟和电源域这些基础资源。7.3 一个真实案例中断风暴让CPU永远醒在WFI之前有一次我遇到一个特别诡异的现象系统跑benchmark时一切正常但只要负载一降下来功耗不降反升而且CPU占用率显示接近100%。用ftrace一抓发现CPU根本没有进入任何idle状态永远在处理中断——准确地说是一个网卡的中断在不合理地重复触发。这个网卡的中断在硬件上被配置成了level触发中断服务程序清除了软件状态但硬件线的level一直没有拉低导致GIC持续把同一个中断送给CPU。CPU忙得连WFI都执行不到。解决办法也不复杂先把该网卡的中断请求加上irq_no_suspend之类的标记或者通过调整IRQ affinity和流量控制让中断频率降下来。这个案子给我留下很深的印象因为它告诉我一个简单的道理ARM电源管理架构本身不会阻止你省电但任何一层的问题都可能把整个省电路径堵死。当你发现CPU很忙但系统没有实际工作时第一反应应该是看中断分布而不是去怀疑cpuidle驱动。7.4 功耗测量与验收别只看稳态数字说到验收很多团队的习惯是跑一个空闲场景读一下电流表数字觉得满意就完事了。这个做法在低功耗开发里并不充分。我建议至少要测三条曲线系统从满载降到空闲的瞬态功耗曲线看是否有明显的掉电过程、系统suspend期间的稳态电流看是否稳定在一个低水平、以及从suspend唤醒回满载的瞬态功耗看唤醒过程有没有异常浪涌。测量的工具方面简单的用高精度万用表串在电源输入端采样频率要够至少要1kS/s以上精确的话用功率分析仪或DAQ同时抓多个电源轨的电流波形再和日志中的时间戳做对齐。有一次我花了很长时间才定位到一个DMA一直没有释放的问题最后就是靠电流波形和内核trace对齐发现suspend流程已经结束但一条DDR总线的电流纹波仍然明显偏离预期说明有设备还在偷偷访问内存。这种问题没有波形对齐纯看日志是发现不了的。再分享一个最后的小技巧调试期间可以在内核启动参数里加上trace_eventcpu_idle,cpu_frequency,power:cpu_suspend让低功耗相关的trace默认打开这样即使现场没法登录shell串口日志也能留下足够的信息。等你把问题解决了再把这个启动参数去掉。在嵌入式现场调试中这一个动作能节约大量复现问题的时间。