细说STM32单片机FreeRTOS空闲任务及低功耗的设计方法及应用
目录
一、空闲任务实现原理
二、设计示例
1、示例功能与CubeMX项目设置
(1)RCC、SYS、Code Generator、USART3、TIM6
(2)GPIO
(3)FreeRTOS
(4)NVIC
2、主程序
3、FreeRTOS对象初始化和功能实现
4、运行与调试
在一个FreeRTOS应用中,系统可能大部分时间运行的都是空闲任务,而在空闲任务里使MCU进入睡眠状态是一种可行的低功耗设计策略。本文将分析利用空闲任务钩子函数实现低功耗的设计原理及编程应用。
一、空闲任务实现原理
空闲任务是FreeRTOS在启动内核的时候自动创建的一个任务,空闲任务的优先级最低,当没有其他任务处于运行状态时,空闲任务就处于运行状态。在实际的FreeRTOS应用中,系统可能大部分时间处于空闲状态。
FreeRTOS中有一个空闲任务钩子函数vApplicationIdleHook(),若将参数configUSE_IDLE_HOOK设置为1,在FreeRTOS进入空闲状态时,就会调用这个钩子函数。这个参数可以在CubeMX中设置,其默认值为0。
MCU有3种低功耗模式:睡眠(Sleep)模式、停止(Stop)模式和待机(Standby)模式。其中,睡眠模式是进入和唤醒响应最快的,通过WFI或WFE指令使系统进入睡眠模式,只要发生中断或事件,系统就从睡眠模式唤醒,继续执行。
利用FreeRTOS空闲任务的钩子函数和MCU睡眠模式的特点,在使用FreeRTOS时实现低功耗的一种基本方法就是:在空闲任务的钩子函数里,执行WFI或WFE指令使MCU进入睡眠模式,在发生中断或事件时,从睡眠模式唤醒。例如,空闲任务钩子函数最简单的代码如下:
/**
Dummy implementation of the callback function vApplicationIdleHook().
*/
#if (configUSE_IDLE_HOOK == 1)
__WEAK void vApplicationIdleHook (void){}
#endif/**
重写弱函数 vApplicationIdleHook().
*/
void vApplicationIdleHook(void)
{HAL_PWR_EnterSLEEPMode(PWR_LOWPOWERREGULATOR_ON,PWR_SLEEPENTRY_WFI);
}
函数HAL_PWR_EnterSLEEPMode()的功能就是使用WFI或WFE指令使MCU进入睡眠模式。
使用这个钩子函数后,系统运行时主任务、空闲任务和MCU处于睡眠模式的时序如下图所示。图中的横坐标是嘀嗒时间点,也就是发生SysTick定时中断的时间点。注意,“Sleep Mode”表示系统处于睡眠模式的时间段,不是某个任务。
- 在t1时刻,Task_Main处于运行状态,一个任务占用CPU的最短时间是1个嘀嗒周期。
- 在12时刻,空闲任务占用CPU,执行空闲任务的钩子函数,MCU进入睡眠模式。
- 在t3时刻,发生SysTick中断,将MCU从睡眠模式唤醒。FreeRTOS进行一次上下文切换,仍然是空闲任务占用CPU,又执行一次空闲任务的钩子函数,MCU又进入睡眠模式。
所以,在空闲任务的钩子函数里使MCU进入睡眠模式,在发生SySTick中断时MCU就会被唤醒。SysTick的中断周期是1ms,也就是睡眠模式一次时间长度不超过1ms,虽然可以连续进入睡眠模式。这如同一个人趴在桌子上睡觉,每隔1分钟被叫醒一次,没事就又趴下睡,虽然可以睡一会儿,但是睡不安稳。
另外,在系统中,除了SySTick定时器的周期中断,HAL的基础时钟(例如TIM6)也会每1ms产生一次中断。要避免HAL的基础时钟干扰,就应该关闭HAL基础时钟的中断。但是SysTick的中断是不能被关闭的,若关闭了,FreeRTOS就无法执行任务调度了。
二、设计示例
1、示例功能与CubeMX项目设置
本文设计一个示例测试在空闲任务钩子函数里实现低功耗。本示例的功能和使用流程如下:
- 在FreeRTOS中,启用空闲任务钩子函数,即设置参数configUSE_IDLE_HOOK为1。
- 创建一个任务Task_Main,用于使LED1闪烁。
- 使用KeyRight键和LED2,用外部中断方式检测KeyRight键状态,使LED2亮灭。目的是测试使用低功耗时是否能正常响应外部中断。
- 引用KEYLED文件夹中的文件,具体方法详见本文作者的其他文章。
- 继续使用旺宝红龙开发板STM32F407ZGT6 KIT V1.0。
- 一些信息可以参考本文作者发布的其他文章:细说STM32单片机HAL和FreeRTOS两个基础时钟的工作原理-CSDN博客 https://wenchm.blog.csdn.net/article/details/148405200?spm=1011.2415.3001.5331
细说STM32单片机FreeRTOS软件定时器及其应用实例-CSDN博客 https://wenchm.blog.csdn.net/article/details/148337431?spm=1011.2415.3001.5331
(1)RCC、SYS、Code Generator、USART3、TIM6
参数设置可见参考文章。
(2)GPIO
本示例使用外部中断方式检测KeyRight键输入。将KeyRight连接的PF6引脚设置为GPIO_EXTI6,下跳沿触发,内部上拉,EXTI6的抢占优先级设置为1。
(3)FreeRTOS
在SYS组件中,将HAL的基础时钟设置为TIM6。启用FreeRTOS,设置接口为CMSIS_V2。在参数设置部分,启用空闲任务的钩子函数,其他参数都保持默认值。创建一个任务Task_Main。
(4)NVIC
设置外部中断抢占优先级为1。
2、主程序
完成设置后,CubeMX会自动生成代码。在CubeIDE中打开项目,将KEY_LED驱动程序目录添加到项目搜索路径,在main()函数中添加用户功能代码,并且在文件main.c中重新实现EXTI中断事件处理的回调函数HAL_GPIO_EXTI_Callback(),用于对KeyRight键按下的中断做出处理。文件main.c的主要代码如下:
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "cmsis_os.h"
#include "usart.h"
#include "gpio.h"/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "keyled.h"
/* USER CODE END Includes *//* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
void MX_FREERTOS_Init(void);/*** @brief The application entry point.* @retval int*/
int main(void)
{/* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* Initialize all configured peripherals */MX_GPIO_Init();MX_USART3_UART_Init();/* USER CODE BEGIN 2 */// Start Menuuint8_t startstr[] = "Demo11_2:Using Idle Hook.\r\n";HAL_UART_Transmit(&huart3,startstr,sizeof(startstr),0xFFFF);uint8_t startstr1[] = "Enter sleep mode by WFI in idle hook.\r\n";HAL_UART_Transmit(&huart3,startstr1,sizeof(startstr1),0xFFFF);uint8_t startstr2[] = "KeyRight[S5] to toggle LED2.\r\n\r\n";HAL_UART_Transmit(&huart3,startstr2,sizeof(startstr2),0xFFFF);HAL_SuspendTick(); // Turn off the interrupt of the HAL basic clock (TIM6)/* USER CODE END 2 *//* Init scheduler */osKernelInitialize();/* Call init function for freertos objects (in cmsis_os2.c) */MX_FREERTOS_Init();/* Start scheduler */osKernelStart();/* We should never get here as control is now taken by the scheduler *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}//以下代码省略,直至/* USER CODE BEGIN 4 */
// KeyRight 按下后的外部中断
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{LED2_Toggle(); //KeyRight使LED2翻转
}
/* USER CODE END 4 *///以下代码省略,
main()函数中,执行函数HAL_SuspendTick()关闭HAL基础时钟的中断,也就是TIM6的中断,避免其周期定时中断将MCU从睡眠模式唤醒。
按键KeyRight使用外部中断EXTI6线,在沙箱段重新实现了EXTI中断的回调函数。它的功能就是在KeyRight键按下时,使LED2的输出翻转。此外,在这个回调函数里,不能使用延时函数HAL_Delay(),因为在main()函数里关闭了HAL基础定时器的中断,无法再使用HAL_Delay()。
3、FreeRTOS对象初始化和功能实现
自动生成的文件freertos.c中,有FreeRTOS对象初始化函数MX_FREERTOS_Init()、任务Task_Main的任务函数框架,以及空闲任务钩子函数vApplicationIdleHook()的代码框架。添加了任务函数和钩子函数的用户功能代码后,文件freertos.c的代码如下:
自动生成includes,并手动添加私有includes:
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "usart.h"
#include "keyled.h"
#include <stdio.h>
/* USER CODE END Includes */
自动生成任务函数的定义:
/* Definitions for Task_Main */
osThreadId_t Task_MainHandle;
const osThreadAttr_t Task_Main_attributes = {.name = "Task_Main",.stack_size = 128 * 4,.priority = (osPriority_t) osPriorityNormal,
};
自动生成函数原型:
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes *//* USER CODE END FunctionPrototypes */void AppTask_Main(void *argument);void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) *//* Hook prototypes */
void vApplicationIdleHook(void);
自动生成钩子函数的框架,手动添加该函数体:
在空闲任务钩子函数vApplicationIdleHook()里调用了函数HAL_PWR_EnterSLEEPMode(),使用WFI指令使MCU进入睡眠模式。在这个钩子函数里,决不能使用具有阻塞功能的函数,例如,带有阻塞等待时间的请求信号量、互斥量的函数或延时函数vTaskDelay()。
/* USER CODE BEGIN 2 */
//空闲任务钩子函数
void vApplicationIdleHook( void )
{/* vApplicationIdleHook() will only be called if configUSE_IDLE_HOOK is setto 1 in FreeRTOSConfig.h. It will be called on each iteration of the idletask. It is essential that code added to this hook function never attemptsto block in any way (for example, call xQueueReceive() with a block timespecified, or call vTaskDelay()). If the application makes use of thevTaskDelete() API function (as this demo application does) then it is alsoimportant that vApplicationIdleHook() is permitted to return to its callingfunction, because it is the responsibility of the idle task to clean upmemory allocated by the kernel to any task that has since been deleted. *///Use WFI instructions to make the MCU enter sleep mode.HAL_PWR_EnterSLEEPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI);
}
/* USER CODE END 2 */
自动生成FreeRTOS初始化代码,在其中创建任务函数句柄变量:
/*** @brief FreeRTOS initialization* @param None* @retval None*/
void MX_FREERTOS_Init(void)
{/* Create the thread(s) *//* creation of Task_Main */Task_MainHandle = osThreadNew(AppTask_Main, NULL, &Task_Main_attributes);
}
自动生成任务函数框架,手动添加函数体代码:
任务Task_Main的功能就是使LED1闪烁,周期是500ms。所以,这个FreeRTOS应用程序绝大部分时间处于空闲状态。
/* USER CODE BEGIN Header_AppTask_Main */
/*** @brief Function implementing the Task_Main thread.* @param argument: Not used* @retval None*/
/* USER CODE END Header_AppTask_Main */
void AppTask_Main(void *argument)
{/* USER CODE BEGIN AppTask_Main *//* Infinite loop */for(;;){LED1_Toggle(); //使LED1闪烁vTaskDelay(500);}/* USER CODE END AppTask_Main */
}
手动添加私有函数体代码:
/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
int __io_putchar(int ch)
{HAL_UART_Transmit(&huart3,(uint8_t*)&ch,1,0xFFFF);return ch;
}
/* USER CODE END Application */
4、运行与调试
构建项目后,我们将其下载到开发板上并运行测试。运行时LED1闪烁,按下KeyRigh键时,LED2会变化。使用电流表测量开发板的工作电流——稳定工作电流为200mA左右。
读者可以建立另一个功能相同,但是不使用任何低功耗处理的示例项目作为对比。用相同的方法测试开发板的电流,可以发现稳定工作电流为240mA。
可见,在空闲任务钩子函数里使系统进入休眠状态的方案减小了40mA的电流,降低功耗的效果是比较明显的。
首次下载或复位后,串口助手显示如下图,同时观察开发板LED,LED1常闪,按下S5键,LED2翻转,默认时LED2灭,按下S5后LED2亮,再次按下S5后,LED2灭,依次类推。