kucu's notes

STM32 deepsleep modes (STOP, STOP2, STANDBY) with FreeRTOS

19 Mar 2021

STOP2 and STANDBY modes are great way to achieve uA and sub-uA consumption. When using an RTOS, the tasks must synchronize when to go to deepsleep, especially to STANDBY mode, which requires most hardware modules to be re-initialized.

  • You must either create complex synchronization between tasks, and possibly create a “managing” task that puts the system into deepsleep,
  • or you can use FreeRTOS hooks to achieve the same, but in a somewhat cleaner way.

I’ll show here the latter one.

The concept

FreeRTOS provides hooks, that are called both before and after sleep. The following snippet is from port.c’s vPortSuppressTicksAndSleep function:

/* port.c, vPortSuppressTicksAndSleep: */
xModifiableIdleTime = xExpectedIdleTime;
configPRE_SLEEP_PROCESSING( xModifiableIdleTime );
if( xModifiableIdleTime > 0 )
{
    __asm volatile ( "dsb" ::: "memory" );
    __asm volatile ( "wfi" );
    __asm volatile ( "isb" );
}
configPOST_SLEEP_PROCESSING( xExpectedIdleTime );

I’d like an API, which allows individual tasks marking themselves as “ready to deepsleep”, and when all critical task set this flag, FreeRTOS should put the system into deepsleep. Something like this:

// wait for timer or button push:
power_deepsleep_allow();
uint32_t notified;
do {
    // will deepsleep here; EXTI/RTC interrups can vTaskNotifyGiveFromISR:
    notified = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
} while (!notified);
power_deepsleep_prevent();

Implementation

Both RTOS hooks (configPRE_SLEEP_PROCESSING and configPOST_SLEEP_PROCESSING) are definable in FreeRTOSConfig.h:

/* FreeRTOSConfig.h */
#include "power.h"
/* ... */
#define configPRE_SLEEP_PROCESSING( t )  power_pre_sleep_processing()
#define configPOST_SLEEP_PROCESSING( t )  power_post_sleep_processing()

Here are the implementation, note how I disable the SYSTICK timer, so I can control the wakeup sources.

static volatile bool can_stop = false;

void power_pre_sleep_processing() {
  if (can_stop) {
    LL_RCC_SetClkAfterWakeFromStop(LL_RCC_STOP_WAKEUPCLOCK_MSI);
    LL_PWR_SetPowerMode(LL_PWR_MODE_STOP2);
    LL_LPM_EnableDeepSleep();
    SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
  }
}

void power_post_sleep_processing() {
  if (can_stop) {
    SysTick->CTRL  |= SysTick_CTRL_TICKINT_Msk;
    LL_LPM_EnableSleep();
  }
}

Regarding the allow/prevent functions, I implemented a simple counting mechanism. For re-entrant semaphores or even more automated solutions, browse the FreeRTOS API. I shot here for the easiest & fastest solution. I fancy however a mechanism that iterate over each task, and checks their state - for example on threadlocal storage - and does this very same thing, but automagically without POWER_REQUIRED_TASK_COUNT_TO_STOP.

#define POWER_REQUIRED_TASK_COUNT_TO_STOP 2
static volatile int stop_enabler_count = 0;

void power_deepsleep_allow() {
  portENTER_CRITICAL();
  stop_enabler_count++;
  can_stop = (stop_enabler_count == POWER_REQUIRED_TASK_COUNT_TO_STOP);
  portEXIT_CRITICAL();
}

void power_deepsleep_prevent() {
  portENTER_CRITICAL();
  stop_enabler_count--;
  can_stop = (stop_enabler_count == POWER_REQUIRED_TASK_COUNT_TO_STOP);
  portEXIT_CRITICAL();
}

Usage

For somewhat more complex example, this is how I LoRaWAN with LMIC library. Whenever I want to send something, I call vTaskResume (which can be called for non-suspened tasks as well). Stopping the task at this critical moment ensures the LoRa transceiver remains in SLEEP mode.

#define OP_NOT_IDLE	(OP_JOINING | OP_TXRXPEND | OP_TXDATA | OP_TESTMODE)
static bool can_shutdown = false;
/* ... */
// task main loop:
while (1) {
    os_runloop_once();

    // offer other tasks to work with LMIC:
    if ( !(LMIC.opmode & OP_NOT_IDLE) ) {
      // os_isQueueFree: (OS.runnablejobs == NULL && OS.scheduledjobs == NULL)
      // OS struct is static in oslmic.h, this function allows checking if 
      // anything is pending.
      if (can_shutdown && os_isQueueFree()) {
        // enable deep-sleep:
        power_deepsleep_allow();
        // * this is the place to stop LMIC resources, like LPTIM, if used
        // stop this task, control back to scheduler:
        vTaskSuspend(NULL);
        // * start here LMIC resources, like LPTIM, if used (and reset LMIC counters)
        // somewhere this task was resumed, go back to normal mode:
        power_deepsleep_prevent();
      }
    }
}
/* ... */
// somewhere in onEvent:
case EV_TXCOMPLETE:
    can_shutdown = true;
    break;

Source