基于FreeRTOS的TICKLess 模式配置详解

一.问题引出

  在裸机开发中,我们通过直接配置相关函数使系统进入低功耗模式,当有中断发送时将处理器唤醒,此配置过程为开发者可控。而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 */

详细分析此函数:

  1. 获得lptim可以计数的最大值,由于lptim定时器的计时是有上限的,总不可能无限长计时咯!因此要先算出这个上限值,具体计算方法根据公式自行理解。
  2. 确保睡眠时间不会超过定时器最大计数值,由于唤醒是需要借助Lptim定时器中断的,因此最长睡眠时间不能超过这个上限。
  3. 关闭滴答定时器,因为滴答定时器会干扰stop睡眠模式。
  4. 根据延时参数计算lptim的定时器中断装载值,也就是根据要睡眠的时间计算lptim的装在值。
  5. 将定时器中断标志位置0 。自有用处
  6. 确认是否可以进入低功耗模式,如果不能恢复正常后退出。要就是确保当前没有用户任务再需要执行了,没有的话就继续执行下面睡眠操作,有的话恢复退出!
  7. 可以进入低功耗模式。
  8. 使能低功耗定时器并开启中断,低功耗定时器配置,psc=DIV1,arr=ulReloadValue,因此中断时间是ulReloadValue/32768 S,也就是根据分频、之前算出的重装载值开启lptim定时器及中断。
  9. 入睡前处理,在进入睡眠前可以关闭系统相关时钟及其他处理,为了在睡眠时获得更低功耗,需要注意的是要快进快出,不可阻塞系统!
  10. 进入Stop模式,此时系统会停止在这儿,等待唤醒后向下执行。
  11. 退出Stop模式之后重新配置系统时钟,执行这一句说明已经退出低功耗了,需要重新配置时钟。
  12. 唤醒后处理,之前睡眠前disable的一些操作,是不是要enable了?
  13. 获取定时器当前计数值,为了给后面补偿系统计时用。
  14. 停止计数器,lptim使命到此结束。
  15. 前面说过有两种方式(外部中断或lptim定时器中断)使系统退出睡眠,现在就是判断是哪种方式唤醒的。也即是判断lptim中断标志位LPTIM_ReloadMatch_flag是否置位。
  16. 两种唤醒方式的补偿值不同,分别计算。
  17. 根据计算出的补偿值补充系统时钟。
  18. 重新开启滴答定时器,恢复正常运行。

四.低功耗TICKLess配置实现步骤

  1. 首先使能TICKLess功能,也就是配置宏configUSE_TICKLESS_IDLE == 2这里面,为0表示不使用TICKLess,为1表示使用系统自带低功耗模式(sleep模式+滴答定时器计时)为2表示自定义实现TICKLess功能。
  2. 设置最小进入TICKLess模式时间。configEXPECTED_IDLE_TIME_BEFORE_SLEEP=2,这里系统默认是2,可根据实际需求改动。
  3. 配置LPTIM定时器,供后续使用。
  4. 实现低功耗具体函数,void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
  5. 编写进入和退出低功耗处理的两个函数,
    入睡前处理:configPRE_SLEEP_PROCESSING( xExpectedIdleTime );
    唤醒后处理:configPOST_SLEEP_PROCESSING( xExpectedIdleTime );

五.注意事项

  1. 由于睡眠状态下是由外部中断唤醒的,不管是lptim还是其他中断,由于中断唤醒系统后OS系统还没有运行起来,因为还没有配置时钟,也没有补偿系统定时器。因此千万不要在中断服务函数中执行大量代码!!!更不能阻塞系统,推荐只能置一个标志位,等到系统完全启动之后根据标志位再执行相应动作。
  2. 意在进入睡眠前一定要关闭滴答定时器,退出时再配置。
  3. 注意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。
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页