M0AGX / LB9MG

Amateur radio and embedded systems

STM32 stop mode & EXTI wakeup without HAL

Low-power modes come in handy (apart from obvious power saving) when the electromagnetic emissions of an MCU have to be reduced. For example when an MCU is placed close to antenna switching relays - you definitely do not want to receive the noise from the MCU in your HF transceiver (even if the clock frequency does not fall in an amateur band).

The trouble with low-power modes is that they are notoriously hard to debug. Often the clocks and voltage regulators have to be (re)configured. The debugger can prevent from entering into some of the modes or change their behavior. The debugger may also not be available because various clocks are stopped and supply rail lose power. Instead you have to rely on an ammeter or a simple LED to indicate which state the MCU is in.

This is a minimum example of stop mode usage in an STM32L011 with wakeup on GPIO (via EXTI). There are several low-power modes in this MCU. The stop mode is the lowest power of them all that preserves RAM and GPIO state. High frequency oscillators are stopped so there should be totally no HF noise emited by the MCU.

STM32 stop mode datasheet note

Initialization

In active mode I want the MCU to run at 16 MHz from the high speed internal oscillator (HSI). Since I am not using any libraries there are two things that have to be set up an power-on: voltage regulator and system clock.

The voltage regulator has several power modes that support different system clock speeds. I am using the highest power/speed setting.
STM32 performance vs voltage

The code first configures the voltage regulator, then enables the high speed oscillator, waits until it is ready and switches system clock to run from that oscillator. It could be called for example somewhere early in main().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stm32l0xx.h>

void power_init(void){
    RCC->APB1ENR |= RCC_APB1ENR_PWREN_Msk; //enable clock to the PWR peripheral to access it
    //set internal regulator to 1.8V to run at full speed, clear wakeup flag (in case it was left set)
    PWR->CR = PWR_CR_VOS_0 | PWR_CR_CWUF_Msk;

    //switch clock to 16 MHz HSI - enable HSI16 RCO
    RCC->CR |= RCC_CR_HSION_Msk;
    while ((RCC->CR & RCC_CR_HSIRDY_Msk) == 0){
        //wait until the RCO becomes available
    }
    //use HSI as system clock, use HSI as system clock on exit from stop mode, no prescalers anywhere
    RCC->CFGR = RCC_CFGR_SW_HSI | RCC_CFGR_STOPWUCK_Msk;

    SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk; //disable deep sleep, "regular" sleep is used in main loop
}

Stop mode entry

This code configures EXTI for falling edge PA9 interrupt. The application also uses PA9 as a regular interrupt. Some consideration is given to the moment right after wakeup but before ISR entry.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void power_enter_stop_mode(void){
    //configure wakeup on PA9 falling edge (beginning of break period) and enter stop mode

    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; //enable deep sleep on WFE/WFI

    //configure EXTI for wakeup event
    EXTI->PR = EXTI_PR_PIF9_Msk; //clear EXTI interrupt/event flag
    EXTI->IMR = EXTI_IMR_IM9_Msk; //enable PA9 EXTI interrupt (will execute after we get out of stop mode)
    EXTI->EMR = EXTI_EMR_EM9_Msk; //enable PA9 EXTI event
    EXTI->FTSR = EXTI_FTSR_FT9_Msk; //enable PA9 falling edge
    EXTI->RTSR = 0; //disable rising edge interrupt

    __disable_irq(); //disable so that we don't end up in the ISR until we reconfigure the clocks after wakeup

    LED_OFF();

    //magic https://devzone.nordicsemi.com/f/nordic-q-a/10932/can-someone-explain-to-me-how-nordic-nrf51822-sleep-mode-works
    __SEV();
    __WFE();
    __WFE(); //we are now in stop mode

    //wakeup on PA9 falling edge.........

    LED_ON();

    power_init(); //regulator & clocks are reinitialized

    __enable_irq(); //now the EXTI interrupt will fire
}

power_init() is called after exit from the stop mode. This is not strictly necessary as register values are generally preserved. Howeverm the regulator can be configured to enter the lowest power mode on stop mode. This means that after wakeup it would also be in that 1.5 V mode and running the device at 16 MHz would lead to an overclock. In my particular application I am not very concerned about the overall energy consumption and wakeup time so I explicitly reinitialize the regulator and clocks on wakeup. The important part is to always clear the wakeup flag (via PWR_CR_CWUF_Msk) in the PWR->CR register. Otherwise the device will immediately wake up after attempting to enter the stop mode again.

The application also uses PA9 as a regular interrupt source (the interrupt has to be of course enabled separately in the NVIC). In active mode this is not a concern. However, when exiting the stop mode via interrupt, the ISR could run using the wrong clock settings, hence the __disable_irq() and __enable_irq() to first exit the stop mode, reinitialize the clocks and only then service the interrupt.