文章目录
一.问题引出
在裸机开发中,我们通过直接配置相关函数使系统进入低功耗模式,当有中断发送时将处理器唤醒,此配置过程为开发者可控。而RTOS系统的运行基于OS内核任务调度,任务之间的切换也由系统自动完成,相对不可控。因此对于系统低功耗配置FreeRTOS为系统低功耗配置提供了一种解决方法------->TICKLess。
二.TICKLess模式简介
RTOS系统的运行为用户任务与系统空闲任务交替运行,因此我们需要在处理器执行空闲任务时进入低功耗状态,当需要执行用户任务或者由中断发生时再将系统从低功耗状态唤醒。
而SYSTick系统滴答定时器为RTOS系统提供系统时钟,且会产生周期性中断,可以将系统从低功耗模式唤醒(sleep或stop模式),因此需要在进入低功耗之前将滴答关闭。对此我们面临了两个问题:
问题一:关闭系统滴答定时器会导致系统节拍计数器停止,系统时钟就会停止。
SYSTick是RTOS系统时钟,关闭SYSTick的话系统运行就停止了,是绝对不允许的。该如何让解决这个问题?我们可以记录下低功耗期间SYSTick关闭的时间,当系统退出低功耗模式后将这段时间补偿给系统节拍计数器,并重新打开SYSTick,正常运行。那么怎样记录睡眠的这段时间呢?STM32L系列单片机都有LPTIM定时器,其时钟可由LSE提供,且不受stop模式影响。因此LPTIM可以充当系统睡眠中的计数器。记录睡眠的时间,并在唤醒后将该时间补偿给系统。完美解决第一个问题。
问题二:如何知道需要睡眠的时间?也就是什么时候唤醒执行用户任务?
系统进入低功耗模式后只能由有用中断唤醒。那么在睡眠期间外部中断可以将系统唤醒。如果没有外部中断呢?那么就只能用之前提到的LPTIM定时器产生的中断,那么LPTIM定时器需要设置唤醒时间,这个时间怎么确定?其实这个时间就是系统任务切换之间的空闲时间,也就是可以进入低功耗的时间。值得庆幸的是FreeRTOS已经替我们算出来这个时间了。就是它:xExpectedIdleTime。
接下来结合代码分析配置及执行过程:
三.TICKLess的具体实现
1.空闲任务函数TICKLess的实现主要在port.c中的下面函数中:
void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
可以看出,此函数的唯一入口参数xExpectedIdleTime 不正是我们想要的空闲时间,也就是系统可以进入低功耗的最长时间,现在的情况就是FreeRTOS告诉我们这个时间,剩下的事情我们可以自己解决了。
那么这个函数又是在哪被调用的呢?前面我们谈到当系统执行空闲任务时可以进入低功耗,那么这个函数会不会在空闲任务执行函数中被调用?找一下果然是!!!
在task.c函数中找到了空闲任务执行函数如下,其中就包含对低功耗处理的调用。
static portTASK_FUNCTION( prvIdleTask, pvParameters ){
/*******************************/
/**************省略************/
/*******************************/
#if ( configUSE_TICKLESS_IDLE != 0 )
{
TickType_t xExpectedIdleTime;
xExpectedIdleTime = prvGetExpectedIdleTime(); //1.获取空闲任务执行时间
/*2.判断这个时间是都大于宏定义的某个值*/
if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )
{
vTaskSuspendAll();
{
configASSERT( xNextTaskUnblockTime >= xTickCount );
xExpectedIdleTime = prvGetExpectedIdleTime();
if( xExpectedIdleTime >= configEXPECTED_IDLE_TIME_BEFORE_SLEEP )
{
traceLOW_POWER_IDLE_BEGIN();
/*3.调用真正的处理函数,也就是我们真正的执行函数,入口参数就是时间*/
portSUPPRESS_TICKS_AND_SLEEP( xExpectedIdleTime );
traceLOW_POWER_IDLE_END();
}
else{
mtCOVERAGE_TEST_MARKER();
}
}
( void ) xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_TICKLESS_IDLE */
}
简单解释:首先判断TICKLess这个功能是否使能,也就是configUSE_TICKLESS_IDLE这个宏定义是否定义,如果使能了TICKLess,那么首先在1获得需要空闲时间xExpectedIdleTime,然后在2处判断这个时间是否大于某个宏定义值,也就是这个时间是都足够大,太短了就没必要进低功耗了,吃力不讨好!如果都满足上面条件的话那么就执行真正的低功耗处理函数,这个函数通过一些宏定义最终在port.c中实现,现在我们转到port.c中看看。
2.低功耗处理函数
#define portMAX_16_BIT_NUMBER 0xffff
#define lptim_CLOCK_HZ (32768/2) //定义lptim的时钟
#if configUSE_TICKLESS_IDLE == 2
void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
{
/*
xMaximumPossibleSuppressedTicks:最大睡眠时间
xMaximumPossibleSuppressedTicks=portMAX_16_BIT_NUMBER * configTICK_RATE_HZ / lptim_CLOCK_HZ
ulReloadValue:lptim重装载值 ulReloadValue = (lptim_CLOCK_HZ * (xExpectedIdleTime - 1UL)/configTICK_RATE_HZ);
ulCompleteTickPeriods:补偿值 根据是否为定时器唤醒分两种情况
ulCount:唤醒后获得的lptim的计数值 ulCount = HAL_LPTIM_ReadCounter(&hlptim1);
*/
uint32_t xMaximumPossibleSuppressedTicks,ulReloadValue, ulCompleteTickPeriods,ulCount;
xMaximumPossibleSuppressedTicks=portMAX_16_BIT_NUMBER * configTICK_RATE_HZ /
lptim_CLOCK_HZ;
//1.获得lptim可以计数的最大值//
printf("xMaximumPossibleSuppressedTicks=%d\r\n",xMaximumPossibleSuppressedTicks);
//printf("xExpectedIdleTime=%d\r\n",xExpectedIdleTime);
/* 2.确保睡眠时间不会超过定时器最大计数值 */
if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks )
{
xExpectedIdleTime =xMaximumPossibleSuppressedTicks;
} /*3.关闭滴答定时器,因为滴答定时器会干扰stop睡眠模式 */
portNVIC_SYSTICK_CTRL_REG &= ~portNVIC_SYSTICK_ENABLE_BIT; /* 4.根据延时参数计算lptim的定时器中断装在值 */
ulReloadValue = (lptim_CLOCK_HZ * (xExpectedIdleTime - 1UL)/configTICK_RATE_HZ);
//printf("ulReloadValue=%d\r\n",ulReloadValue);
__dsb( portSY_FULL_READ_WRITE ); //数据清除同步
__isb( portSY_FULL_READ_WRITE ); //指令清除同步
/* 5.将定时器中断标志位置0 */
LPTIM_ReloadMatch_flag=pdFALSE;
/* 6.确认是否可以进入低功耗模式,如果不能恢复正常后退出 */
if( eTaskConfirmSleepModeStatus() == eAbortSleep )
{
/* 重新配置滴答定时器 */
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
//开启滴答定时器
portNVIC_SYSTICK_LOAD_REG = configCPU_CLOCK_HZ/configTICK_RATE_HZ - 1UL;
//重新加载reload值
}
/* 7.可以进入低功耗模式 */
else
{
/* 8.使能低功耗定时器,低功耗定时器配置,psc=DIV1,arr=ulReloadValue,因此中断时间是ulReloadValue/32768S */
MX_LPTIM1_Init(LPTIM_PRESCALER_DIV2,ulReloadValue,0);
/* 9.入睡前处理 */
configPRE_SLEEP_PROCESSING( xExpectedIdleTime );
/* 如果睡眠时间大于零 */
if( xExpectedIdleTime > 0 )
{
__dsb( portSY_FULL_READ_WRITE );
//printf("enter stop!\r\n");
_HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU); //清除wakeup flag
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
//10.进入Stop模式
SystemClock_Config(); //11.退出Stop模式之后重新配置系统时钟//
printf("exit stop!\r\n");
__isb( portSY_FULL_READ_WRITE);
}
/* 12.唤醒后处理 */
configPOST_SLEEP_PROCESSING( xExpectedIdleTime );
/* 13.获取定时器当前计数值 */
ulCount = HAL_LPTIM_ReadCounter(&hlptim1);
//printf("ulCount=%d\r\n",ulCount);
/* 14.停止计数器 */
HAL_LPTIM_PWM_Stop_IT(&hlptim1);
/* 15是定时器引发的中断 */
if( LPTIM_ReloadMatch_flag != pdFALSE )
{
// printf("定时器中断引起的\r\n");
ulCompleteTickPeriods = xExpectedIdleTime - 1UL;
}
/* 16是外部中断唤醒的 */
else
{
//printf("外部中断引起的\r\n");
ulCompleteTickPeriods = (ulCount * configTICK_RATE_HZ)/ lptim_CLOCK_HZ;
}
//printf("ulCompleteTickPeriods=%d\r\n",ulCompleteTickPeriods);
/*进入临界区*/
portENTER_CRITICAL();
//uwTick += ulCompleteTickPeriods;
//更新 HAL 基本时基
vTaskStepTick( ulCompleteTickPeriods ); //17.补充时钟
/* 18.重新开启滴答定时器 */
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT; //开启滴答定时器
portNVIC_SYSTICK_LOAD_REG = configCPU_CLOCK_HZ/configTICK_RATE_HZ - 1UL; //重新加载reload值
/*退出临界区*/
portEXIT_CRITICAL();
}
}#endif /* #if configUSE_TICKLESS_IDLE */
详细分析此函数:
- 获得lptim可以计数的最大值,由于lptim定时器的计时是有上限的,总不可能无限长计时咯!因此要先算出这个上限值,具体计算方法根据公式自行理解。
- 确保睡眠时间不会超过定时器最大计数值,由于唤醒是需要借助Lptim定时器中断的,因此最长睡眠时间不能超过这个上限。
- 关闭滴答定时器,因为滴答定时器会干扰stop睡眠模式。
- 根据延时参数计算lptim的定时器中断装载值,也就是根据要睡眠的时间计算lptim的装在值。
- 将定时器中断标志位置0 。自有用处
- 确认是否可以进入低功耗模式,如果不能恢复正常后退出。要就是确保当前没有用户任务再需要执行了,没有的话就继续执行下面睡眠操作,有的话恢复退出!
- 可以进入低功耗模式。
- 使能低功耗定时器并开启中断,低功耗定时器配置,psc=DIV1,arr=ulReloadValue,因此中断时间是ulReloadValue/32768 S,也就是根据分频、之前算出的重装载值开启lptim定时器及中断。
- 入睡前处理,在进入睡眠前可以关闭系统相关时钟及其他处理,为了在睡眠时获得更低功耗,需要注意的是要快进快出,不可阻塞系统!
- 进入Stop模式,此时系统会停止在这儿,等待唤醒后向下执行。
- 退出Stop模式之后重新配置系统时钟,执行这一句说明已经退出低功耗了,需要重新配置时钟。
- 唤醒后处理,之前睡眠前disable的一些操作,是不是要enable了?
- 获取定时器当前计数值,为了给后面补偿系统计时用。
- 停止计数器,lptim使命到此结束。
- 前面说过有两种方式(外部中断或lptim定时器中断)使系统退出睡眠,现在就是判断是哪种方式唤醒的。也即是判断lptim中断标志位LPTIM_ReloadMatch_flag是否置位。
- 两种唤醒方式的补偿值不同,分别计算。
- 根据计算出的补偿值补充系统时钟。
- 重新开启滴答定时器,恢复正常运行。
四.低功耗TICKLess配置实现步骤
- 首先使能TICKLess功能,也就是配置宏configUSE_TICKLESS_IDLE == 2这里面,为0表示不使用TICKLess,为1表示使用系统自带低功耗模式(sleep模式+滴答定时器计时)为2表示自定义实现TICKLess功能。
- 设置最小进入TICKLess模式时间。configEXPECTED_IDLE_TIME_BEFORE_SLEEP=2,这里系统默认是2,可根据实际需求改动。
- 配置LPTIM定时器,供后续使用。
- 实现低功耗具体函数,void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
- 编写进入和退出低功耗处理的两个函数,
入睡前处理:configPRE_SLEEP_PROCESSING( xExpectedIdleTime );
唤醒后处理:configPOST_SLEEP_PROCESSING( xExpectedIdleTime );
五.注意事项
- 由于睡眠状态下是由外部中断唤醒的,不管是lptim还是其他中断,由于中断唤醒系统后OS系统还没有运行起来,因为还没有配置时钟,也没有补偿系统定时器。因此千万不要在中断服务函数中执行大量代码!!!更不能阻塞系统,推荐只能置一个标志位,等到系统完全启动之后根据标志位再执行相应动作。
- 意在进入睡眠前一定要关闭滴答定时器,退出时再配置。
- 注意LPTIM的配置,因为它决定了最大睡眠时间,xMaximumPossibleSuppressedTicks=portMAX_16_BIT_NUMBER * configTICK_RATE_HZ / lptim_CLOCK_HZ。这里面portMAX_16_BIT_NUMBER=0xFFFF是固定的,不能修改。configTICK_RATE_HZ=1000由系统决定,也不能修改。因此我们只能修改lptim_CLOCK_HZ,也就是LPTIM的时钟频率,系统默认是32728,计算得出最大睡眠时间是1999ms,修改lptim_CLOCK_HZ=32768/2之后系统最大睡眠时间是1999*2ms。