打印
[应用相关]

基于FreeRTOS的TICKLess 模式配置详解

[复制链接]
1045|8
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
coshi|  楼主 | 2021-7-7 13:09 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
一.问题引出
  在裸机开发中,我们通过直接配置相关函数使系统进入低功耗模式,当有中断发送时将处理器唤醒,此配置过程为开发者可控。而RTOS系统的运行基于OS内核任务调度,任务之间的切换也由系统自动完成,相对不可控。因此对于系统低功耗配置FreeRTOS为系统低功耗配置提供了一种解决方法------->TICKLess。


使用特权

评论回复
沙发
coshi|  楼主 | 2021-7-7 13:10 | 只看该作者
二.TICKLess模式简介
  RTOS系统的运行为用户任务与系统空闲任务交替运行,因此我们需要在处理器执行空闲任务时进入低功耗状态,当需要执行用户任务或者由中断发生时再将系统从低功耗状态唤醒。

  而SYSTick系统滴答定时器为RTOS系统提供系统时钟,且会产生周期性中断,可以将系统从低功耗模式唤醒(sleep或stop模式),因此需要在进入低功耗之前将滴答关闭。对此我们面临了两个问题:
  问题一:关闭系统滴答定时器会导致系统节拍计数器停止,系统时钟就会停止。
  SYSTick是RTOS系统时钟,关闭SYSTick的话系统运行就停止了,是绝对不允许的。该如何让解决这个问题?我们可以记录下低功耗期间SYSTick关闭的时间,当系统退出低功耗模式后将这段时间补偿给系统节拍计数器,并重新打开SYSTick,正常运行。那么怎样记录睡眠的这段时间呢?STM32L系列单片机都有LPTIM定时器,其时钟可由LSE提供,且不受stop模式影响。因此LPTIM可以充当系统睡眠中的计数器。记录睡眠的时间,并在唤醒后将该时间补偿给系统。完美解决第一个问题。
  问题二:如何知道需要睡眠的时间?也就是什么时候唤醒执行用户任务?
  系统进入低功耗模式后只能由有用中断唤醒。那么在睡眠期间外部中断可以将系统唤醒。如果没有外部中断呢?那么就只能用之前提到的LPTIM定时器产生的中断,那么LPTIM定时器需要设置唤醒时间,这个时间怎么确定?其实这个时间就是系统任务切换之间的空闲时间,也就是可以进入低功耗的时间。值得庆幸的是FreeRTOS已经替我们算出来这个时间了。就是它:xExpectedIdleTime。
  接下来结合代码分析配置及执行过程:


使用特权

评论回复
板凳
coshi|  楼主 | 2021-7-7 13:12 | 只看该作者
三.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中看看。


使用特权

评论回复
地板
coshi|  楼主 | 2021-7-7 13:14 | 只看该作者
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 */


使用特权

评论回复
5
coshi|  楼主 | 2021-7-7 13:14 | 只看该作者
详细分析此函数:

  • 获得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是否置位。
  • 两种唤醒方式的补偿值不同,分别计算。
  • 根据计算出的补偿值补充系统时钟。
  • 重新开启滴答定时器,恢复正常运行。



使用特权

评论回复
6
coshi|  楼主 | 2021-7-7 13:15 | 只看该作者
四.低功耗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 );


使用特权

评论回复
7
coshi|  楼主 | 2021-7-7 13:17 | 只看该作者
五.注意事项
由于睡眠状态下是由外部中断唤醒的,不管是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。


使用特权

评论回复
8
木木guainv| | 2021-8-6 12:35 | 只看该作者
这种模式主要应用在什么情况下啊

使用特权

评论回复
9
xiaoqizi| | 2021-8-6 12:39 | 只看该作者
各个系统的区别大吗

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

95

主题

3301

帖子

4

粉丝