Part 3 · Firmware Systems34 min read
Power Management
A mains-powered device can afford to spin in a busy loop forever. A battery-powered one cannot: the difference between a sensor that lasts three weeks and one that lasts three years is almost entirely firmware. This lesson shows where the current goes, how the STM32F4’s low-power modes work and what they cost to enter and leave, how to structure firmware so the CPU spends most of its life asleep, how to measure current properly, and how to turn a current-versus-time profile into a battery-life number you can defend. Along the way you will see why the STM32F407 is a fine chip to learn on and the wrong chip to put on a coin cell.
Where the power goes
Section titled “Where the power goes”The current a board draws is the sum of many small consumers, and you have to find all of them.
The CPU and digital logic. CMOS logic draws current mainly when it switches. Dynamic power follows
P_dynamic = a * C * V^2 * fwhere a is the fraction of gates switching per cycle, C the switched capacitance, V the core voltage, and f the clock frequency. Two consequences: current scales roughly linearly with clock frequency (datasheets quote a figure in µA per MHz), and voltage matters quadratically, which is why lowering the core voltage pays more than lowering the clock.
Leakage. Transistors leak even when nothing switches. Leakage rises steeply with temperature and with the amount of powered silicon (RAM, flash, peripherals). It is what remains in deep sleep and it is why “RAM retained” modes cost more than “RAM off” modes.
Peripherals. Every peripheral with its clock enabled draws current, even if idle. A timer counting with nobody listening, a UART enabled but unused, the ADC powered up between conversions: each costs from tens of µA to a few mA.
Things outside the MCU, which are often the biggest:
| Consumer | Typical current | Note |
|---|---|---|
| LED | 2 to 20 mA | Often the largest load on a “low-power” board |
| Internal pull-up held low (40 kΩ at 3.3 V) | about 80 µA | A pressed button, or a pull-up on a pin driven low |
| Radio transmitting (BLE, sub-GHz) | 5 to 50 mA | Wi-Fi and cellular: 100 mA to amps in bursts |
| Sensor in continuous mode | 0.1 to 5 mA | Most have a one-shot mode at a fraction of that |
| LDO regulator quiescent current | 1 µA to 5 mA | Old regulators can dwarf a sleeping MCU |
| Debug probe / USB bridge on a dev board | mA | The Discovery’s ST-Link is always powered from USB |
The pull-up line deserves a second look: 3.3 V across a 40 kΩ internal pull-up is 82 µA, continuously, for as long as the pin is held low. Ten of those and you have burned almost a milliamp doing nothing.
The STM32F4 low-power modes
Section titled “The STM32F4 low-power modes”The Cortex-M core has two sleep instructions: WFI (wait for interrupt) and WFE (wait for event). What “sleep” means is decided by the SLEEPDEEP bit in SCB->SCR and, on the STM32, by the power controller PWR at 0x40007000 (on APB1, clock enable PWREN in RCC_APB1ENR bit 28).
| Mode | How to enter | What stops | What is kept | Wake sources | Wake time | Typical current |
|---|---|---|---|---|---|---|
| Run | nothing | everything | tens of mA at 168 MHz | |||
| Sleep | WFI/WFE, SLEEPDEEP = 0 | CPU clock | everything; peripherals keep running | any interrupt or event | a few cycles | several mA to tens of mA, depending on clock and peripherals |
| Stop | SLEEPDEEP = 1, PDDS = 0 | all clocks in the 1.2 V domain, PLL, HSE, HSI | SRAM, registers, GPIO state | any EXTI line (pins, RTC, PVD, USB wakeup) | tens of µs, more with flash powered down | a few hundred µA |
| Standby | SLEEPDEEP = 1, PDDS = 1 | everything except the backup domain | RTC, 80 bytes of backup registers, 4 KB backup SRAM (if enabled); main SRAM is lost | WKUP pin (PA0), RTC alarm/wakeup, NRST, IWDG | like a reset | a few µA |
The current figures are orders of magnitude for the F407 at room temperature; the datasheet’s power consumption tables give exact typical and maximum values for each clock, regulator setting and peripheral combination. Learn to read those tables; they are the input to every battery calculation.
Notice the gap between Stop and Standby: two orders of magnitude, paid for by losing RAM and waking through a reset. On the F4 there is no mode in between. Low-power families fill that gap: an STM32L4 in its Stop 2 mode retains all SRAM and wakes in microseconds at around 1 to 2 µA, and an nRF52 idles with RAM retained and the RTC running at a couple of µA. Keep that in mind for the battery calculation at the end.
Sleep mode and the idle loop
Section titled “Sleep mode and the idle loop”The simplest and most important power optimisation is: when there is nothing to do, execute WFI. The core stops its clock, peripherals keep running, and the next interrupt wakes it within a few cycles. A busy loop polling a flag at 168 MHz becomes a CPU that runs only when something happens.
The naive version has a race:
#include <stdbool.h>
#include "stm32f4xx.h"
extern volatile bool event_flag;
void idle_loop_racy(void){ for (;;) { if (!event_flag) { /* an interrupt that sets event_flag right here is not noticed ... */ __WFI(); /* ... and we sleep until the NEXT interrupt */ } event_flag = false; /* handle the event */ }}If the interrupt fires between the check and the WFI, its handler runs, sets the flag, returns, and the core then sleeps, with the event waiting unprocessed until some unrelated interrupt arrives. That might be the next SysTick in a millisecond, or nothing for an hour.
The fix relies on a precise property of WFI: it wakes the core when an interrupt becomes pending that would be taken if PRIMASK were clear, even while PRIMASK masks it.
#include <stdbool.h>
#include "stm32f4xx.h"
extern volatile bool event_flag;void handle_event(void);
void idle_loop(void){ for (;;) { __disable_irq(); /* PRIMASK = 1: handlers cannot run ... */ if (!event_flag) { __DSB(); __WFI(); /* ... but a pending interrupt still wakes us */ } __enable_irq(); /* the pending handler runs here */ if (event_flag) { event_flag = false; handle_event(); } }}With interrupts masked, nothing can slip in between the check and the sleep. If the interrupt arrived before the check, the flag is already set and we skip the WFI. If it arrives after, it becomes pending, wakes the core, and its handler runs the moment __enable_irq() clears PRIMASK. The __DSB() before WFI makes sure every outstanding memory write has completed, for example the write that cleared a peripheral’s interrupt flag, so the core does not go to sleep with a write still in flight that would have changed whether it should sleep. This “disable, check, sleep, enable” sequence is the canonical idle loop; RTOS idle hooks do the same thing.
WFE is the other sleep instruction. It sleeps until an event: an interrupt (with SEVONPEND set, even a disabled one), an external event line, or a SEV instruction from software or another core. It also has a one-bit event register, so an event that arrives before WFE makes the next WFE return immediately, which avoids the race by construction. It is used in spin-lock and multi-core code; for single-core interrupt-driven firmware, the WFI pattern above is clearer.
SLEEPONEXIT (SCB->SCR bit 1) makes the core go back to sleep automatically when it returns from the last active interrupt handler, without ever returning to main. A design where all work happens in ISRs can set it once and never execute another WFI. It saves the unstacking and restacking of the exception frame on each wake, at the cost of a less conventional structure.
Stop mode
Section titled “Stop mode”Stop mode turns off the clocks of the whole core domain: CPU, flash interface, SRAM, and all peripherals except those that run from the low-speed oscillators (RTC, IWDG). SRAM and register contents are retained, so execution continues after the WFI as if nothing had happened, except for one important detail.
#include <stdint.h>
#include "stm32f4xx.h"
void clock_init_168mhz(void); /* from Clocks, reset, and timing */
/* Call with interrupts disabled (PRIMASK = 1), as in the idle loop. */void enter_stop_mode(void){ RCC->APB1ENR |= RCC_APB1ENR_PWREN; (void)RCC->APB1ENR; PWR->CR &= ~PWR_CR_PDDS; /* deep sleep means Stop, not Standby */ PWR->CR |= PWR_CR_LPDS | PWR_CR_FPDS; /* low-power regulator, flash off */ SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* a pending tick would wake us */
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; __DSB(); __WFI(); SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk; /* later WFIs are plain Sleep */
clock_init_168mhz(); /* we woke up on the 16 MHz HSI */ SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;}Line by line:
PDDS = 0selects Stop rather than Standby for deep sleep.LPDSputs the voltage regulator in low-power mode andFPDSpowers down the flash during Stop. Both lower the Stop current further and both lengthen the wake-up time, because the regulator and flash need time to come back. If you need the fastest wake-up, leave them clear and accept the higher current.- SysTick is stopped explicitly. In Stop its clock is off anyway, but a SysTick exception that became pending just before would make
WFIreturn immediately. - On wake from Stop the STM32F4 always runs from the HSI at 16 MHz. The PLL and HSE were switched off. Your UART baud rates, timer periods, and
SystemCoreClockare all wrong until you reconfigure the clock tree. That is whyenter_stop_modemust be called with interrupts masked: the waking interrupt’s handler should run afterclock_init_168mhz()has restored 168 MHz, not before. - SysTick did not count while stopped, so a
millis()built on it has lost however long the chip slept. Keep wall-clock time in the RTC, which keeps running, and correct your tick count from it after each wake.
What wakes the chip from Stop? Only EXTI lines, because the NVIC has no clock. GPIO pins through EXTI (configured as in GPIO in depth), the RTC alarm and wakeup timer (EXTI lines 17 and 22), the PVD, and a few others. A UART byte cannot wake the F4 from Stop on its own; if you need that, route the RX pin to an EXTI line as well and accept losing the first byte, or stay in Sleep. Also clear every pending EXTI flag and RTC flag before entering, or the chip wakes immediately.
Standby mode
Section titled “Standby mode”Standby turns off nearly everything. Only the backup domain survives: the RTC, the 20 backup registers (80 bytes), and, if you enable its regulator, the 4 KB backup SRAM. Main SRAM loses its contents. Waking up is a reset: execution starts again at the reset vector, the startup code runs, and main has to figure out why it is running.
#include <stdbool.h>#include <stdint.h>
#include "stm32f4xx.h"
void enter_standby(void){ RCC->APB1ENR |= RCC_APB1ENR_PWREN; (void)RCC->APB1ENR; PWR->CR |= PWR_CR_PDDS; /* deep sleep means Standby */ PWR->CR |= PWR_CR_CWUF; /* clear a stale wake-up flag */ PWR->CSR |= PWR_CSR_EWUP; /* PA0 rising edge wakes us */ SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; __DSB(); __WFI(); for (;;) { /* not reached: wake-up is a reset */ }}
bool woke_from_standby(void){ RCC->APB1ENR |= RCC_APB1ENR_PWREN; (void)RCC->APB1ENR; bool standby = (PWR->CSR & PWR_CSR_SBF) != 0u; PWR->CR |= PWR_CR_CSBF; /* clear the standby flag */ return standby;}PA0 is the WKUP pin on the F407, and on the Discovery it is also the user button, so pressing the button wakes the board. EWUP enables the pin as a wake source; while it is enabled, PA0 is forced to input with a pull-down regardless of its GPIO configuration. At boot, woke_from_standby tells a cold power-on apart from a wake-up by reading the SBF flag, and the reset flags in RCC_CSR from Watchdogs, faults, and recovering from them add the rest of the story.
Standby suits devices that wake rarely, do a burst of work, and go back to sleep: a data logger that samples every ten minutes, a remote control that sleeps until a button press. Any state that must survive goes in the backup registers or backup SRAM, or in flash.
Choosing a mode
Section titled “Choosing a mode”| Requirement | Mode |
|---|---|
| Wake within a few cycles, peripherals (UART, DMA, timers) keep working | Sleep |
| Keep RAM and context, wake on a pin or the RTC within tens of µs | Stop |
| Lowest current, sleeping for seconds or more, can afford a reboot | Standby |
| Wake on a UART byte without losing it | Sleep (on the F4) |
The deeper the sleep, the longer and more expensive the wake-up: restarting the PLL takes time at full current, and a Standby wake means running the whole startup again. If the device wakes a thousand times a second, a deep mode can cost more energy than it saves. Compare the energy of one wake-up against the energy saved by the lower sleep current over the sleep interval.
The event-driven low-power architecture
Section titled “The event-driven low-power architecture”Low-power firmware is event-driven firmware. The structure from State machines and event-driven firmware is exactly what you need: every piece of work is triggered by an interrupt (a timer tick, a pin change, a received byte, a DMA completion), handlers do the minimum and post an event, the main loop processes events until the queue is empty, and then the idle loop sleeps.
current ^ | wake process sleep wake | _|___________________|_ _|__ | | clock up | work | | |___| | |_________________________| | sleep current +----------------------------------------------------------> time <-- t_active, high current --> <------ t_sleep, very low current ------>Everything you do to save power shortens the high part or lowers the low part. Two tactics follow directly:
- Never busy-wait. A
delay_ms(10)while a sensor converts keeps the CPU at full current for 10 ms. Start the conversion, set an RTC or timer wake-up for 10 ms, and sleep. - Race to sleep. Do the work as fast as you can, then sleep. Running at a lower clock lowers the active current but stretches the active time by the same factor, so the dynamic energy per task is about the same, while everything else that draws current during the active phase (regulator, peripherals, leakage, the sensor waiting for you) draws it for longer. Lowering the clock wins only when it also lets you lower the core voltage, because of the
V^2term.
On the F407 you can trade top speed for a lower core voltage: the VOS bit in PWR->CR selects regulator Scale 1 (the default, up to 168 MHz) or Scale 2 (up to 144 MHz), and it can only be changed while the PLL is off. You can also run the APB buses at lower prescalers and the system clock straight from the HSI when nothing needs speed.
Timed wake-ups with the RTC
Section titled “Timed wake-ups with the RTC”A periodic sensor needs a timer that runs while the core sleeps. SysTick and the general-purpose timers stop in Stop mode; the RTC keeps running from the low-speed oscillator. The STM32F407 has no separate low-power timer (newer families have an LPTIM), so its RTC wakeup timer is the tool:
#include <stdint.h>
#include "stm32f4xx.h"
/* Periodic wake-up from Stop every `seconds` (1..32), clocked from the LSI. */void rtc_wakeup_init(uint32_t seconds){ RCC->APB1ENR |= RCC_APB1ENR_PWREN; (void)RCC->APB1ENR; PWR->CR |= PWR_CR_DBP; /* allow backup-domain writes */ RCC->CSR |= RCC_CSR_LSION; /* ~32 kHz internal RC oscillator */ while (!(RCC->CSR & RCC_CSR_LSIRDY)) { } RCC->BDCR = (RCC->BDCR & ~RCC_BDCR_RTCSEL) | RCC_BDCR_RTCSEL_1 | RCC_BDCR_RTCEN;
RTC->WPR = 0xCAu; /* unlock RTC registers */ RTC->WPR = 0x53u; RTC->CR &= ~RTC_CR_WUTE; /* stop the wakeup timer ... */ while (!(RTC->ISR & RTC_ISR_WUTWF)) { /* ... until WUTR may be written */ } RTC->WUTR = seconds * 2000u - 1u; /* LSI / 16 = ~2 kHz */ RTC->CR = (RTC->CR & ~RTC_CR_WUCKSEL) | RTC_CR_WUTIE | RTC_CR_WUTE; RTC->WPR = 0xFFu; /* lock again */
EXTI->IMR |= EXTI_IMR_MR22; /* RTC wakeup is EXTI line 22 */ EXTI->RTSR |= EXTI_RTSR_TR22; NVIC_SetPriority(RTC_WKUP_IRQn, 6u); NVIC_EnableIRQ(RTC_WKUP_IRQn);}
void RTC_WKUP_IRQHandler(void){ /* Clear WUTF without writing 1 to INIT (which would enter init mode). */ RTC->ISR = ~(RTC_ISR_WUTF | RTC_ISR_INIT) | (RTC->ISR & RTC_ISR_INIT); EXTI->PR = EXTI_PR_PR22; /* write 1 to clear */}The RTC lives in the backup domain, so writing its clock selection in RCC->BDCR needs DBP set first. RTCSEL = 10 (the _1 bit alone) selects the LSI. The RTC registers are write-protected; writing 0xCA then 0x53 to WPR unlocks them, any other value locks them again. The wakeup counter can only be reloaded while it is stopped, which WUTWF confirms. With WUCKSEL = 000 the counter runs at RTCCLK/16, about 2 kHz from the LSI, so 2000 counts is about one second, and the 16-bit counter limits this setting to about 32 seconds.
The handler must clear both the RTC flag and the EXTI pending bit; forget either and the chip either re-enters the handler forever or wakes immediately from the next Stop. The LSI is an RC oscillator that can be off by several percent and drifts with temperature. Use the LSE (32.768 kHz crystal) if your board has one; the setup differs only in the oscillator enable and RTCSEL value.
Tickless idle in an RTOS
Section titled “Tickless idle in an RTOS”An RTOS wakes the CPU on every tick (often 1 kHz) just to increment a counter and find nothing to do. Tickless idle removes those wake-ups: when the idle task runs, the kernel computes how long until the next task needs to run, stops the tick, programs a timer to wake the chip at that moment, sleeps, and on wake-up advances its tick count by however long it actually slept.
FreeRTOS supports this with configUSE_TICKLESS_IDLE. Setting it to 1 uses the port’s built-in SysTick version, which can only use Sleep mode because SysTick stops in Stop. For Stop mode you supply your own portSUPPRESS_TICKS_AND_SLEEP, built on the RTC:
#include <stdint.h>
#include "FreeRTOS.h"#include "task.h"#include "stm32f4xx.h"
void rtc_wakeup_start_ms(uint32_t ms); /* your RTC driver */uint32_t rtc_wakeup_elapsed_ms(void);void rtc_wakeup_stop(void);void enter_stop_mode(void); /* from above: restores 168 MHz on wake */
#define MAX_SLEEP_MS 30000u /* RTC wakeup counter range */
/* FreeRTOSConfig.h: #define configUSE_TICKLESS_IDLE 2 #define portSUPPRESS_TICKS_AND_SLEEP(x) app_sleep(x) */void app_sleep(TickType_t idle_ticks){ uint32_t ms = (uint32_t)idle_ticks * portTICK_PERIOD_MS; if (ms > MAX_SLEEP_MS) { ms = MAX_SLEEP_MS; } __disable_irq(); if (eTaskConfirmSleepModeStatus() == eAbortSleep) { __enable_irq(); /* a task became ready meanwhile */ return; } rtc_wakeup_start_ms(ms); enter_stop_mode(); /* stops SysTick, sleeps, restores clocks */ uint32_t slept = rtc_wakeup_elapsed_ms(); rtc_wakeup_stop(); vTaskStepTick(slept / portTICK_PERIOD_MS); __enable_irq(); /* the waking ISR, if any, runs now */}This is a sketch: the RTC helper functions are yours to write (with a sub-second wakeup clock rather than the one-second setting above), and a production version also restores the SysTick phase so the next tick is not early or late. The key steps are all there. eTaskConfirmSleepModeStatus is checked with interrupts masked, so an interrupt that readied a task cannot be lost. The sleep is clamped to what the wake-up timer can count. If an unrelated interrupt (a button) wakes the chip early, rtc_wakeup_elapsed_ms reports the real sleep time and vTaskStepTick advances the kernel’s time by exactly that, so delays stay correct. Zephyr does the same thing automatically through its power management subsystem when the SoC support provides a low-power timer.
Stopping the leaks
Section titled “Stopping the leaks”Once the CPU sleeps, the remaining current is usually somewhere you did not look.
Clock-gate idle peripherals. Clear the peripheral’s enable bit in RCC_AHB1ENR/APB1ENR/APB2ENR when you are not using it, and power down analog blocks (ADC ADON, DAC) between uses. In Stop mode peripheral clocks stop anyway, but in Sleep and Run they do not.
Put unused pins in analog mode. A floating digital input picks up noise; every time the voltage wanders through the Schmitt trigger’s threshold region, the input buffer draws a burst of current. Analog mode disconnects the digital input buffer entirely and is the lowest-leakage state on STM32. A pin with a pull-up or pull-down is also stable, but analog is better.
#include "stm32f4xx.h"
/* Every pin of ports C and E analog: digital input buffer off, no leakage. */void unused_ports_to_analog(void){ RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN | RCC_AHB1ENR_GPIOEEN; (void)RCC->AHB1ENR; GPIOC->MODER = 0xFFFFFFFFu; GPIOC->PUPDR = 0u; GPIOE->MODER = 0xFFFFFFFFu; GPIOE->PUPDR = 0u; RCC->AHB1ENR &= ~(RCC_AHB1ENR_GPIOCEN | RCC_AHB1ENR_GPIOEEN); /* config is kept */}MODER = 11 for every pin is analog mode. GPIO registers keep their values when the port clock is gated, so you can turn the clock off afterwards and save its current too. On a real board, do this per pin from your board support file: only pins that are truly unused, and never PA13/PA14 (SWD) in a debug build. Pins connected to external chips need thought: an input driven by a powered-down chip floats, and an output driving a powered-down chip can back-power it through its protection diodes.
Look at the board, not just the MCU. The Discovery board is a development board: its ST-Link, accelerometer, audio DAC and USB circuitry all draw current. It has a jumper (JP1, labelled IDD) in the MCU’s supply path precisely so you can measure the MCU alone. On your own product, budget the regulator’s quiescent current, every pull-up resistor that can be held low, voltage dividers for battery measurement (switch them with a transistor), and every LED.
Disconnect the debugger. As you saw in Debugging and tracing on real hardware, the DBG_SLEEP, DBG_STOP and DBG_STANDBY bits in DBGMCU->CR keep clocks running in low-power modes so the probe stays connected, and they survive a system reset. A probe attached, or a debug session earlier without a power cycle, gives you a current reading that has nothing to do with the field.
Measuring current
Section titled “Measuring current”You cannot optimise what you have not measured, and measuring low-power devices is harder than it looks, because the current spans five or six orders of magnitude within milliseconds: microamps asleep, tens of milliamps awake.
- Multimeter in series. Fine for steady states (the board in Stop, nothing happening). Useless for averages of a pulsed profile, and its auto-ranging can momentarily open the circuit or drop enough voltage to brown out the MCU.
- Shunt resistor and oscilloscope. A small resistor in the supply line and a scope across it gives the current waveform. Choose the shunt for the range: 10 Ω turns 50 mA into a 0.5 V drop (too much, your MCU sees 2.8 V instead of 3.3 V) and 1 µA into 10 µV (lost in the noise). One shunt cannot cover both ends, which is why the next option exists.
- Power profilers such as the Nordic Power Profiler Kit II and the Joulescope supply the board, switch measurement ranges automatically in microseconds, and record current from sub-µA to hundreds of mA at high sample rates. They integrate the charge for you and show the average over any window. For battery-powered work, one of these pays for itself on the first project.
What you are building is a current profile: a plot of current against time over one full cycle of the device’s behaviour (one sample period, one reporting interval), from which you read each phase’s current and duration. The average current is the total charge over the cycle divided by the cycle’s length:
I_avg = (I1 * t1 + I2 * t2 + ... + In * tn) / TWorked example: a coin-cell sensor
Section titled “Worked example: a coin-cell sensor”A temperature sensor node samples once per second and sends a batch of readings over a low-power radio once per minute. It runs from a CR2032 coin cell, which we will count as 200 mAh. Here is the profile measured on an STM32F407 board using Stop mode with the low-power regulator and flash power-down, waking on the RTC:
| Phase | Current | Duration per 1 s cycle | Charge per cycle |
|---|---|---|---|
| Stop, waiting for the RTC | 0.30 mA | 995 ms | 298.5 µC |
| Awake at 16 MHz: wake-up, read sensor, store sample | 6 mA | 5 ms | 30.0 µC |
| Sensor’s own conversion current | 0.5 mA | 5 ms | 2.5 µC |
| Radio burst: 20 mA for 10 ms once per 60 s, averaged | 3.3 µC | ||
| Total | 1 s | 334.3 µC |
Charge per second in µC is average current in µA, so the average is 334 µA. Battery life is capacity over average current:
200 mAh / 0.334 mA = 599 h = 25 daysDerate for reality: a coin cell delivers less than its rated capacity at low temperature and under pulse loads, and you want margin for component variation. With 80% usable capacity, about 20 days. Look at where the charge goes: 89% is the Stop current. Nothing you do to the active code matters until the sleep current comes down, and on the F407 it cannot come down much further without Standby, which loses RAM and turns every sample into a full reboot.
Now the same firmware design on a low-power part, such as an STM32L4-class MCU in Stop 2 at 2 µA with RAM retained, and about 100 µA/MHz when running:
| Phase | Current | Duration | Charge |
|---|---|---|---|
| Stop 2, RAM retained, RTC running | 0.002 mA | 995 ms | 2.0 µC |
| Awake at 16 MHz | 1.6 mA | 5 ms | 8.0 µC |
| Sensor conversion | 0.5 mA | 5 ms | 2.5 µC |
| Radio, averaged | 3.3 µC | ||
| Total | 1 s | 15.8 µC |
200 mAh / 0.0158 mA = 12,660 h = 1.44 years, about 14 months at 80% usable capacityTwenty times longer, from choosing the right part. And now the balance has shifted: sleep is 13% of the budget, while the active phase, the sensor and the radio dominate. That is when firmware optimisation starts paying: batching the radio to once every ten minutes cuts its share by 10 times, using the sensor’s lowest-power one-shot mode helps, and shortening the active phase (fewer instructions, no busy-waiting, faster wake) matters. The order of work is always the same: measure, find the largest term, attack that one.
Two more coin-cell realities. A CR2032 has an internal resistance of roughly 10 Ω or more when fresh, rising as it discharges and in the cold. A 20 mA radio burst through 20 Ω drops 0.4 V, which can pull the supply below the MCU’s brown-out threshold while the cell still has most of its capacity. A bulk capacitor across the cell to supply the burst, and limiting peak current, are part of the power design. And the cell self-discharges by around 1% per year, which starts to matter for multi-year designs.
Duty cycling everything else
Section titled “Duty cycling everything else”The MCU is not the only thing that can sleep.
- Sensors: use one-shot (forced) mode instead of continuous conversion, or power the sensor from a GPIO pin or a load switch and turn it off entirely between samples. Account for its start-up time.
- Radios: the radio’s energy is dominated by start-up and protocol overhead, not payload. Sending 60 readings in one packet per minute costs far less than 60 packets. For BLE, a longer connection interval or advertising interval directly lowers average current.
- External flash and displays: put them in their deep power-down modes with the documented command, and remember the wake-up delay.
- Voltage dividers and pull-ups you only need occasionally: switch them.
Brown-out versus a flat battery
Section titled “Brown-out versus a flat battery”As a battery discharges, its voltage drops slowly, then collapses. Near the end, every current peak pulls it lower. The STM32’s brown-out reset (BOR) holds the chip in reset below a threshold set in the option bytes (on the F4, levels from roughly 2.1 V to 2.7 V, or off), so it never runs with a supply too low for reliable flash reads. That protects you from executing garbage, but it does not tell you the battery is low, and a device that resets every time the radio transmits looks, from the field, like a firmware bug.
The programmable voltage detector (PVD, PVDE and PLS in PWR->CR) raises an interrupt (EXTI line 16) when the supply falls below a chosen level above the BOR threshold. Use it, or an ADC measurement of the battery under load, to detect a low battery early, stop power-hungry operations, save state to flash while there is still enough voltage to program it, and report the condition. Measure the battery during the radio burst, not at rest, because the loaded voltage is what browns the chip out.
Common mistakes
Section titled “Common mistakes”- Floating input pins. Unused or unconnected inputs float and draw current through the input buffer. Set every unused pin to analog mode.
- A forgotten LED. A status LED that stays on costs more than everything else combined. Blink it for milliseconds, or not at all in battery mode.
- Busy-waiting instead of sleeping.
delay_ms()and polling loops keep the CPU at full current. Use a timer or RTC wake-up andWFI. - Measuring with the debugger attached. Debug bits keep clocks running and persist across resets. Disconnect the probe and power-cycle before measuring.
- Peripherals left enabled. An idle ADC, timer, or UART clock still draws current. Gate peripheral clocks and power down analog blocks when not in use.
- Forgetting the clock after Stop. The F4 wakes on the 16 MHz HSI; UART output is garbage until you restore the PLL. Reconfigure the clock tree before the waking ISR runs.
- A lost wake-up in the idle loop. Checking a flag and then executing
WFIwith interrupts enabled can sleep through an event. Mask interrupts, check,WFI, unmask.
Exercises
Section titled “Exercises”- Compute it yourself: write a small host C program that takes a table of phases (current in µA, duration in µs, how often per hour) and prints the average current and battery life for a given capacity. Check it against both tables in the worked example (334 µA and 25 days; 15.8 µA and 1.44 years, both before derating).
- Replace the busy loop in your blinky with a SysTick-driven design and the
idle_looppattern usingWFI. Measure the board current through the Discovery’s IDD jumper (JP1) before and after with a multimeter. Expect a clear drop even though the clock is unchanged. - Add
enter_stop_modewith an RTC wake-up every 2 seconds. Toggle an LED briefly on each wake and print a message over USART2. First, deliberately skipclock_init_168mhz()and observe the garbled UART output; explain the exact ratio of the wrong baud rate. Hint: after Stop, SYSCLK is the 16 MHz HSI but the APB1 prescaler keeps its value, so APB1 runs at 4 MHz instead of 42 MHz and the line runs at about 11000 baud. - Use Standby with PA0 as the wake-up source: the board blinks three times, enters Standby, and wakes when you press the user button. Use
woke_from_standby()and the backup registers to keep a wake-up counter that survives Standby but resets on power-on. Print it on each wake. - Hard: on a board with a power profiler, record the current profile of exercise 3. Identify every phase (Stop, wake-up and PLL start, active, UART transmission), measure each one’s current and duration from the trace, compute the average current from your table, and compare it with the profiler’s own average. Explain any difference larger than 10%.
Check yourself
Section titled “Check yourself”Question 1:An idle loop does if (!event_flag) __WFI(); with interrupts enabled. Occasionally events are handled hundreds of milliseconds late. Why?
The check and the sleep are not atomic. Masking interrupts before the check closes the window, and WFI still wakes on a pending interrupt while PRIMASK is set.
Question 2:After waking from Stop mode on the STM32F407, UART output is garbled although the code did not change any UART register. What happened?
Stop mode switches off the PLL and HSE. The F4 resumes on the HSI, and every clock-derived setting is wrong until the clock tree is reconfigured.
Question 3:A sensor node on an STM32F407 averages 334 µA, of which 298 µA is Stop-mode current. Which change helps battery life the most?
The sleep current is 89% of the total. Optimisations to the other phases change at most the remaining 11%, so the sleep current must be attacked first.
Question 4:Why does running the same task at half the clock frequency usually not save energy, even though the active current halves?
Dynamic energy per operation is roughly independent of frequency at a fixed voltage. Stretching the active phase only adds the energy of everything else that is on. Lowering the clock wins when it also allows a lower core voltage.
Question 5:You measure 2 mA in Stop mode instead of the expected few hundred µA. The probe is still connected and the board has not been power-cycled since the last debug session. What is the most likely explanation?
With DBG_STOP set the debug logic keeps clocks alive so the probe stays connected. These bits are only cleared by a power-on reset, so disconnect and power-cycle before measuring.
Key takeaways
Section titled “Key takeaways”- Dynamic power scales with frequency and with voltage squared; leakage remains when everything stops and grows with temperature and retained silicon.
- The F4 offers Sleep (CPU stopped, a few cycles to wake), Stop (clocks off, RAM kept, wake on EXTI, resumes on the HSI) and Standby (a few µA, RAM lost, wake is a reset).
- The canonical idle loop is: disable interrupts, check for work,
DSB,WFI, enable interrupts. - Low-power firmware is event-driven: no busy-waits, timed wake-ups from the RTC, race to sleep.
- Unused pins in analog mode, peripheral clocks gated, LEDs and pull-ups budgeted, and the debugger disconnected: the remaining current is usually outside the CPU.
- Battery life comes from a measured current profile: average current is total charge per cycle over cycle time; attack the largest term first, and on a coin cell that is usually the sleep current.
Next: Performance and code size optimization, where you measure cycles and bytes and learn which changes actually make code faster and smaller.