STM32U5 STOP2 with FreeRTOS: Treat Wake-up Time as a Firmware Budget
On STM32U5 projects, low power is not just a CubeMX checkbox named STOP2. It is a system contract between clocks, GPIO states, peripherals, interrupt priorities, RTOS timing, and the product requirement that says how quickly the device must become useful after an event. If that contract is not explicit, the firmware may look correct in the lab and still lose interrupts, sample late, or consume several times the expected current in the field.
Why STOP2 is attractive, and why it is easy to misuse
STOP2 is usually the first serious low-power mode considered for battery STM32U5 designs because it preserves enough state to resume firmware without a cold boot while allowing the core and many clocks to be stopped. For a sensor node, a metering product, a handheld tool, or an industrial device that wakes on GPIO, RTC, LPUART, or a periodic acquisition deadline, this is often the right trade-off. The application keeps RAM context and wakes much faster than from standby, but current can be much lower than a simple sleep loop.
The mistake I see in product code is treating STOP2 as an isolated call to HAL_PWREx_EnterSTOP2Mode(). The function is only the final instruction. The product behaviour depends on what happened before it and what happens immediately after wake-up: which pins were left floating, whether SysTick was suspended, whether FreeRTOS expected tickless idle, which clock tree is restored, whether DMA or a peripheral was still active, and whether the wake-up interrupt is allowed to pre-empt the right code path.
Start from the wake-up budget
A useful design question is not “can the MCU enter STOP2?” but “how many milliseconds are available between the external event and the first valid product action?” That is the wake-up budget. A reed switch might allow tens of milliseconds. A radio preamble, a motor commutation event, or a short user button press may not. A metering pulse may require deterministic capture even if the application task processes it later.
Write the budget down with four numbers: wake-up source latency, clock restoration time, peripheral re-initialisation time, and application scheduling time. Then instrument those numbers with a GPIO and a logic analyser. Without measurement, low-power firmware tends to become a pile of “it should be fine” assumptions.
FreeRTOS tickless idle is not the whole design
FreeRTOS tickless idle can suppress periodic ticks while the system is idle, and its low-power hook is a good place to enter a deeper sleep state. But the kernel does not know the electrical state of the board, the analogue front end settling time, the exact UART wake-up constraints, or whether the next acquisition can tolerate a slower clock restore. Those are board and product decisions.
On STM32, a common pattern is to let FreeRTOS decide that the system can sleep for a given expected idle time, then apply a product-specific policy: only enter STOP2 if the expected idle time is long enough to repay entry and exit overhead; do not enter it while a driver owns a peripheral transaction; configure wake-up sources before entry; suspend the HAL tick if SysTick is used; and restore the system clock before returning to normal tasks.
A minimal STOP2 entry shape
The exact register and HAL calls vary by Cube version and board, but the structure below is the important part. The low-power path is explicit, guarded, and measurable.
#include "stm32u5xx_hal.h"
#include "FreeRTOS.h"
#include "task.h"
#include <stdbool.h>
static volatile bool lp_forbidden;
void board_low_power_lock(void) { lp_forbidden = true; }
void board_low_power_unlock(void) { lp_forbidden = false; }
static bool product_can_enter_stop2(TickType_t expected_idle_ticks)
{
const TickType_t min_stop2_ticks = pdMS_TO_TICKS(25);
if (lp_forbidden) {
return false;
}
if (expected_idle_ticks < min_stop2_ticks) {
return false;
}
/* Add product guards here: pending DMA, sensor conversion,
flash write, radio transaction, debug mode, active UART TX, etc. */
return true;
}
void vPortSuppressTicksAndSleep(TickType_t expected_idle_ticks)
{
if (!product_can_enter_stop2(expected_idle_ticks)) {
__WFI();
return;
}
taskENTER_CRITICAL();
/* Race check: an interrupt may have made a task ready while entering. */
if (eTaskConfirmSleepModeStatus() == eAbortSleep) {
taskEXIT_CRITICAL();
return;
}
HAL_SuspendTick();
HAL_GPIO_WritePin(LP_TRACE_GPIO_Port, LP_TRACE_Pin, GPIO_PIN_SET);
board_prepare_for_stop2();
HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);
SystemClock_Config();
board_restore_after_stop2();
HAL_GPIO_WritePin(LP_TRACE_GPIO_Port, LP_TRACE_Pin, GPIO_PIN_RESET);
HAL_ResumeTick();
taskEXIT_CRITICAL();
}
This example deliberately contains a lp_forbidden lock. In real products, low power bugs often appear because a new driver starts a DMA transfer, flash operation, or sensor conversion and nobody tells the power policy. A simple lock is not elegant architecture by itself, but it makes the contract visible. More mature projects can replace it with reference-counted constraints or a central power manager.
GPIO leakage is a firmware problem too
ST low-power documentation repeatedly points engineers back to GPIO configuration, and for good reason. A floating input, an enabled pull fighting an external resistor, or a peripheral pin left in an unintended alternate function can dominate the current budget. Firmware teams sometimes treat this as a hardware-only issue, but the final pin state before STOP2 is firmware-controlled.
For each board revision, keep a low-power pin table: pin name, external circuit, STOP2 mode, pull, wake-up role, and expected level. Then implement that table in one board function rather than scattering pin changes through drivers. During current measurement, compare the board against that table before changing random CubeMX settings.
Practical example: a battery sensor with RTC and EXTI wake-up
Consider a battery industrial sensor that samples every minute, but must also wake immediately when a tamper input changes. The product budget says: wake and timestamp the tamper edge within 5 ms, sample and publish normal data within 100 ms after the minute tick, and keep average current low enough for a multi-year battery.
I would use RTC wake-up for the periodic acquisition and EXTI for the tamper pin. The EXTI ISR should do the minimum: capture a timestamp source that survives the mode choice, set an event flag, and wake the relevant task. The task then restores the sensor rail if needed, waits the documented analogue settling time, reads the sensor, and logs whether the wake was RTC, EXTI, or both. STOP2 entry is allowed only when no acquisition, flash log write, or communication transaction is active. A trace GPIO wraps STOP2 entry and exit during validation so the wake-up budget is measured, not guessed.
Practical checklist
- Define the wake-up budget in milliseconds before choosing the sleep mode.
- List every wake source and verify whether it works from STOP2 on the selected STM32U5 part.
- Restore the system clock immediately after STOP2 before using timing-sensitive peripherals.
- Use a guard or power manager so drivers can block STOP2 during DMA, flash, radio, or sensor operations.
- Build a board-level GPIO low-power table and test current against it.
- Measure entry, wake-up, and first-useful-action timing with a GPIO trace.
- Test with debug disconnected; debug features can change low-power behaviour and current.
- Log wake reasons and unexpected aborts during field trials.
How I would approach this on a client project
First I would separate three artefacts: a product wake-up budget, a board low-power pin table, and a firmware power-state policy. Then I would implement the smallest STOP2 path with one wake source and a GPIO trace, measure current and latency, and only then add the second and third wake sources. I would not start by tuning every CubeMX option at once.
In code review, I would look for hidden blockers: drivers that start DMA without a power constraint, UART logging that keeps clocks alive, SysTick assumptions after STOP2, ISR priorities incompatible with FreeRTOS rules, and peripheral re-initialisation buried in application tasks. The goal is not the lowest current number in a demo. The goal is a product that wakes on the right event, at the right time, with explainable current consumption and logs that make field issues debuggable.