☰
RISC-V动态调频全链路解析:WFI、SBI CPPC与cpufreq协同
2026/10/7 7:41:12 网站建设 项目流程

刚拿到那块RISC-V开发板的时候,我盯着功耗表上纹丝不动的电流,第一反应是电源管理没生效。CPU明明已经进入了WFI空闲循环,Linux cpufreq的调度器也一直在往低档位调,可/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq读出来还是满频,CPU温度也降不下来。折腾了几天才把链路理清楚:RISC-V的动态调频不是一个开关能解决的,它需要WFI空闲态、SBI CPPC扩展与Linux cpufreq三层协同,哪一层断了都会出现“看着在省电、实际在跑满”的拧巴状态。

这篇文章想聊的就是这条链路本身。适合正在做RISC-V板级Bring Up的工程师、写SBI固件的朋友,以及想把CPU功耗调到能看的程度的内核玩家。我会先拆三个角色的分工,再分别讲透WFI、SBI CPPC和cpufreq,最后给出一套能落地的调试路径和踩坑记录。

1. 先分清三个角色:WFI、SBI CPPC、Linux cpufreq各管哪一段

1.1 一条从“省电”到“调频”的完整链路

很多人刚开始看RISC-V电源管理时,会把WFI、CPPC、cpufreq当成三个并列的“省电功能”,这是最大的误解。实际上它们处在完全不同的层级,覆盖的是两种完全不同的需求:一个是CPU无事可做时怎么停下来,一个是CPU有事要做时该跑多快。

WFI是RISC-V指令集层面的东西,属于指令和微架构的范畴。它解决的是“空闲”问题:当前线程没有可执行的任务时,内核把CPU停住,等着中断来唤醒。SBI CPPC是M-mode固件提供的服务接口,解决的是“性能等级谁来管”的问题:CPU该处在哪一个性能点,由S-mode的操作系统发出请求,但真正去写PLL、调电压的脏活累活,由M-mode的OpenSBI或者厂商固件完成。Linux cpufreq则处于最上层,解决的是“策略”问题:操作系统根据运行队列的负载、调度器的利用率、唤醒延迟要求,决定当前时刻应该把CPU推到多高。

三者串在一起就成了这样一条链路:调度器发现CPU闲着 → 内核进入cpuidle → 执行WFI停下来;调度器发现负载上来了 → schedutil算出目标频率 → cpufreq驱动调SBI CPPC → M-mode固件切频切压。每一步都依赖前一步的结果。

1.2 为什么非得分这么细,不能一层管到底

我见过不少板子把频率调节整个放进固件,Linux里就留一个固定频点。这样做不是不行,而是太笨:操作系统最清楚当前的负载特性和延迟要求,它知道这是一个需要立刻响应的中断上下文,还是一个可以慢慢算的后台任务,这些信息固件根本拿不到。反过来,硬件背后的电压表、PLL参数、温控策略,Linux内核也不想知道,每个SoC都不一样,内核里塞一堆板级寄存器操作代码会变成灾难。

RISC-V选择用SBI CPPC做折中:操作系统只表达“我想让这个CPU处在多高的性能等级”,固件负责把这个等级翻译成实际的频率和电压。这跟ARM生态里PSCI加CPPC的思路很像,只是RISC-V用SBI把这层抽象独立出来了。所以你在RISC-V平台上看到一个奇怪的现象:内核调频时不是直接写时钟控制器的寄存器,而是发起一次ecall陷入M-mode,让固件去干。第一次看到的人都会愣一下,但这正是RISC-V设计上刻意保留的灵活性和安全边界。

1.3 用表格快速建立整体认知

组件所在层级核心作用典型入口
WFI指令集与微架构停止取指,等待中断,降低空闲功耗wfi指令,由内核idle循环发出
SBI CPPCM-mode固件接口提供性能等级读写的服务,隐藏硬件细节ecall触发,SBI扩展号9
Linux cpufreqS-mode内核子系统根据负载策略决定目标频率并下发请求scaling_governor、target_index、fast_switch

这张表我建议你贴在手边。以后只要出现“频率没降下来”的问题,先问一句:是WFI没进入,还是策略没生效,还是SBI调用没成功?定位到层,问题就解决了一半。

2. WFI空闲态:别让CPU在空转时白白耗电

2.1 WFI指令干了什么:一条指令引发的暂停

WFI的全称是Wait For Interrupt,一条看起来不起眼的指令。CPU执行到它时会停止取指、停止发射指令,进入一种等待状态,直到有一个已使能的中断请求到来。注意“已使能”三个字,如果中断没有使能或者没有pending,CPU会一直等下去。

容易忽略的一个细节是:如果你的中断已经处于pending状态,WFI可以立即返回。这是RISC-V规范允许的行为,也是很多人踩坑的地方。我曾经见过一个同事在裸机代码里写了“while(1) { wfi(); }”来等中断,结果中断标志位没清干净,WFI执行一次就返回,然后循环又刷一遍,CPU根本没停下来,功耗自然一点没降。这个坑在Linux里不太容易出现,因为内核的idle流程会先把中断状态处理好,但如果你在写固件或者裸机程序,一定要记得这个特性。

从微架构角度看,执行WFI后核心时钟可以被门控,流水线、乱序执行部件、部分缓存预取逻辑都会停下来,动态功耗可以压掉很大一部分,静态漏电流则取决于工艺和当前电压域。不同SoC对WFI的实现深度差别很大,有的只停主流水线,有的会顺带把二级缓存的部分逻辑也降频,所以同样的代码在不同板子上测出来的省电效果不一样,这很正常。

2.2 Linux怎么把WFI包进cpuidle框架

Linux内核并不会在发现CPU空闲时直接不管三七二十一就执行wfi,它是通过cpuidle框架来管理的。cpuidle做什么事呢?它维护了一组idle状态,每个状态有不同的功耗、不同的唤醒延迟,然后由一个governor根据当前系统的下一个唤醒事件预测来选状态。

RISC-V Linux里最基础的idle状态通常就是WFI,对应代码实现大致长这样:

static void __cpuidle cpu_do_idle(void) { asm volatile("wfi" ::: "memory"); } void arch_cpu_idle(void) { cpu_do_idle(); }

这段逻辑放在arch/riscv的代码里,它做的事情就是让当前CPU进入WFI。看起来简单,但把它接进cpuidle框架之后,整条链路就复杂了:idle循环会先调用cpuidle governor的select方法,预测接下来可能有多长时间没有任务,再决定是进入WFI这种浅状态,还是进入需要更多步骤的深睡眠状态。预测对了,省电效果明显;预测错了,唤醒频繁反而费电。

2.3 WFI被谁唤醒:中断是唯一钥匙

WFI等的是中断,所以任何中断都能把它唤醒。对Linux系统来说,最常见的唤醒源有几个:本地定时器中断、IPI处理器间中断、外设中断比如网卡或者串口。

这里就引出一个看起来很反直觉的结论:一个跑着通用Linux系统的CPU,可能刚进入WFI几十微秒就被定时器中断拍醒,然后发现没有新任务,再次进入WFI,如此反复。这种情况下CPU并没有真正“睡饱”,功耗改善也不明显。所以现代内核都会把无用的周期tick停掉,也就是tickless idle,让CPU在没有任务时不被周期性的时钟中断骚扰。RISC-V平台同样依赖这个机制,你可以通过内核配置CONFIG_NO_HZ_IDLE来启用。

另外要理解,从WFI醒来的过程不是瞬时完成的。中断到达后,CPU需要重新取指、恢复流水线状态,这个延迟决定了WFI状态的退出延迟。如果板子上跑的是对延迟极度敏感的网络转发业务,建议通过cpuidle的延迟参数控制,别让内核动不动就往深睡眠状态钻。WFI虽然是浅状态,已经比完全不睡眠强很多,但仍不是零延迟恢复。

2.4 从WFI往下走:何时考虑SBI SUSP

WFI只是让CPU停下来,它不负责关掉整个电源域,也不负责处理内存的内容保持问题。如果你的目标是把功耗从“CPU核心空闲”进一步压到“整个CPU域断电”,那就需要走更深层次的睡眠,在RISC-V生态里对应的是SBI的Suspend扩展。

SBI SUSP扩展允许OS把CPU挂到固件定义的更深状态,比如关闭一级缓存、切断核心电源等。这些状态通常唤醒延迟更长,进入和退出的开销也更大。实际项目中我一般建议先验证WFI这层的功耗对不对,再考虑深睡,因为深睡涉及的电源序列、内存一致性、唤醒源配置都要多得多,排查问题的复杂度会指数上升。把WFI这层吃透,先把水平线立住,后面做suspend也就有了参照物。

3. SBI CPPC:M-mode固件提供的“调频服务总线”

3.1 为什么RISC-V需要类似ACPI CPPC的抽象

先聊一下CPPC的起源。CPPC全称是Collaborative Processor Performance Control,最早来自ACPI生态,核心思想是“协作”:操作系统根据负载制定性能策略,固件负责把策略转化为具体的电压、频率控制信号。也就是说,两边不是上下级关系,而是分工关系。

RISC-V早期做调频时,有一种很粗暴的路径:直接在设备树里描述时钟和频率表,让Linux用普通的clk API去改频率。这在简单的教学平台上能跑,但真正做产品时问题很多。不同SoC的频率控制寄存器布局不同,有些还需要在改频前后做时钟切换,如果每块板子都让内核适配一遍,工作量不可控。RISC-V引入SBI CPPC,本质上就是把“如何改这个SoC的频率”封装进M-mode固件,S-mode只通过一套统一接口表达意图。

我个人的看法是,CPPC模型特别适合RISC-V的生态现状:RISC-V的碎片化程度比ARM高,指令集一致但SoC实现百花齐放。没有一层固件抽象,Linux要面对无数种时钟拓扑;有了SBI CPPC,Linux只面对一套接口,剩下的交给各家的固件去拼。

3.2 SBI CPPC扩展包含哪些接口

RISC-V的SBI规范从v2.0开始加入CPPC扩展,它定义了一套让S-mode查询和设置CPU性能等级的服务。扩展里有几个核心功能,用表格列出来最清楚:

功能作用
PROBE探测固件是否支持CPPC扩展
GET_CAPS获取该CPU的性能能力范围,比如最低等级、最高等级、标称等级
GET_LEVEL读取当前生效的性能等级
SET_LEVEL请求将某个CPU设置到指定性能等级
GET/SET_REGISTER_BY_ID按寄存器ID读写CPPC协作寄存器

这里有一个关键概念:SBI CPPC操作的对象是“性能等级”,不是直接写频率。等级通常是一个整数,比如0到255,也可以映射到0到100的百分比,固件内部自己维护一张“等级-频率-电压”的对应表。Linux下发“把CPU0设到等级80”,固件可能就把PLL分频比调整到对应目标,同时把电压调节器设置到合适档位。内核不需要知道PLL寄存器地址、不需要知道有没有DVFS的支持,这在工程上非常干净。

接口调用是通过ecall完成的,下面是示意代码,真实工程中会封装在SBI相关库或内核驱动里:

static int sbi_cppc_set_level(u32 cpuid, u32 perf_level) { struct sbiret ret; ret = sbi_ecall(SBI_EXT_CPPC, SBI_CPPC_SET_LEVEL, cpuid, perf_level, 0, 0, 0, 0); if (ret.error) return sbi_err_map_linux_errno(ret.error); return 0; }

调用约定的细节在SBI规范里写得很清楚,第一号参数给目标CPU的hartid或者cpuid,第二号参数给期望的性能等级,返回值携带错误码。实际接入时要注意,有些平台把cpuid和hartid做了重映射,别直接把Linux的逻辑CPU号当硬件号传进去。

3.3 一个set调用的实现与底层约定

固件侧的CPPC实现通常是这样的:收到SET_LEVEL请求后,先检查等级是否在能力范围内,然后根据等级查表,得到目标频率和电压,最后操作时钟控制器和电源管理单元。这里最容易出的问题不在接口本身,而在等级表的校准。

很多固件初期从OpenBMC或者其他参考代码里抄一张表就上了,没有考虑温度漂移、不同bin的芯片体质差异。结果Linux请求等级80,固件照着表切到1.6GHz,但电压余量不足,运行一段时间后出现随机死机。这种问题非常难查,因为看起来像是应用程序bug。所以如果我在一个高主频RISC-V SoC上做调频验证,第一件事是先跑稳定性测试,用真正的计算密集型负载把每个等级跑一遍,确认电压、频率、温度三者匹配。

另外,SBI CPPC的set操作不像ACPI那样有一堆寄存器读写细节,它把后台复杂度都藏进固件了。好处是内核驱动简单,坏处是出问题时黑盒程度更高。调试时建议固件侧把每次调频请求的日志打出来,至少打印“收到CPU x的等级请求y,实际切到频率z”,这样内核侧看到的请求和固件侧看到的动作能一一对上。

4. Linux cpufreq与SBI CPPC如何协同工作

4.1 cpufreq三层结构:policy、driver、governor

cpufreq框架在Linux里存在了很多年,结构其实很稳定:policy描述一组共享频率控制关系的CPU,driver负责把频率请求落到硬件或固件,governor负责决定目标频率。RISC-V平台上这三层全部存在,只是driver这一层变成了SBI CPPC的调用者。

理解policy很重要。不少SoC上多个CPU核共享同一个时钟域,比如四核共用一颗PLL,这时候四个核必须跑同一个频率。cpufreq会把这四个核放进同一个policy,任何一核的负载升高,整个policy的频率都会被拉高。你可以通过/sys/devices/system/cpu/cpu0/cpufreq/affected_cpus看到哪些CPU被绑在一起。RISC-V平台同样存在这种共享时钟拓扑,所以看频率变化时不要只盯着一个核,要看整个policy的行为。

governor的选择直接决定调频风格。performance governor把频率钉在最高点,适合低延迟场景但是费电;powersave钉在最低点,适合能接受性能打折的场景;用户手动写频的时候用userspace;老牌的ondemand靠采样CPU占用率来调频;而schedutil是让频率跟着调度器的负载走,响应更平滑,也是现代内核里比较主流的默认选择。

4.2 schedutil如何算出“想跑的频率”

schedutil跟传统governor最大的区别是,它不靠周期性的采样来计算CPU占用率,而是直接看调度器维护的利用率。这个利用率是调度器以衰减统计的方式计算的负载,能反映近期任务的真实需求,而不是一个拍脑袋的百分比。

从算法上讲,目标频率可以简单理解成:当前利用率相对于最大容量的比例,乘以最大频率,再留一点余量。用公式描述就是:

target_freq = max_freq * util / capacity

假设一颗CPU最高跑2GHz,调度器算出来的util是满负载的60%,那schedutil的基准目标就是1.2GHz左右,之后再经过perf margin、频率取整这些步骤,变成最终的请求频率。RISC-V平台在做这个计算时没什么特殊之处,它和x86、ARM用的完全是同一套代码路径。

有一点值得留意:schedutil的“利用率”不是当前瞬时占用率,而是一个带时间惯性、经过一定衰减的负载估计,所以频率不会瞬间从最低跳到最高,也不会突然掉到最低。这样设计是为了减少调频抖动,代价是负载突增时频率爬升会有少量延迟。对延迟敏感的应用,可以把governor切成performance,或者利用cpufreq的频点约束来保底。

4.3 从调度负载到SBI SET_CPPC_LEVEL的完整链路

我画过很多次这条链路,每次排查调频问题时都要对着它走一遍。完整路径可以拆成这样:

  1. 调度实体被唤醒,schedutil的update_util回调触发;
  2. schedutil计算当前policy的目标频率;
  3. cpufreq驱动收到请求,判断走fast_switch还是慢路径;
  4. 驱动把目标频率换算成CPPC性能等级,调用SBI CPPC的SET_LEVEL;
  5. SBI固件收到ecall,查表得到PLL和电压参数;
  6. 固件操作时钟控制单元切换频率,可能还要先调整电压再切频;
  7. 驱动返回实际生效的频率,调度器更新相关状态。

最有趣的是第4步。cpufreq框架里面通常存的是频率值,比如1500000表示1.5GHz,但SBI CPPC要的是等级,比如80。这个从频率到等级的换算关系,内核需要从固件或者设备树拿到。常见的做法是固件在GET_CAPS里告诉内核“最高等级多少、标称等级多少、最低等级多少”,内核按比例换算,或者直接把频率表暴露出来。换算关系如果对不上,就会出现“内核想设1.5GHz,实际切到1.2GHz”的问题。

第6步也很容易出状况。有些平台的频率切换不是瞬时完成的,PLL重新锁定需要微秒级时间。如果固件没有处理好切换期间的时钟毛刺,轻则频率跳变不顺畅,重则导致总线协议错误。这个问题在Linux侧看不出来,必须在固件侧做处理:切换前选择安全的中间时钟源,等PLL稳定后再切换回新频点。

4.4 fast_switch还是慢路径?响应速度的取舍

cpufreq驱动有两种典型请求路径。一种是老的target_index路径,它在进程上下文执行,可以做比较重的操作,比如加锁、睡眠、调电压,适合慢速调节。另一种是fast_switch路径,它要求driver在原子上下文里无条件快速切换频率,不能睡眠,适合schedutil这种需要频繁更新频率的场景。

SBI CPPC非常适合实现fast_switch,因为SBI的ecall本身不会睡眠,它就是一个陷入和返回的同步过程。内核驱动实现fast_switch时,不做任何加锁,直接构造参数发起SBI调用,固件很快返回,整个过程开销只有一次特权陷入,实测下来响应速度比走慢路径好不少。这也是RISC-V平台调频能做到较好实时性的基础。

不过fast_switch对固件实现有要求:固件收到SET_LEVEL后不能做太慢的操作,更不能休眠。如果固件内部居然去等待什么互斥量或者做I2C通信,那fast_switch就会变成噩梦。所以做SBI固件时,CPPC的后端实现要尽量把PLL切换做得原子化,该关中断关中断,该缓存寄存器缓存寄存器。

5. 实操:从固件到内核让整条调频链跑起来

5.1 内核侧需要打开什么配置

让cpufreq驱动跑起来的第一步是内核配置。你需要确保打开了cpufreq核心框架、schedutil governor以及对应的CPPC驱动。相关配置项一般在CPU Frequency scaling菜单下,典型的有CONFIG_CPU_FREQ、CONFIG_CPU_FREQ_GOV_SCHEDUTIL,以及CPPC相关的驱动选项。RISC-V平台接入SBI CPPC时,还需要平台驱动在初始化时把手柄绑定到CPPC接口上。

配置的时候有一个常被忽略的坑:如果在内核启动参数里传了cpufreq=off,整个框架都不会初始化,频率自然不动。检查启动日志时看到cpufreq: cpufreq_online: CPU0: Running at new freq这类信息,才说明框架正常启动。如果你用的是设备树系统,还需要确认每个CPU节点都有频率能力描述,比如operating-points-v2或者等效的CPPC属性。没有描述的话,cpufreq驱动的probe会找不到policy而失败。

5.2 固件侧如何确认SBI CPPC可用

固件侧的确认比内核侧更基础。如果你是自编译OpenSBI,需要先确认版本是支持CPPC扩展的,并且你的平台代码实现了CPPC后端的probe函数。可以在启动阶段打印SBI扩展探测结果,OpenSBI一般会打印harthi、scanner等信息,如果看不到CPPC相关支持,多半是编译时没有包含对应后端。

更直接的做法是写一个小内核模块或者利用现有的SBI测试工具,调用一次PROBE和GET_CAPS,看返回错误码。如果PROBE返回“不支持”,要么固件版本太旧,要么平台后端没实现。如果GET_CAPS返回的等级范围是0到0,那说明固件虽然声明了接口,但能力表是空的,同样没法用。我用过一个早期版本的SBI实现,PROBE成功但GET_CAPS全是零,查了半天才发现是等级表没初始化,这类问题必须靠固件日志定位。

5.3 观察调频动作的几条命令

系统跑起来之后,我一般会用下面这几条命令快速确认调频链路是否正常工作:

# 查看当前governor cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 查看当前频率与最大最小频率 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq # 高频刷新查看频率变化趋势 watch -n 0.2 "cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq" # 用负载制造频率变化 stress-ng --cpu 1 --timeout 30 &

如果看scaling_cur_freq在负载起来后还是没有变化,先检查governor是不是performance。performance模式下频率恒定在最高点,不是bug,是策略如此。如果确定是schedutil但频率仍不动,再检查scaling_driver是什么,确认驱动被正确加载。

进一步追踪可以用内核的tracepoint,在tracefs里打开cpufreq相关事件:

echo 1 > /sys/kernel/tracing/events/power/cpu_frequency/enable cat /sys/kernel/tracing/trace

这样能看到内核实际发出的频率请求记录。但要注意,如果你走的是fast_switch路径,部分tracepoint可能不会记录得那么细致,需要配合固件日志。我调试的时候习惯同时开Linux的trace和OpenSBI的日志,两边对时间戳,能看到内核发出等级请求和固件切入实际频率之间的完整因果链。

5.4 一次“频率不变化”的完整排查

拿我最近调的一块RISC-V板子举例。现象是:负载跑起来了,scaling_cur_freq一直停在最低频,怎么折腾都不升。

我的排查顺序是这样的。第一步查governor,确认是schedutil,排除performance策略。第二步查scaling_driver,发现驱动确实加载了,CPPC接口也注册成功。第三步看dmesg,没有SBI调用报错。到这里内核侧看起来一切正常,但频率就是不动。

后来我在固件里加了日志,发现内核根本没有发出SET_LEVEL请求。问题不在SBI调用本身,而在于schedutil认为当前CPU不需要高频率。为什么?因为负载跑在了另一个核上,而这两个核在同一个policy里,按理说任何一核负载高都会拉高整个policy频率。仔细看才发现,这个平台把CPU0和CPU1放在同一个policy,但我的测试程序用taskset绑到了CPU2,而CPU2是另一个独立policy,我看的是CPU0的scaling_cur_freq,当然纹丝不动。

这提醒我一个很重要的原则:排查频率问题时不要只看单个CPU的文件,先确认affected_cpus的集合范围,再确认负载到底落在哪颗核上。很多时候不是链路断了,而是我们看错了观测点。

6. 常见问题速查与实操心得

6.1 常见问题速查表

现象可能原因排查方法
CPU进入idle但功耗下降不明显WFI频繁被唤醒;tick未停止;外设中断过多检查IRQ统计,确认CONFIG_NO_HZ_IDLE;用cat /proc/interrupts看唤醒源
scaling_cur_freq一直不变governor是performance;SBI不支持;驱动未加载查看scaling_governor与scaling_driver;确认SBI PROBE结果
切换频率时报错性能等级越界;CPU编号映射错误;固件等级表为空先调GET_CAPS确认范围;确认cpuid与hartid映射
调频后系统随机死机电压余量不足;PLL切换流程有问题逐个等级跑稳定性压力;固件打印实际切到的频率与电压
频率在高低之间剧烈抖动负载波动大;schedutil跟随过紧观察util变化趋势,考虑调整任务负载或使用其他governor
不同核频率表现不一致核在不同policy;共享时钟域判断错误查看affected_cpus,确认policy包含的核集合

这张表是我在RISC-V平台上调电源管理时的真实问题合集,每条都踩过。特别是第一条,很多人以为CPU执行了wfi就一定省电,实际上如果唤醒频率太高,CPU在“睡-醒”之间反复切换,功耗可能比忙等还难看。

6.2 那些不太容易发现的坑

第一个坑是CPU编号映射。SBI规范里的CPPC接口通常接收hartid,而Linux内部用的是逻辑CPU编号。虽然大多数平台上两者一致,但一旦启用了CPU hotplug或者某些异构调度,逻辑号和hartid就可能错位。传错编号后固件可能不会报错,而是把等级设置到了另外一个核上,表现就是“A核请求调频,B核的频率变了”。排查办法是让固件在每次SET_LEVEL日志里同时打印CPU编号和hartid,两边核对。

第二个坑是频率等级和真实频率的换算精度。我曾经遇到内核请求等级60,固件查表后切到1.4GHz,但内核下一次请求读到的实际频率还是这个值,看起来一切正常。后来想再加AOP的时才发现,等级到频率的映射不是线性的,等级越低,频率间隔越小,内核如果按线性比例估算频率,就会在低频段产生比较大的偏差。所以设计固件时建议把等级表做成显式数组暴露给内核,而不是让内核猜。

第三个坑是共享时钟域下的多核调度。四核共享频率时,任何一个核的高负载都会拉高其他核的频率,这意味着一个满载的后台任务可能让另一个延迟敏感核的频率也随之升高,短期看起来是“好事”,长期来看整颗CPU的功耗和发热都被拉高。RISC-V平台上如果追求精细的电源管理,硬件的做法是给不同核划分独立电压域或频率域,软件上则要搭配任务调度的cpu affinity设计,不然CPPC层面的协作再顺畅也救不了物理共享的限制。

6.3 给SoC和固件团队的三条建议

如果你恰好是做RISC-V SoC或者SBI固件的,我有三条从软件侧反馈过去的建议。

第一,性能等级表一定要做成可校准的。默认表只能作为参考,实际芯片的良率差异、温度特性都会影响频率-电压对应关系,最好能在固件里留出校准接口,让Firmware在启动时或者量产时填充正确的表项。

第二,CPPC后端实现要经得住频繁调用的压力测试。schedutil有可能在一秒内发起很多次SET_LEVEL请求,如果固件每次请求都去等待锁或者做慢速外设操作,操作系统看起来就是“调频特别卡”。固件侧可以加一个小的状态机,只在目标等级变化时才真正操作时钟树,等级没变就快速返回。

第三,不要把CPPC和SUSP混合调试。两种机制都涉及电源和时钟,它们互相影响。先单独验证CPPC在正常运行时能不能稳定调频,再验证WFI和深睡眠的功耗,最后才把它们组合在一起。我见过很多团队把两个问题混在一起查,最后耗了几周才发现是深睡眠状态没正确恢复时钟导致频率数据损坏。

这个内容后续其实还有很大的扩展空间。比如把SBI SUSP扩展加进来,让CPU从“WFI停住”进一步走到“整核断电”,以及和内核的CPU hotplug机制配合;再比如在RISC-V上用cpufreq的调频能力配合调度器的DSU或者EAS方案,把性能功耗平衡做到任务级别。不过这些都是后话,先把WFI、SBI CPPC和Linux cpufreq这条主线吃透,RISC-V平台的电源管理对你来说就不会再是一团迷雾了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询