驱动篇:inux 电源管理的系统架构和驱动(二)
发布日期:2021-06-29 11:35:16 浏览次数:2 分类:技术文章

本文共 7546 字,大约阅读时间需要 25 分钟。

驱动篇:inux 电源管理的系统架构和驱动(二)

CPUFreq 的策略

SoC CPUFreq 驱动只是设定了 CPU 的频率参数,以及提供了设置频率的途径,但是它并不会管 CPU 自身究竟应该运行在哪种频率上。究竟频率依据的是哪种标准,进行何种变化,而这些完全由 CPUFreq 的策略( policy )决定,这些策略如表所示。
在这里插入图片描述在 Android 系统中,则增加了 1 个交互策略,该策略适合于对延迟敏感的 UI 交互任务,当有 UI 交互任务的时候,该策略会更加激进并及时地调整 CPU 频率。
总而言之,系统的状态以及 CPUFreq 的策略共同决定了 CPU 频率跳变的目标, CPUFreq 核心层并将目标频率传递给底层具体 SoC 的 CPUFreq驱动,该驱动修改硬件,完成频率的变换
在这里插入图片描述用户空间一般可通过 /sys/devices/system/cpu/cpux/cpufreq 节点来设置 CPUFreq 。譬如,我们要设置 CPUFreq 到 700MHz ,采用 userspace 策略,
则运行如下命令:

# echo userspace > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor# echo 700000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed

CPUFreq 的性能测试和调优

Linux 3.1 以后的内核已经将 cpupower-utils 工具集放入内核的 tools/power/cpupower 目录中,该工具集当中的cpufreq-bench 工具可以帮助工程师分析采用 CPUFreq 后对系统性能的影响。

cpufreq-bench 工具的工作原理是模拟系统运行时候的 “ 空闲 → 忙 → 空闲 → 忙 ” 场景,从而触发系统的动态频率变化,然后在使用 ondemand 、 conservative 、 interactive 等策略的情况下,计算在做与 performance 高频模式下同样的运算完成任务的时间比例。

交叉编译该工具后,可放入目标电路板文件系统的 /usr/sbin/ 等目录下,运行该工具:

# cpufreq-bench -l 50000 -s 100000 -x 50000 -y 100000 -g ondemand -r 5 -n 5 -v

会输出一系列的结果,我们提取其中的 Round n 这样的行,它表明了 -g ondemand 选项中设定的 ondemand 策略相对于 performance 策略的性能比例,假设值为:

Round 1 - 39.74%Round 2 - 36.35%Round 3 - 47.91%Round 4 - 54.22%Round 5 - 58.64%

这显然不太理想,我们在同样的平台下采用 Android 的交互策略,得到新的测试结果:

Round 1 - 72.95%Round 2 - 87.20%Round 3 - 91.21%Round 4 - 94.10%Round 5 - 94.93%

一般的目标是在采用 CPUFreq 动态调整频率和电压后,性能应该为 performance 这个高性能策略下的 90% 左右,这样才比较理想

CPUFreq 通知
CPUFreq 子系统会发出通知的情况有两种: CPUFreq 的策略变化或者 CPU 运行频率变化。
在策略变化的过程中,会发送 3 次通知:

·CPUFREQ_ADJUST :所有注册的 notifier 可以根据硬件或者温度的情况去修改范围(即 policy->min 和 policy-> max );

·CPUFREQ_INCOMPATIBLE :除非前面的策略设定可能会导致硬件出错,否则被注册的 notifier 不能改变范围等
设定;
·CPUFREQ_NOTIFY :所有注册的 notifier 都会被告知新的策略已经被设置。

在频率变化的过程中,会发送 2 次通知:

·CPUFREQ_PRECHANGE :准备进行频率变更;
·CPUFREQ_POSTCHANGE :已经完成频率变更。

notifier 中的第 3 个参数是一个 cpufreq_freqs 的结构体,包含 cpu ( CPU 号)、 old (过去的频率)和 new (现在的频率)这 3 个成员。发送 CPUFREQ_PRECHANGE 和 CPUFREQ_POSTCHANGE 的代码如下:

srcu_notifier_call_chain(&cpufreq_transition_notifier_list,CPUFREQ_PRECHANGE, freqs);srcu_notifier_call_chain(&cpufreq_transition_notifier_list,CPUFREQ_POSTCHANGE, freqs);

如果某模块关心 CPUFREQ_PRECHANGE 或 CPUFREQ_POSTCHANGE 事件,可简单地使用 Linux notifier 机制监控。譬如, drivers/video/sa1100fb.c 在 CPU 频率变化过程中需对自身硬件进行相关设置,因此它注册了 notifier 并在CPUFREQ_PRECHANGE 和 CPUFREQ_POSTCHANGE 情况下分别进行不同的处理

CPUFreq notifier 案例:

fbi->freq_transition.notifier_call = sa1100fb_freq_transition;cpufreq_register_notifier(&fbi->freq_transition, CPUFREQ_TRANSITION_NOTIFIER);...sa1100fb_freq_transition(structnotifier_block *nb, unsigned long val,void *data){
struct sa1100fb_info *fbi = TO_INF(nb, freq_transition); struct cpufreq_freqs *f = data; u_int pcd;switch (val) {
case CPUFREQ_PRECHANGE:set_ctrlr_state(fbi, C_DISABLE_CLKCHANGE);break;case CPUFREQ_POSTCHANGE: pcd = get_pcd(fbi->fb.var.pixclock, f->new); fbi->reg_lccr3 = (fbi->reg_lccr3& ~0xff) | LCCR3_PixClkDiv(pcd); set_ctrlr_state(fbi, C_ENABLE_CLKCHANGE); break; } return 0;}

此外,如果在系统挂起 / 恢复的过程中 CPU 频率会发生变化,则 CPUFreq 子系统也会发出CPUFREQ_SUSPENDCHANGE 和 CPUFREQ_RESUMECHANGE 这两个通知。

值得一提的是,除了 CPU 以外,一些非 CPU 设备也支持多个操作频率和电压,存在多个 OPP 。 Linux 3.2 之后的内核也支持针对这种非 CPU 设备的 DVFS ,该套子系统为 Devfreq 。与 CPUFreq 存在一个 drivers/cpufreq 目录相似,在内核中也存在一个 drivers/devfreq 的目录。

CPUIdle 驱动

目前的 ARM SoC 大多支持几个不同的 Idle 级别, CPUIdle 驱动子系统存在的目的就是对这些 Idle 状态进行管理,并根据系统的运行情况进入不同的 Idle 级别。具体 SoC 的底层 CPUIdle 驱动实现则提供一个类似于 CPUFreq 驱动频率表的 Idle 级别表,并实现各种不同 Idle 状态的进入和退出流程。

对于 Intel 系列笔记本计算机而言,支持 ACPI ( Advanced Configuration and Power Interface ,高级配置和电源接口),一般有 4 个不同的 C 状态(其中 C0 为操作状态, C1 是 Halt 状态, C2 是 Stop-Clock 状态, C3 是 Sleep 状态),如表所示。

在这里插入图片描述而对于 ARM 而言,各个 SoC 对于 Idle 的实现方法差异比较大,最简单的 Idle 级别莫过于将 CPU 核置于 WFI (等待中断发生)状态,因此在默认情况下,若 SoC 未实现自身的芯片级 CPUIdle 驱动,则会进入 cpu_do_idle (),对于 ARM V7 而言,其实现位于 arch/arm/mm/proc-v7.S 中:

ENTRY(cpu_v7_do_idle)dsb           @ WFI may enter a low-power modewfimovpc, lrENDPROC(cpu_v7_do_idle)

与 CPUFreq 类似, CPUIdle 的核心层提供了如下 API 以用于注册一个 cpuidle_driver 的实例:

int cpuidle_register_driver(struct cpuidle_driver *drv);

并提供了如下 API 来注册一个 cpuidle_device :

int cpuidle_register_device(struct cpuidle_device *dev);

CPUIdle 驱动必须针对每个 CPU 注册相应的 cpuidle_device ,这意味着对于多核 CPU 而言,需要针对每个 CPU 注册一次。cpuidle_register_driver ()接受 1 个 cpuidle_driver 结构体的指针参数,该结构体是 CPUIdle 驱动的主体,其定义如下:

cpuidle_driver 结构体

struct cpuidle_driver {
const char *name; struct module *owner; int refcnt; /* used by the cpuidle framework to setup the broadcast timer */ unsigned int bctimer:1; /* states array must be ordered in decreasing power consumption */ struct cpuidle_state states[CPUIDLE_STATE_MAX]; int state_count; int safe_state_index; /* the driver handles the cpus in cpumask */ struct cpumask *cpumask;};该结构体的关键成员是 1 个 cpuidle_state 的表,其实该表就是用于存储各种不同 Idle 级别的信息

cpuidle_state 结构体:

struct cpuidle_state {
char name[CPUIDLE_NAME_LEN]; char desc[CPUIDLE_DESC_LEN]; unsigned int flags; unsigned int exit_latency; /* in US */ int power_usage; /* in mW */ unsigned int target_residency; /* in US */ bool disabled; /* disabled on all CPUs */ int (*enter) (struct cpuidle_device *dev, struct cpuidle_driver *drv, int index); int (*enter_dead) (struct cpuidle_device *dev, int index); /* * CPUs execute ->enter_s2idle with the local tick or entire timekeeping * suspended, so it must not re-enable interrupts at any point (even * temporarily) or attempt to change states of clock event devices. */ void (*enter_s2idle) (struct cpuidle_device *dev, struct cpuidle_driver *drv, int index);};name 和 desc 是该 Idle 状态的名称和描述, exit_latency 是退出该 Idle 状态需要的延迟, enter ()是进入该 Idle 状态的实现方法。

忽略细节,一个具体的 SoC 的 CPUIdle 驱动实例可见于 arch/arm/mach-ux500/cpuidle.c (最新的内核已经将代码转移到了 drivers/cpuidle/cpuidle-ux500.c 中),它有两个 Idle 级别,即 WFI 和 ApIdle ,其具体实现框架如代码清单所示:

ux500CPUIdle 驱动案例

staticatomic_t master = ATOMIC_INIT(0);static DEFINE_SPINLOCK(master_lock);static DEFINE_PER_CPU(struct cpuidle_device, ux500_cpuidle_device);static inline int ux500_enter_idle(struct cpuidle_device *dev,struct cpuidle_driver *drv, int index){
...}staticstruct cpuidle_driver ux500_idle_driver = {
.name = "ux500_idle", .owner = THIS_MODULE, .en_core_tk_irqen = 1, .states = {
ARM_CPUIDLE_WFI_STATE, {
.enter= ux500_enter_idle, .exit_latency = 70, .target_residency = 260, .flags= CPUIDLE_FLAG_TIME_VALID, .name = "ApIdle", .desc = "ARM Retention",}, }, .safe_state_index = 0, .state_count = 2,};/* * For each cpu, setup the broadcast timer because we will * need to migrate the timers for the states >= ApIdle. */static void ux500_setup_broadcast_timer(void *arg){
intcpu = smp_processor_id(); clockevents_notify(CLOCK_EVT_NOTIFY_BROADCAST_ON, &cpu);}int __init ux500_idle_init(void){
... ret = cpuidle_register_driver(&ux500_idle_driver); ... for_each_online_cpu(cpu) {
device = &per_cpu(ux500_cpuidle_device, cpu); device->cpu = cpu; ret = cpuidle_register_device(device); ... } ...}device_initcall(ux500_idle_init);

与 CPUFreq 类似,在 CPUIdle 子系统中也有对应的 governor 来抉择何时进入何种 Idle 级别的策略,这些 governor 包括CPU_IDLE_GOV_LADDER 、 CPU_IDLE_GOV_MENU 。 LADDER 在进入和退出 Idle 级别的时候是步进的,它以过去的 Idle 时间作为参考,而 MENU 总是根据预期的空闲时间直接进入目标 Idle 级别。前者适用于没有采用动态时间节拍的系统(即没有选择 NO_HZ 的系统),不依赖于 NO_HZ 配置选项,而后者依赖于内核的 NO_HZ 选项。

图中演示了 LADDER 步进从 C0 进入 C3 ,而 MENU 则可能直接从 C0 跳入 C3 :

在这里插入图片描述CPUIdle 子系统还通过 sys 向 userspace 导出了一些节点:

· 一类是针对整个系统的 /sys/devices/system/cpu/cpuidle ,通过其中的 current_driver 、current_governor 、 available_governors 等节点可以获取或设置 CPUIdle 的驱动信息以及 governor 。
· 一类是针对每个 CPU 的 /sys/devices/system/cpu/cpux/cpuidle ,通过子节点暴露各个在线的 CPU 中每个不同 Idle 级别的 name 、 desc 、 power 、latency 等信息。
综合以上的各个要素,可以给出 Linux CPUIdle 子系统的总体架构:
在这里插入图片描述

转载地址:https://blog.csdn.net/zytgg123456/article/details/110568436 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:驱动篇:inux 电源管理的系统架构和驱动(三)
下一篇:驱动篇:inux 电源管理的系统架构和驱动(一)

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年04月19日 12时36分28秒