FreeRTOS 学习:(四十六)Tickless 低功耗模式
详细讲解了 FreeRTOS 的 Tickless 低功耗模式
| 上一篇 | 下一篇 |
|---|---|
| 软件定时器 |
Tickless 低功耗模式
1)背景
很多应用场合对于功耗的要求很严格,比如可穿戴低功耗产品、物联网低功耗产品等。一般 MCU 都有相应的低功耗模式,裸机开发时可以使用 MCU 的低功耗模式。FreeRTOS 也提供了一个叫 Tickless 的低功耗模式,方便带 FreeRTOS 操作系统的应用开发。
STM32 裸机的低功耗模式有三种:睡眠模式、停止模式、待机模式,详情如下:

Tickless 低功耗模式的本质是通过调用指令 WFI 指令实现睡眠模式。 这种模式下,系统时钟仍然开启,只是 HCLK 对 CPU 内核的时钟输入被关闭了,也就是说这种模式下,除了 CPU 内核时钟关闭了以外,其他的时钟源以及外设全部正常运行(对裸机睡眠模式不了解的也没关系,FreeRTOS 源码将模式都处理好了,只需要配置即可)。
2)Tickless 模式详解
模式详解熟悉一下就行,主要看后面的配置
之前的任务运行时间统计实验的一个示例结果如下:

可以看出,在整个系统的运行过程中,其实大部分时间是在执行空闲任务的,是在系统中的所有其它任务都阻塞或被挂起时才运行的。
为了可以降低功耗,又不影响系统运行,该如何做?可以在本该空闲任务执行的期间,让 MCU 进入相应的低功耗模式;当其他任务准备运行的时候,唤醒 MCU 退出低功耗模式。
不过这样会有两个难点:
- 进入低功耗之后,多久唤醒?也就是下一个要运行的任务如何被准确唤醒?
- 任何中断均可唤醒 MCU,若滴答定时器频繁中断,会影响低功耗的效果(滴答定时器的中断作为 RTOS 的心脏是不能关的)
解决方法是:
- 将滴答定时器的中断周期修改为低功耗运行时间
- 退出低功耗后,需补上系统时钟节拍数
- 比如说:所有任务都阻塞了,最小阻塞时间为 20ms,则将滴答定时器的中断周期修改为 20ms,然后进入低功耗 20ms 后,会被滴答定时器的中断唤醒,从而退出低功耗模式,然后需要补上系统时钟节拍数(假设节拍是 1ms 一次,那么就会调用
vTaskStepTick(20),一下将系统节拍数 +20 ),最后将滴答定时器的中断周期改回到初始状态( 1ms 一次)。 - 当然,如果是由其他外设中断唤醒的,那么也需要补上系统时钟节拍数。不过不同中断唤醒,所需要补的系统节拍数的计算方式是不一样的。
值得庆幸的是:FreeRTOS 的低功耗 Tickless 模式机制已经处理好了这些难点,只要自己配置即可。
2.1)内部机制实现简要介绍
了解即可
相关的 Tickless 低功耗模式机制的实现大部分是在 IDLE_task 空闲任务的内部实现的。具体图解如下:

2.2)最大单次睡眠时间
configUSE_TICKLESS_IDLE = 1
依赖 SysTick(24位计数器),最大时间由内核在初始化时计算:
// FreeRTOS源码 (port.c)
ulTimerCountsForOneTick = configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ; // 每节拍计数值
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick; // 最大抑制节拍数
计算公式:
最大时间 = ( 2 24 − 1 ) / ( S Y S C L K / T I C K _ R A T E ) × ( 1 / T I C K _ R A T E ) (单位:秒) 最大时间 = (2^{24} - 1) / (SYSCLK / TICK\_RATE) × (1 / TICK\_RATE) (单位:秒) 最大时间=(224−1)/(SYSCLK/TICK_RATE)×(1/TICK_RATE)(单位:秒)
2 24 − 1 2^{24} - 1 224−1 = 0xFFFFFF 。
典型示例(STM32F4):
| 参数 | 值 |
|---|---|
| SYSCLK(HCLK) | 168 MHz |
| TICK_RATE | 1000 Hz (1ms) |
| 每节拍计数值 | 168,000 |
| 最大抑制节拍 | 16,777,215 / 168,000 ≈ 99 |
| 最大单次睡眠 | 99 ms |
典型示例(STM32F1):
| 参数 | 值 |
|---|---|
| SYSCLK(HCLK) | 72 MHz |
| TICK_RATE | 1000 Hz (1ms) |
| 每节拍计数值 | 72,000 |
| 最大抑制节拍 | 16,777,215 / 72,000 ≈ 233 |
| 最大单次睡眠 | 233 ms |
最大单次睡眠时间到了之后,会进行:系统精准唤醒 → 补偿时间 → 重新评估 → 继续分段睡眠(若仍空闲)。
核心流程(以 200ms 空闲、F4 的最大单次睡眠 99ms 为例,F1的同理):

3)Tickless 模式相关配置项
-
configUSE_TICKLESS_IDLE
此宏用于使能低功耗 Tickless 模式(1:使能,默认 0:失能,2:由用户自己实现低功耗模式 ),在
FreeRTOSConfig.h中定义 。 -
configEXPECTED_IDLE_TIME_BEFORE_SLEEP
此宏用于定义系统进入相应低功耗模式的最短时长(默认为 2 ,单位是系统节拍),在
FreeRTOS.h中定义。 -
configPRE_SLEEP_PROCESSING(x),声明了但无内容,需要自己写内容
此宏用于定义需要在系统进入低功耗模式前执行的事务,如:进入低功耗前关闭外设时钟,以达到进一步降低功耗的目的。
-
configPOST_SLEEP_PROCESSING(x),声明了但无内容,需要自己写内容
此宏用于定义需要在系统退出低功耗模式后执行的事务,如:退出低功耗后开启之前关闭的外设时钟,以使系统能够正常运行。
注意①: 使用起来很简单,就是要将第一个宏置 1 ,然后有需要的话手动补充后两个宏,后两个宏的函数内容也就是开关外设时钟。
注意②: 如果不使用后面两个宏的话(不额外关闭一些外设的时钟的话),单单关闭 CPU 内核的时钟,其实低功耗的程度并不明显。并且如果额外关闭的外设本身功耗就比较小的话,其实效果也不大。
4)实验
① 实验目的: 学习使用 FreeRTOS 中的低功耗 Tickless 模式,并观察该模式是否对功耗有明显降低。
② 实验设计: 将在原先二值信号量的课堂源码中,加入低功耗模式,最后对比这个两个实验的功耗结果,观察 Tickless 模式对于降低功耗是否有用。
一般的习惯是:在 FreeRTOSConfig.h 中声明,然后在 FreeRTOS_Code.c 中定义具体内容,不要忘记互相引用头文件。
③ 代码:
FreeRTOSConfig.h 文件代码如下(自定义的两个部分在最后面):
#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H
/* 头文件 */
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include <stdint.h>
#include "FreeRTOS_Code.h"
extern uint32_t SystemCoreClock;
/* 基础配置项 */
#define configUSE_PREEMPTION 1 /* 1: 抢占式调度器, 0: 协程式调度器, 无默认需定义 */
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 /* 1: 使用硬件计算下一个要运行的任务, 0: 使用软件算法计算下一个要运行的任务, 默认: 0 */
#define configUSE_TICKLESS_IDLE 1 /* 1: 使能tickless低功耗模式, 默认: 0 */
#define configCPU_CLOCK_HZ SystemCoreClock /* 定义CPU主频, 单位: Hz, 无默认需定义 */
#define configSYSTICK_CLOCK_HZ (configCPU_CLOCK_HZ / 8)/* 定义SysTick时钟频率,当SysTick时钟频率与内核时钟频率不同时才可以定义, 单位: Hz, 默认: 不定义 */
#define configTICK_RATE_HZ 1000 /* 定义系统时钟节拍频率, 单位: Hz, 无默认需定义 */
#define configMAX_PRIORITIES 32 /* 定义最大优先级数, 最大优先级=configMAX_PRIORITIES-1, 无默认需定义 */
#define configMINIMAL_STACK_SIZE 128 /* 定义空闲任务的栈空间大小, 单位: Word, 无默认需定义 */
#define configMAX_TASK_NAME_LEN 16 /* 定义任务名最大字符数, 默认: 16 */
#define configUSE_16_BIT_TICKS 0 /* 1: 定义系统时钟节拍计数器的数据类型为16位无符号数, 无默认需定义 */
#define configIDLE_SHOULD_YIELD 1 /* 1: 使能在抢占式调度下,同优先级的任务能抢占空闲任务, 默认: 1 */
#define configUSE_TASK_NOTIFICATIONS 1 /* 1: 使能任务间直接的消息传递,包括信号量、事件标志组和消息邮箱, 默认: 1 */
#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1 /* 定义任务通知数组的大小, 默认: 1 */
#define configUSE_MUTEXES 1 /* 1: 使能互斥信号量, 默认: 0 */
#define configUSE_RECURSIVE_MUTEXES 1 /* 1: 使能递归互斥信号量, 默认: 0 */
#define configUSE_COUNTING_SEMAPHORES 1 /* 1: 使能计数信号量, 默认: 0 */
#define configUSE_ALTERNATIVE_API 0 /* 已弃用!!! */
#define configQUEUE_REGISTRY_SIZE 8 /* 定义可以注册的信号量和消息队列的个数, 默认: 0 */
#define configUSE_QUEUE_SETS 1 /* 1: 使能队列集, 默认: 0 */
#define configUSE_TIME_SLICING 1 /* 1: 使能时间片调度, 默认: 1 */
#define configUSE_NEWLIB_REENTRANT 0 /* 1: 任务创建时分配Newlib的重入结构体, 默认: 0 */
#define configENABLE_BACKWARD_COMPATIBILITY 0 /* 1: 使能兼容老版本, 默认: 1 */
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 0 /* 定义线程本地存储指针的个数, 默认: 0 */
#define configSTACK_DEPTH_TYPE uint16_t /* 定义任务堆栈深度的数据类型, 默认: uint16_t */
#define configMESSAGE_BUFFER_LENGTH_TYPE size_t /* 定义消息缓冲区中消息长度的数据类型, 默认: size_t */
/* 内存分配相关定义 */
#define configSUPPORT_STATIC_ALLOCATION 0 /* 1: 支持静态申请内存, 默认: 0 */
#define configSUPPORT_DYNAMIC_ALLOCATION 1 /* 1: 支持动态申请内存, 默认: 1 */
#define configTOTAL_HEAP_SIZE ((size_t)(10 * 1024)) /* FreeRTOS堆中可用的RAM总量, 单位: Byte, 无默认需定义 */
#define configAPPLICATION_ALLOCATED_HEAP 0 /* 1: 用户手动分配FreeRTOS内存堆(ucHeap), 默认: 0 */
#define configSTACK_ALLOCATION_FROM_SEPARATE_HEAP 0 /* 1: 用户自行实现任务创建时使用的内存申请与释放函数, 默认: 0 */
/* 钩子函数相关定义 */
#define configUSE_IDLE_HOOK 0 /* 1: 使能空闲任务钩子函数, 无默认需定义 */
#define configUSE_TICK_HOOK 0 /* 1: 使能系统时钟节拍中断钩子函数, 无默认需定义 */
#define configCHECK_FOR_STACK_OVERFLOW 0 /* 1: 使能栈溢出检测方法1, 2: 使能栈溢出检测方法2, 默认: 0 */
#define configUSE_MALLOC_FAILED_HOOK 0 /* 1: 使能动态内存申请失败钩子函数, 默认: 0 */
#define configUSE_DAEMON_TASK_STARTUP_HOOK 0 /* 1: 使能定时器服务任务首次执行前的钩子函数, 默认: 0 */
/* 运行时间和任务状态统计相关定义 */
#define configGENERATE_RUN_TIME_STATS 1 /* 1: 使能任务运行时间统计功能, 默认: 0 */
#if configGENERATE_RUN_TIME_STATS
#include "timer.h"
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ConfigureTimeForRunTimeStats()
extern uint32_t FreeRTOSRunTimeTicks;
#define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTimeTicks
#endif
#define configUSE_TRACE_FACILITY 1 /* 1: 使能可视化跟踪调试, 默认: 0 */
#define configUSE_STATS_FORMATTING_FUNCTIONS 1 /* 1: configUSE_TRACE_FACILITY为1时,会编译vTaskList()和vTaskGetRunTimeStats()函数, 默认: 0 */
/* 协程相关定义 */
#define configUSE_CO_ROUTINES 0 /* 1: 启用协程, 默认: 0 */
#define configMAX_CO_ROUTINE_PRIORITIES 2 /* 定义协程的最大优先级, 最大优先级=configMAX_CO_ROUTINE_PRIORITIES-1, 无默认configUSE_CO_ROUTINES为1时需定义 */
/* 软件定时器相关定义 */
#define configUSE_TIMERS 1 /* 1: 使能软件定时器, 默认: 0 */
#define configTIMER_TASK_PRIORITY ( configMAX_PRIORITIES - 1 ) /* 定义软件定时器任务的优先级, 无默认configUSE_TIMERS为1时需定义 */
#define configTIMER_QUEUE_LENGTH 5 /* 定义软件定时器命令队列的长度, 无默认configUSE_TIMERS为1时需定义 */
#define configTIMER_TASK_STACK_DEPTH ( configMINIMAL_STACK_SIZE * 2) /* 定义软件定时器任务的栈空间大小, 无默认configUSE_TIMERS为1时需定义 */
/* 可选函数, 1: 使能 */
#define INCLUDE_vTaskPrioritySet 1 /* 设置任务优先级 */
#define INCLUDE_uxTaskPriorityGet 1 /* 获取任务优先级 */
#define INCLUDE_vTaskDelete 1 /* 删除任务 */
#define INCLUDE_vTaskSuspend 1 /* 挂起任务 */
#define INCLUDE_xResumeFromISR 1 /* 恢复在中断中挂起的任务 */
#define INCLUDE_vTaskDelayUntil 1 /* 任务绝对延时 */
#define INCLUDE_vTaskDelay 1 /* 任务延时 */
#define INCLUDE_xTaskGetSchedulerState 1 /* 获取任务调度器状态 */
#define INCLUDE_xTaskGetCurrentTaskHandle 1 /* 获取当前任务的任务句柄 */
#define INCLUDE_uxTaskGetStackHighWaterMark 1 /* 获取任务堆栈历史剩余最小值 */
#define INCLUDE_xTaskGetIdleTaskHandle 1 /* 获取空闲任务的任务句柄 */
#define INCLUDE_eTaskGetState 1 /* 获取任务状态 */
#define INCLUDE_xEventGroupSetBitFromISR 1 /* 在中断中设置事件标志位 */
#define INCLUDE_xTimerPendFunctionCall 1 /* 将函数的执行挂到定时器服务任务 */
#define INCLUDE_xTaskAbortDelay 1 /* 中断任务延时 */
#define INCLUDE_xTaskGetHandle 1 /* 通过任务名获取任务句柄 */
#define INCLUDE_xTaskResumeFromISR 1 /* 恢复在中断中挂起的任务 */
/* 中断嵌套行为配置 */
#ifdef __NVIC_PRIO_BITS
#define configPRIO_BITS __NVIC_PRIO_BITS
#else
#define configPRIO_BITS 4
#endif
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 /* 中断最低优先级 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 /* FreeRTOS可管理的最高中断优先级 */
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_API_CALL_INTERRUPT_PRIORITY configMAX_SYSCALL_INTERRUPT_PRIORITY
/* FreeRTOS中断服务函数相关定义 */
#define xPortPendSVHandler PendSV_Handler
#define vPortSVCHandler SVC_Handler
/* 断言 */
#define vAssertCalled(char, int) printf("Error: %s, %d\r\n", char, int)
#define configASSERT( x ) if( ( x ) == 0 ) vAssertCalled( __FILE__, __LINE__ )
/* Tickless 低功耗模式用户定义函数 */
#define configPRE_SLEEP_PROCESSING( x ) PRE_SLEEP_PROCESSING() /* 需要在系统进入低功耗模式前执行的事务 */
#define configPOST_SLEEP_PROCESSING( x ) POST_SLEEP_PROCESSING() /* 需要在系统进入低功耗模式后执行的事务 */
/* FreeRTOS MPU 特殊定义 */
//#define configINCLUDE_APPLICATION_DEFINED_PRIVILEGED_FUNCTIONS 0
//#define configTOTAL_MPU_REGIONS 8
//#define configTEX_S_C_B_FLASH 0x07UL
//#define configTEX_S_C_B_SRAM 0x07UL
//#define configENFORCE_SYSTEM_CALLS_FROM_KERNEL_ONLY 1
//#define configALLOW_UNPRIVILEGED_CRITICAL_SECTIONS 1
/* ARMv8-M 安全侧端口相关定义。 */
//#define secureconfigMAX_SECURE_CONTEXTS 5
#endif /* FREERTOS_CONFIG_H */
FreeRTOS_Code.c 文件代码如下:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#include "FreeRTOS_Code.h"
/* ----------------------------------------------------------------------------------- */
#define START_TASK_PRIO 1 // 任务优先级
#define START_STK_SIZE 128 // 任务堆栈大小
TaskHandle_t StartTask_Handler; // 任务句柄
void start_task(void *pvParameters); // 任务函数
#define TASK1_PRIO 2 // 任务优先级
#define TSAK1_STK_SIZE 128 // 任务堆栈大小
TaskHandle_t Task1_Handler; // 任务句柄
void task1(void *p_arg); // 任务函数
#define TASK2_PRIO 3 // 任务优先级
#define TSAK2_STK_SIZE 128 // 任务堆栈大小
TaskHandle_t Task2_Handler; // 任务句柄
void task2(void *p_arg); // 任务函数
/* ----------------------------------------------------------------------------------- */
QueueHandle_t semphore_handle; // 信号量句柄
/**
* @brief 进入低功耗前所需要执行的操作
* @param 无
* @retval 无
*/
void PRE_SLEEP_PROCESSING(void)
{
__HAL_RCC_GPIOA_CLK_DISABLE();
__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();
}
/**
* @brief 退出低功耗后所需要执行的操作
* @param 无
* @retval 无
*/
void POST_SLEEP_PROCESSING(void)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
__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();
}
/* ----------------------------------- 主函数 ---------------------------------------- */
void freertos_code(void)
{
/* 创建二值信号量 */
semphore_handle = xSemaphoreCreateBinary();
if(semphore_handle != NULL)
{
printf("二值信号量创建成功!!!\r\n");
}
/* 创建开始任务 */
xTaskCreate((TaskFunction_t )start_task, // 任务函数
(const char* )"start_task", // 任务名称
(uint16_t )START_STK_SIZE, // 任务堆栈大小
(void* )NULL, // 传递给任务函数的参数
(UBaseType_t )START_TASK_PRIO, // 任务优先级
(TaskHandle_t* )&StartTask_Handler); // 任务句柄
vTaskStartScheduler(); // 开启任务调度器
}
/* ---------------------------------- 任务函数 --------------------------------------- */
/**
* @brief 开始任务函数
* @param 无
* @retval 无
*/
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); // 进入临界区 -----------
/* 创建 TASK1 任务 */
xTaskCreate((TaskFunction_t )task1,
(const char* )"task1",
(uint16_t )TSAK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_PRIO,
(TaskHandle_t* )&Task1_Handler);
/* 创建 TASK2 任务 */
xTaskCreate((TaskFunction_t )task2,
(const char* )"task2",
(uint16_t )TSAK2_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK2_PRIO,
(TaskHandle_t* )&Task2_Handler);
vTaskDelete(StartTask_Handler); // 删除开始任务
taskEXIT_CRITICAL(); // 退出临界区 -----------
}
/**
* @brief TASK1 任务函数
* @param 无
* @retval 无
* @note 释放信号量
*/
void task1(void *pvParameters)
{
uint8_t key = 0;
BaseType_t err;
while(1)
{
key = KEY_Scan(0);
if(key == KEY0_PRES)
{
if(semphore_handle != NULL)
{
err = xSemaphoreGive(semphore_handle);
if(err == pdPASS)
{
printf("信号量释放成功!!\r\n");
}else printf("信号量释放失败!!\r\n");
}
}
vTaskDelay(10);
}
}
/**
* @brief TASK2 任务函数
* @param 无
* @retval 无
* @note 键值队列出队
*/
void task2(void *pvParameters)
{
uint32_t i = 0;
BaseType_t err;
while(1)
{
err = xSemaphoreTake(semphore_handle,portMAX_DELAY); /* 获取信号量并死等 */
if(err == pdTRUE)
{
printf("获取信号量成功\r\n");
}else printf("已超时%d\r\n",++i);
}
}
FreeRTOS_Code.h 文件代码如下:
#ifndef __FREERTOS_CODE_H
#define __FREERTOS_CODE_H
void freertos_code(void);
#endif
main.c 文件代码如下:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "timer.h"
#include "FreeRTOS.h"
#include "task.h"
#include "FreeRTOS_Code.h"
int main(void)
{
HAL_Init();
sys_stm32_clock_init(RCC_PLL_MUL9); // 设置时钟,72M
delay_init(72); // 延时函数初始化
usart_init(115200); // 初始化串口
LED_Init(); // 初始化LED
KEY_Init(); // 按键初始化
freertos_code(); //FreeRTOS代码
}
实验结果如下:

串口打印的结果和之前的二值信号量的结果一样,但由于没有功耗测试仪,所以功耗省了多少看不到。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)