4、RTOS在低功耗中的应用:FreeRTOS Tickless模式、任务调度与休眠、定时器管理

好,咱们进入正题。TWS耳机的电源管理,说白了就是跟电池容量做斗争。你想想看,那么小的腔体,塞个几十毫安时的电池,还要撑好几个小时,这活儿不精细不行。

RTOS在这里扮演什么角色?它不只是调度任务,更是整个功耗管理的“大脑”。我这些年调过的TWS方案,从CSR到BES再到中科蓝讯,底层都离不开RTOS的功耗控制机制。今天咱们就聊聊FreeRTOS里最核心的三个武器:Tickless模式、任务调度与休眠、定时器管理。

4.1 Tickless模式:让系统真正“停下来”

先问个问题:为什么RTOS默认会一直“滴答滴答”地跑?

传统RTOS有一个固定的系统心跳(SysTick),比如每1ms中断一次。这个中断用来做任务切换、时间片轮转。但问题来了——如果所有任务都在等某个事件,比如等用户按一下按键,那这1ms一次的中断就纯属浪费电。

我早期做一款运动耳机时就踩过这个坑。待机电流标称2mA,实测却到了3.5mA。查了半天,发现SysTick中断占了很大一部分功耗。后来切到Tickless模式,待机电流直接降到了1.2mA。

Tickless模式的核心思想:没有任务需要调度时,干脆停掉系统滴答。等下一个任务该醒来的时候,再通过硬件定时器唤醒。

关键配置:在FreeRTOSConfig.h中设置

#define configUSE_TICKLESS_IDLE         1
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2  // 单位:tick

第二个参数的意思是:如果预计空闲时间超过2个tick,就进入Tickless休眠。

这里有个细节要注意——进入Tickless后,系统滴答计数器会“丢失”一些tick。FreeRTOS通过记录休眠时长,在唤醒后补偿回来。嗯,这个补偿逻辑我建议你仔细看看源码,很多坑都出在这里。

4.2 任务调度与休眠:谁该睡,谁该醒

TWS耳机里任务很多:蓝牙协议栈、音频编解码、按键检测、LED控制、电量计算……但真正需要一直跑的任务,其实没几个。

我个人习惯把任务分成三类:

任务类型 典型例子 调度策略
实时性任务 音频I2S传输、蓝牙基带 高优先级,禁止休眠
周期性任务 电量检测、LED闪烁 中等优先级,用定时器触发
事件型任务 按键处理、配对请求 低优先级,阻塞等待

你想想看,按键检测任务如果每10ms轮询一次,那CPU大部分时间都在做无用功。正确的做法是:让任务阻塞在信号量或队列上,等外部中断来唤醒。

我的经验:在TWS项目中,我会把按键检测做成“中断+任务”的配合模式。按键按下时触发GPIO中断,中断服务函数里只发一个信号量,真正的消抖和逻辑处理交给任务去做。这样CPU在空闲时能深度休眠,按键响应也快。

还有一个容易被忽略的点:任务栈大小。栈太大,内存浪费;栈太小,溢出崩溃。我见过一个同事把每个任务栈都设成512字节,结果蓝牙协议栈任务频繁溢出,系统不断重启,功耗反而更高。建议用uxTaskGetStackHighWaterMark()函数实际测量一下,再调整。

4.3 定时器管理:精准与功耗的平衡

FreeRTOS提供软件定时器,底层依赖硬件定时器。在低功耗场景下,定时器的管理直接决定了系统能睡多深。

这里有个矛盾:软件定时器需要系统滴答来计数,但Tickless模式下滴答又停了。怎么解?

FreeRTOS的做法是:在进入Tickless前,计算下一个软件定时器何时到期,然后把这个时间设给一个硬件定时器。等硬件定时器溢出,再唤醒系统。

我曾经踩过的坑:有一次我把软件定时器的周期设成了10ms,但Tickless模式要求空闲时间至少2个tick(假设1 tick = 1ms)。结果系统刚想睡,10ms定时器就到期了,根本睡不下去。CPU一直在“准备休眠→被唤醒→准备休眠→被唤醒”的循环里,功耗比不休眠还高。

解决方案:把短周期定时器合并,或者用硬件定时器直接处理短周期任务。

在TWS耳机里,我常用的定时器策略是这样的:

  • 音频相关的定时:直接用硬件定时器,精度要求高,不能被打断
  • 用户交互的定时:比如长按3秒进入配对模式,用软件定时器,允许一定误差
  • 低功耗周期的定时:比如每30秒上报一次电量,用Tickless模式下的唤醒定时器

说白了,定时器的选择就是“精度”和“功耗”的取舍。精度要求高的,别省电;精度要求低的,大胆睡。

4.4 实战建议:TWS耳机中的典型配置

最后,我分享一个我在量产项目中用过的FreeRTOS低功耗配置,供你参考:

// FreeRTOSConfig.h 关键配置
#define configUSE_TICKLESS_IDLE         1
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 5  // 5个tick以上才休眠
#define configTICK_RATE_HZ              1000      // 1ms一个tick
#define configMINIMAL_STACK_SIZE        128       // 最小任务栈

// 主循环中的休眠调用
void vApplicationIdleHook(void)
{
    // 进入深度休眠前,关闭不用的外设时钟
    // 比如关闭LED、关闭I2C、关闭SPI
    // 只保留蓝牙唤醒和按键唤醒
    EnterDeepSleep();
}

注意那个configEXPECTED_IDLE_TIME_BEFORE_SLEEP,我设成5而不是2。为什么?因为TWS耳机里经常有短时突发任务,比如蓝牙每隔几ms就要处理一个包。如果阈值设得太小,系统频繁进出休眠,反而浪费唤醒时的启动时间。

嗯,今天就聊到这儿。下一节咱们会深入讲讲外设的电源管理,比如怎么在休眠时保持蓝牙连接不断开。那个更有意思。