FreeRTOS低功耗管理
低功耗管理
很多应用场合对于空耗的要求很严格,比如可穿戴低功耗产品、物联网低功耗产品等。一般 MCU 都有相应的低功耗模式,裸机开发时可以使用 MCU 的低功耗模式。FreeRTOS 也提供了一个叫 Tickless 的低功耗模式,方便带 FreeRTOS 操作系统的应用开发
1. 低功耗管理介绍
1.1 STM32 低功耗模式
STM32 本身就支持低功耗模式,以 STM32F1 为例,其有三种低功耗模式:睡眠(Sleep)模式、停止(Stop)模式、待机(Standby)模式
三种模式的对比如下图示:
STM32 的电源管理系统主要分为以下三个部分:1 为备份域;2 为调压器供电电路;3 为 ADC 电源电路
1.2 FreeRTOS Tickless 低功耗模式
简单应用中处理器大量的时间都在处理空闲任务,所以可以考虑当处理器处理空闲任务时进入低功耗模式,当需要处理应用层代码时再将处理器从低功耗模式唤醒。一般采用基于时间片轮转的抢占式任务调度机制的低功耗设计思路为:当 idle 任务运行时,进入低功耗模式;在适当的条件下,通过中断或外部事件唤醒 MCU
但是该设计的缺陷是每当 OS 系统定时器产生中断时,也会将 MCU 从低功耗模式中唤醒,而频繁的进入 / 唤醒低功耗模式使得 MCU 无法进入深度睡眠,对于低功耗设计而言是不合理的。FreeRTOS 中设计的低功耗模式 ——Tickless Idle Mode,可以让 MCU 更长时间的处于低功耗模式
Tickless Idle Mode 的设计思想在于尽可能的在 MCU 空闲时使其进入低功耗模式,因此需要解决以下问题:
合理的进入低功耗模式,避免频繁使 MCU 在低功耗和运行模式下进行不必要的切换。RTOS 的系统时钟源于硬件的某个周期性定时器(Cotex-M 内核多数采用 SysTick),RTOS 的任务调度器可以预期到下一个周期性任务(或定时器任务)的触发时间,从而调整系统时钟定时器中断触发时间,以避免 RTOS 进入不必要的时间中断,从而更长时间停留在低功耗模式中。此时 RTOS 的时钟不再是周期的而是动态的(在原有的时钟基准时将不再产生中断,即 Tickless)当 MCU 被唤醒时,通过某种方式为系统时钟提供补偿。MCU 可能被动态调整过的系统时钟中断或突发性的外部事件所唤醒,都可以通过运行在低功耗模式下的某种定时器来计算出 MCU 处于低功耗模式下的时间,在 MCU 唤醒后对系统时间进行软件补偿软件实现时,根据具体的应用情景和 MCU 低功耗特性来处理问题。尤其是 MCU 的低功耗特性,不同 MCU 处于不同的低功耗模式下所能使用的外设(主要是定时器)是不同的,RTOS 的系统时钟可以进行适当的调整
2. Tickless 低功耗模式实现
2.1 宏 configUSE_TICKLESS_IDLE
要想使用 Tickless 模式,必须将 FreeRTOSConfig.h 中的如下宏置 1;FreeRTOS 只提供了个别的硬件平台模式,STM32 采用模式 1 即可,如果采用其他模式,配置为 2
1
#define configUSE_TICKLESS_IDLE 1 //启用低功耗Tickless模式
2.2 宏 portSUPPRESS_TICKS_AND_SLEEP
使能了 Tickless 模式后,当空闲任务是唯一可运行的任务(其他任务都处于阻塞或挂起态)以及系统处于低功耗模式的时间大于 configEXPECTED_IDLE_TIME_BEFORE_SLEEP
个时钟节拍时,FreeRTOS 内核就会调用宏 portSUPPRESS_TICKS_AND_SLEEP
来处理低功耗相关的工作
1
2
3
4
5
6
#ifndef portSUPPRESS_TICKS_AND_SLEEP \
extern void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime );
#define portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime ) \
vPortSuppressTicksAndSleep( xExpectedIdleTime )
#endif
//参数 xExpectedIdleTime 表示处理器将要在低功耗模式运行的时长
函数 vPortSuppressTicksAndSleep 是实际的低功耗执行代码,本来需要用户自己实现,但是针对 STM32 平台,FreeRTOS 已经帮我们实现了,其源码如下示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
__weak void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime){
uint32_t ulReloadValue,ulCompleteTickPeriods,ulCompletedSysTickDecrements, ulSysTickCTRL;
TickType_t xModifiableIdleTime;
/* 判断系统最小时间片(systick定时器的Reload值)是否大于systick的最大装载周期 */
if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks ){
/* MCU在低功耗模式运行的时长 */
xExpectedIdleTime = xMaximumPossibleSuppressedTicks;
}
/* 关闭systick定时器 */
portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT;
/* systick重载值= 当前的systick计数值+单次系统tick装载值*(系统最小时间片-1)*/
ulReloadValue = portNVIC_SYSTICK_CURRENT_VALUE_REG + ( ulTimerCountsForOneTick * ( xExpectedIdleTime - 1UL ) );
/* 装载值若大于补偿周期,则要减去补偿周期 */
if( ulReloadValue > ulStoppedTimerCompensation ){
ulReloadValue -= ulStoppedTimerCompensation;
}
/* 关闭中断,虽然关闭了中断,但可以唤醒CPU,不进行中断处理 */
__disable_irq();
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
/* 是否有其他任务,进入了就绪态 */
if( eTaskConfirmSleepModeStatus() == eAbortSleep ){
/* 不能进入低功耗模式,并将当前的systick计数值放到systick装载寄存器中 */
portNVIC_SYSTICK_LOAD_REG = portNVIC_SYSTICK_CURRENT_VALUE_REG;
/* 重新启动systick */
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
/* 重新赋值装载寄存器值为一个系统的tick周期. */
portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
/* 开启中断 */
__enable_irq();
}
else{
/* 可以进入低功耗模式,装载休眠systick装载值 */
portNVIC_SYSTICK_LOAD_REG = ulReloadValue;
/* 清除systick当前计数值 */
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* 启动systick定时器*/
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
xModifiableIdleTime = xExpectedIdleTime;
/* 进入低功耗前要处理的事情,需要用户实现休眠处理,以进一步降低功耗 */
configPRE_SLEEP_PROCESSING( &xModifiableIdleTime );
if( xModifiableIdleTime > 0 ){
/* 让CPU休眠 */
__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();
/* 判断是由外部中断还是systick定时器计时时间到唤醒的 */
if( ( ulSysTickCTRL & portNVIC_SYSTICK_COUNT_FLAG_BIT ) != 0 ){//systick唤醒的
uint32_t ulCalculatedLoadValue;
/*systick恢复值= 单个tick周期值- (休眠装载值-当前systick计数值)*/
ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL ) - ( ulReloadValue - portNVIC_SYSTICK_CURRENT_VALUE_REG );
/* 保护处理:装载值很小或大,都赋值为1个tick周期 */
if( ( ulCalculatedLoadValue < ulStoppedTimerCompensation ) || ( ulCalculatedLoadValue > ulTimerCountsForOneTick ) ){
ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL );
}
/* 装载恢复systick装载值 */
portNVIC_SYSTICK_LOAD_REG = ulCalculatedLoadValue;
/* 休眠周期的补偿值,单位为tick */
ulCompleteTickPeriods = xExpectedIdleTime - 1UL;
}
else{//外部中断唤醒的,需进行时间补偿
/* 休眠运行装载值= 休眠装载值-当前systick计数值)*/
ulCompletedSysTickDecrements = ( xExpectedIdleTime * ulTimerCountsForOneTick ) - portNVIC_SYSTICK_CURRENT_VALUE_REG;
/* 休眠运行周期,单位为tick值 */
ulCompleteTickPeriods = ulCompletedSysTickDecrements / ulTimerCountsForOneTick;
/* 装载恢复systick装载值 */
portNVIC_SYSTICK_LOAD_REG = ( ( ulCompleteTickPeriods + 1UL ) * ulTimerCountsForOneTick ) - ulCompletedSysTickDecrements;
}
/* 清除systick计数值,重启systick定时器,恢复systick周期为1个tick值 */
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
portENTER_CRITICAL();
{
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
vTaskStepTick( ulCompleteTickPeriods );//补偿系统时钟
portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
}
portEXIT_CRITICAL();
}
}
2.3 宏 configPRE_SLEEP_PROCESSING() 和 configPOST_SLEEP_PROCESSING()
在低功耗设计中不仅是将处理器设置到低功耗模式就行了,有时还需要做一些其他处理,比如将处理器降低到合适的频率、修改时钟源(切换到内部时钟源)、关闭外设时钟以及关闭其他功能模块电源等
1
2
3
4
#if configUSE_TICKLESS_IDLE == 1
#define configPRE_SLEEP_PROCESSING PreSleepProcessing//进入低功耗前要处理的事情
#define configPOST_SLEEP_PROCESSING PostSleepProcessing//退出低功耗后要处理的事情
#endif /* configUSE_TICKLESS_IDLE == 1 */
弱符号函数 PreSleepProcessing 和 PostSleepProcessing 需要用户自已根据需要编写
2.4 宏 configEXPECTED_IDLE_TIME_BEFORE_SLEEP
处理器工作在低功耗模式的时间没有任何限制,可以等于 1 个时钟节拍,但是时间太短的话就没有意义,比如 1 个时钟节拍,刚进入低功耗模式就要退出低功耗模式。因此需要对工作在低功耗模式的时间加一个限制,宏 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 就是用来完成此功能的
默认情况下此宏设置为 2 个时钟节拍,且最小不能小于 2 个时钟节拍
1
2
3
4
5
6
#ifndef configEXPECTED_IDLE_TIME_BEFORE_SLEEP
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2
#endif
#if configEXPECTED_IDLE_TIME_BEFORE_SLEEP < 2
#error configEXPECTED_IDLE_TIME_BEFORE_SLEEP must not be less than 2
#endif
3. Tickless 低功耗模式实例
本实例介绍如何使用 FreeRTOS 的低功耗 Tickless 模式。本例程是在二值信号量例程的基础上增加了低功耗模式
使用 STM32CubeMX 将 FreeRTOS 移植到工程中,创建两个任务、一个二值信号量,开启串口中断。
LED_Task:闪烁 LED1,提示系统运行正常 CMDprocess_Task:根据串口收到的指令,控制不同的 LED2/LED3 的亮灭 二值信号量:用于串口中断和 CMDprocess_Task 任务间的同步
3.1 STM32CubeMX 设置
- RCC 设置外接 HSE,时钟设置为 72M
- PC0/PC1/PC2 设置为 GPIO 推挽输出模式、上拉、高速、默认输出电平为高电平
- USART1 选择为异步通讯方式,波特率设置为 115200Bits/s,传输数据长度为 8Bit,无奇偶校验,1 位停止位;开启串口中断
- 激活 FreeRTOS,添加任务,设置任务名称、优先级、堆栈大小、函数名称等参数
- 动态创建二值信号量
- 启用低功耗 Tickless 模式
- 使用 FreeRTOS 操作系统,需要将 HAL 库的 Timebase Source 从 SysTick 改为其他定时器,选好定时器后,系统会自动配置 TIM
- 输入工程名,选择路径(不要有中文),选择 MDK-ARM V5;勾选 Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击 GENERATE CODE,生成工程代码
3.2 MDK-ARM 软件编程
- 添加低功耗相关函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__weak 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();
}
__weak 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();
}
- 添加 LEDTask、CMDprocessTask 任务函数代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/******************LEDTask**************************/
void LEDTask(void const * argument){
for(;;){
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);
osDelay(500);
}
}
/******************CMDprocessTask*******************/
void CMDprocessTask(void const * argument){
BaseType_t err = pdFALSE;
for(;;){
if(BinarySemHandle != 0){
err = xSemaphoreTake(BinarySemHandle,portMAX_DELAY);
if(err == pdPASS){
printf("CMDprocessTask take the binary Semaphore!\r\n");
printf("Received CMD is:");
for(int i =0;i<8;i++)
printf("%c",RxBuff[i]);
printf("\n");
if(strncmp((char *)RxBuff,"LED2on",6) == 0)
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_1,GPIO_PIN_RESET);
else if(strncmp((char *)RxBuff,"LED2off",6) == 0)
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_1,GPIO_PIN_SET);
else if(strncmp((char *)RxBuff,"LED3on",6) == 0)
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_2,GPIO_PIN_RESET);
else if(strncmp((char *)RxBuff,"LED3off",6) == 0)
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_2,GPIO_PIN_SET);
else
printf("invalid CMD,please input LED2on LED2off LED3on or LED3off\r\n");
}
else
osDelay(10);
}
}
}
- 添加串口中断回调函数:串口接收完命令后释放二值信号量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
RxBuff[Rx_Count++]=RxByte;
if((RxByte==0x0A)&&(BinarySemHandle!=0)){
xSemaphoreGiveFromISR(BinarySemHandle,NULL);
printf("Semaphore Give FromISR succesed!\r\n");
Rx_Count=0;
}
if(Rx_Count > 8){
printf("Wrong CMD, Please Check...!\r\n");
memset(RxBuff,0,sizeof(RxBuff));
Rx_Count=0;
}
while(HAL_UART_Receive_IT(&huart1,&RxByte,1)==HAL_OK);
}
- 在 main.c 中开启串口接收中断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(void){
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
printf("BinarySemaphore test....\r\n");
if(HAL_OK == HAL_UART_Receive_IT(&huart1,&RxByte,1))
printf("UART_Receive_IT successed!\r\n");
else
printf("UART_Receive_IT failed!\r\n");
/* USER CODE END 2 */
MX_FREERTOS_Init();
osKernelStart();
while (1)
{
}
}
3.3 下载验证
编译无误下载到开发板后,打开串口调试助手,通过串口命令可以控制 LED2/LED3 的亮灭
使用 USB 电流计可以观察到开启了 Tickless 模式后,系统的工作电流会有所降低
以上转载自博主“安迪西嵌入式”的FreeRTOS专栏,仅作学习记录,如有侵权,请联系删除。