一、基本概念
DVFS(Dynamic Voltage and Frequency Scaling)动态电压频率调节,是一种实时的电压和频率调节技术。在 CMOS 电路中功率消耗主要可以分为动态功率消耗和静态功率消耗,公式如下:
其中 C 代表负载电容的容值,V 是工作电压,α 是当前频率下的翻转率,f为工作频率,I_dq 代表静态电流。公式的前部分代表的是动态功率消耗,后部分则代表的是静态功率消耗。从公式中可以看出,想要降低动态功率消耗可以从C、V、α、f着手,对于软件来讲常用的调节方式只涉及到V、f 两个因素。
二、代码解析
1、数据结构
系统中存在 7 大总线:ARM_CLK、AXI_CLK、DSP_CLK、APP_CLK、MPH_CLK、GE_CLK、VS_CLK,DVFS 系统的所有工作都围绕这几大总线以及核心电压CORE_VOLTAGE展开,代码里面用到的数据结构有:DVFS_REQ、DVFS_CMD、DVFS_NODE、DVFS_CDB,定义如下:
typedef struct { const char* module; /* 发出调节请求的模块名称 */ unsigned char flag; /* 调节频率的标记: 有最低、最高、锁定三种 */ DVFS_CLK_e clk; /* 申请调节的时钟总线,有7大总线 */ int freq; /* 期望频率值 */ } DVFS_REQ; typedef struct { DVFS_CBF cb; /* 回调函数 */ int arg; /* 参数 */ int num; /* 本次请求的数目 */ DVFS_REQ reqs[MAX_REQ_NUM]; /* 请求存放的数组,最多8个请求 */ } DVFS_CMD; typedef struct { struct list_head head; /* 链表节点 */ DVFS_REQ req; /* 当前请求 */ } DVFS_NODE; typedef struct { // cmd queue managment DVFS_CMD q_array[CMD_Q_LENGTH]; uint8_t q_idx_r; uint8_t q_idx_w; uint8_t q_idx_p; spinlock_t q_lock; // worker thread management wait_queue_head_t q_wait; /* 等待队列 */ struct task_struct* task; /* 任务 */ // list of request for each clock DVFS_NODE* req_lists[DVFS_CLK_ALL]; /* 指向每条总线的请求链表 */ // clock/voltage setttings int curr_freq_idx[DVFS_CLK_ALL]; /* 当前频率的索引值 */ int trgt_freq_idx[DVFS_CLK_ALL]; /* 目标频率的索引值 */ struct clk* hw_clock[DVFS_CLK_ALL]; /* 时钟设置 */ uint8_t curr_vol_idx; /* 当前电压索引值 */ uint8_t trgt_vol_idx; /* 目标电压索引值 */ unsigned long vol_jiffies; /* 电压调节时间记录,用于记录调节间隔 */ } DVFS_CDB;其中 DVFS_REQ 用于描述一个调节请求,存放了相关信息; DVFS_CMD 用于多个请求构造的一次调节操作; DVFS_NODE 作为链表节点,每一条总线的调节请求都由各自的链表维护; DVFS_CDB则存放了 DVFS 系统的大部分信息,系统中只存在一个全局的 DVFS_CDB 变量。
2、函数接口
当要调用 DVFS 系统时,通常的调用方法如下:
DVFS_CREATE_REQ(reqs[0], "camera", AXI_CLK, axi_freq, DVFS_REQ_MIN); /* 创建一个调节请求 */ init_completion(&cam_complete); dvfs_send_reqs(reqs, 1, suspend_cb, 0); /* 发送调节请求 */ wait_for_completion(&cam_complete); /* 等待调节完成 */通过 DVFS_CREATE_REQ 宏定义创建一个调节请求,宏定义原型如下:
#define DVFS_CREATE_REQ(req, MODULE, CLK, FREQ, FLAG) { \ req.module = MODULE;\ req.clk = CLK;\ req.flag = FLAG;\ req.freq = FREQ;\ }宏主要用于构造一个 DVFS_REQ 结构,然后通过 dvfs_send_reqs 构造 DVFS_CMD 完成调节并执行回调函数 suspend_cb。
3、代码解析
在 DVFS 系统的初始化过程中,做的工作有如下几个:a、初始化 cdb->hw_clock;b、创建每条总线的链表;c、创建 proc 文件节点;d、创建等待队列以及内核线程专门用于 DVFS。当初始化完成之后就启动内核线程 dvfs_thread_func,在这里面会等待 dvfs_send_reqs 发出唤醒事件,然后进行电压和频率的调节并执行回调函数。下面我们从发送调节请求开始解析,函数 dvfs_send_reqs实现如下:
int dvfs_send_reqs(DVFS_REQ *reqs, int num, DVFS_CBF cb, int arg) { int ret; DVFS_CMD *cmd; unsigned long irq_flags; if ((ret = sanity_check(reqs, num))) { /* 检查参数有效性 */ return ret; } if (num == 1 && reqs[0].clk == APP_CLK) { /* APP_CLK 可以快速处理 */ mutex_lock(&app_lock); /* 互斥体 */ ret = dvfs_process_req(&dvfs_cdb, reqs); /* 处理调节请求,如果需要调节返回非 0 值 */ if (ret) { dvfs_cdb.curr_freq_idx[APP_CLK] = dvfs_cdb.trgt_freq_idx[APP_CLK]; clk_set_rate(dvfs_cdb.hw_clock[APP_CLK], dvfs_get_rate(APP_CLK, dvfs_cdb.curr_freq_idx[APP_CLK])); } mutex_unlock(&app_lock); return 0; } spin_lock_irqsave(&dvfs_cdb.q_lock, irq_flags); /* 数据更新 */ cmd = &dvfs_cdb.q_array[dvfs_cdb.q_idx_w & CMD_Q_MASK]; /* 构造 cmd */ cmd->cb = cb; cmd->num = num; cmd->arg = arg; memcpy(cmd->reqs, reqs, num * sizeof(DVFS_REQ)); smp_mb(); dvfs_cdb.q_idx_w++; spin_unlock_irqrestore(&dvfs_cdb.q_lock, irq_flags); wake_up_all(&dvfs_cdb.q_wait); /* 唤醒等待队列 */ return 0; } EXPORT_SYMBOL(dvfs_send_reqs);函数首先进行参数有效性验证,然后判断是否只调节 APP_CLK,因为 APP_CLK 用于外设不影响系统运行,所以可以直接调节。如果不只调节 APP_CLK 接下来将会构造 DVFS_CMD,然后唤醒内核线程 dvfs_thread_func,实现如下:
int dvfs_thread_func(void *data) { DVFS_CDB *cdb = (DVFS_CDB*)data; DVFS_CMD *cmd; do { /* 等待唤醒 */ if(wait_event_interruptible(cdb->q_wait, (cdb->q_idx_w != cdb->q_idx_r)) != 0) continue; /* 判断是否需要调节电压和频率 */ if (cal_target_freq(cdb)) { dvfs_reset_clk(cdb); /* 开始调节 */ } /* 触发回调函数 */ if (cmd->cb != NULL) { cmd->cb(cmd->arg); } } while (1); }当线程被唤醒后会根据函数 cal_target_freq 判断当前是否需要进行电压和频率的调节,cal_target_freq 里面首先根据传递的 CMD 更新链表,然后根据链表记录去判断是否需要调节频率,如果需要调节就会根据目标频率值通过查表( 电压 - 频率表)查找系统支持的最合适的频率值。接着就来到了 DVFS 系统最重要的函数 dvfs_reset_clk,电压和频率调节都将在这个函数里面完成:
void dvfs_reset_clk(DVFS_CDB *cdb) { int idx; int vol; reqs_updated: cal_target_vol_idx(cdb); /* 首先计算电压是否需要调节 */ /* 需要升高电压 */ if (cdb->trgt_vol_idx > cdb->curr_vol_idx && arm_regulator) { for (idx = cdb->curr_vol_idx + 1; idx <= cdb->trgt_vol_idx; ++idx) { unsigned int interval_ms = jiffies_to_msecs(jiffies - cdb->vol_jiffies); if (interval_ms < 100) { /* 距离上次调节小于 100ms 则需等待 */ msleep(100 - interval_ms); if (cal_target_freq(cdb)) goto reqs_updated; } if (idx + 1 <= cdb->trgt_vol_idx) ++idx; vol = voltage_table[idx].voltage; regulator_set_voltage(arm_regulator, vol, vol); cdb->curr_vol_idx = idx; cdb->vol_jiffies = jiffies; dvfs_dbg("DVFS increase arm voltage to %d uV\n", vol); } } do_reset_clk(cdb); /* 调节频率 */ if (cal_target_freq(cdb)) /* 查看当前是否有新的调节请求 */ goto reqs_updated; /* 需要降低电压 */ if (cdb->trgt_vol_idx < cdb->curr_vol_idx && arm_regulator) { for (idx = cdb->curr_vol_idx - 1; idx >= cdb->trgt_vol_idx; --idx) { unsigned int interval_ms = jiffies_to_msecs(jiffies - cdb->vol_jiffies); if (interval_ms < 100) { msleep(100 - interval_ms); if (cal_target_freq(cdb)) goto reqs_updated; } vol = voltage_table[idx].voltage; regulator_set_voltage(arm_regulator, vol, vol); cdb->curr_vol_idx = idx; cdb->vol_jiffies = jiffies; dvfs_dbg("DVFS decrease arm voltage to %d uV\n", vol); } } }
函数里面先进行电压调节满足“升频率时先升电压,降频率时后降电压”的准则并且两次电压的调节需要间隔 100ms以上,然后通过 do_reset_clk 进行频率调节,实现如下:
static void do_reset_clk(DVFS_CDB *cdb) { int clk, idx; for (clk = 0; clk < DVFS_CLK_ALL; clk++) { /* 依次调节频率 */ int curr = cdb->curr_freq_idx[clk]; int trgt = cdb->trgt_freq_idx[clk]; if (ARM_CLK == clk && curr != trgt) { /* 调整 arm 频率,需要经过 cpufreq 子系统 */ unsigned int i; struct cpufreq_freqs freqs = { .flags = 0, .old = frequency_table[clk][curr].freq / 1000, .new = frequency_table[clk][trgt].freq / 1000, }; for_each_online_cpu(i) { freqs.cpu = i; /* 函数向挂载在这个 cpu 上所有的驱动发出一个信号,驱动接收到这个信号则调用相应的处理函数 */ cpufreq_notify_transition(&freqs, CPUFREQ_PRECHANGE); } } if (curr < trgt) { /* 如果当前频率比目标频率低 */ for (idx = curr+1; idx <= trgt; idx++) { /* 逐级调节频率可以保证系统的稳定性 */ if (idx == trgt) { clk_set_rate(cdb->hw_clock[clk], frequency_table[clk][idx].freq); } else if (frequency_table[clk][idx].need_delay) { clk_set_rate(cdb->hw_clock[clk], frequency_table[clk][idx].freq); msleep(1); } } } else if (curr > trgt) { /* 如果当前比目标频率高 */ for (idx = curr-1; idx >= trgt; idx--) { /* 逐级调节频率可以保证系统的稳定性 */ if (idx == trgt) { clk_set_rate(cdb->hw_clock[clk], frequency_table[clk][idx].freq); } else if (frequency_table[clk][idx].need_delay) { clk_set_rate(cdb->hw_clock[clk], frequency_table[clk][idx].freq); msleep(1); } } } cdb->curr_freq_idx[clk] = trgt; /* 更新索引记录 */ if (ARM_CLK == clk && curr != trgt) { unsigned int i; struct cpufreq_freqs freqs = { .flags = 0, .old = frequency_table[clk][curr].freq / 1000, .new = frequency_table[clk][trgt].freq / 1000, }; if (freqs.new >= 806000) { freqs.new = 1200000; } for_each_online_cpu(i) { freqs.cpu = i; /* 通知函数在 cpu 频率的调节过程中调用两次,驱动处理函数通过 cpufreq_register_notifier 注册 */ cpufreq_notify_transition(&freqs, CPUFREQ_POSTCHANGE); } } } }
对于频率需要满足逐级调节的准则以保证系统的稳定性,ARM_CLK 的调节需要经过 cpufreq 子系统。至此,电压频率调节完成,线程将会执行调节请求的回调函数然后进入睡眠。
* 调试注意:
1、处理器需要可靠的电压 - 频率对应关系,这个需要较长时间的测试
2、升频率时先升电压,降频率时后降电压
3、逐级调节电压和频率有助于提升系统稳定性
4、每次调节电压和频率后,尤其是升电压之后等待一定时间再升频率
5、通常是在各模块的驱动程序里面进行电压和频率的调节,用户程序通过对各模块驱动的调用以达到调节的目的
6、某些情况下为了获得更好的用户体验还可以对一些用户程序进行特殊处理让其直接将电压频率调节到最优值
7、驱动程序里面升高电压频率时需要同步等待操作完成才能继续后续工作,因为一些硬件模块的工作对电压频率比较敏感
8、用户程序为了获得更好的体验而升高电压频率,则不必同步等待其完成,因为用户程序对时序基本无要求
* 调试心得:
对于所有的软件bug,不能怀疑程序被处理器错误执行了,这个是可以保证的,唯一需要怀疑的就是程序的逻辑是否正确,一切bug都可以看做是没有完善的错误处理引起的,这是指导思想。(个人经验)