ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RISC-V 电源管理与动态调频实战:从 WFI 到 SBI CPPC 再到 Linux cpufreq

RISC-V 电源管理与动态调频实战:从 WFI 到 SBI CPPC 再到 Linux cpufreq RISC-V 这两年在嵌入式、服务器、甚至移动端都开始铺开了但真到做产品的时候很多人会发现一个尴尬的事CPU 跑起来容易让它该省电的时候省电、该冲性能的时候冲性能却很难。尤其是从 ARM 平台转过来的兄弟习惯了cpufreq、cpuidle那一套现成的框架到了 RISC-V 上发现底层多了一层 SBI 固件中间还夹着个 CPPC 的抽象整个链路一下子变得不那么直观了。这篇东西就是把我自己在 RISC-V 平台上折腾电源管理和动态调频的整个过程梳理一遍从最底层的 WFI 空闲态到 SBI CPPC 接口再到 Linux 侧的cpufreq框架怎么协同工作尽量把每一层的职责和衔接点讲清楚。不管你是刚接触 RISC-V 电源域的驱动工程师还是已经在调频率但被各种频率设不上去空闲功耗下不来折磨过的老手应该都能从里面找到点有用的东西。1. 先把 RISC-V 电源管理的三层结构理清楚很多人一上来就去改cpufreq的 governor改了半天发现频率根本没动问题往往出在没搞清楚 RISC-V 电源管理是分层的。它不像传统的 x86 那样OS 直接操作 MSR 就能控制 P-StateRISC-V 因为要兼容各种不同的硬件实现把电源管理抽象成了三个层次每一层各管各的事跨层去操作基本是白费力气。1.1 从硬件到 OS 的职责划分最底下是硬件层也就是 CPU 核心本身支持的电源状态。RISC-V 特权规范里定义了 WFIWait For Interrupt指令这是最基础的空闲机制。核心执行 WFI 之后会进入低功耗状态直到有中断到来才被唤醒。注意WFI 只是停住时钟等中断它本身不涉及电压调节也不涉及频率变化这是很多人第一个误解的地方。中间是固件层也就是 SBISupervisor Binary Interface实现通常跑在 M-mode。SBI 规范里定义了 CPPCCollaborative Processor Performance Control相关的扩展把性能控制这件事从 OS 手里接管了一部分。为什么要有这一层因为频率和电压的调节往往需要和电源管理芯片、时钟控制器打交道这些操作在 M-mode 做更安全也更贴近硬件。最上面是OS 层也就是 Linux 内核里的cpufreq和cpuidle子系统。cpufreq负责频率策略cpuidle负责空闲状态选择。在 RISC-V 上这两个子系统最终都要通过 SBI 调用落到固件层去执行。提示如果你发现改了 governor 但频率纹丝不动先确认固件层有没有正确实现 CPPC 扩展很多问题根本不在内核侧。1.2 为什么 RISC-V 不直接让 OS 控制频率这个问题我被问过很多次。核心原因有两个一是安全隔离频率电压调节涉及硬件时序让 S-mode 的 OS 直接操作风险太大二是实现多样性不同厂商的时钟树、电源域设计差异巨大用一个统一的 SBI 接口把差异屏蔽掉OS 侧就不用为每款芯片写一套驱动。这带来的直接后果就是你在 OS 侧看到的频率未必是硬件真实跑的频率。OS 通过 CPPC 请求一个性能值固件层可能因为温度、功耗墙等原因给你打个折扣。所以调试的时候光看/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq是不够的最好配合固件侧的日志或者硬件性能计数器一起看。1.3 三层之间的数据流长什么样把数据流串起来看会更清楚。当系统负载变化时cpufreq的 governor比如schedutil根据调度器的负载信息算出一个目标频率然后调用cpufreq_driver的target或target_index回调。在 RISC-V 上这个回调最终会走 SBI CPPC 的sbi_cppc_write接口把期望的性能值写给固件。固件收到之后去操作实际的时钟和电源控制器完成频率切换。反方向固件会把当前实际的性能值通过sbi_cppc_read暴露出来OS 侧读取后更新scaling_cur_freq。空闲态则是另一条路径CPU 没事干的时候cpuidlegovernor 选择一个空闲状态最终执行 WFI 或者更深度的休眠指令。理解了这条链路后面调什么问题心里就有谱了——是 governor 没算出对的频率还是 SBI 调用失败了还是固件没真正执行一层层往下查就行。2. WFI 空闲态最基础也最容易被忽视的一环WFI 看起来简单一条指令的事但实际项目里因为 WFI 用不对导致的功耗问题一点都不少。我见过最离谱的一个案例设备待机功耗比预期高了将近一倍最后查出来是某个核心的空闲循环里根本没进 WFI一直在空转轮询。2.1 WFI 到底做了什么没做什么WFI 的全称是 Wait For Interrupt执行后核心会停止执行指令进入一种低功耗的等待状态直到本地中断控制器收到中断。这里有几个关键点必须说清楚WFI只停核心的时钟不关电源也不降电压。所以它的省电效果是有限的属于浅睡眠。WFI 的唤醒延迟非常短通常几个到几十个时钟周期适合频繁进出的场景。WFI 是特权指令在 M-mode 和 S-mode 都可以执行但行为可能因实现而异。很多人以为进了 WFI 就万事大吉其实如果中断来得太频繁核心频繁进出 WFI省下来的电可能还不够唤醒开销。这时候就需要更深的空闲状态也就是cpuidle里定义的那些 deeper state。2.2 Linux 里 WFI 是怎么被触发的在 Linux 的 RISC-V 实现里WFI 通常作为cpuidle的最浅状态state 0存在。当调度器发现某个 CPU 没有可运行任务时会调用cpuidle框架框架根据 governor 的选择进入某个空闲状态。对于 state 0最终执行的就是 WFI 指令。这里有个细节值得注意RISC-V 的cpuidle驱动里进入 WFI 之前会做一些准备工作比如检查是否有 pending 的中断、关闭一些不必要的外设时钟等。这些准备工作如果做得不好会导致该醒的时候醒不过来或者醒了之后状态不对的问题。/* 典型的 RISC-V cpuidle 进入 WFI 的简化逻辑 */ static int riscv_cpuidle_enter(struct cpuidle_device *dev, struct cpuidle_driver *drv, int index) { /* 检查是否有 pending 中断避免错过唤醒 */ if (!riscv_has_pending_irq()) __asm__ __volatile__(wfi); return index; }上面这段是简化后的逻辑实际内核代码要复杂得多但核心思想就是这样先确认没有待处理的中断再进 WFI否则可能出现中断已经来了但核心还是进了 WFI导致响应延迟。2.3 实测中 WFI 相关的几个坑第一个坑是中断亲和性配置不当。如果所有中断都绑到 CPU0那 CPU0 会频繁被唤醒根本进不了 WFI而其他核心又闲着。解决办法是把中断分散到不同核心或者用irqbalance之类的工具动态调整。第二个坑是定时器精度和 WFI 的配合。RISC-V 的定时器中断通过 SBI 的 timer 扩展设置如果精度太高比如 1ms 一次那核心每 1ms 就被唤醒一次WFI 的省电效果大打折扣。实际项目里要根据需求调整 tick 频率能用NOHZ_IDLE就用上。第三个坑是WFI 和调试的冲突。调试的时候如果核心进了 WFIJTAG 可能连不上得先想办法唤醒。这个在开发阶段很烦人建议调试时临时禁用深度空闲状态。注意WFI 的省电效果和唤醒延迟是一对矛盾。进得越深省电越多但唤醒越慢。选择空闲状态时一定要结合业务的实际延迟要求不能只看功耗数字。3. SBI CPPCRISC-V 性能控制的中间桥梁CPPC 这个概念最早来自 ACPI本意是让 OS 和硬件协同决定性能等级。RISC-V 把它搬到了 SBI 里做成了一个标准接口。理解 CPPC 是理解 RISC-V 动态调频的关键因为它是 OS 和硬件之间唯一的性能协商通道。3.1 CPPC 的核心概念性能值而非频率CPPC 最反直觉的一点是它传递的不是频率而是性能值performance value。这个性能值是一个抽象的数字固件层负责把它映射到实际的频率和电压。为什么要这么设计因为不同核心在不同电压下的频率表现不一样直接传频率会让 OS 承担太多硬件细节。CPPC 里几个关键寄存器在 SBI 里对应不同的调用概念含义典型用途Desired PerformanceOS 期望的性能值governor 写入目标Actual Performance硬件实际达到的性能值OS 读取当前状态Minimum Performance允许的最低性能限制下限Maximum Performance允许的最高性能限制上限Energy Performance Preference能效偏好在性能和功耗间权衡这个设计的好处是OS 只需要关心我要多快不用管这对应多少 MHz、多少 mV。坏处是调试的时候多了一层映射你得知道固件是怎么把性能值翻译成频率的。3.2 SBI CPPC 的调用流程在 Linux 侧SBI CPPC 通过sbi_cppc_read和sbi_cppc_write两个接口暴露给上层。cpufreq驱动在初始化时会探测固件是否支持 CPPC 扩展如果支持就注册一个基于 CPPC 的cpufreq_driver。/* 简化的 CPPC cpufreq 驱动注册逻辑 */ static int cppc_cpufreq_probe(struct platform_device *pdev) { /* 探测 SBI CPPC 扩展是否可用 */ if (!sbi_cppc_available()) return -ENODEV; /* 读取硬件支持的性能范围 */ cppc_get_perf_caps(cpu, caps); /* 注册 cpufreq 驱动 */ policy-min caps.lowest_perf; policy-max caps.highest_perf; policy-cpuinfo.min_freq caps.lowest_freq; policy-cpuinfo.max_freq caps.highest_freq; return cpufreq_register_driver(cppc_cpufreq_driver); }这段逻辑里cppc_get_perf_caps会通过 SBI 调用去问固件你这核心最低能跑多少性能、最高能跑多少性能固件返回之后OS 才知道自己的调节范围。如果这一步返回的范围不对后面 governor 怎么调都是错的。3.3 固件层实现 CPPC 时最容易出的问题固件层的实现质量直接决定了整个电源管理能不能用。我踩过的几个典型问题性能值映射不线性。有些固件实现里性能值 0-100 映射到频率不是线性的可能 0-50 对应低频段50-100 对应高频段中间有个跳变。这会导致 governor 在中间区域调节时频率跳来跳去功耗和性能都不稳定。Actual Performance 更新不及时。OS 写完 Desired Performance 后会读 Actual Performance 来确认。如果固件更新这个值有延迟OS 会以为频率没切成功反复重试造成额外的开销。EPP 支持不完整。Energy Performance Preference 是 CPPC 里很重要的一个概念它告诉硬件我更在乎性能还是更在乎省电。有些固件只实现了读写接口但内部根本不理会这个值导致 OS 设了也没用。提示调试 CPPC 问题时建议先在固件侧加日志把每次读写的性能值和对应的实际频率打出来这样能快速定位是映射问题还是执行问题。4. Linux cpufreq 在 RISC-V 上的适配细节cpufreq是 Linux 里管频率的老框架了在 x86 和 ARM 上都很成熟。但到了 RISC-V因为中间多了 SBI 这一层很多默认行为需要调整。这一章就讲讲实际适配中会遇到的具体问题。4.1 governor 的选择schedutil 还是 ondemandcpufreq支持多种 governorRISC-V 上最常用的是schedutil和ondemand。两者的核心区别在于频率决策的依据ondemand周期性采样 CPU 利用率根据阈值决定升频还是降频。实现简单但响应有延迟且容易在负载波动时频繁切换。schedutil直接和调度器挂钩利用调度器的负载跟踪信息做决策。响应更快理论上更精准但对调度器的依赖更强。在 RISC-V 上我个人更推荐schedutil因为它和 CPPC 的配合更自然。schedutil算出的目标频率可以直接映射到 CPPC 的性能值中间少了一层转换。不过要注意schedutil需要调度器的frequency invariance支持如果固件没有正确报告性能缩放信息schedutil的决策会失准。# 查看当前 governor cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 切换为 schedutil echo schedutil /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor4.2 频率表从哪来ACPI 还是设备树在 x86 上频率表通常来自 ACPI 的_PSS对象。在 RISC-V 上情况复杂一些如果平台支持 ACPI可以从 ACPI 拿如果不支持就得从设备树Device Tree里读或者干脆通过 CPPC 动态查询。设备树里的频率表长这样cpu0: cpu0 { compatible riscv; /* 操作点定义 */ operating-points-v2 cpu0_opp_table; }; cpu0_opp_table: opp-table { compatible operating-points-v2; opp-shared; opp-1000000000 { opp-hz /bits/ 64 1000000000; opp-microvolt 800000; }; opp-1500000000 { opp-hz /bits/ 64 1500000000; opp-microvolt 900000; }; };这里定义了两个操作点1GHz0.8V 和 1.5GHz0.9V。内核启动时会解析这些信息构建频率表。如果设备树里没写或者写错了cpufreq就找不到可用的频率只能报错退出。4.3 一个真实的调频失败排查过程说个我实际遇到的案例。平台是某款 RISC-V 四核处理器设备树里频率表配好了governor 也设成了schedutil但跑压力测试时发现频率始终卡在最低档上不去。排查过程是这样的第一步确认 governor 是否在工作。读scaling_governor是schedutil没问题。读scaling_cur_freq一直是 1GHz说明确实没升频。第二步看 governor 的决策日志。打开schedutil的调试信息发现它算出的目标频率其实是 1.5GHz但写下去没生效。第三步怀疑是 CPPC 写入失败。在 SBI 调用处加日志发现sbi_cppc_write返回了错误码。查 SBI 规范这个错误码表示不支持该性能值。第四步去问固件同事发现固件里性能值的有效范围配错了最高只到 80而 OS 按 0-100 去写超过 80 就被拒绝。最后改固件的性能值范围问题解决。这个案例说明调频问题一定要从 OS 往固件一层层查光在内核侧折腾是找不到根因的。4.4 多核场景下的频率一致性RISC-V 多核平台上如果多个核心共享一个时钟域也就是所谓的opp-shared那频率必须保持一致。这时候cpufreq的policy会把多个核心绑在一起任何一个核心请求升频其他核心也跟着升。这种设计简化了硬件但带来了一个问题如果只有一个核心忙其他核心闲着整个时钟域都得升到高频功耗上不划算。解决办法是尽量把任务集中到少数核心上让其他核心能进深度空闲状态。Linux 的调度器里有EASEnergy Aware Scheduling可以帮忙做这个决策但需要固件提供准确的能耗模型。5. 空闲态与调频的协同别让它们各干各的cpuidle和cpufreq是两个独立的子系统但它们的目标是一致的在满足性能需求的前提下尽量省电。如果两者各干各的很容易出现频率降下来了但核心还在空转或者核心进了深度睡眠但频率还挂在高档这种浪费。5.1 为什么需要协同举个简单的例子。假设一个核心当前负载很低schedutil决定把频率降到最低。但如果这个核心接下来要进深度空闲状态那降频其实没多大意义因为核心马上就停了。反过来如果核心刚从深度空闲唤醒cpufreq还停留在低频那唤醒后的性能会受影响。理想的协同是在进入空闲状态前评估是否需要调整频率在退出空闲状态时快速恢复到合适的频率。Linux 内核里有一些机制在做这件事比如cpuidle的governor会参考cpufreq的状态schedutil也会考虑空闲时间。5.2 实际配置中的取舍在实际项目里协同策略要根据业务特点来定。我一般会分两种情况延迟敏感型业务比如实时控制优先保证唤醒后的性能空闲状态选浅一点频率不要降太狠。宁可多耗点电也不能让响应变慢。吞吐型业务比如批量计算可以大胆用深度空闲和低频因为任务之间没有严格的延迟要求省电优先。配置上可以通过cpuidle的latency参数来控制。每个空闲状态都有一个退出延迟exit latencygovernor 会根据当前的延迟容忍度来选择。如果业务对延迟敏感就把容忍度调低这样 governor 就不会选那些退出延迟大的深度状态。# 查看各空闲状态的退出延迟 cat /sys/devices/system/cpu/cpu0/cpuidle/state*/latency # 查看各状态的进入次数判断实际使用情况 cat /sys/devices/system/cpu/cpu0/cpuidle/state*/usage5.3 一个功耗优化实例之前做过一个边缘计算盒子待机功耗一直下不来。用powertop一看发现 CPU 有 30% 的时间停留在cpuidle的 state 0也就是 WFI没进更深的 state 1。查下来原因是 state 1 的退出延迟被设成了 100us而系统里有个定时器每 50us 触发一次导致 governor 认为进 state 1 不划算一直停在 state 0。解决办法有两个一是把那个高频定时器的周期拉长二是优化 state 1 的退出延迟。最后两边都做了点工作定时器改成 200usstate 1 的延迟优化到 60us待机功耗降了差不多 25%。这个案例说明空闲态的优化不能只看 CPU 侧系统里的定时器、外设中断都会影响。做功耗优化时一定要用工具看清楚是谁在频繁唤醒 CPU。6. 调试工具与实战排查思路前面讲了原理和配置这一章专门讲怎么排查问题。RISC-V 电源管理的调试比 x86 麻烦因为多了一层固件很多信息拿不到。但掌握几个关键工具和思路大部分问题还是能定位的。6.1 必看的几个 sysfs 节点Linux 把cpufreq和cpuidle的状态都暴露在 sysfs 里这是最直接的观测手段路径含义关注点/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq当前频率是否随负载变化/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor当前 governor是否符合预期/sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state各频率停留时间判断频率分布/sys/devices/system/cpu/cpu0/cpuidle/state*/usage各空闲态进入次数判断空闲态使用/sys/devices/system/cpu/cpu0/cpuidle/state*/time各空闲态停留时间判断省电效果这几个节点配合起来看基本能判断出系统是在正常工作还是出了问题。比如time_in_state显示 90% 时间在最低频但业务明明很忙那肯定是调频没生效。6.2 用 ftrace 跟踪调频调用链sysfs 只能看结果要看过程就得用 ftrace。cpufreq和cpuidle都有对应的 tracepoint可以跟踪每次频率切换和空闲进入的详细信息。# 开启 cpufreq 相关 tracepoint echo 1 /sys/kernel/debug/tracing/events/power/cpufreq_frequency_table_target/enable echo 1 /sys/kernel/debug/tracing/events/power/cpufreq_set_target/enable # 查看跟踪结果 cat /sys/kernel/debug/tracing/trace通过 trace你能看到 governor 算出的目标频率、实际设置的频率、以及中间的耗时。如果发现 governor 算出的频率和实际设置的不一致那问题就在驱动或固件层。6.3 固件侧信息的获取固件侧的信息最难拿因为 SBI 调用是黑盒。但有几个办法可以间接观测一是利用 SBI 的调试扩展如果固件支持有些实现会提供性能计数器的读取接口能看到实际的频率和电压。二是在固件里加日志通过串口或者共享内存输出。这个需要固件同事配合但最直接有效。三是用硬件性能计数器比如 RISC-V 的mcycle和minstret通过计算单位时间内的指令数来反推实际频率。这个方法不需要固件支持但精度有限。# 通过 perf 读取硬件计数器需要内核支持 perf stat -e cycles,instructions sleep 16.4 排查思路总结把排查思路整理成一个流程遇到问题可以按这个顺序走确认现象是频率上不去还是功耗下不来还是响应变慢先明确问题。看 sysfs检查 governor、当前频率、空闲态使用情况判断问题出在哪个子系统。开 ftrace跟踪具体的调用链看 governor 的决策和驱动的执行是否一致。查固件如果 OS 侧一切正常但硬件没反应问题就在固件需要固件侧配合。验证修复改完之后用同样的方法复测确认问题真的解决了而不是被其他因素掩盖了。这个流程看起来简单但实际排查时最容易犯的错就是跳步。比如一上来就怀疑固件结果发现是 governor 配错了。按顺序走能少走很多弯路。7. 几个容易被忽略的配置细节最后聊几个细节问题都是我在实际项目里踩过的单独拎出来说说。7.1 设备树里的 clock-latency 别乱填设备树里有个clock-latency属性表示频率切换的延迟。这个值如果填得太大governor 会认为切换频率代价高倾向于不切填得太小又可能导致频繁切换。一般建议填实际测量值的 1.5 到 2 倍留点余量。cpu0: cpu0 { clock-latency 50000; /* 50us根据实测调整 */ };7.2 注意 CPU 热插拔对频率表的影响RISC-V 平台支持 CPU 热插拔但热插拔时频率表可能会变化。如果cpufreq驱动没有正确处理这个事件可能出现核心上线后频率表不对的问题。内核里有cpufreq_online和cpufreq_offline回调驱动需要实现这些回调来更新状态。7.3 温度对调频的影响很多 RISC-V 平台有温度保护机制温度过高时会自动降频。这个降频是固件层做的OS 侧可能看不到。如果你发现频率莫名其妙被限制先查查温度。thermal_zone节点能看到当前温度cat /sys/class/thermal/thermal_zone0/temp如果温度确实偏高那降频是正常的保护行为需要从散热或者功耗墙配置上解决而不是去改cpufreq。7.4 别忽视 idle 状态下的外设功耗CPU 进了深度空闲但如果外设还在全速运行整体功耗还是下不来。做功耗优化时要把 CPU、内存、外设当成一个整体来看。比如网卡在没有流量时应该进低功耗模式屏幕背光应该调暗这些和 CPU 调频是同等重要的。我个人的经验是先优化外设再优化 CPU。因为外设的功耗往往是大头而且优化起来相对简单。等外设都处理好了再回头抠 CPU 的那点功耗效果会更明显。整个 RISC-V 电源管理和动态调频的链路说到底就是三层之间的配合WFI 管最基础的空闲SBI CPPC 管性能协商Linux cpufreq 管策略决策。任何一层出问题整个系统都不对劲。调试的时候别急着改代码先把每一层的状态看清楚顺着数据流一层层查大部分问题都能定位。这套东西我在几个项目里反复用过虽然每次硬件平台不一样但思路是通用的。
返回列表