
做RISC-V低功耗平台调优这两年我最大的感受是光看指令集手册根本不够。RISC-V的省电逻辑不是“哪个指令能省电”这么简单而是架构指令WFI、固件接口SBI、操作系统策略Linux cpufreq/governor三层协同的结果。市面上讲单点的教程很多但能把WFI空闲态、SBI CPPC、Linux cpufreq串成一个完整链路的资料很少。这篇东西就是冲着这个缺口来的适合正在做RISC-V SoC底层软件、BSP移植或者被“为什么我的板子功耗下不来”折磨的工程师。我会把这几个模块的原理、对接方式、踩坑经验一次讲透。1. 把 RISC-V 功耗问题拆成三层来看1.1 为什么 RISC-V 的功耗管理比 ARM 更“碎片化”ARM 的生态有 Power State Coordination InterfacePSCI这样一个统一固件接口Linux、固件、硬件按这套约定办事大家照着做就行。RISC-V 这边的情况不太一样指令集是统一的但功耗管理接口一直在演进。早年只有一条 WFI 指令可以“暗示”硬件进入低功耗后来规范里陆续加了 SBI HSMHart State Management处理核心启停、SBI System Suspend 处理系统挂起再到 SBI CPPC 来标准化性能与功耗的协作控制。对做 BSP 的人来说这意味着你没法只对着一个文档抄作业得把架构手册、SBI 规范、内核驱动三层串起来看。我见过不少团队把精力全花在调 cpufreq 频率表上结果空闲功耗一直压不下去。根因往往不是频率没调对而是 WFI 之后 CPU 根本没有真正进入低功耗状态或者固件里中断唤醒路径把状态机搞乱了。反过来也有人只盯着 WFI觉得“只要执行了 WFI 硬件就会自动省电”完全忽略了动态调频缺失导致的运行功耗飙升。这俩必须一起看。1.2 三层模型架构指令、固件服务、内核策略用一句话概括三层关系内核定策略固件做翻译硬件干脏活。架构层WFI、WFE 这类指令是 CPU 提供给软件的“省电暗示”但具体省多少电由微架构实现决定。固件层SBI CPPC、SBI HSM 运行在 M 模式机器模式给 S 模式监管模式的 Linux 提供一个“不暴露硬件寄存器细节”的性能功耗控制接口。硬件寄存器怎么配、PLL 怎么锁、电压域怎么切都是固件的事。内核层Linux 的 cpuidle 框架负责“什么时候闲、闲多深”cpufreq 框架负责“什么时候快、快多少”这两个框架通过调度器互相配合最终把策略落到 WFI 指令和 SBI 调用上。这里先给整篇文章画个地图第 2 章讲 WFI 与 cpuidle第 3 章讲 SBI CPPC 协议第 4 章讲 Linux cpufreq 怎么跟前面两者对接第 5 章讲三者协同起来后的完整数据流第 6 章给排查清单。2. WFI 空闲态看似简单实则全是细节2.1 WFI 的语义一条“暗示”而非“命令”RISC-V 规范里WFIWait For Interrupt的定位非常特殊它是一条 hint 指令也就是说软件执行它之后硬件可以选择什么都不做直接当 NOP 处理也可以选择停掉时钟、关掉流水线、降低电压甚至进入 retention 状态。规范给硬件留了极大的自由度这带来的后果就是——同一个 Linux 内核镜像在不同 RISC-V 核上跑出来的空闲功耗可能差一个数量级。从软件的角度看WFI 的定义很明确执行 WFI 后处理器会暂停执行直到一个全局中断包括待处理的、使能的中断变为可接受状态。关键是“可接受”——如果中断已经被屏蔽如mstatus.MIE0或sstatus.SIE0那么即使中断挂起WFI 也不会唤醒实际上规范对此有争议主流做法是 WFI 唤醒后软件再检查中断状态不满足条件就继续 WFI。这直接涉及经典的“WFI 丢失唤醒”问题后面我会专门说。在 Linux 中这个指令被包在 arch 层的 idle 实现里路径大致是// arch/riscv/kernel/process.c简化示意 void arch_cpu_idle(void) { local_irq_disable(); if (!need_resched()) { wfi(); } local_irq_enable(); }有些实现了 WFI 的硬件还会配合mhartid对应的时钟源做“事件抑制”也就是说执行 WFI 后如果后续没有 pending 中断核心时钟直接停掉功耗降到几乎只剩漏电。2.2 Linux cpuidle 如何管理“多级空闲”现代 RISC-V SoC 不会只提供一种空闲状态。典型情况是状态进入方式效果唤醒延迟WFICPU 执行 WFI停掉本核流水线/时钟极短几微秒CPU 局部断电固件通过 SBI HSM/SUSP 参与关掉本核电源域中等几十微秒簇/系统深度睡眠固件协调多个核掉电大部分系统长毫秒级以上Linux cpuidle 框架用一组cpuidle_state描述这些状态每个状态都带target_residency低于这个时间就不值得进和exit_latency退出延迟。内核中的menugovernor 和teogovernorTimer Events Oriented就是靠这两个参数做决策的。对于 RISC-V深度 idle 状态一般通过设备树里的cpu-idle-states节点描述配local-timer-stop属性告诉内核这个状态下本地定时器会停需要借助外部定时器补偿。这个属性特别容易漏——漏了之后内核以为本地定时器还在走结果进入深度睡眠后定时器不触发任务调度直接卡死。我在调试一个语音模块时踩过一次现象是系统死得不均匀有时几分钟一卡有时几小时一卡最后发现是cpu-idle-states里少标了local-timer-stop。2.3 WFI 与中断唤醒丢失唤醒的经典坑WFI 丢失唤醒lost wakeup是 RTOS/嵌入式领域的老问题Linux 其实早就规避了但如果你自己写裸机调度器或者改造内核就一定要小心。问题模式如下线程准备睡眠先关闭本地中断检查条件不满足执行 WFI就在“检查条件”和“执行 WFI”之间中断到达并被 CPU 记录为 pending由于中断被关闭WFI 不被唤醒于是 CPU 一直睡下去任务永远得不到调度。Linux 的解法是在 arch_cpu_idle 里local_irq_disable()之后、wfi()之前检查need_resched()并且在 WFI 唤醒后重新开中断再走调度。本质就是“睡前再查一次条件”。但这要求之前的中断已经在硬件上被 pending这正是 WFI 的唤醒条件。如果你在固件里做类似操作务必要在两者之间加fence iorw之类的屏障避免编译器/CPU 重排导致检查顺序错乱。2.4 实操心得WFI 功耗验证别只看电流表很多人接下来说“我已经执行 WFI 了为什么整机功耗没降”——这非常正常。因为 WFI 只是让 CPU 核心停转可 SoC 里还挂着 DDR 自刷新、外设时钟、总线矩阵等一堆功耗大头。你测到的整机电流没降不代表 WFI 没生效而是省下来的那部分功耗被淹没在外设功耗里了。我建议做两步隔离第一步通过perf或 trace 确认 CPU 确实有大比例时间停在 idle state看cpuidle的 state 统计第二步单独量核心供电 rail 的电流而不是整机电流。很多开发板不把核心供电单独引出那就临时飞线用微安级电流探头抓。另外注意如果中断风暴密集CPU 根本没法长时间停在 WFI这时即使 WFI 功耗再低也没意义先拿cat /proc/interrupts看看中断分布再说。3. SBI CPPC让操作系统“请求”而非“命令”性能3.1 从 ACPI CPPC 说起CPPCCollaborative Processor Performance Control最早是 ACPI 规范里的概念核心思想是协作式性能控制OS 不直接写硬件频率寄存器而是提交一个“性能请求”performance level由固件/硬件根据温度、功耗余量、当前负载决定实际频率和电压。这么设计的好处很明显在异构复杂系统里OS 根本不可能准确知道所有工艺参数和电源约束硬凑频率表反而容易撞功耗墙。RISC-V 的 SBI 规范借鉴了这一思路定义了 SBI CPPC 扩展。它让运行在 S 模式的 Linux 通过 ecall 陷入 M 模式固件完成两类操作一类是查询读到当前性能、额定性能、反馈计数器等另一类是设置提交期望性能等级。对整个软件栈来说CPPC 像一层“翻译官”——OS 只需要表达需求不需要关心底下是 DVFS 调压、动态时钟还是某种私有电源管理单元。3.2 SBI CPPC 的核心调用类型SBI CPPC 在规范里主要提供这几类能力以功能而言不完全等同于某个实现版本Probe查询当前 hart 或 CPU 是否支持 CPPC支持的话返回性能寄存器组的信息。Get/Set按属性 ID 读取或写入某个 CPPC 寄存器。典型属性包括最高性能、最低性能、额定性能、当前性能、反馈计数器等。Request提交期望性能等级OS 把“我大概想要多快”告诉固件固件结合自身约束给出实际值。从 Linux 角度看这套接口很像一个“跨权限级别的寄存器读写服务”。大家用的时候要特别注意CPPC 属性是抽象概念不是物理寄存器地址。同一套内核代码A 芯片的 CPPC 属性“当前性能”可能映射到 PMU 里的计数器B 芯片可能映射到调压器的反馈回路。这种抽象正是 SBI CPPC 的初衷——让 OS 层代码与硬件解耦。3.3 SBI CPPC 与设备树/ACPI 的关系目前 RISC-V 平台上 CPPC 的“暴露方式”主要有两条路ACPI 路径服务器场景下固件通过 ACPI CPPC 表把 capability 暴露给 OSLinux 走cppc_cpufreq驱动这跟 ARM64 服务器上已经非常成熟的方案是同一条路线。设备树路径嵌入式场景下更多的是在设备树里配 OPPOperating Performance Points频率电压按表走如果想走 CPPC则通过 SBI CPPC 扩展与 M 模式固件交互。在社区和厂商的推进下SBI CPPC 正逐步成为 RISC-V 上“设备树 Linux cpufreq”和“ACPI CPPC”之间的一种统一固件服务。主线内核的 RISC-V cpufreq 目前仍是基于设备树的cpufreq-dt路线为主同时 ACPI CPPC 驱动也在不断适配 RISC-V。做产品选型时我建议先看固件侧能力如果你的 OpenSBI 或商业固件已经支持 SBI CPPC以后换用 CPPC 后端会比重新调 OPP 表省很多事。3.4 实操心得如何快速验证 SBI CPPC 是否可用最直接的办法是看内核日志和 SBI 返回码。如果你的固件支持 SBI CPPC 扩展在 Linux 下可以用一个很小的 S-mode 测试程序直接调 ecall探测扩展号是否存在。我自己的经验是第一步先做“只读探测”—只调用 Probe 和 Get拿到最高/最低/额定性能值确认固件返回值非零且单调再谈设置。打开内核相关选项时留意依赖关系CPPC 通常需要CONFIG_ACPI_CPPC_LIB这类通用库。如果内核配置里没开那么即使固件支持驱动也会直接 ENOTSUPP。另外在 SBI 调用里返回值中的error字段要按规范解读比如ERR_NOT_SUPPORTED表示该功能没实现别把它当成普通错误忽略掉。4. Linux cpufreq策略如何落到频率上4.1 cpufreq 核心框架与非 RISC-V 特有部分Linux cpufreq 的架构分三层核心层cpufreq.c管理全局策略和 sysfs 接口governor 层决定“目标频率”是多少驱动层负责把目标频率变成硬件操作。对做底层的人来说要学会通过 sysfs 观察这三层是否正常# 查看 CPU 当前频率与可用频率 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies # 查看当前 governor cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 主动设置为 performance调试时常用 echo performance /sys/devices/system/cpu/cpu0/cpufreq/scaling_governorRISC-V 芯片的 cpufreq 驱动通常借助cpufreq-dt配合设备树完成。只要设备树里给 CPU 节点配了operating-points/operating-points-v2和对应的clocks、cpu-supply再在 Kconfig 里打开CONFIG_CPUFREQ_DT大部分 DVFS 需求就能满足。4.2 设备树 OPP 表怎么写才有“可调性”OPP 表是 Linux 做 DVFS 最常见的数据来源。一个规范的设备树片段大致如下cpu0: cpu0 { compatible riscv; ... operating-points-v2 cpu_opp_table; }; cpu_opp_table: opp-table { compatible operating-points-v2; opp-shared; opp-1000000000 { opp-hz /bits/ 64 1000000000; opp-microvolt 900000; opp-supported-hw 0x7; clock-latency-ns 2000000; }; opp-1500000000 { opp-hz /bits/ 64 1500000000; opp-microvolt 1050000; opp-supported-hw 0x7; clock-latency-ns 2000000; }; };写 OPP 表时有几个细节直接影响稳定性和性能opp-microvolt表示该频点对应的核心电压调压器驱动会按这个值去设电压。多档电压之间如果差值太小碰到 bulk CMOS 工艺波动较大的芯片可能出现“低频反而挂”的怪事。我建议在量产前把每档电压都做-5% 的 margin 测试。clock-latency-ns是频率切换的预估延迟governor 在选择是否立刻切换时参考它。填得太小内核频繁跳频切换损耗变大填得太大调速过于保守性能响应迟钝。opp-shared表示同一簇内核心是否共享频率。如果是大小核架构部分核共享电压域必须标清楚否则 dvfs 会打架。opp-supported-hw可以按 SoC 版本屏蔽某些频点防止不同批次芯片跑了不该跑的频率。4.3 governor 怎么选从 ondemand 到 schedutilcpufreq governor 决定了“根据什么信号来调频”。早期 Linux 用ondemand按采样周期看 CPU 负载后来主推schedutil直接接入调度器用 PELTPer-Entity Load Tracking负载信号做连续调频。schedutil的好处是响应快、跟任务迁移贴合得好代价是跟调度器的耦合更深一旦调度器负载模型失真频率也会跟着“神经质”。在 RISC-V 平台上做低功耗产品我一般建议先跑schedutil EASEnergy Aware Scheduling。EAS 依赖energy-model设备树节点里面要有dynamic-power-coefficient这类系数。注意这个系数不是随便填的——它是频率、电压、电容相关的一个归一化值。填得不准EAS 的功耗对比就是错的调度器可能把一个任务放在“名义上省电、实际上费电”的小核上。4.4 实操心得cpufreq 调频“有气无力”时查什么如果发现 cpufreq 写入目标频率后实际频率纹丝不动我一般按下面的顺序排查看scaling_driver是不是cpufreq-dt如果不是驱动可能没 bind 成功。看设备树clocks配置assigned-clock-rates是否被定死很多平台 bootloader 会锁一个固定频率导致调频回调里 clk_set_rate 失败。看调压器cpu-supply对应的 regulator 是否配置了regulator-min-microvolt/regulator-max-microvolt范围不够宽OPP 表里电压写再高也调不上去。看时钟源是否为此 CPU 专用如果 CPU 和某个外设共用 PLL而外设驱动把 PLL 锁定了cpufreq 想改频率会被 clk 框架拒绝。看 soc 的 cpufreq 驱动是否支持该平台platform/soc里有没有注册 cpu_dev没有的话驱动都无法 probe。5. WFI、CPPC、cpufreq 三者的完整协同链路5.1 一个典型事件流任务变少时发生了什么简单串一下整个链路比如平板上的突发任务结束系统趋于空闲调度器的负载下降PELT 衰减schedutil的周期性回调基于负载均值算出更低的目标频率。cpufreq 核心把目标频率传给驱动驱动通过 OPP 查表、调压器调压、clk 框架切频完成 DVFS。所有任务睡眠或阻塞后调度器发现 runqueue 为空调用 cpuidle 的 governor 选择最合适的 idle state。CPU 执行 WFI或者更进一步通过 SBI HSM / SUSP 让固件参与断电核心进入低功耗状态。一个中断到来CPU 从 WFI/深度 idie 被唤醒进入中断处理随后调度器重新balancing负载、再次调频。这整个过程里cpuidle 和 cpufreq 看起来是两套独立框架但实际上它们在调度器里通过一个叫sugov的机制和idle_injection的场景交汇。比较重要的是如果 cpuidle 选了一个exit_latency很大的深度状态而 cpufreq 又想在唤醒后立刻提到最高频这个“加速”带来的收益可能会被“深度睡眠唤醒”的延迟吃掉。所以菜单/teo governor 里那些 target_residency 参数影响的不只是 idle 本身也会间接放大或缩小 cpufreq 调频的效果。5.2 从 SBI CPPC 到 cpufreq 的协同模式如果平台用了 SBI CPPCLinux cpufreq 就不直接操作 clk/regulator 低频切换了而是通过 ecall 提交“性能请求”。这时频率怎么变、电压怎么变由 M 模式固件决定。这种模式下有个有意思的点OS 并不知道固件最终选了哪个频率只能通过反馈寄存器读回来。从工程角度看SBI CPPC 模式至少有两个好处省掉内核里一大堆平台私有 quirks。不用为每颗芯片写一套调频驱动固件帮你适配了。节电决策更全局。固件能同时看到多个 hart 的请求做合并仲裁而 OS 每个 CPU 各自为政容易在共享电压域上互相冲突。代价是调试难度上升以前你可以拿示波器量clk_cpu确认频率变化现在频率由固件内部状态机决定你得先确认固件日志和 CPPC 反馈值。做这类平台时我建议在固件里保留一个“虚拟频率节点”的调试输出否则线上问题根本没法定界。5.3 实践建议三种平台形态的配置要点平台形态空闲策略推荐调频策略推荐注意点低功耗 MCU 类 RISC-VWFI 轻度时钟门控固定低频 ondemand不要上太复杂的 cpuidle governor以免 ROM 空间紧张嵌入式 Linux单核/双核2~3 级 idleschedutil OPP 表重点调 target_residency防止频繁进出深度 sleep多核应用处理器/服务器多级 idle CPU 热插拔schedutil / CPPC注意共享电压域的调频仲裁关注 EAS 功耗模型6. 问题排查与避坑清单6.1 高频问题速查表结合我和朋友团队的经验把 RISC-V 低功耗调试中的典型问题整理成了下面这张表现象可能原因排查手段scaling_cur_freq常年不变设备树 assigned-clock-rates 锁频 / clk 驱动不支持 set_rate检查 clk 树手动调用 clk_set_rate 测试执行 WFI 后整机功耗没降外设功耗占大头 / 中断风暴频繁唤醒看/proc/interrupts分 rail 测电流系统进入深度 idle 后无法唤醒遗漏local-timer-stop/ 固件唤醒源配置错误检查 cpu-idle-states 属性测 GPIO 唤醒schedutil 调频过于激进负载采样周期过短 / PELT 参数不适合调整schedutil的 rate_limit或改用 ondemand 对比SBI CPPC 调用返回错误固件扩展版本不匹配 / ecall 参数错误先用只读 Probe 验证再查 SBI 返回码电压域共享的两个核频率互相拖拽OPP 表没标opp-shared/ 固件仲裁缺失确认 DT 中的 cluster 关系必要时改固件策略EAS 总是把任务放到“费电”核dynamic-power-coefficient 填写失真用实际功耗测试反推系数6.2 上手必会的三件工具ftrace跟踪 cpuidle 的 state 切换和 cpufreq 的频率变化可以这样用cd /sys/kernel/debug/tracing echo 1 events/cpuidle/enable echo 1 events/cpufreq/enable echo 1 tracing_on cat trace_pipe观察每个 CPU 的 idle state 停留时间和调频事件基本能肉眼定位“频繁进深睡-唤醒-提频”的震荡问题。perf sched分析调度器是否因为 WFI 唤醒延迟而被拖慢。perf sched latency能看到 wakeup latency 的分布如果大部分唤醒事件都对应深度 idle 的 exit就说明target_residency配置保守了或 governor 选态不准。debugfs 里的 cpufreq 节点部分内核版本把cpufreq的 target 和实际频率都暴露在 debugfs 下配合 trace 一起看能区分“内核想调但没调成”与“压根不想调”。6.3 两个容易忽略的工程细节第一个是WFI 与 cache/TLB 的关系。在支持硬件页表漫步器的核上WFI 并不会自动 flush TLB但进入深度睡眠时如果 L2 掉电唤醒回来的第一件事就得做local_flush_tlb_all()。Linux 的 RISC-V 代码里idle 路径和热插拔路径有一些细微差别很多人移植时直接搬运结果从 deep idle 唤醒后访问非法地址。我的建议是在固件侧把“断电前 flush”和“上电后 flush”做成强制操作而不是依赖内核正确执行。第二个是中断唤醒时的中断控制器状态。RISC-V 的 PLIC平台级中断控制器和 CLINT核心本地中断控制器在深度睡眠恢复后有些实现需要重新设置SIESupervisor Interrupt Enable位。内核通常会处理但如果你发现唤醒后周期性 tick 丢失重点检查 CLINT 的定时器比较器是否在睡眠期间被清掉。这个问题的隐蔽性极高我曾在某一次 bring-up 里花了整整两天才定位到是 CLINT 寄存器掉电后复位导致定时器失效最后在固件唤醒路径里重新初始化 CLINT 解决。最后一个重要提醒不要直接在 S 模式下操作 M 模式的电源管理寄存器。经常有同学图省事在 Linux 驱动里直接 readl/writel 访问 PMU 寄存器。这在裸机或 RTOS 里没问题但一旦上了 LinuxS 模式的权限隔离限制可能导致访问被忽略或触发异常。规范做法是走 SBI 调用让 M 模式固件来处理。你会发现把规范当规范遵守反而省去了一堆 debug 时间。最后分享一点个人体会RISC-V 的功耗管理本质上是“把控制权层层上收”的过程。底层硬件越来越不愿意暴露物理寄存器而是希望软件通过 WFI、SBI CPPC 这样的抽象接口来表达意图。刚开始做的时候这套抽象会让人觉得“摸不着硬件、心里没底”可一旦把链路理顺你会发现它带来的可移植性和工程可维护性远超预期。我的经验是拿到一块新 RISC-V 平台先花半天时间把这三个东西搞清楚——WFI 在目标核上的实现方式、固件支持哪些 SBI 扩展、内核 cpufreq 走的是 DT 还是 CPPC——比闷头调参数有用得多。后续你在这个平台上做的每一个功耗优化都能精准落到某一个明确的层级上不会像无头苍蝇一样到处试。