FreeRTOS教程10 低功耗
1、准备材料
STM32CubeMX软件(Version 6.10.0)
Keil µVision5 IDE(MDK-Arm)
2、学习目标
本文主要学习 FreeRTOS 低功耗的相关知识,包括HAL 库基础时钟、FreeRTOS 基础时钟、低功耗处理和 Tickless 模式等知识
3、前提知识
3.1、HAL 库基础时钟
当我们使用 STM32CubeMX 软件配置一个基本的工程时,往往需要首先在 Pinout & Configuration 页面 RCC 中配置 HSE 和 LSE ,然后在 SYS 中配置 Debug 和 Timebase Source,这些都是必不可少的配置步骤,其中 Timebase Source 可以选择默认的 SysTick ,也可以选择任何一个定时器外设
3.1.1、使用 SysTick 定时器
学习 STM32 HAL 库开发,在 SYS 中配置 Timebase Source 时,一般将时基源保持默认的 SysTick 即可,那么这个默认的 SysTick 是如何被初始化以及使用呢?
3.1.1.1、工作原理
打开 “STM32CubeMX教程1 工程建立” 文章配置的 STM32 空工程,找到 main.c 文件中的 main() 主函数,SysTick 在主函数第一个被执行的函数HAL_Init() 中得到初始化,具体如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.1.png)
其中滴答定时器频率 uwTickFreq 参数默认为 HAL_TICK_FREQ_DEFAULT(1KHZ) ,当然也可以根据需要修改为 10HZ 和 100HZ,如下述枚举类型定义
typedef enum
{
HAL_TICK_FREQ_10HZ = 100U,
HAL_TICK_FREQ_100HZ = 10U,
HAL_TICK_FREQ_1KHZ = 1U,
HAL_TICK_FREQ_DEFAULT = HAL_TICK_FREQ_1KHZ
} HAL_TickFreqTypeDef;
当初始化完毕之后,滴答定时器就会以固定频率发生中断,然后进入中断回调函数 SysTick_Handler() 中,滴答定时器中断默认就会开启
3.1.1.2、中断处理
在 STM32CubeMX 软件的 NVIC 管理页面,可以发现默认开启的滴答定时器中断 Time base: System tick timer ,在软件上该中断不可关闭,但是可以设置中断优先级,具体如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.2.png)
在 stm32f4xx_it.c 文件中可以找到滴答定时器的回调函数 SysTick_Handler() ,其只调用了 HAL_IncTick() 函数,该函数只做了一件事情,就是每次发生滴答定时器中断的时候,将一个名为 uwTick 的全局变量加 1 ( uwTickFreq 参数默认为1),具体如下所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.3.png)
根据这个全局变量的值我们就可以做一些延时的工作,比如常用到的 HAL_Delay() 延时函数就是通过滴答定时器中断来实现的,具体如下所述
/**
* @brief HAL 库延时函数
* @param Delay:延时时间,单位为ms
* @retval None
*/
__weak void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay;
/* 最少等待一个频率时间 */
if (wait < HAL_MAX_DELAY)
{
wait += (uint32_t)(uwTickFreq);
}
/* 空循环延时等待 */
while((HAL_GetTick() - tickstart) < wait)
{
}
}
另外还有 HAL_SuspendTick() 和 HAL_ResumeTick() 两个控制滴答定时器中断停止和启动的函数,具体如下所述
/**
* @brief 挂起滴答定时器中断
* @retval None
*/
__weak void HAL_SuspendTick(void)
{
/* 禁用 SysTick 中断 */
SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
}
/**
* @brief 恢复挂起的滴答定时器中断
* @retval None
*/
__weak void HAL_ResumeTick(void)
{
/* 使能 SysTick 中断 */
SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;
}
3.1.2、使用其他定时器
当 SysTick 被其他软件使用时(比如本系列教程的 FreeRTOS),STM32 还可以选择任何一个定时器外设作为其 HAL 库的时基源,比如选择基础定时器 TIM6
3.1.2.1、工作原理
当在 STM32CubeMX 软件中配置 SYS 中的 Timebase Source 为 TIM6 然后生成工程之后,与 “3.1.1、使用 SysTick 定时器” 小节不同的是,其首先会在 Core 文件夹下多出一个名为 stm32f4xx_hal_timebase_tim.c 的文件,该文件中涉及了所有关于 TIM6 作为 HAL 库系统嘀嗒定时器的配置程序,使用 TIM6 作为 HAL 库系统嘀嗒定时器的初始化步骤如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.4.png)
上图内容其实就是将基础定时器 TIM6 初始化为一个周期为 1ms 的定时器,并且启动其周期中断回调,如果对上述代码不了解可以阅读 “STM32CubeMX教程5 TIM 定时器概述及基本定时器” 实验
3.1.2.2、中断处理
当选择基础定时器 TIM6 作为 SysTick 时,在STM32CubeMX软件的 NVIC 管理中 TIM6 的中断就会被强制打开并且软件内不可关闭,但是同样可以修改优先级,如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.5.png)
同样可以在 stm32f4xx_it.c 文件中可以找到 TIM6 的中断回调函数 TIM6_DAC_IRQHandler() ,该函数调用了定时器的统一中断处理函数 HAL_TIM_IRQHandler() ,该函数根据使用的不同定时器功能最终调用不同的中断回调函数,这里读者只需要知道其调用了定时器周期回调函数 HAL_TIM_PeriodElapsedCallback() 即可,该函数由 STM32CubeMX 软件在 main.c 文件中自动生成,具体如下所示
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* USER CODE BEGIN Callback 0 */
/* USER CODE END Callback 0 */
if (htim->Instance == TIM6) {
HAL_IncTick();
}
/* USER CODE BEGIN Callback 1 */
/* USER CODE END Callback 1 */
}
从函数体内内容可以看出其原理与 “3.1.1.2、中断处理” 小节所叙述的一致,故此处不再赘述
3.2、FreeRTOS 基础时钟
STM32CubeMX 软件配置使用 FreeRTOS 时,默认将 SysTick 滴答定时器分配给 FreeRTOS 使用,因此如果 HAL 库的时基源也为 SysTick 时,在生成工程代码时软件就会警告用户:“当使用 RTOS 时,强烈建议使用除 Systick 之外的 HAL 时基源,可以从 SYS 下的 Pinout 选项卡更改 HAL 时基源”,具体如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.6.png)
因此在 STM32 需要使用 FreeRTOS 时,一般将 SysTick 分配给 FreeRTOS 使用,而 HAL 库的时基源一般选择除 SysTick 之外的定时器外设,同时如果用户明确自己不需要使用 HAL 库的 HAL_Delay() 延时函数,则可以关闭 HAL 库的时基源
3.2.1、工作原理
FreeRTOS 的 SysTick 系统时基源是在 vPortSetupTimerInterrupt() 函数中被初始化的,在该函数中有一个名为 configUSE_TICKLESS_IDLE 参数用于设置是否使用 Tickless 模式,这是 FreeRTOS 中提供的一种低功耗模式,将在后面小节介绍,其对 SysTick 的初始化是直接对 SysTick 的寄存器进行操作的,其调用流程如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.7.png)
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.8.png)
SysTick 的寄存器可以阅读 Arm® Cortex®-M4 Processor Technical Reference Manual手册 “4.1 System control registers” 小节,如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.9.png)
3.2.2、中断处理
当将 SysTick 分配给 FreeRTOS 初始化并开启对应中断之后,SysTick 的中断会被定义在 cmsis_os2.c 文件中(从 FreeRTOS_v10.3.1 之后),该函数清除了中断标志然后调用了 FreeRTOS 定义的硬件接口文件中的 xPortSysTickHandler() 函数,在 xPortSysTickHandler() 函数中增加了 RTOS 滴答定时器计数量,然后挂起 PednSV 中断,请求上下文切换,具体如下所述
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.10.png)
根据上面的分析我们知道了一件关于 FreeRTOS 很重要的事情,也就是:FreeRTOS 的任务调度发起是在系统滴答定时器中断中发起的,然后真正进行上下文切换处理是在 PendSV 中断中执行的
3.3、低功耗处理
3.3.1、睡眠、停止和待机模式
在 “STM32CubeMX教程25 PWR 电源管理 - 睡眠、停止和待机模式” 文章中曾经介绍了关于 STM32 电源管理的睡眠、停止和待机三种低功耗模式,在一个由 FreeRTOS 管理的系统中,一般只使用其中的睡眠模式即可,因为停止和待机模式的唤醒条件相对较为苛刻,感兴趣的读者请自行阅读上述文章
3.3.2、低功耗思路
在一个 FreeRTOS 管理的多任务系统中,当所有任务处理完毕进入阻塞状态等待下次处理时机时,空闲任务会一直执行,如果同时使能了 configUSE_IDLE_HOOK 参数,则每当处理器将要进入空闲任务时,就会先进入空闲任务钩子函数中
因此我们可以在空闲任务钩子函数中设置处理器进入睡眠模式,但是同时也会存在一个问题,就是每次滴答定时器中断都会将处理器唤醒,这样其运行时序图应该如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.11.png)
3.3.3、Tickless 模式
上述低功耗思路中存在的一个问题:“每次滴答定时器中断都会将处理器唤醒” ,FreeRTOS 提供了一个 Tickless 模式,当处理器空闲时会一直处于睡眠状态,然后在任务即将退出阻塞状态之前处理器提前被唤醒,理想的低功耗模式应该如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.12.png)
要使用 Tickless 模式只需要启用 configUSE_TICKLESS_IDLE 参数即可,该参数可以通过 STM32CubeMX 软件设置,有三个可以配置的选项,选择 Built in functionality enabled 对应的参数值为 1 ,表示使用 FreeRTOS 内建的函数实现 Tickless 低功耗功能,选择 User defined functionality enabled 则对应的参数值为 2 ,表示使用用户自定义的函数实现 Tickless 低功耗功能,一般选择使用 FreeRTOS 现成的函数来实现 Tickless 低功耗功能,如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.13.png)
3.3.3.1、工作原理
当启用 Tickless 之后,系统满足以下两点时就会自动进入睡眠模式
- 空闲任务正在运行
- 可运行低功耗的时间大于参数 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 设定值时(默认为2)
用户需要注意的是进入睡眠的时间有最大值 xMaximumPossibleSuppressedTicks 限制,该变量在设置滴答定时器中断 vPortSetupTimerInterrupt() 函数中被计算,当 MCU 频率为168MHz,FreeRTOS 频率为 1000Hz 时,该值为 99 ,也即单次最长进入睡眠时间的最大值为 99 个节拍,具体如下所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.14.png)
3.3.3.2、vPortSuppressTicksAndSleep() 函数详解
vPortSuppressTicksAndSleep() 是 Tickless 模式实现的具体函数,该函数会在启用 Tickless 模式后在空闲任务中被调用,具体可以参考 “freeRTOS 低功耗模式 和 空闲任务” 文章
4、实验一:Tickless 模式的使用
4.1、实验目标
- 创建任务 Task_Main,在任务中实现 GREEN_LED 和 RED_LED 的闪烁程序
- 启用/关闭 Tickless 模式,对比两种不同情况下开发板的工作电流
4.2、CubeMX相关配置
首先读者应按照 "FreeRTOS教程1 基础知识" 章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求
本实验需要初始化开发板上 GREEN_LED 和 RED_LED 两个 LED 灯作为显示,具体配置步骤请阅读“STM32CubeMX教程2 GPIO输出 - 点亮LED灯”,注意虽开发板不同但配置原理一致,如下图所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.15.png)
单击 Middleware and Software Packs/FREERTOS ,在 Configuration 中单击 Tasks and Queues 选项卡,双击默认任务修改其参数,如下所示
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.16.png)
然后在 Configuration 中单击 Config parameters 选项卡,在 Kernel settings 中找到 USE_TICKLESS_IDLE 参数,将其设置为 Disabled 或者 Built in functionality enabled,进行对比实验
![](https://blog-picture-240114.oss-cn-beijing.aliyuncs.com/FreeRTOS/10.17.png)
最后配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可
4.3、添加其他必要代码
首先实现任务 Task_Main 使其每隔 500ms 改变一次 GREEN_LED 和 RED_LED 的状态,具体如下所述
void AppTask_Main(void *argument)
{
/* USER CODE BEGIN AppTask_Main */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
vTaskDelay(pdMS_TO_TICKS(500));
}
/* USER CODE END AppTask_Main */
}
然后再进入睡眠模式之前关闭系统滴答定时器,在退出睡眠模式之后开启系统滴答定时器,具体如下所述
__weak void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
/* place for user code */
HAL_SuspendTick();
}
__weak void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
/* place for user code */
HAL_ResumeTick();
}
4.4、烧录验证
在开启 Tickless 和关闭 Tickless 两种模式下读者可以自行测试开发板工作电流,对比开启和关闭两种模式下工作电流的变化
参考资料
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf