FIRMWARE DEVELOPMENT • DEC 18, 2025 • 8 MIN READ

Optimizing ARM Cortex-M Firmware: Power Efficiency Techniques

Battery life is decided by what your firmware does while it is doing nothing. Which is most of the time.

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.

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:

Common Questions

Frequently Asked Questions

What is the single biggest win in low-power firmware?

Spending as much time as possible in the deepest sleep mode the application allows. Everything else is secondary. A device that is awake 1% of the time and asleep at sub-microamp currents for the other 99% will beat a device with beautifully optimised active-mode code that never sleeps properly. Design the architecture around sleeping, and the rest follows.

Why is my current draw higher than the datasheet says?

Almost always something outside the MCU, or a peripheral you forgot to turn off. Unconfigured GPIO pins floating at intermediate voltages draw current. Pull-ups on I2C lines draw current continuously. External sensors, LEDs and regulators have their own quiescent draw. The datasheet figure is for the MCU core alone under ideal conditions, and your board is not that.

Does a faster clock always mean more power?

Not necessarily, and this trips people up. Power scales with frequency, but a faster clock finishes the work sooner and returns to sleep sooner. This is the race-to-sleep principle. Whether it wins depends on your part and your workload, so measure both. What is unambiguous is that running fast and then idling in a shallow sleep is the worst of both worlds.

How do I actually measure microamp-level current?

Not with a multimeter. You need an instrument that can capture microamps and milliamps in the same trace, because your device swings between them, and the average is what determines battery life. A dedicated low-power analyser or a current probe with sufficient dynamic range is the right tool. Without proper measurement you are guessing, and guesses about power are almost always wrong.

Need a device that runs for years, not weeks?

Ultra-low-power firmware is a specialism. We have shipped sensors that run three years on two AA cells. Tell us your battery target.

REQUEST A CONSULTATION

Keep Reading

Related Articles