低功耗Tickless
低功耗Tickless
MCU低功耗
这些是固定好了的,自己不能改动
| 模式 | 功耗等级 | CPU状态 | 外设状态 | 唤醒方式 | 特点 |
|---|---|---|---|---|---|
| Sleep(睡眠) | 低 | CPU停止,外设继续工作 | 所有时钟继续运行(可选) | 任意中断 | 快速进入/退出,数据保持 |
| Stop(停止) | 更低 | CPU和大部分时钟停止 | SRAM/寄存器保持 | 外部中断、RTC、USART唤醒等 | 功耗与恢复速度平衡 |
| Standby(待机) | 最低 | CPU完全断电 | SRAM、寄存器丢失(除备份区) | WKUP脚、RTC闹钟、复位 | 等效“关机”,唤醒后重新启动程序 |
sleep模式
工作机制:
- CPU 停止执行指令(即暂停运行)。
- 外设仍然工作(比如 UART、ADC、定时器)。
- 所有寄存器、RAM 内容保持不变。
唤醒来源:
- 任意中断(EXTI、SysTick、USART、DMA…)
- 唤醒后从中断返回继续执行。
典型用途:
- 稍微省电,同时保持外设响应能力。
- 比如在空闲循环里进入 Sleep,等待外设事件。
__WFI(); // Wait For Interrupt
__WFE(); //wait for event当SCR寄存器的bit1(SLEEPONEXIT)为0时立即休眠,当为1的时候退出休眠
Stop模式
工作机制:
- CPU 和主时钟(HCLK、PLL、HSI/HSE)全部停止。
- SRAM 和寄存器内容保持。
- LSI / LSE / RTC 可继续运行。
- 可选主稳压器或低功耗稳压器。
唤醒来源:
- 外部中断(EXTI)
- RTC Alarm / Wakeup timer
- USART / I2C 唤醒(部分芯片支持)
唤醒后:
- 需要重新配置时钟系统(如重新使能 PLL)。
- 程序从唤醒点继续运行。
典型用途:
- 电池设备中长时间等待事件但仍需快速恢复的场合。
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
Standby模式
HAL_PWR_EnterSTANDBYMode();工作机制:
- 所有时钟关闭。
- CPU 和主 SRAM 断电。
- RTC 和备份寄存器保持。
- 唤醒相当于上电复位。
唤醒来源:
- 外部唤醒引脚(WKUP1~WKUPx)
- RTC Alarm / Wakeup
- IWDG(独立看门狗)
- NRST 复位
唤醒后:
- 从系统复位开始(程序重新执行
main())。 - 可通过备份寄存器判断是否是“待机唤醒”。
典型用途:
- 极限节能、长时间待机的设备(如手环、传感节点)。

FreeRTOS降低低功耗
这个可以在进入低功耗和退出低功耗自定义外设
extern void PreSleepProcessing(uint32_t ulExpectedIdleTime);
extern void PostSleepProcessing(uint32_t ulExpectedIdleTime);
//进入低功耗模式前需要处理的事情
//ulExpectedIdleTime:低功耗模式运行时间
void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
//关闭某些低功耗模式下不使用的外设时钟
__HAL_RCC_GPIOB_CLK_DISABLE();
__HAL_RCC_GPIOC_CLK_DISABLE();
__HAL_RCC_GPIOD_CLK_DISABLE();
__HAL_RCC_GPIOE_CLK_DISABLE();
__HAL_RCC_GPIOF_CLK_DISABLE();
__HAL_RCC_GPIOG_CLK_DISABLE();
__HAL_RCC_GPIOH_CLK_DISABLE();
}
//退出低功耗模式以后需要处理的事情
//ulExpectedIdleTime:低功耗模式运行时间
void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
//退出低功耗模式以后打开那些被关闭的外设时钟
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
}CPU大部分时间都在执行空闲任务,所以考虑当处理器执行空闲任务的时候进入低功耗模式。一般会在空闲任务的钩子函数里面低功耗处理逻辑。
理想情况:在空闲里低功耗,其他和之前一样不变
问题1:滴答定时器中断会周期性的唤醒处理器,效果会打折扣
解决:引出Tickless模式,当进入空闲任务周期后就延长SysTick中断,其他中断发生时才会被唤醒
问题2:系统时钟中断被延长,就算任务在中断里被唤醒,他不会算上在空闲任务里的时间,任务的时序就混乱了,不正常
解决:开一个定时器,记录下在空闲任务里睡着的时间,在任务被唤醒时加上补偿的时间
问题3:要将CPU唤醒现在只有中断,任务怎么和中断一样把CPU从低功耗唤醒?
解决:开一个定时器,周期设置为任务的时间值,时间一到就中断唤醒CPU
总结:进入低功耗记录时间点,关闭SYStick,设置好定时器周期;定时器中断一产生,任务就被唤醒,时钟也补偿好了
具体实现
先置1
#define configUSE_TICKLESS_IDLE 1 //1启用低功耗tickless模式Tickless模式启动条件(两个条件必须同时满足):
- 只有空闲任务在运行 - 其他所有任务都在阻塞或挂起状态
- 预计睡眠时间足够长 - 至少要睡够
configEXPECTED_IDLE_TIME_BEFORE_SLEEP个tick(默认2个)
#ifndef configEXPECTED_IDLE_TIME_BEFORE_SLEEP
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2
#endif空闲任务运行
↓
检查:是否只有我在运行? 是 → 检查:能睡多久?
↓
如果睡眠时间 > 2个tick → 调用 portSUPPRESS_TICKS_AND_SLEEP(xExpectedIdleTime)
↓
进入真正的低功耗睡眠
↓
定时器在xExpectedIdleTime时间后唤醒系统xExpectedIdleTime:这是最重要的参数,表示"还能睡多久"- 计算方式:下一个任务唤醒时间 - 当前时间
- 单位:时钟节拍数(tick)
- 例子:如果下一个任务50ms后要运行,tick周期1ms,那么
xExpectedIdleTime = 50
对于STM32用户:
- 不用自己写
portSUPPRESS_TICKS_AND_SLEEP()函数 - FreeRTOS已经为Cortex-M芯片提供了现成的实现
- 只需要配置
configUSE_TICKLESS_IDLE = 1即可
简单理解
就像你午睡:
- 条件1:手头没工作(只有空闲任务运行)
- 条件2:能睡至少2小时以上才值得躺下(睡眠时间>2个tick)
- 闹钟:设好下次开会的时间(xExpectedIdleTime)
- 睡觉:进入低功耗模式
- 唤醒:闹钟响,起来继续工作
底层调用
#if configUSE_TICKLESS_IDLE == 1
/**
* @brief FreeRTOS Tickless Idle 模式下的睡眠函数(弱定义,可用户重写)
*
* 当系统检测到任务空闲,可以连续空闲 xExpectedIdleTime 个 tick 时,
* 就会调用该函数,让 MCU 进入低功耗状态(比如睡眠或停止模式)。
*
* Tickless 的核心思想:暂停 SysTick 中断计时器,让 CPU 进入睡眠;
* 醒来后补偿系统的 Tick 计数,从而保持时间连续。
*/
__weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{
uint32_t ulReloadValue; // SysTick 的重装载值(表示要睡多久)
uint32_t ulCompleteTickPeriods; // 实际经过的完整 tick 数
uint32_t ulCompletedSysTickDecrements; // SysTick 递减次数
uint32_t ulSysTickCTRL; // SysTick 控制寄存器临时变量
TickType_t xModifiableIdleTime;
/*-----------------------------------------------------------
* ① 限制最大可睡眠时间,防止溢出
*-----------------------------------------------------------*/
if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks )
{
xExpectedIdleTime = xMaximumPossibleSuppressedTicks;
}
/*-----------------------------------------------------------
* ② 暂时关闭 SysTick 计数器
* (因为我们要自己计算时间,不让它继续中断)
*-----------------------------------------------------------*/
portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT;
/*-----------------------------------------------------------
* ③ 计算 SysTick 的重装载值(休眠时间)
* - 减 1 是因为当前 tick 周期已经走了一部分
*-----------------------------------------------------------*/
ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG
+ ( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) );
/* 停止 SysTick 带来的误差补偿 */
if( ulReloadValue > ulStoppedTimerCompensation )
{
ulReloadValue -= ulStoppedTimerCompensation;
}
/*-----------------------------------------------------------
* ④ 进入临界区(不使用 taskENTER_CRITICAL)
* - 因为要保留能唤醒的中断
*-----------------------------------------------------------*/
__disable_irq();
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
/*-----------------------------------------------------------
* ⑤ 检查是否允许进入低功耗模式
* 若有任务待调度/被唤醒,就取消睡眠
*-----------------------------------------------------------*/
if( eTaskConfirmSleepModeStatus() == eAbortSleep )
{
/* 恢复 SysTick 正常运行 */
portNVIC_SYSTICK_LOAD_REG = portNVIC_SYSTICK_CURRENT_VALUE_REG;
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
__enable_irq();
}
else
{
/*-------------------------------------------------------
* ⑥ 设置 SysTick 的新 reload 值并清零当前计数
*-------------------------------------------------------*/
portNVIC_SYSTICK_LOAD_REG = ulReloadValue;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* 重新启动 SysTick */
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
/*-------------------------------------------------------
* ⑦ 准备进入睡眠模式
*-------------------------------------------------------*/
xModifiableIdleTime = xExpectedIdleTime;
configPRE_SLEEP_PROCESSING( xModifiableIdleTime ); // 用户自定义:可在这里关闭外设等省电动作
if( xModifiableIdleTime > 0 )
{
/* 执行 WFI 指令 —— 等待中断唤醒 */
__dsb( portSY_FULL_READ_WRITE );
__wfi();
__isb( portSY_FULL_READ_WRITE );
}
configPOST_SLEEP_PROCESSING( xExpectedIdleTime ); // 唤醒后的用户自定义钩子函数
/*-------------------------------------------------------
* ⑧ 醒来后:再次停止 SysTick(准备补偿时间)
*-------------------------------------------------------*/
ulSysTickCTRL = portNVIC_SYSTICK_CTRL_REG;
portNVIC_SYSTICK_CTRL_REG = ( ulSysTickCTRL & ~portNVIC_SYSTICK_ENABLE_BIT );
/* 重新打开中断 */
__enable_irq();
/*-------------------------------------------------------
* ⑨ 判断是谁唤醒的(Tick 中断 or 其他中断)
*-------------------------------------------------------*/
if( ( ulSysTickCTRL & portNVIC_SYSTICK_COUNT_FLAG_BIT ) != 0 )
{
/* 情况A:SysTick 自己超时唤醒 */
uint32_t ulCalculatedLoadValue;
ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL )
- ( ulReloadValue - portNVIC_SYSTICK_CURRENT_VALUE_REG );
/* 防止负数或太小的 reload 值 */
if( ( ulCalculatedLoadValue < ulStoppedTimerCompensation )
|| ( ulCalculatedLoadValue > ulTimerCountsForOneTick ) )
{
ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL );
}
portNVIC_SYSTICK_LOAD_REG = ulCalculatedLoadValue;
/* 已完成的 tick = 期望值 - 1 */
ulCompleteTickPeriods = xExpectedIdleTime - 1UL;
}
else
{
/* 情况B:被其他中断提前唤醒 */
ulCompletedSysTickDecrements = ( xExpectedIdleTime * ulTimerCountsForOneTick )
- portNVIC_SYSTICK_CURRENT_VALUE_REG;
/* 计算经过了多少完整 tick */
ulCompleteTickPeriods = ulCompletedSysTickDecrements / ulTimerCountsForOneTick;
/* 设置下一个 reload 值,补齐剩余 tick */
portNVIC_SYSTICK_LOAD_REG =
( ( ulCompleteTickPeriods + 1UL ) * ulTimerCountsForOneTick )
- ulCompletedSysTickDecrements;
}
/*-------------------------------------------------------
* ⑩ 恢复 SysTick 正常运行 + 同步系统时钟
*-------------------------------------------------------*/
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
portENTER_CRITICAL();
{
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
/* 补偿系统 Tick 计数器,让时间保持正确 */
vTaskStepTick( ulCompleteTickPeriods );
/* 恢复 SysTick 的标准重装载值 */
portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
}
portEXIT_CRITICAL();
}
}
#endif /* #if configUSE_TICKLESS_IDLE */flowchart TD
A[开始:vPortSuppressTicksAndSleep(xExpectedIdleTime)] --> B[判断 xExpectedIdleTime 是否超过最大可抑制tick数]
B -->|是| C[限制 xExpectedIdleTime = xMaximumPossibleSuppressedTicks]
B -->|否| D[继续]
C --> D[停止SysTick计数器,防止计数期间进入睡眠]
D --> E[计算新的 SysTick 重装值 (ulReloadValue)]
E --> F[关中断 __disable_irq(),防止睡眠过程被打断]
F --> G[调用 eTaskConfirmSleepModeStatus() 检查是否允许进入低功耗]
G -->|返回 eAbortSleep| H[放弃睡眠:恢复SysTick计数并退出]
G -->|允许进入低功耗| I[设置新的SysTick重装值,清零当前计数器]
I --> J[重新启动SysTick计数器]
J --> K[调用 configPRE_SLEEP_PROCESSING() 进入前预处理钩子函数]
K --> L[执行 __WFI() 等待中断唤醒(若xModifiableIdleTime>0)]
L --> M[调用 configPOST_SLEEP_PROCESSING() 后处理钩子函数]
M --> N[停止 SysTick,读取控制寄存器]
N --> O[开中断 __enable_irq()]
O --> P{判断是否SysTick中断唤醒}
P -->|是| Q[计算新的装载值,说明Tick中断已发生]
P -->|否| R[计算睡眠时长对应的完整Tick数]
Q --> S[重装SysTick计数器,修正Tick步进]
R --> S[重装SysTick计数器,修正Tick步进]
S --> T[进入临界区 portENTER_CRITICAL()]
T --> U[重新启动SysTick,更新任务节拍 vTaskStepTick()]
U --> V[退出临界区 portEXIT_CRITICAL()]
V --> W[结束函数,恢复到正常运行状态]低功耗管理相关配置
//进入低功耗模式前需要处理的事情
//ulExpectedIdleTime:低功耗模式运行时间
void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
//关闭某些低功耗模式下不使用的外设时钟
__HAL_RCC_GPIOB_CLK_DISABLE();
__HAL_RCC_GPIOC_CLK_DISABLE();
__HAL_RCC_GPIOD_CLK_DISABLE();
__HAL_RCC_GPIOE_CLK_DISABLE();
__HAL_RCC_GPIOF_CLK_DISABLE();
__HAL_RCC_GPIOG_CLK_DISABLE();
__HAL_RCC_GPIOH_CLK_DISABLE();
}
//退出低功耗模式以后需要处理的事情
//ulExpectedIdleTime:低功耗模式运行时间
void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
//退出低功耗模式以后打开那些被关闭的外设时钟
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
}重点:配置步骤
第一步:修改 FreeRTOSConfig.h 宏定义
这是开关,必须在配置文件中开启。
/* 1. 启用 Tickless 模式 */
/*
0 = 关闭(默认)
1 = 使用 FreeRTOS 自带的默认实现(基于 SysTick,仅支持轻度睡眠 Sleep Mode)
2 = 用户自定义实现(通常用于深度睡眠 Stop Mode,需要用 LPTIM 或 RTC)
*/
#define configUSE_TICKLESS_IDLE 1
/* 2. 设置进入低功耗的最小阈值 */
/*
只有当空闲时间大于这个值(Tick数)时,才进入低功耗。
如果只空闲 1ms,进入睡眠的开销比省下的电还多,就不睡了。
通常设为 2 到 5。
*/
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2
/* 3. 定义睡前和醒后的处理宏(可选但推荐) */
/*
这两个宏会在进入睡眠指令(WFI)执行前后被自动调用。
你可以在这里关闭外设时钟、降低主频等。
*/
#define configPRE_SLEEP_PROCESSING( x ) PreSleepProcessing( x )
#define configPOST_SLEEP_PROCESSING( x ) PostSleepProcessing( x )第二步:实现睡前/醒后的处理函数(关键)
在你的 main.c 或专门的 power_management.c 中实现上面定义的宏函数。这是你控制硬件去“关灯”的地方。
/* 进入睡眠前被调用 */
void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
// 1. 关闭不用的外设时钟(GPIO, UART, ADC等)以省电
// 2. 如果是 STM32,可以在这里把 Flash 掉电
// 3. 甚至可以降低系统主频
// 这里的 xModifiableIdleTime 是个指针,你甚至可以修改它来强制缩短睡眠时间
// 但通常不需要动
// (仅作示例,STM32 HAL库)
// HAL_SuspendTick(); // 停止 HAL 库的 Tick,防止它唤醒
}
/* 醒来后立刻被调用 */
void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
// 1. 恢复外设时钟
// 2. 恢复 Flash 供电
// 3. 恢复主频
// (仅作示例)
// HAL_ResumeTick();
}第三步:理解底层实现(port.c)
如果你把 configUSE_TICKLESS_IDLE 设为 1,FreeRTOS 会自动编译 port.c 中的 vPortSuppressTicksAndSleep() 函数。
它的默认逻辑是:
- 关中断: 防止被打断。
- 算时间: 计算 SysTick 还需要数多少下才能到下一个任务的时间点。
- 伪装停 SysTick: 重新配置 SysTick 的重装载值(Reload Value)为最大值,让它不要频繁中断。
- 执行 WFI: 执行汇编指令 wfi (Wait For Interrupt)。此时 CPU 停止运行,时钟停止,进入 Sleep 模式。
- 唤醒: 当定时时间到(或者有外部中断按键按下),CPU 醒来。
- 补时间: 看看睡了多久,把 OS 的系统时间变量 xTickCount 加上去,假装中间的时间正常流逝了。
第四步:进阶——实现深度睡眠(Stop Mode)
注意: 默认的 configUSE_TICKLESS_IDLE = 1 只能实现 Sleep Mode(CPU 停,外设不停,SysTick 不停)。如果你要实现 Stop Mode(主时钟 HSI/HSE 都会停掉,电流降到 uA 级别),默认实现就不行了,因为 SysTick 没时钟会挂掉。
你需要:
将 configUSE_TICKLESS_IDLE 设为 2。
自己重写 vPortSuppressTicksAndSleep() 函数。
使用 LPTIM (低功耗定时器) 或 RTC (实时时钟) 来唤醒,因为它们在 Stop 模式下还能走字。
自定义伪代码
void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{
// 1. 停掉 SysTick
// 2. 配置 LPTIM,让它在 xExpectedIdleTime 后中断
// 3. 启动 LPTIM
// 4. 进入 STOP 模式 (这是最省电的一步)
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 5. ... 睡着了 ... Zzz ...
// 6. 醒来!(通常是因为 LPTIM 中断或者外部引脚中断)
// 7. 恢复系统时钟 (因为 Stop 模式唤醒后默认是内部低速时钟,必须切回高速)
SystemClock_Config();
// 8. 修正 xTickCount (补回睡着的时间)
// 9. 重新启动 SysTick
}
