来源 | 最后一个bug
做过低功耗产品的同学,应该都比较了解,低功耗不仅仅是硬件一方面的事情,软件有很多方面都可以做优化。
比如,本文讲述的,在RTOS运行阶段的一些优化策略:空闲时把主频降下来也是非常不错办法,并且这个办法几乎没什么代价。
1
动态功耗
动态功耗这笔账,其实主要是关注主频和空闲时间。
现在的 CPU 都带空闲指令,ARM 这边就是 WFI(Wait for interrupt)和 WFE(Wait for event)。这俩干的事一样,把 CPU 里某一部分的时钟门控掉,那部分的动态功耗在空闲时直接归零。
大家应该在日常的编码中有看到过,比如典型的写法就这样:
/******************************************
* Fuction: 系统空闲任务——经典写法
* Author : (公众号:最后一个bug)
******************************************/
void OS_Idle(void)
{
for (;;) { /* 死循环,只有中断能把你捞出去 */
__WFI(); /* 等中断,中断来了接着往下走 */
}
}
大部分系统里 OS_Idle() 就是这个死循环,唯一的出口是中断。中断一进来,CPU 醒过来,接着往下跑。
2
别用while空等硬件
想省电,自然是让 CPU 少跑点代码,多留点空闲。但这得靠好的算法、好的编译器、好的运行时库。
还有个更直接的:别等硬件。
/******************************************
* Fuction: 死等驱动器回包——反例
* Author : (公众号:最后一个bug)
******************************************/
void Drv_WaitFrame_Bad(void)
{
while (DRV_IS_BUSY()) {
; /* CPU 在这儿满速空转,一个周期都没省下 */
}
}
/******************************************
* Fuction: 让出 CPU 的等法
* Author : (公众号:最后一个bug)
******************************************/
void Drv_WaitFrame_Good(void)
{
while (DRV_IS_BUSY()) {
OS_TaskDelay_us(30U); /* 这 30us 交给别的任务,或者干脆进 idle */
}
}
在操作系统中说实在的也是不推荐这种死循环等待的,CPU 满速空转,不但一个周期都没省下来,反而负载率较高。至少至少得弄个带延时的周期轮询。
微秒级的延时在常规 RTOS 里往往还是个忙等 —— tick 一般是毫秒级的,vTaskDelay(1) 最短就是 1ms。embOS-Ultra 这一类把 tick 做到微秒的,才是真把 CPU 让出去。
3
空闲降频
其实进了 idle,动态功耗并没有归零。
因为有些部件还在被时钟推着走,最典型的就是中断控制器。既然还在被时钟推着,空闲时的动态功耗就仍然和主频成正比。

之前做个一个测试,空闲时的功耗差不多是运行时的一半。然后把主频速度降低后发现功耗更低。
类似的只要把 OS_Idle() 改一改就行了:
/******************************************
* Fuction: 空闲任务——先降频再等中断
* Author : (公众号:最后一个bug)
******************************************/
void OS_Idle(void)
{
for (;;) {
CPU_SetHclkDiv(4U); /* 空闲了,HCLK 先砍到 1/16 */
__WFI(); /* 睡到中断来;中断入口会把频率拨回满速 */
}
}
记得需要在唤醒的位置先把频率拨回满速,比如在中断入口:
/******************************************
* Fuction: 中断入口——先把频率拨回满速
* Author : (公众号:最后一个bug)
******************************************/
void TIM2_IRQHandler(void)
{
CPU_SetHclkDiv(0U); /* 第一件事:回满速,别让中断慢吞吞地跑 */
/* ... 后面才是真正的中断处理 ... */
}
通常这里的处理只影响分频器,不会去碰时钟源或者 PLL,不然就得重新锁定等待,还比较麻烦。

经常听到有人说“我的系统平时就有没有空闲的时候”,其实设计得好的系统,CPU 大部分时间都在空闲。
想想你现在面对的这台电脑,绝大部分时间是在等你。嵌入式系统一个道理,CPU 快到离谱,只要程序写得像样,真正干活的时间也就是很短的一小段。
5
定时器的坑
空闲降频的代价还是有的,那就是每个中断都得把 CPU 切回高速,如果是那些需要快速响应的项目尤为关注。
降频了响应会慢一点。但你把主频砍到 1/16,一般来说对于消费类项目也不至于出什么乱子。如果太慢的话真出乱子了就别降那么狠,降到 1/2,照样能省蛮多。
不过似乎现在ARM 把 Cortex 的系统定时器跟 CPU 频率耦合在一起了。
SysTick 的时钟源只能二选一:内核时钟 HCLK,或者 HCLK/8。不管选哪个,它都跟着主频跑。主频降一半,tick 就从 1ms 变 2ms,拿它当全局时基的系统,时间直接乱套。
/******************************************
* Fuction: 换分频后把系统 tick 重新校准
* Author : (公众号:最后一个bug)
******************************************/
#define SYST_BASE 0xE000E010UL
#define SYST_LOAD (*(volatile uint32_t *)(SYST_BASE + 0x04UL))
#define SYST_VAL (*(volatile uint32_t *)(SYST_BASE + 0x08UL))
void SysTick_Reconfig(uint32_t hclk_hz, uint32_t tick_hz)
{
uint32_t reload = (hclk_hz / tick_hz) - 1U;
if (reload > 0x00FFFFFFUL) { /* LOAD 只有 24 位,塞不下就别写 */
return;
}
SYST_LOAD = reload;
SYST_VAL = 0U; /* 清了计数,下一拍就是整周期 */
}
168MHz 要 1ms 的 tick,reload 算出来是 167999。降频到 1/16 之后要是不重新校准,同一个 reload 就变成 16ms 一拍 —— 差了整整 16 倍。
所以要么你改频率的时候顺手把系统定时器重新编程,或要么就干脆换一个不同时钟源的定时器。
还有就是分频器一动,挂在 AHB 和 APB 上的外设频率全跟着变 —— UART 的 BRR、CAN 的位定时、SPI 和 I2C 的分频,都拿 PCLK 算出来的。降频期间要是有外设正在收数据,尤其是那个负责把你从 idle 里叫醒的,先确认它不受影响,不然唤醒不了就麻烦了~
------------ END ------------