Key takeaways
- Sleep current decides battery life, not active-mode efficiency. A periodic sensor spends over 99% of its life idle, so getting standby from 50 µA to 1 µA turns a one-year device into a five-year one.
- Configure every unused GPIO pin. Floating pins sit at intermediate voltages where the input buffer conducts continuously. This leak alone routinely dwarfs a carefully optimised sleep budget.
- Gate peripherals you are not using. Enable a peripheral immediately before use and disable it immediately after. Init code that turns everything on and never turns anything off is a common cause of hundreds of microamps in 'deep sleep'.
- Race to sleep is a genuine competing principle. Running faster to finish sooner can beat running slower, because deep sleep is so much cheaper than any active mode. Measure; do not assume.
- Use DMA and interrupts, never polling. Let DMA move data while the core sleeps, and make the main loop little more than an instruction that enters sleep.
- Measure with the right instrument. A multimeter cannot resolve microamps and milliamps in one trace, and the average across the whole duty cycle is what determines battery life.
The number that decides everything
A battery-powered sensor that wakes once an hour, takes a reading, transmits it, and sleeps, spends more than 99% of its life doing nothing. Which means battery life is overwhelmingly determined by how much current the device draws while it is doing nothing.
This has a direct consequence for how you optimise. Shaving cycles off your active-mode code feels productive and achieves almost nothing. Getting the sleep current from 50 microamps down to 1 microamp changes the product from a one-year device into a five-year device.
Start there. Always.
Sleep modes, and choosing the right one
ARM Cortex-M parts offer a hierarchy of low-power states, and vendors name them inconsistently, which causes endless confusion. The underlying structure is consistent, though: the deeper you go, the less power you draw and the more state you lose.
- Sleep. The core stops; peripherals keep running. Wake-up is immediate. Savings are modest.
- Stop. Clocks are largely gone but RAM and register contents survive. Wake-up costs microseconds. This is the sweet spot for most sensor applications, and on an STM32L you are looking at single-digit microamps or below.
- Standby. Nearly everything is off. RAM contents are lost. Wake-up is effectively a reset. Sub-microamp currents are achievable, at the cost of rebuilding your state on every wake.
The engineering judgement is about what you can afford to lose. If your application can reconstruct its state cheaply on wake, Standby gives you the best numbers. If it maintains context that is expensive to rebuild, Stop mode with RAM retention is usually the better trade even though it draws more.
Use the RTC as the wake-up source for periodic work. It keeps running in the deep modes and costs almost nothing, which is exactly what you want from the one thing that stays awake.
Turn off what you are not using
Every peripheral you enable draws current, and it keeps drawing current whether or not your code is using it. On STM32 parts, peripherals are gated through the RCC registers, and a peripheral whose clock you never enabled costs you nothing.
The trap is initialisation code that enables everything at start-up because it is convenient, and never turns anything off. We have picked up projects drawing hundreds of microamps in what was supposed to be deep sleep, purely because the ADC, the UART and two timers were still clocked and idling.
Enable a peripheral immediately before you use it, and disable it immediately after. It is slightly more code and it is the difference between a product that works and one that does not.
The GPIO problem nobody mentions
This one costs more projects more battery life than any other single mistake, and it is invisible in code review.
An unconfigured GPIO pin floats. Its voltage drifts to somewhere in the middle, and in that intermediate region the input buffer's transistors are both partially conducting, which draws current continuously. Multiply by every unused pin on the package and you have a leak that dwarfs your carefully optimised sleep current.
Configure every unused pin. Set them to analogue mode, or drive them to a defined level. Do not leave a single pin floating. This is a five-minute change that has rescued more of our designs than any clever algorithmic optimisation.
The same discipline applies to external components. I2C pull-ups conduct whenever the bus is held low. An LED left on draws milliamps, which is thousands of times your sleep budget. A linear regulator with poor quiescent current can single-handedly ruin the numbers regardless of what the MCU does.
Clock scaling and racing to sleep
Dynamic power scales with clock frequency, so a slower clock draws less. That much is straightforward, and running the core at the minimum frequency that meets your timing requirements is sound practice. STM32L parts offer the MSI oscillator specifically for this, letting you dial the frequency to what the task actually needs.
But there is a competing principle, and it is genuinely competing rather than a trick question. Race to sleep says: run fast, finish the work as quickly as possible, and get back into deep sleep sooner. Because deep sleep is so much cheaper than any active mode, minimising active time can beat minimising active power.
Which wins depends on the part, the workload and the wake-up overhead. There is no universal answer, and anyone who gives you one has not measured. What is not in dispute is that the worst outcome is running slowly and failing to sleep deeply, which is what you get by default if you do not think about it at all.
Let DMA do the work while the core sleeps
Direct memory access is a power optimisation, though it is usually taught as a throughput optimisation.
Consider reading a batch of ADC samples. The naive implementation has the core sit in a loop, polling and copying, burning active-mode current for the entire duration. The DMA implementation configures the transfer, puts the core to sleep, and lets the DMA controller move the data. The core wakes on the completion interrupt, having drawn sleep current throughout.
The data arrives either way. In one case the CPU was awake for all of it and in the other it was not. Use DMA for anything involving moving a block of data: ADC captures, SPI and I2C transactions, UART transfers.
Interrupts, not polling
Polling is the natural way to write embedded code and it is corrosive to battery life. A loop that checks whether a button has been pressed keeps the core awake to discover, repeatedly, that it has not.
Restructure around events. The core sleeps. A peripheral interrupt wakes it. It handles the event, and it goes straight back to sleep. The main loop should be, almost literally, an instruction that enters sleep.
Keep interrupt service routines short. Time spent in an ISR is time spent at full active current, so set a flag and handle the work in the main context if the work is substantial. And be careful with the wake-up itself: an interrupt that fires far more often than it needs to will destroy your power budget no matter how efficient the handler is.
Measure, because your intuition is wrong
Everything above is theory until you put an instrument on the rail, and the gap between what engineers believe their firmware is doing and what it is actually doing is consistently large.
A standard multimeter is the wrong tool. Your device swings between microamps in sleep and tens of milliamps during a radio transmission, and what determines battery life is the average across that whole cycle. You need an instrument that resolves microamps and milliamps in a single trace with enough bandwidth to catch the transmit burst.
What you are looking for on that trace is: does the sleep current match the datasheet, or is something still on? Is the device waking more often than you think? Is the wake-up duration longer than expected? Is there a peak you cannot account for? Every one of those questions has caught a real bug in our lab, and none of them was visible from reading the code.
What this adds up to
Our agricultural sensor nodes achieve more than three years on two AA cells. There is no single trick behind that figure. It is an STM32L0 in deep sleep for essentially its entire life, every unused pin configured, every peripheral gated off, an event-driven architecture with no polling anywhere, DMA handling the sensor reads, and LoRaWAN transmission duty-cycled down to roughly one packet per hour.
Each individual decision is unremarkable. Together they are the difference between a device that gets deployed once and forgotten, and a device that someone has to visit with a ladder and a fresh battery every six months.
Sources and further reading
Primary references for the standards, regulations and figures cited above:
- STM32L0 Series — ultra-low-power MCUs — STMicroelectronics reference for Stop/Standby mode currents and the MSI clock.
- Arm Cortex-M — low-power modes and the Wait-For-Interrupt model — Arm's technical reference on Cortex-M sleep, deep sleep and WFI/WFE.
- STM32 Ultra-Low-Power Features — application notes — ST application notes covering peripheral clock gating (RCC), GPIO configuration and DMA-driven low-power design.