目录
(1)RCC、SYS、Code Generator、USART3、TIM6、GPIO、NVIC
在一个FreeRTOS应用中,系统可能大部分时间运行的都是空闲任务,而在空闲任务里使MCU进入睡眠状态是一种可行的低功耗设计策略。本文将分析利用Tickless模式实现低功耗的设计原理及编程应用。
一些内容需要参考本文作者发布的其他文章:
细说STM32单片机FreeRTOS空闲任务及低功耗的设计方法及应用-优快云博客 https://wenchm.blog.youkuaiyun.com/article/details/148448798?spm=1011.2415.3001.5331
一、 Tickless模式的原理和功能
在空闲任务钩子函数里使MCU进入睡眠模式,这种方法虽然有一定的降低功耗的效果,但是存在一个问题:就是每次SysTick中断时都会唤醒MCU,而这个周期是1ms,系统的唤醒非常频繁。
理想的低功耗模式应该像上图所示的情况:在t2时刻进入睡眠模式后,暂时关闭SysTick中断,这样就不会在后面的t3、t4等时刻唤醒MCU;而在t2时刻进入低功耗状态的时候,FreeRTOS就计算出下一个非空闲任务的运行时刻,例如,t6时刻,在t6时刻又开启SysTick中断进行正常的任务调度。MCU还需要能正常响应其他中断,如外部中断,在中断发生时,提前结束预定时间的睡眠状态,并且对嘀嗒信号计数值进行补偿,使FreeRTOS继续正常运行。
要实现这样的功能,只是处理空闲任务的钩子函数是不够的。好在FreeRTOS已经做好了这些功能,它有一个Tickless模式,可自动实现上图所示的低功耗功能。要使用Tickless低功耗模式,需要设置一个参数configUSE_TICKLESS_IDLE。
这个参数在Kernel settings参数组,有以下3种可选值。
- Disabled,对应参数值0,表示不使用Tickless功能。
- Built in functionality enabled,对应参数值1,使用FreeRTOS内建的函数实现Tickless低功耗功能。
- User defined functionality enabled,对应参数值2,使用用户定义的函数实现Tickless低功耗功能。
这个参数的默认值是Disabled。如果要使用Tickless低功耗功能,一般设置参数值为1,也就是使用FreeRTOS内建的函数实现Tickless低功耗功能。
FreeRTOS还有一个参数configEXPECTED_IDLE_TIME_BEFORE_SLEEP,默认值为2,表示当空闲任务持续至少2个节拍时,FreeRTOS才会启动Tickless低功耗模式。这个参数无法在CubeMX里修改,但是可以在文件FreeRTOSConfig.h中重新定义。
当参数configUSE_TICKLESS_IDLE设置为1,且空闲任务预期的持续时间大于参数configEXPECTED_IDLE_TIME_BEFORE_SLEEP设置的值时,FreeRTOS就会在进行上下文切换时计算MCU处于睡眠模式的预期节拍数,然后调用内部定义的一个弱函数vPortSuppressTicksAndSleep(),停止SysTick定时器中断,使MCU进入睡眠模式。
在达到预期的睡眠时间,或者任何其他中断将MCU从睡眠模式唤醒时,FreeRTOS会自动计算嘀嗒信号计数值的补偿值,在MCU被唤醒时将嘀嗒计数值加上补偿值,以保持嘀嗒计数值的持续性。
睡眠模式持续时间的预期值有个最大值,这个最大值不是用户在任务里执行vTaskDelay()使任务进入阻塞时间的长度,而是在SysTick定时器初始化函数vPortSetupTimerInterrupt()里计算的,此函数的源代码:
/*
* Setup the timer to generate the tick interrupts. The implementation in this
* file is weak to allow application writers to change the timer used to
* generate the tick interrupt.
*/
void vPortSetupTimerInterrupt( void );
/*
* Setup the systick timer to generate the tick interrupts at the required
* frequency.
*/
__attribute__(( weak )) void vPortSetupTimerInterrupt( void )
{
/* Calculate the constants required to configure the tick interrupt. */
#if( configUSE_TICKLESS_IDLE == 1 )
{
ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
}
#endif /* configUSE_TICKLESS_IDLE */
/* Stop and clear the SysTick. */
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* Configure SysTick to interrupt at the requested rate. */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
当configUSE_TICKLESS_IDLE等于1时,系统会计算一个全局变量xMaximumPossibleSuppressedTicks的值,在STM32F407的HCLK设置为168MHz,SysTick默认频率为1000Hz时,这个值是99。也就是说,在Tickless模式下,如果没有其他中断将MCU从睡眠模式唤醒,睡眠模式一次最多也只能持续99个节拍。99个节拍之后,MCU被唤醒,经过一些处理后再进入睡眠模式。
如果参数configUSE_TICKLESS_IDLE设置为2,就需要用户自己定义函数实现Tickless低功耗功能,也就是需要用户重新实现弱函数vPortSuppressSTicksAndSleep()。这种情况一般用于将FreeRTOS移植到其他处理器时。因为在STM32 MCU上已经移植好了,所以一般就用FreeRTOS内建的函数实现Tickless低功耗功能。
二、Tickless模式的使用示例
1、示例功能与CubeMX项目设置
本文的示例测试Tickless低功耗模式的使用,示例的功能和使用流程如下。
- 配置FreeRTOS时,将参数USE_TICKLESS_IDLE设置为Built in functionality enabled。
- 创建一个任务Task_Main,用于使LED1闪烁。
- 使用KeyRight键和LED2,用外部中断方式检测KeyRight键,使LED2亮灭,用于测试使用Tickless低功耗模式时,能否正常响应外部中断。
(1)RCC、SYS、Code Generator、USART3、TIM6、GPIO、NVIC
参数设置可见参考文章。
(2)FreeRTOS
在SYS组件中,将HAL的基础时钟设置为TIM6。启用FreeRTOS,设置接口为CMSIS_V2。在参数设置部分,将USE_TICKLESS_IDLE设置为Built in functionality enabled。创建一个任务Task_Main。
2、主程序
完成设置后,CubeMX自动生成代码。在CubeIDE中打开项目,将KEY_LED驱动程序目录添加到项目搜索路径,稍微修改main()函数中的代码,完成后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 Menu
uint8_t startstr[] = "Demo11_2: Tickless Mode.\r\n";
HAL_UART_Transmit(&huart3,startstr,sizeof(startstr),0xFFFF);
uint8_t startstr1[] = "LED1 is toggled in Task_Main.\r\n";
HAL_UART_Transmit(&huart3,startstr1,sizeof(startstr1),0xFFFF);
uint8_t startstr2[] = "Press KeyRight[S5] to toggle LED2.\r\n\r\n";
HAL_UART_Transmit(&huart3,startstr2,sizeof(startstr2),0xFFFF);
/* 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 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
LED2_Toggle(); // press KeyRight to toggle LED2
uint8_t startstr3[] = "LED2 toggled after S5 pressed.\r\n";
HAL_UART_Transmit(&huart3,startstr3,sizeof(startstr3),0xFFFF);
}
/* USER CODE END 4 */
//省略以下代码
在main()函数中,执行函数HAL_SuspendTick()的语句被注释掉了,这是因为在后面的代码里有更灵活的处理方法。
3、FreeRTOS对象初始化和功能实现
在使用内建函数的Tickless模式时,CubeMX在文件freertos.c中自动生成了两个弱函数的代码框架。为任务函数AppTask_Main()添加用户功能代码,完成后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,
};
自动生成任务函数、RTOS初始化函数原型:
/* 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) */
自动生成PreSleepProcessing、PostSleepProcessing函数框架,手动添加函数体代码:
函数PreSleepProcessing()是函数vPortSuppressTicksAndSleep()里执行WFI指令之前调用的一个回调函数,函数PostSleepProcessing()是在执行WFI指令之后调用的一个回调函数。
可以重新实现这两个函数以进行一些处理。例如,本示例在main()函数中没有关闭HAL基础时钟的中断,就可以改为在每次进入睡眠模式之前关闭HAL基础时钟的中断,在退出睡眠模式后再重新打开HAL基础时钟的中断。
这样,在HCLK为168MHz、TICK_RATE_HZ为1000时,MCU进入睡眠模式可以最多持续99个节拍,而不是像之前使用空闲任务钩子函数的方法那样只持续一个节拍就被SysTick中断唤醒。Tickless模式使用简便,所以在实际中一般就使用Tickless模式实现低功耗。
/* USER CODE BEGIN PREPOSTSLEEP */
__weak void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
/* place for user code */
HAL_SuspendTick(); //Turn off interrupts of HAL base clock (TIM6)
}
__weak void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
/* place for user code */
HAL_ResumeTick(); //Turn on interrupts of HAL base clock (TIM6)
}
/* USER CODE END PREPOSTSLEEP */
自动生成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);
}
自动生成任务函数框架,手动添加函数体代码:
/**
* @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 flashes
//逐条打印到串口助手
printf("LED1 flashes regularly.\r\n");
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定时闪烁,按下KeyRight键可以使LED2变化。测量稳定工作电流为202mA,与启用IdleHook时的稳定工作电流相比较相差很小。
发现按键抖动的厉害,是因为没有也不能设计消抖的延时。