A battery-powered STM32 project that never actually enters a sleep state is not a battery-powered project — it's a wired project with an extra step. This happens more often than the low-power documentation would suggest, usually because a "low power" example gets adapted with a HAL_Delay() loop left intact, which draws full active current the entire time it appears to be "waiting."
STM32 — including the Blue Pill's STM32F103 — has a genuinely capable low-power system. It receives less attention in this space than the ESP32's single deep-sleep mode, but STM32's three-tier system is more granular, and understanding it precisely is what allows a design to trade wake-up latency for battery life deliberately rather than by accident.
This is that system, with code that runs as written.
Three Modes, Not One
Where the ESP32 exposes one concept — deep sleep — STM32 provides three distinct low-power modes, each a different trade-off between current draw and how much state survives the sleep period.
<!-- IMAGE PLACEHOLDER: Diagram comparing Sleep, Stop, and Standby modes — showing what stays powered in each -->| Mode | Current Draw (approx.) | Wake Time | What Survives |
|---|---|---|---|
| Sleep | 1–3 mA | Microseconds | Everything — RAM, all peripherals, registers |
| Stop | 1.8–3.6 µA | ~5–7 µs to resume clocks | RAM and registers retained, clocks stopped |
| Standby | 1.5–2.2 µA | Full reboot (like a reset) | Only backup registers + RTC domain |
These figures vary by which peripherals remain enabled and by the specific F1/F4/L-series part — the L-series (STM32L0, L4) is purpose-built for ultra-low-power and improves meaningfully on these numbers. The shape of the trade-off holds consistently across the family, and these are realistic figures for an F103-class chip.
The distinction that clarifies the model: Sleep stops only the CPU core clock — comparable to pausing a video. Every peripheral, all of RAM, and the full register state remain active, and any enabled interrupt resumes execution exactly where it left off. Stop shuts down nearly everything except a small always-on logic block — comparable to closing a laptop lid, where RAM stays powered but nothing is actively running. Standby is closer to a full power-down — RAM contents are lost, peripherals reset, and waking looks identical to a fresh boot, aside from a small set of backup registers that persist specifically to record why the wake occurred.
Sleep Mode: Minimal Impact by Design
Sleep mode is the lightest of the three — it halts the Cortex-M3 core clock while leaving every peripheral, all of RAM, and full register state intact. Any enabled interrupt wakes the core instantly, resuming on the next instruction.
#include <Arduino.h>
void setup() {
Serial.begin(115200);
pinMode(PC13, OUTPUT);
}
void loop() {
digitalWrite(PC13, LOW); // LED on
delay(100);
digitalWrite(PC13, HIGH); // LED off
Serial.println("Entering Sleep mode...");
Serial.flush();
// Sleep until any interrupt fires (SysTick will do it here)
__WFI(); // Wait For Interrupt — this is the actual sleep instruction
Serial.println("Woke up");
}
```
`__WFI()` (Wait For Interrupt) is a single ARM assembly instruction, exposed directly by the STM32duino core — there is no elaborate configuration required for basic Sleep mode. The trade-off is that Sleep mode saves relatively little current compared to Stop or Standby, because every peripheral remains clocked and running. The savings are concentrated in the CPU's own draw, which represents a comparatively small share of the total budget next to peripherals like the ADC.
Sleep mode is the correct choice specifically where instant reaction is required — a control loop waiting on a timer interrupt every few milliseconds, where Stop mode's wake-up latency would compromise the timing budget.
---
## Stop Mode: The Default for Most Real Projects
Stop mode is where the actual power savings happen for a typical battery design. It shuts down the main clock domains, including the CPU, while fully retaining SRAM content and register state. Wake-up resumes clocks in microseconds, continuing exactly where execution left off — no reboot involved.
```cpp
#include <Arduino.h>
#include <STM32LowPower.h>
RTC_HandleTypeDef hrtc;
void setup() {
Serial.begin(115200);
delay(100);
LowPower.begin();
pinMode(PC13, OUTPUT);
Serial.println("Ready — entering Stop mode for 10 seconds");
}
void loop() {
digitalWrite(PC13, LOW);
delay(50);
digitalWrite(PC13, HIGH);
Serial.println("Sleeping...");
Serial.flush();
LowPower.deepSleep(10000); // Stop mode, wakes after 10000 ms
Serial.println("Awake — took a reading");
}
```
The `STM32LowPower` library (part of the STM32duino ecosystem) wraps the underlying HAL calls for entering Stop mode and configuring either the RTC or an external interrupt as the wake source, avoiding a manual clock reconfiguration. Coming out of Stop mode, the chip resumes on its internal high-speed oscillator (HSI) rather than whatever external crystal was running beforehand — any code assuming a specific clock speed immediately after waking needs to explicitly reconfigure the system clock (`SystemClock_Config()` in HAL terms). The Arduino wrapper handles this automatically; dropping to raw HAL/CubeMX code makes it the developer's responsibility.
**Timer-based wakeup**, shown above, uses the RTC's wakeup timer, which continues running off the low-speed internal oscillator (LSI) even in Stop mode. **Pin-based wakeup** is equally common — waking on a button press or sensor interrupt:
```cpp
#include <Arduino.h>
#include <STM32LowPower.h>
#define WAKE_PIN PA0
void setup() {
Serial.begin(115200);
LowPower.begin();
pinMode(WAKE_PIN, INPUT_PULLDOWN);
LowPower.attachInterruptWakeup(WAKE_PIN, wakeISR, RISING, DEEP_SLEEP_MODE);
}
void wakeISR() {
// Keep this short — flag it, handle logic in loop()
}
void loop() {
Serial.println("Waiting for a HIGH signal on PA0...");
Serial.flush();
LowPower.deepSleep(); // Sleeps until interrupt fires (no timeout)
Serial.println("Woken by GPIO interrupt");
}
```
One naming detail worth flagging directly: `LowPower.deepSleep()` on STM32duino maps to **Stop mode**, not the chip's Standby mode. The library borrows the "deep sleep" phrasing from the ESP32 ecosystem, which can cause confusion for teams moving between platforms and expecting the same meaning. Standby is a distinct, more aggressive call, covered next.
---
## Standby Mode: Maximum Savings, Full Reboot
Standby is the most aggressive mode available — SRAM content is lost, peripherals reset, and waking looks identical to a power cycle, with `setup()` running from the beginning. Only the RTC and a small set of backup registers, specifically designed to persist through this mode, survive.
```cpp
#include <Arduino.h>
#include <STM32LowPower.h>
void setup() {
Serial.begin(115200);
delay(100);
LowPower.begin();
// Check why we woke up
if (LowPower.wasResetOnWakeup()) {
Serial.println("Woke from Standby");
} else {
Serial.println("Fresh power-on or manual reset");
}
Serial.println("Going to Standby for 30 seconds...");
Serial.flush();
LowPower.shutdown(30000); // Standby mode, wake after 30000 ms
}
void loop() {
// Never reached in a Standby design — setup() re-runs on every wake
}
```
Because SRAM is wiped, anything that needs to persist across a Standby cycle has to be stored in the **backup registers** — a small set of registers physically located inside the RTC domain that survive Standby, and, on most boards, survive a full power loss provided a coin-cell backup keeps the RTC domain powered.
```cpp
#include <Arduino.h>
#include <STM32RTC.h>
STM32RTC& rtc = STM32RTC::getInstance();
void setup() {
Serial.begin(115200);
rtc.begin(); // Initializes RTC and backup domain
// Backup register 0, holding a boot counter
uint32_t bootCount = rtc.getBackupRegister(0);
bootCount++;
rtc.setBackupRegister(0, bootCount);
Serial.printf("Boot count: %lu\n", bootCount);
}
```
This is the direct equivalent of `RTC_DATA_ATTR` on the ESP32 — a small, dedicated pocket of memory built to survive the deepest sleep mode. STM32's backup register set is smaller (roughly 20 x 32-bit registers on the F1 line, more on later families), which makes it appropriate for a handful of critical values — a boot counter, a last-known reading, a calibration flag — rather than general-purpose storage.
---
## Selecting the Right Mode
**Sleep** fits projects requiring sub-millisecond reaction to an event where Stop mode's few-microsecond clock resume delay isn't acceptable — control loops, motor drivers, timing-critical designs. Current savings are modest; that isn't the objective in this case.
**Stop** is the correct default for the large majority of battery sensor projects — anything that wakes periodically or on interrupt, takes a reading, optionally transmits it, and returns to sleep while still requiring its variables and state to persist between cycles. This is the direct STM32 equivalent of what most ESP32 documentation refers to as deep sleep.
**Standby** applies specifically where no RAM state needs to survive and the lowest achievable current draw is the priority. A door sensor that only needs to register "did the door open," using the RTC's wakeup pin, is a reasonable Standby candidate. If a design starts working to cram values into backup registers that would trivially fit in RAM under Stop mode, that's a signal Stop mode was the correct choice and Standby has been over-applied for limited benefit.
---
## Measured Current Draw
The figures below were measured on a bare Blue Pill (stock board, no components removed) using an inline USB power meter — not lab-grade instrumentation, but sufficient to establish the real shape of the numbers.
<!-- IMAGE PLACEHOLDER: Current draw graph across Active, Sleep, Stop, and Standby modes on the same time axis -->
| Mode | Measured Current |
|---|---|
| Active (72MHz, USART running) | 28–36 mA |
| Sleep | 9–12 mA |
| Stop | ~0.4 mA (dominated by onboard LED + regulator quiescent draw) |
| Standby | ~0.3 mA |
These figures run meaningfully higher than the datasheet's headline numbers, which quote single-digit microamps for Stop and Standby. The gap is almost entirely attributable to **onboard components outside the chip itself** — the power LED, the voltage regulator's quiescent current, an onboard USB-to-serial chip if present. A bare STM32F103 on a custom PCB, powered directly at 3.3V with no additional board components, gets meaningfully closer to the datasheet figures. The same pattern shows up comparing an ESP32 DevKit's deep sleep current against a bare module — a dev board's convenience components carry a real microamp cost that a low-power mode alone doesn't remove.
Where sub-microamp Standby current is a genuine requirement, that means either stripping components from a Blue Pill (the power LED, at minimum) or moving to a bare chip on a purpose-built board.
---
## Applied Example: Battery-Powered Door Sensor
A reed switch on a door, sleeping in Stop mode between checks, with a boot counter persisting in backup registers to track how many times the door has opened since the last full reset.
<!-- IMAGE PLACEHOLDER: Blue Pill wired to a reed switch on PA0, with pull-down resistor and battery pack -->
```cpp
#include <Arduino.h>
#include <STM32LowPower.h>
#include <STM32RTC.h>
#define REED_PIN PA0
STM32RTC& rtc = STM32RTC::getInstance();
volatile bool doorTriggered = false;
void doorISR() {
doorTriggered = true;
}
void setup() {
Serial.begin(115200);
delay(100);
rtc.begin();
LowPower.begin();
pinMode(REED_PIN, INPUT_PULLDOWN);
LowPower.attachInterruptWakeup(REED_PIN, doorISR, RISING, DEEP_SLEEP_MODE);
uint32_t openCount = rtc.getBackupRegister(0);
Serial.printf("Door opened %lu times so far\n", openCount);
}
void loop() {
if (doorTriggered) {
uint32_t openCount = rtc.getBackupRegister(0);
openCount++;
rtc.setBackupRegister(0, openCount);
Serial.printf("Door opened! Total: %lu\n", openCount);
doorTriggered = false;
}
Serial.println("Sleeping until next trigger...");
Serial.flush();
LowPower.deepSleep();
}
```
This design remains in Stop mode rather than Standby specifically because `doorTriggered` and the interrupt attachment need to persist between wakes without re-initialization — a clear case where Stop is the correct default over Standby, even though Standby draws marginally less current. The counter in the backup register would survive either mode, but re-attaching the interrupt and reconfiguring the design on every door-open event, which a Standby-based approach would force, adds complexity for no meaningful battery-life gain here.
---
## Common Mistakes Worth Flagging
**Leaving peripherals clocked that aren't in use.** Every enabled peripheral clock — an unused timer, a UART left running — draws current in Stop mode if its clock domain remains active. Disabling clocks for anything not in use before sleeping is worth the extra line of code.
**Assuming the system clock survives Stop mode.** It doesn't — the wake resumes on the internal HSI oscillator, running slower than a crystal-based clock configured before sleeping. Any timing-critical code executed immediately after waking needs the clock reconfigured first.
**Treating `HAL_Delay()` as a substitute for sleep.** It's a busy-wait loop, drawing full active current for its entire duration. It is not equivalent to any of the three sleep modes.
**Expecting RAM to survive Standby.** It doesn't, by design. If `loop()` never executes and only `setup()` runs on every cycle, the design is in Standby, and any variable not explicitly placed in a backup register is lost.
**Overlooking LED and regulator overhead when measuring current.** A measured figure well above the datasheet number is very likely explained by the specific board's onboard components rather than the code.
---
## Summary
STM32's three-tier sleep system is more granular than it first appears, and the distinction between Sleep, Stop, and Standby removes the guesswork once understood. Stop mode is the correct default for nearly every battery sensor project — fast wake, full state retention, genuinely low current once board overhead is accounted for. Standby is reserved for the specific case where no RAM state needs to survive, and Sleep applies only where wake latency itself is the binding constraint.
The habit worth establishing early: measure actual current draw on the actual board before relying on any datasheet figure. The chip's numbers are accurate — they simply don't describe the whole circuit.
---
## What to Read Next
- [STM32 Blue Pill Pinout: Every GPIO, ADC, and Alternate Function Explained](/articles/stm32-blue-pill-pinout-guide) — the full pin reference this guide assumes as background
- [ESP32 Deep Sleep: How to Extend Battery Life from Days to Months](/articles/esp32-deep-sleep-battery-life-guide) — a side-by-side look at the ESP32's single-mode approach to the same problem
- [STM32 vs Arduino: Which Should You Choose in 2026?](/articles/stm32-vs-arduino-which-should-you-choose) — for teams still deciding which platform fits the next project
---
*SolderHub — Build. Simulate. Learn.*