FreeRTOS的Tickless和STM32的STOP结合实现嵌入式低功耗 (amobbs.com 阿莫电子论坛)
本帖最后由 10xjzheng 于 2015-9-29 21:42 编辑
首先我们先来看下结果,如下图所示,我建立了两个LED翻转的任务,一个是200ms间隔,一个是500ms间隔,如第一二个波形所示,
第三波形如果是高电平,则处于STOP模式,如果是低电平,则处于run模式。
将波形进行放大,可以看到芯片及时地在任务1、2执行之前进行翻转。
首先我们来讨论下嵌入式系统的低功耗,简单地思路是干完活进入空闲任务的时候进入,然后醒来查看是否有任务需要执行,没有的话就继续进入空闲任务,这样子看来看来似乎还是可以的。
下面我们来看一个案例,只有一个任务,运行频率时1Hz,下面中我们可以看到在这1s内CPU不断地醒来,睡去,那么我们不禁回想,
既然已经知道任务地执行频率时1Hz,为什么还要频繁地查询?频繁地查询其实是很耗时的,因为进入和退出都需要一定的时间。
因此Tickless应运而生,在操作系统的管理之下,计算下一次应该醒来的准确时间,以实现长而少的睡眠-唤醒,而不是跟着tick的频
率无谓地醒来检查。正如我放上来的第一张图,可以看到需要翻转的时候,CPU就醒来才进行翻转。
接着我们需要结合CPU的低功耗模式来看看那种模式比较合适。
有三种模式,这三种模式工作的单元越来越少,而功耗也越来越低,首先standby不符合我的系统,醒来相当于复位。sleep和stop的
主要区别是stop模式下HSI、HSE时钟也会被关掉(这就是为什么系统醒来之后会好像慢了一样),stop模式的功耗也更低,粗略测
试时几mA,关掉HSI、HSE对我的系统没有什么影响,最多醒来的时候再配置一下。但是唤醒源不能再是任何的时钟了,只能是
EXTI Line,包括了RTC wake up定时器,于是我得首先将systick转变成RTC wake up timer,让它来担任其系统心跳的职责,修改的
步骤如下。
freeRTOS官方已经意识到担任心跳的定时器有可能限制于低功耗、在tickless模式溢出等因素不能进行工作,因此有文章介绍了怎么
修改为别的定时器来担任这个职责。
http://www.freertos.org/low-power-ARM-cortex-rtos.html
主要分为几个步骤:
1.配置新的定时器,然后将其覆盖掉其中的定时器配置。这里面有个很重要的点,将定时器的优先级配置为最低,至于为什么,见Cortex-M3中的图。
- #if configOVERRIDE_DEFAULT_TICK_CONFIGURATION == 0
- 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_16_BIT_NUMBER+1) / ulTimerCountsForOneTick;
- // ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
- ulStoppedTimerCompensation=0;
- }
- #endif /* configUSE_TICKLESS_IDLE */
- /* Configure RTC to interrupt at the requested rate. */
- RTC_Config();
- // 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 );
- }
- #endif /* configOVERRIDE_DEFAULT_TICK_CONFIGURATION */
复制代码
2.删除掉Systick中断函数调用的osSystickHandler();,给RTC的wake up 定时器中断。
接着我们就要开始配置tickless模式了。
1.首先将configUSE_TICKLESS_IDLE这个宏置1 or 2,1表示使用systick,2表示使用另外的定时器。
这个宏为1,则使用官方在port.c中的移植。
这个宏为2,可以使用自己的一些移植。
实际上我是使能为1,使用非systick中断,但是对应修改官方移植好的函数。
2.配置下面几个宏
这个宏是你定时器计数的频率,我的RTC配置为2KHz,所以这里配置为2000
#define configCPU_CLOCK_HZ ( 2000 )
这个宏是心跳的频率,我设置为1000
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
这几个设置的作用见前面的函数vPortSetupTimerInterrupt,这个函数中间还有变量ulStoppedTimerCompensation,这个变量是在
配置tickless的过程中暂时关掉定时器需要设置的偏移,至于多少要用逻辑分析仪测试一下,然后配置下就可以,上面直接配置为0。
2.修改函数vPortSuppressTicksAndSleep,这个函数就是系统调用进入stop模式之前的配置,这里涉及到一些定时器最大可以定时
的时间等等参数的配合。这个函数有一个参数,这个参数表示要延时的tick数,函数中我们可以将其转化为定时器要延时的时间个数。
这个函数在系统的管理之下,只有满足两个条件才运行。
1.首先所有的任务都是挂起或者阻塞的状态。
2.下一次系统应该醒来的时间应该大于configEXPECTED_IDLE_TIME_BEFORE_SLEEP,这个宏是用户可以定义的,默认配置为2。
- /*-----------------------------------------------------------*/
- #if configUSE_TICKLESS_IDLE == 1
- __weak void vPortSuppressTicksAndSleep( TickType_t xExpectedIdleTime )
- {
- uint32_t ulReloadValue, ulCompleteTickPeriods, ulCompletedSysTickDecrements, ulSysTickCTRL;
- TickType_t xModifiableIdleTime;
- uint32_t i,j;
- /* 确保计算出来的不会超过xMaximumPossibleSuppressedTicks */
- if( xExpectedIdleTime > xMaximumPossibleSuppressedTicks )
- {
- xExpectedIdleTime = xMaximumPossibleSuppressedTicks;
- }
- //计算定时器需要重装的值
- ulReloadValue = ( ulTimerCountsForOneTick * xExpectedIdleTime );
- //允许减去一些偏移,ulStoppedTimerCompensation在函数vPortSetupTimerInterrupt中可以进行配置
- //可以通过逻辑分析仪测量,单位跟定时器是一样的
- if( ulReloadValue > ulStoppedTimerCompensation )
- {
- ulReloadValue -= ulStoppedTimerCompensation;
- }
- /* 暂时关闭掉RTC定时器,配置RTC,这会导致系统时钟的一些漂移*/
- RTC_WakeUpCmd(DISABLE);
- //进入临界段
- __disable_irq();
- //最后检查下是否可以进入低功耗模式,因为有可能在这个配置过程中发生中断,导致某些任务就绪等等
- if( eTaskConfirmSleepModeStatus() == eAbortSleep )
- {
- //将RTC配置为原来的配置
- RTC_SetWakeUpCounter(ulTimerCountsForOneTick-1);
- //使能RTC
- RTC_WakeUpCmd(ENABLE);
- //退出临界段
- __enable_irq();
- }
- else
- {
- //重装值为前面计算好的
- RTC_SetWakeUpCounter(ulReloadValue);
- //使能RTC定时器
- RTC_WakeUpCmd(ENABLE);
- xModifiableIdleTime = xExpectedIdleTime;
- //进入低功耗之前需要做的配置,比如关闭外设时钟
- configPRE_SLEEP_PROCESSING( xModifiableIdleTime );
- if( xModifiableIdleTime > 0 )
- {
- __dsb( portSY_FULL_READ_WRITE );
- //进入低功耗stop模式,在这里插入CPU进入低功耗的语句
- PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
- // __isb( portSY_FULL_READ_WRITE );
- }
- //退出低功耗模式应该做的事情
- configPOST_SLEEP_PROCESSING( xExpectedIdleTime );
- //重新使能中断
- __enable_irq();
- //没有这个延时RTCWakeupFlag居然为0,我操,不知道为什么
- for(i=0;i<5;i++);
- printf("44:%d-\r\n",RTCWakeupFlag);
- //看下是不是tick中断到期,因为其他的中断也是可以唤醒CPU,执行到这里来的,计算重载值
- if( RTCWakeupFlag==SET )
- {
- uint32_t ulCalculatedLoadValue;
- // Uart_Sendbyte1('A');
- //这里省略精确的计算
- // //精确计算下一个需要延时的时间,一个tick需要延时的时间减去从长延时恢复之后到现在一共过了的时间
- // ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL ) - ( ulReloadValue - portNVIC_SYSTICK_CURRENT_VALUE_REG );
- // //计算出来的越界了,可能是因为函数configPOST_SLEEP_PROCESSING运行太久了!
- // if( ( ulCalculatedLoadValue < ulStoppedTimerCompensation ) || ( ulCalculatedLoadValue > ulTimerCountsForOneTick ) )
- // {
- // ulCalculatedLoadValue = ( ulTimerCountsForOneTick - 1UL );
- // }
- //计算修正的值
- ulCompleteTickPeriods = xExpectedIdleTime - 1UL;
- }
- else //不是tick中断,其他的中断
- {
- // Uart_Sendbyte1('B');
- //已经完成的定时器计数
- // ulCompletedSysTickDecrements = ( xExpectedIdleTime * ulTimerCountsForOneTick ) - RTC_GetWakeUpCounter();
- //计算已经完成的延时的tick数,偏少
- // ulCompleteTickPeriods = ulCompletedSysTickDecrements / ulTimerCountsForOneTick;
- //这个是错误的,由于RTC没有
- ulCompleteTickPeriods = xExpectedIdleTime - 1UL;
- // //小数部分
- // portNVIC_SYSTICK_LOAD_REG = ( ( ulCompleteTickPeriods + 1 ) * ulTimerCountsForOneTick ) - ulCompletedSysTickDecrements;
- }
- printf("55:%d-\r\n",RTCWakeupFlag);
- //最后把计算值重装到寄存器
- RTC_SetWakeUpCounter( ulTimerCountsForOneTick - 1UL );
- portENTER_CRITICAL();
- {
- //修正Tick计数
- vTaskStepTick( ulCompleteTickPeriods );
- }
- portEXIT_CRITICAL();
- }
- }
- #endif /* #if configUSE_TICKLESS_IDLE */
复制代码
不足:
由于唤醒定时器位数是有限的,可能有的时候tickless要延时很久,而定时器无法延时比较久的时间。
这个当然是通过分次睡眠唤醒来解决咯。
问题:
如果只有tick中断还好,如果是其他的中断让CPU醒来呢?这个时候该怎么处理?
portSUPPRESS_TICKS_AND_SLEEP是在空闲任务中被调用的,发生外部中断之后,首先会执行中断服务函数,中断服务函数运行过
程中如果没有任务就绪的话,那么很简单,程序会回到进入睡眠的那个地方,在空闲任务中,又重新执行vPortSuppressTicksAndSleep
进入睡眠模式。在外部中断中也有可能有一些任务会被就绪,中断出来之后会先进行任务切换到高优先级任务,等到所有的任务都执行
完毕了,就进入空闲任务中,也就是我们之前进入睡眠模式的那个地方,空闲任务又重新执行了函数vPortSuppressTicksAndSleep进入
tickless。CPU执行了一大圈才回到进入到空闲任务中的睡眠模式的那个点,貌似没有问题。有几个小细节不知道大家有没有注意到,我
们的tick计数值还没有更新,tick定时器也没有重新配置,唤醒之后需要配置外设启动那些也没有做。因为之前我们假设了从哪里休眠就
从哪里唤醒,但是这次不是这样子的!这是需要注意的点,我现在想到的解决办法就是不在中断中让任务就绪。你可能会说,那延时的
那些任务不是也会影响到吗?no,no,no!如果没有进入休眠之前的那个点,那么tick数还没有更新呢。由tick管理的这些任务包括软
件定时器的回调函数那些都不会被就绪的,所以这个是没有问题的。
问题:
下面这两个在进入休眠的前后是干什么的呢?
__dsb( portSY_FULL_READ_WRITE );
__isb( portSY_FULL_READ_WRITE );
见下面网友回复。
问题:
宏configPRE_SLEEP_PROCESSING、configPOST_SLEEP_PROCESSING给的参数xModifiableIdleTime是干什么的呢?
见下面网友回复。
tickless的一些资料:
http://mcuoneclipse.com/2013/07/06/low-power-with-freertos-tickless-idle-mode/
http://www.freertos.org/low-power-ARM-cortex-rtos.html
http://www.freertos.org/low-power-tickless-rtos.html