What? Why? Because batteries. Batteries go dead, and we don't want that. But we also might need an RTOS like FreeRTOS to do complicated preemptive multitasking. The idea is, the system wakes up, does it's complicated RTOS stuff and then goes back to sleep. But what sleep; we need to start there. Let's be specific and say Infineon PSOC 6 Cortex M4 microcontrollers. Now, PSOC 6 MCUs also have an M0+ processor. At the time of major work back at the ranch, this M0+ was good for booting the system. The latest Infineon tooling may allow expanded capability, but for this blog, I'm going to ignore it as much as possible so as not to unnecessarily complicate and obfustate. The MCU has 3 sleep modes of interest:
- Sleep - Code execution stops, but the MCU's only sleeping like a duck; one eye open, and it still can draw over a milliamp.
- Deep Sleep - This sleep mode deactivates the high speed clock, but keeps the low speed clock and associated peripherals running. Sleep current is in single-digit microamps.
- Hibernate - This is night-night mode. Everything important is off and there's a dedicated wakeup pin (like for power button) that wakes the system back up through a POR. A couple other choice peripherals, such as RTC alarm, can also wake the system up. Sleep current is less than a microamp.
For a system that is running, Deep Sleep is a good option because it retains SRAM and does not exit through POR. Additionally, the Power Mode Transition Times (the time it takes to wakeup) is way faster; 25uS max as compared to 500uS typical (there is no stated max value). So Deep Sleep it is.
SysTick
With the sleep mode out of the way, it's time to discuss the RTOS. What makes it chooch? Magic? No! The SysTick timer; a simple 24-bit timer that FreeRTOS sets up to trigger an interupt so it can run periodically. SysTick is setup in the CM4 RTOS file port.c in a default weak implementation __attribute__( ( weak ) ) void vPortSetupTimerInterrupt( void ). Now if your system is plugged into the wall, you might not care how fast SysTick is running. On a battery you might want to slow it down or even change the clock source as described in the docs. The code generated by ModusToolbox Device Configurator provides global variable SystemCoreClock that gets set to the initialized value of CLK_FAST in SystemCoreClockUpdate(), located in the generated system_psoc6_cm4.c file. You can use this to set the required configCPU_CLOCK_HZ define, then set the actual tick rate in hertz using define configTICK_RATE_HZ, both located in FreeRTOSConfig.h (a default template file is provided - just search the Modus libraries for FreeRTOSConfig).
Tickless Idle
When going to Deep Sleep, we want the SysTick timer, hence the RTOS, turned off, and FreeRTOS provides a means, called Tickless Idle, to get there gracefully as described here. But there's a catch...a hook really; the Idle Task Hook. First, the Idle Task is an internal RTOS task that runs at the lowest priority, when no other user-defined tasks are running. This task can call an idle task hook vApplicationSleep(), which the developer defines to put the system into the desired sleep mode. FreeRTOSConfig.h sets up all the defines up to do this, including macro portSUPPRESS_TICKS_AND_SLEEP() and configUSE_TICKLESS_IDLE:
/* Check if the ModusToolbox Device Configurator Power personality parameter
* "System Idle Power Mode" is set to either "CPU Sleep" or "System Deep Sleep".
*/
#if defined(CY_CFG_PWR_SYS_IDLE_MODE) && \
((CY_CFG_PWR_SYS_IDLE_MODE == CY_CFG_PWR_MODE_SLEEP) || \
(CY_CFG_PWR_SYS_IDLE_MODE == CY_CFG_PWR_MODE_DEEPSLEEP))
/* Enable low power tickless functionality. The RTOS abstraction library
* provides the compatible implementation of the vApplicationSleep hook:
* https://github.com/cypresssemiconductorco/abstraction-rtos#freertos
* The Low Power Assistant library provides additional portable configuration layer
* for low-power features supported by the PSoC 6 devices:
* https://github.com/cypresssemiconductorco/lpa
*/
extern void vApplicationSleep( uint32_t xExpectedIdleTime );
#define portSUPPRESS_TICKS_AND_SLEEP( xIdleTime ) vApplicationSleep( xIdleTime )
#define configUSE_TICKLESS_IDLE 2
#else
#define configUSE_TICKLESS_IDLE 0
#endif
/* Deep Sleep Latency Configuration */
#if( CY_CFG_PWR_DEEPSLEEP_LATENCY > 0 )
#define configEXPECTED_IDLE_TIME_BEFORE_SLEEP CY_CFG_PWR_DEEPSLEEP_LATENCY
#endif
Setting Device Configurator Power Personality Parameter, which is not what it's really called, results in the generated BSP code including the proper defines. Here's the device configurator "Power - Parameters" pane.
The System Idle Power Mode setting ends up in the modus.design file srss[0].power[0], Personality block. The param...hold on, that's where the name comes from! Anyway, the idlePwrMode parameter is set to what becomes the define CY_CFG_PWR_MODE_DEEPSLEEP, and when compiled, file sysfg_system.h is created and gets CY_CFG_PWR_SYS_IDLE_MODE set to CY_CFG_PWR_MODE_DEEPSLEEP. A note on configUSE_TICKLESS_IDLE; it's set to 2. The FreeRTOS docs say setting it to 2 allows user defined functionality in any FreeRTOS port, but I could not find anywhere a value of 2 is used. Non-zero values are used to enable things like a weak implementation of vApplicationSleep() and the call logic in tasks.c. So I'm saying a value of 1 would work for the Infineon FreeRTOS port.
The Idle Task
Here's an diagram showing a pre-emptive RTOS scheduler with 3 user tasks with different priorities and the FreeRTOS Idle Task that runs when all of the user tasks are in the blocking state. Let's just say for the sake of argument that the whole shoot'n match starts off from a MCWDT0 timer ISR that sets the event bit for Task 1. Tasks 1 and 2 do all the real work of the system and the job of Task 3 is to check system status and keep running if the system should stay awake or block if it can go to sleep. When Task 3 blocks there's going to be a period of time when all user tasks are now blocking - the period of time until a MCWDT0 timer interrupt; MCWDT0 being a timer that can run from a clock that remains active in Deep Sleep. So the next task that will run is the idle task that immediately calls vApplicationSleep(). No! In tasks.c, you'll find the idle task only calls vApplicationSleep() if the expected idle time (the time until a task is expected to run), is greater than define configEXPECTED_IDLE_TIME_BEFORE_SLEEP. This value defaults to 2 in FreeRTOS.h, but can be overridden by setting the Deep Sleep Latency in Device Configurator, which allows a value of 1, and the FreeRtosConfig.h template will happily set configEXPECTED_IDLE_TIME_BEFORE_SLEEP to that value, which will then throw an error because FreeRTOS requires a value of at least 2. Now I found where configEXPECTED_IDLE_TIME_BEFORE_SLEEP is checked, but not where it is used, but the internet says there could be a timer underflow if set less than 2, or the overhead of sleeping/waking for short periods could end up increasing power consumption. Since I'm setting CY_CFG_PWR_DEEPSLEEP_LATENCY to 0, I get the default value and all is right with the world.
vApplicationSleep()
Here's an example user-defined vApplicationSleep() function.
void vApplicationSleep( uint32_t xExpectedIdleTime )
{
uint32_t interruptStatus;
Cy_SysTick_Disable();
interruptStatus = Cy_SysLib_EnterCriticalSection();
eSleepModeStatus sleep_status = eTaskConfirmSleepModeStatus();
if(sleep_status != eAbortSleep)
{
/*Do whatever is needed to prepare for Deep Sleep*/
...
...
/*Go to Deep Sleep*/
Cy_SysPm_CpuEnterDeepSleep(CY_SYSPM_WAIT_FOR_INTERRUPT);
/*Do whatever is needed to wake back up.*/
...
...
}
Cy_SysLib_ExitCriticalSection(interruptStatus);
Cy_SysTick_Enable();
}
As you can see, the first thing I do is disable the SysTick, which stops kernel operation. Next, after entering the required (per the RTOS docs) critical section, call eTaskConfirmSleepModeStatus() to get the sleep_status. It will equal eNoTasksWaitingTimeout (no tasks are waiting for the aforementioned scheduling timeout as we're not using timeouts for scheduling) or eAbortSleep (that MCWDT0 interrupt snuck in at the last microsecond and set a task event bit and a task is ready to run right now). As long as we don't need to abort, do whatever is needed before going to Deep Sleep (highly system dependent, like maybe hit a GPIO to put some peripheral to sleep) and enter Deep Sleep, waiting for the MCWDT0 interrupt...but we're in a critical section you say. Never fear! The MCWDT0 is routed to the Wakeup Interrupt Controller which does not care about interrupts being disabled at the CPU level. So wake she will. After waking, do whatever is needed to wake back up (like wake that peripheral), exit the critical section and re-enable SysTick. Now I know what your saying, what about stepping SysTick? Yes, it would be more proper to call vTaskStepTick() before exiting the critical section, but that would require figuring out how long the system was asleep with a second timer and math. I hate math. Also, this RTOS implementation is super simple. MCWDT0 wakes the system, we exit the critical section, the MCDWT0 interrupt get's serviced and sets the event bit and off we go. Note that I'm also not using the HAL drivers because 2001 is a boring movie. Fell asleep less than half way through.
Summary!
Now you know this FreeRTOS feature exists. That's half the battle! I recommend reading the FreeRTOS docs as you sit a spell thinking about how to apply Tickless Idle logic to your project. Good luck!