3. CPU功耗管理:WFI/WFE指令详解、Tickless模式原理与实现、动态调频调压(DVFS)基础

各位同学,咱们今天聊聊CPU功耗管理。说实话,这块内容是我在低功耗项目里花时间最多的地方。很多朋友觉得功耗优化就是选个低功耗芯片,其实不然。CPU本身怎么跑、怎么停、怎么睡,这里面的门道深着呢。

我习惯把CPU功耗管理分成三个层次:指令级、系统级、电压频率级。咱们一个一个来拆解。

3.1 WFI与WFE指令:让CPU先歇会儿

先说说最基础的WFI和WFE。这两个指令,说白了就是让CPU进入一种「待机」状态。我刚开始做RT-Thread移植时,总觉得这俩指令差不多,后来踩了坑才发现区别大了去了。

3.1.1 WFI (Wait For Interrupt)

WFI执行后,CPU会立刻停止执行指令。它要等到一个中断来了,才会被唤醒。注意,是「任何」中断都能唤醒它。

我在项目中遇到过一个问题:某个外设的中断频繁触发,导致CPU刚睡下就被叫醒。你想想看,这比不睡还费电。因为进WFI和出WFI本身也有功耗开销。

我的经验:WFI适合用在「确定一段时间内没有任务」的场景。比如系统空闲时,让CPU睡一会儿,等下一个定时器中断或者按键中断来唤醒。

代码实现其实很简单:

/* RT-Thread中进入WFI的典型写法 */
void cpu_enter_wfi(void)
{
    __asm volatile("wfi");
}

3.1.2 WFE (Wait For Event)

WFE和WFI有点像,但有个关键区别:WFE可以被事件(Event)唤醒,不一定是中断。事件可以是另一个CPU核发出来的,也可以是外设产生的。

嗯,这里要注意:WFE有个「事件寄存器」的概念。如果这个寄存器已经被置位了,执行WFE会直接返回,不会真的睡下去。这个特性在某些场景下很有用,但也容易让人困惑。

特性 WFI WFE
唤醒源 任何中断 事件或中断
事件寄存器影响 有(可能跳过睡眠)
典型应用 系统空闲睡眠 多核同步、自旋锁等待
功耗 极低 极低
避坑指南:我曾经在一个多核项目里用WFE做同步,结果发现某个核一直不睡。查了半天,原来是事件寄存器被之前的操作意外置位了。所以用WFE前,最好先清一下事件寄存器。

3.2 Tickless模式:让系统时钟也歇歇

好,CPU能睡了。但还有个问题:RT-Thread的时钟节拍(Tick)会定期产生中断。就算CPU没事干,Tick中断还是会把它叫醒。这就像你刚睡着,闹钟每1ms响一次——这觉还怎么睡?

Tickless模式就是解决这个问题的。它的核心思想:没有任务要调度时,干脆把系统Tick停掉。

3.2.1 原理分析

说白了,Tickless模式就是动态调整下一次Tick的触发时间。系统空闲时,计算一下「下一个定时任务还有多久到期」,然后把硬件定时器设成那个时间点。中间这段时间,CPU可以安心睡大觉。

我习惯把Tickless比作「智能闹钟」。普通模式是每1ms响一次,Tickless模式是「到点再叫我」。

3.2.2 RT-Thread中的实现

RT-Thread的Tickless实现主要在 components/drivers/hwtimer/hwtimer.c 里。核心流程是这样的:

  1. 系统空闲线程运行时,检查是否有定时任务
  2. 如果没有,计算下一个定时任务的时间
  3. 把硬件定时器设成那个时间点
  4. 执行WFI/WFE让CPU睡眠
  5. 定时器到期或中断来了,唤醒CPU
  6. 补偿丢失的Tick计数

代码层面,关键函数是 rt_tickless_handler

/* Tickless模式下的Tick中断处理 */
void rt_tickless_handler(void)
{
    rt_tick_t tick_delta;
    
    /* 计算实际过去了多少个Tick */
    tick_delta = rt_hw_tickless_calc_delta();
    
    /* 补偿Tick计数 */
    rt_tick_increase_by(tick_delta);
    
    /* 检查是否有定时器到期 */
    rt_timer_check();
}
核心要点:Tickless模式省电的关键在于「睡眠时间要足够长」。如果每次只睡几十微秒就被唤醒,那还不如不睡。我一般建议睡眠时间至少大于100微秒才有实际意义。

3.2.3 实际效果

我在一个STM32L4的项目里做过对比测试。同样是空闲状态:

  • 普通模式:电流约1.2mA
  • Tickless模式:电流约0.3mA

省了将近4倍。当然,这个数字跟具体芯片和配置有关,但趋势是明显的。

注意:Tickless模式会影响系统的时间精度。因为Tick补偿是「事后」做的,如果中间有多个定时任务同时到期,处理顺序可能会有细微变化。对时间敏感的应用要小心。

3.3 动态调频调压(DVFS)基础

最后聊聊DVFS。WFI和Tickless解决的是「没事干时怎么办」,DVFS解决的是「有事干但不需要全力时怎么办」。

你想想看,CPU跑200MHz能完成的任务,跑100MHz也能完成,只是慢一点。那为什么不让它跑100MHz呢?电压也能降一点,功耗能省不少。

3.3.1 基本原理

DVFS的核心公式:功耗 ∝ 频率 × 电压²。注意电压是平方关系,所以降电压的效果比降频率更明显。

但电压不能随便降。频率越高,需要的电压也越高。这就是所谓的「频率-电压对应表」。我习惯叫它「甜蜜点表」——每个频率点对应一个最低稳定电压。

频率(MHz) 最低电压(V) 典型功耗(mW)
200 1.2 120
100 1.0 40
50 0.9 15
10 0.8 3

3.3.2 实现策略

RT-Thread本身没有内置DVFS框架,但我们可以结合CPU利用率来做。我个人常用的策略是:

  1. 每100ms采样一次CPU利用率
  2. 利用率低于30%时,降一档频率
  3. 利用率高于70%时,升一档频率
  4. 频率变化后,等待电压稳定再执行任务

代码示例:

/* 简单的DVFS策略实现 */
void dvfs_policy(void)
{
    int cpu_load = get_cpu_load();
    
    if (cpu_load < 30) {
        /* 负载低,降频 */
        set_cpu_freq(50);  /* 降到50MHz */
        set_cpu_voltage(0.9);
    } else if (cpu_load > 70) {
        /* 负载高,升频 */
        set_cpu_voltage(1.2);  /* 先升压 */
        delay_us(10);          /* 等待电压稳定 */
        set_cpu_freq(200);     /* 再升频 */
    }
}
我的建议:频率切换时,一定要「先升压再升频,先降频再降压」。顺序搞反了,芯片可能会死机。我曾经因为这个顺序问题,在产线上折腾了两天。

3.3.3 实际应用中的坑

DVFS不是万能的。我遇到过几个典型问题:

  • 切换延迟:频率和电压切换需要时间,频繁切换反而增加功耗
  • 外设兼容性:有些外设对CPU频率有要求,比如USB需要精确的48MHz时钟
  • 实时性影响:降频后任务执行时间变长,可能影响实时响应

嗯,这里要记住:DVFS是「锦上添花」的技术。先把WFI和Tickless做好,再考虑DVFS。基础打牢了,上层优化才有效果。

总结一下:CPU功耗管理三件套——WFI/WFE让CPU能睡,Tickless让系统时钟能停,DVFS让CPU跑得省。三者配合使用,才能达到最佳效果。我在实际项目中,这三层优化加起来,通常能把系统功耗降低60%-80%。

好了,这一章的内容就到这里。下一章咱们聊聊外设的功耗管理,那又是另一番天地了。