Mastering the FreeRTOS™ Real Time Kernel V1.0 - 11 Low Power Support
文档来自:Mastering-the-FreeRTOS-Real-Time-Kernel.v1.0.pdf。
关注FreeRTOS下低功耗实现,主要包括Idle Hook和Tickless Idle两种实现方式。
1 Low Power Support
11.1 Power Saving Introduction
FreeRTOS通过IDLE task hook和tickless idle来进入低功耗模式。
Tickless IDLE模式期间停止周期性Tick中断,允许MCU处于低功耗模式,知道发生中断或者RTOS的任务需要转换为Ready状态。然后再重新启动Tick中断,并对Tickcount进行校正调整。Tickless模式的原理是在空闲任务时,使其进入低功耗模式,以节省系统功耗。
11.2 FreeRTOS Sleep Modes
FreeRTOS中支持三种睡眠模式:
-
eAbortSleep: 此模式表示需要终止即将进入的睡眠状态。这可能是因为一个任务已经准备好执行(即它的状态变为就绪),一个上下文切换已经被挂起,或者一个滴答中断已经发生但由于调度器被挂起而被推迟。在这种情况下,实时操作系统(RTOS)将不会进入睡眠模式,以确保能够及时响应这些事件。
-
eStandardSleep: 此模式允许进入一个睡眠状态,但这个睡眠状态不会持续超过预期的空闲时间。这适用于系统预计在不久的将来会有任务需要执行,因此需要保持一个较轻的睡眠状态,以便可以快速唤醒并处理即将到来的任务。
-
eNoTasksWaitingTimeout: 此模式在没有任何任务等待超时时进入,这意味着系统可以安全地进入一个较深的睡眠状态,这种状态可能只能通过外部中断或复位来退出。这适用于系统预计在较长时间内不会有任何任务需要执行或中断发生的情况,可以最大限度地节省能源。
这些睡眠模式的选择和使用取决于应用程序的具体需求和预期行为。通过合理配置和使用这些模式,开发者可以优化他们的 FreeRTOS 应用程序以实现更好的电源管理,特别是在对功耗有严格要求的嵌入式系统中。
11.3 Functions and Enabling Built-in Tickless Idle Functionality
Tickless IDLE功能通过在 FreeRTOSConfig.h
中将 configUSE_TICKLESS_IDLE
定义为 1 来启用。用户定义的Tickless IDLE功能可以通过在 FreeRTOSConfig.h
中将 configUSE_TICKLESS_IDLE
定义为 2 来为任何 FreeRTOS 提供移植。
当启用无Tickless IDLE功能时,内核将在满足以下两个条件时调用 portSUPPRESS_TICKS_AND_SLEEP()
宏:
- 空闲任务是唯一能够运行的任务,因为所有应用程序任务要么处于阻塞状态,要么处于挂起状态。
- 在内核计划将应用程序任务从阻塞状态转换出来之前,至少会经过 n 个完整的滴答周期,其中 n 由
FreeRTOSConfig.h
中的configEXPECTED_IDLE_TIME_BEFORE_SLEEP
定义。
这意味着当系统处于空闲状态,没有其他任务需要执行,并且在未来一段时间内没有任务需要唤醒时,FreeRTOS 将调用 portSUPPRESS_TICKS_AND_SLEEP()
宏,允许微控制器进入低功耗状态以节省能源。configEXPECTED_IDLE_TIME_BEFORE_SLEEP
的值决定了微控制器在进入低功耗状态之前需要等待的滴答周期数,从而确保在进入低功耗状态之前,没有即将到来的任务切换或定时器回调需要处理。
11.3.1 The portSUPPRESS_TICKS_AND_SLEEP() Macro
portSUPPRESS_TICKS_AND_SLEEP()
函数中的 xExpectedIdleTime
参数的值等于在将任务移动到就绪状态之前需要经过的滴答周期总数。因此,该参数值代表了微控制器在滴答中断被关闭的情况下,可以安全地保持在深度睡眠状态的时间。
这个时间是基于系统当前的状态和任务调度需求计算出来的。如果系统预计在接下来的 xExpectedIdleTime
个滴答周期内没有任何任务需要被调度到就绪状态,那么微控制器就可以利用这段时间进入深度睡眠模式,从而节省能源。一旦超过这个时间或者有任务需要被调度,微控制器将被唤醒,滴答中断将被重新启用,系统恢复正常的调度和时间管理功能。
11.3.2 The vPortSuppressTicksAndSleep Function
vPortSuppressTicksAndSleep()
函数在 FreeRTOS 中定义,并且可以被用来实现无滴答模式。这个函数在 FreeRTOS 的 Cortex-M 端口层中被弱定义(weakly defined),意味着应用程序开发者可以根据自己的需求重写(override)这个函数。
在无滴答模式下,vPortSuppressTicksAndSleep()
通常用于在确定没有任务需要立即执行时,让微控制器进入低功耗状态。它负责停止滴答中断,将微控制器置于睡眠状态,并在适当的时间重新启动滴答中断,从而允许微控制器从睡眠状态唤醒并继续执行任务。
11.3.3 The eTaskConfirmSleepModeStatus Function
如果 eTaskConfirmSleepModeStatus()
在 portSUPPRESS_TICKS_AND_SLEEP()
函数内被调用时返回 eNoTasksWaitingTimeout
,那么微控制器可以无限期地保持深度睡眠状态。eTaskConfirmSleepModeStatus()
仅在满足以下条件时返回 eNoTasksWaitingTimeout
:
- 没有使用软件定时器,因此调度器在未来任何时间都不会执行定时器回调函数。
- 所有应用程序任务要么处于挂起状态,要么处于带有
portMAX_DELAY
超时值的阻塞状态,因此调度器在未来任何固定时间都不会将任务从阻塞状态转换出来。
这意味着,在没有任务或定时器需要调度器立即介入的情况下,系统可以安全地进入一种可以由外部中断唤醒的深度睡眠模式。
为了避免竞态条件,FreeRTOS 调度器在调用 portSUPPRESS_TICKS_AND_SLEEP()
之前会被挂起,并在 portSUPPRESS_TICKS_AND_SLEEP()
完成时恢复。这确保了应用程序任务在微控制器退出低功耗状态和 portSUPPRESS_TICKS_AND_SLEEP()
完成执行之间不能执行。此外,portSUPPRESS_TICKS_AND_SLEEP()
函数需要在停止计时器和进入睡眠模式之间创建一个小的临界区,以确保可以进入睡眠模式。eTaskConfirmSleepModeStatus()
应该在这个临界区内被调用。
此外,FreeRTOS 还为用户提供了在 FreeRTOSConfig.h
中定义的另外两个接口函数。这些宏允许应用程序编写者在微控制器进入低功耗状态之前和之后分别添加额外的步骤。
11.3.4 The configPRE_SLEEP_PROCESSING configuration
进入低功耗之前,对系统进行配置以降低工号,包括关闭外设时钟,降低CPU主频等。
11.3.5 The configPOST_SLEEP_PROCESSING configuration
退出低功耗模式后执行的函数,主要任务为恢复系统主频以及外设恢复。
11.4 Implementing portSUPPRESS_TICKS_AND_SLEEP() Macro
在 FreeRTOSConfig.h
中定义 portSUPPRESS_TICKS_AND_SLEEP()
来提供他们自己的实现。如果使用的 FreeRTOS 端口提供了 portSUPPRESS_TICKS_AND_SLEEP()
的默认实现,那么应用程序开发者可以通过在 FreeRTOSConfig.h
中定义 portSUPPRESS_TICKS_AND_SLEEP()
来覆盖默认实现。
11.5 Idle Task Hook Function
在 FreeRTOS 中,Idle Task可以选择性地调用一个用户定义的钩子或回调函数,即所谓的Idle Hook。由于Idle Task运行在最低优先级,因此这种Idle Hook函数只会在没有更高优先级的任务可以运行时被执行。这使得Idle Hook函数成为将处理器置于低功耗状态的理想位置,从而在没有处理任务需要执行时自动节省能源。只有当在 FreeRTOS 配置文件 FreeRTOSConfig.h
中将 configUSE_IDLE_HOOK
设置为 1 时,空闲挂钩才会被调用。
空闲挂钩(idle hook)在空闲任务运行期间会被重复调用。以下是一些关于空闲挂钩函数实现和使用的重要准则:
-
非阻塞性: 空闲挂钩函数不能调用任何可能导致它阻塞的 API 函数。因为空闲任务在没有更高优先级任务可运行时才会执行,如果空闲挂钩导致阻塞,它将无法响应系统状态的变化。
-
快速执行: 空闲挂钩应该快速执行其任务并返回,以确保系统能够及时响应外部事件和中断。
-
资源清理: 如果应用程序使用了
vTaskDelete()
API 函数来删除任务,空闲任务负责清理被删除任务所占用的资源。因此,空闲挂钩必须定期返回,以允许空闲任务执行其资源清理工作。 -
避免使用 API: 避免在空闲挂钩中使用任何可能改变任务状态或调度器状态的 FreeRTOS API,如
xTaskCreate
、vTaskDelay
等。 -
低功耗操作: 空闲挂钩是将处理器置于低功耗状态的理想位置,但必须确保这些操作不会阻塞空闲任务。
-
系统调度器的考虑: 空闲挂钩的实现不应干扰 FreeRTOS 调度器的正常工作。例如,不要在空闲挂钩中调用可能导致调度器重新评估的任务优先级调整函数。
-
硬件特定操作: 可以在空闲挂钩中执行硬件特定的操作,如关闭未使用的外设或进入特殊的 CPU 睡眠模式,但必须确保这些操作可以被外部中断或事件安全地唤醒。
-
测试和验证: 在实际硬件上对空闲挂钩的行为进行彻底测试,确保它不会引入不可预见的行为或系统问题。
遵循这些准则可以确保空闲挂钩函数能够有效地帮助系统在没有任务执行时节省能源,同时保持系统的响应性和稳定性。
2 FreeRTOS中Tickless Idle代码分析
FreeRTOSConfig.h中打开:
#define configUSE_TICKLESS_IDLE 1
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 10
Tickless Idle处理在Idle Task中进行:
_start
main
vTaskStartScheduler
xTaskCreate
prvIdleTask
----------------------------------------------
| vApplicationIdleHook--Idle Hook函数。 |
-----------------------------------------------------------------------------------------------------------------------------
| prvGetExpectedIdleTime--获取Idle可持续时间,然后和configEXPECTED_IDLE_TIME_BEFORE_SLEEP比较。如果超过阈值,允许进入suspend流程。 |
| vTaskSuspendAll--Suspend调度器,将不会发生Task切换,但是中断使能。 |
| prvGetExpectedIdleTime--再次获取Idle可持续时间,再次比较检查是否可以进入suspend睡眠。 |
| configPRE_SUPPRESS_TICKS_AND_SLEEP_PROCESSING |
| portSUPPRESS_TICKS_AND_SLEEP--自定义的停止Tick和睡眠函数。 |
| xTaskResumeAll |
-----------------------------------------------------------------------------------------------------------------------------
portSUPPRESS_TICKS_AND_SLEEP实现参考如下:
/* First define the portSUPPRESS_TICKS_AND_SLEEP() macro. The parameter is the time, in ticks, until the kernel next needs to execute. */ #define portSUPPRESS_TICKS_AND_SLEEP( xIdleTime ) vApplicationSleep( xIdleTime )--宏被定义为调用vApplicationSleep(xIdleTime)函数,其中xIdleTime是内核下次需要执行任务之前的滴答数。
/* Define the function that is called by portSUPPRESS_TICKS_AND_SLEEP(). */ void vApplicationSleep( TickType_t xExpectedIdleTime ) { unsigned long ulLowPowerTimeBeforeSleep, ulLowPowerTimeAfterSleep; eSleepModeStatus eSleepStatus;
/* Read the current time from a time source that will remain operational while the microcontroller is in a low power state. */ ulLowPowerTimeBeforeSleep = ulGetExternalTime();--函数被用来获取当前时间,这个时间源在微控制器进入低功耗状态时仍然有效。
/* Stop the timer that is generating the tick interrupt. */ prvStopTickInterruptTimer();--函数停止生成滴答中断的定时器。
/* Enter a critical section that will not effect interrupts bringing the MCU out of sleep mode. */ disable_interrupts();--禁用中断,以进入一个不会影响微控制器退出睡眠模式的临界区。
/* Ensure it is still ok to enter the sleep mode. */ eSleepStatus = eTaskConfirmSleepModeStatus();--函数调用,返回一个状态,指示是否仍然可以进入睡眠模式。 if( eSleepStatus == eAbortSleep )--如果状态是 eAbortSleep,则表示有任务已经准备好执行或上下文切换挂起,此时不进入睡眠状态,重新启动滴答定时器,并退出临界区。 { /* A task has been moved out of the Blocked state since this macro was executed, or a context siwth is being held pending. Do not enter a sleep state. Restart the tick and exit the critical section. */ prvStartTickInterruptTimer(); enable_interrupts(); } else { if( eSleepStatus == eNoTasksWaitingTimeout )--如果状态是 eNoTasksWaitingTimeout,则表示没有任务等待超时,可以安全地进入深度睡眠状态,不需要配置定时唤醒中断。 { /* It is not necessary to configure an interrupt to bring the microcontroller out of its low power state at a fixed time in the future. */ prvSleep();--使微控制器进入低功耗状态。 } else--如果状态是 eStandardSleep,则需要配置唤醒中断,以便在预定时间唤醒微控制器。 { /* Configure an interrupt to bring the microcontroller out of its low power state at the time the kernel next needs to execute. The interrupt must be generated from a source that remains operational when the microcontroller is in a low power state. */ vSetWakeTimeInterrupt( xExpectedIdleTime );--根据xExpectedIdleTime设置外部WakeTimer的唤醒时间。
/* Enter the low power state. */ prvSleep();--使微控制器进入低功耗状态。
/* Determine how long the microcontroller was actually in a low power state for, which will be less than xExpectedIdleTime if the microcontroller was brought out of low power mode by an interrupt other than that configured by the vSetWakeTimeInterrupt() call. Note that the scheduler is suspended before portSUPPRESS_TICKS_AND_SLEEP() is called, and resumed when portSUPPRESS_TICKS_AND_SLEEP() returns. Therefore no other tasks will execute until this function completes. */ ulLowPowerTimeAfterSleep = ulGetExternalTime();--再次被调用以确定微控制器实际在低功耗状态中的时间。
/* Correct the kernels tick count to account for the time the microcontroller spent in its low power state. */ vTaskStepTick( ulLowPowerTimeAfterSleep - ulLowPowerTimeBeforeSleep );--用来校正由于低功耗状态而错过的滴答数。 } /* Exit the critical section - it might be possible to do this immediately after the prvSleep() calls. */ enable_interrupts();--重新启用中断,退出临界区。
/* Restart the timer that is generating the tick interrupt. */ prvStartTickInterruptTimer();--重新启动滴答中断定时器。 } }