Low-Power Embedded Design: Sleep Modes, Duty Cycling and Battery Life
A battery-powered sensor that measures temperature once a minute does useful work for perhaps a few milliseconds per reading. If the microcontroller stays fully awake the rest of the time, the battery drains in days. If it sleeps between readings, the same battery can last for years. Low-power design is the art of making the device sleep as deeply and as often as possible, and making every waking moment short.
This article covers where power goes in a microcontroller, the sleep modes available, the hardware details that silently waste current, and how to calculate battery life before building anything.
Where the Power Goes
CMOS logic consumes two kinds of power. Dynamic power is used each time a node switches and follows the classic relation below. Static power is leakage current that flows even when nothing switches.
P_dynamic = alpha x C x V^2 x f alpha : fraction of nodes switching per cycle C : switched capacitance V : supply voltage f : clock frequency
Three conclusions follow. Lowering the clock lowers dynamic power in proportion. Lowering the supply voltage helps even more, because of the square term. And stopping the clock entirely removes dynamic power, leaving only leakage, which is what sleep modes do. Microcontroller datasheets usually quote active current per MHz and separate figures for each sleep mode; always use those figures for your exact part and temperature.
Sleep Modes
Names differ between vendors, but most microcontrollers offer a ladder of modes. Deeper modes save more power but switch off more of the chip and take longer to wake.
| Mode (generic) | What stops | Typical wake sources | Trade-off |
|---|---|---|---|
| Sleep / idle | CPU clock only; peripherals run | Any enabled interrupt | Instant wake, modest savings |
| Stop / deep sleep | Most clocks; RAM and registers kept | External interrupt, RTC alarm, some comms peripherals | Large savings; clocks must be restarted on wake |
| Standby / shutdown | Almost everything; RAM usually lost | Wake-up pin, RTC, reset | Lowest current; wake is like a reset |
On ARM Cortex-M, the core enters sleep with the WFI (wait for interrupt) instruction. Setting the SLEEPDEEP bit in the System Control Register selects the vendor's deep-sleep mode instead of plain sleep, and the SLEEPONEXIT bit makes the core go back to sleep automatically after each ISR, which suits fully interrupt-driven designs. The fragment below is MCU-specific and uses CMSIS names.
#include "stm32l0xx.h" /* any CMSIS device header provides SCB and __WFI */
void enter_sleep(void)
{
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk; /* plain sleep: CPU stops, peripherals run */
__WFI();
}
void enter_deep_sleep(void)
{
/* Vendor-specific setup goes here, e.g. choosing Stop mode and
regulator settings in the PWR registers, per the reference manual. */
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; /* WFI now enters deep sleep */
__DSB(); /* complete outstanding memory writes */
__WFI();
SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk; /* back to normal after wake-up */
/* After Stop mode many parts wake on a slow internal clock:
re-initialise the PLL and system clock here. */
}
Calculating Average Current and Battery Life
A duty-cycled device alternates between short active bursts and long sleeps. Its average current is the time-weighted sum of each phase. The program below runs on any PC and does the arithmetic for a sensor node. The current values are example inputs for the calculation, not figures for any particular chip; replace them with datasheet or measured values.
#include <stdio.h>
typedef struct {
const char *name;
double current_ma; /* current in this phase, mA */
double time_ms; /* time spent per cycle, ms */
} phase_t;
int main(void)
{
/* Example inputs: replace with your measured values */
const phase_t phases[] = {
{ "wake + measure", 5.0, 10.0 },
{ "radio transmit", 20.0, 5.0 },
{ "sleep", 0.005, 59985.0 }, /* rest of a 60 s cycle */
};
const int n = (int)(sizeof phases / sizeof phases[0]);
const double battery_mah = 1000.0;
const double derating = 0.8; /* margin for self-discharge, temperature, ageing */
double charge = 0.0, period = 0.0;
for (int i = 0; i < n; i++) {
charge += phases[i].current_ma * phases[i].time_ms; /* mA x ms */
period += phases[i].time_ms;
printf("%-15s %8.3f mA for %8.1f ms\n",
phases[i].name, phases[i].current_ma, phases[i].time_ms);
}
double avg_ma = charge / period;
double hours = battery_mah * derating / avg_ma;
printf("cycle period : %.1f s\n", period / 1000.0);
printf("average current : %.4f mA\n", avg_ma);
printf("estimated life : %.0f hours (%.1f years)\n", hours, hours / 8760.0);
return 0;
}
wake + measure 5.000 mA for 10.0 ms radio transmit 20.000 mA for 5.0 ms sleep 0.005 mA for 59985.0 ms cycle period : 60.0 s average current : 0.0075 mA estimated life : 106684 hours (12.2 years)
The result looks wonderful, and it teaches the most important lesson of low-power design: at such small duty cycles, the sleep current dominates. Here the sleep phase contributes 0.005 mA of the 0.0075 mA average, two-thirds of the total. A 12-year figure is also beyond the shelf life of many batteries, so self-discharge, not the circuit, would set the real limit. Halving active time barely matters; halving sleep current nearly doubles battery life.
Hidden Current Drains
- Floating input pins. An unconnected CMOS input can sit midway between the rails and draw current through its input stage. Configure unused pins as analog inputs or as outputs, as the reference manual recommends.
- Pull-up resistors held low. A 10 kilo-ohm pull-up to 3.3 V on a pressed button draws 0.33 mA, far more than the sleeping MCU.
- Voltage regulator quiescent current. Some linear regulators draw milliamps with no load; choose one specified for low quiescent current.
- Voltage dividers for battery measurement that are always connected. Switch them with a MOSFET or use high-value resistors.
- Power LEDs and debug interfaces. A board's power LED can outweigh everything else; an attached debugger can keep internal debug logic powered.
- Peripheral clocks left enabled for unused peripherals, which adds active current.
Firmware Habits for Low Power
- Make the design interrupt-driven; never busy-wait. Every polling loop keeps the CPU awake.
- Run fast and sleep: finishing work quickly at a higher clock and sleeping longer often beats running slowly all the time, but measure both.
- Use DMA so the CPU can sleep during long transfers.
- Use an RTC or low-power timer for wake-ups, since it keeps running in deep sleep.
- Batch work, for example store ten readings and transmit once, because the radio is usually the largest consumer.
- Measure with a proper instrument such as a source-measure unit or a current profiler. A multimeter cannot follow short current spikes.
Summary
Dynamic power scales with frequency and the square of voltage, and sleep modes remove it by stopping clocks. Choose the deepest sleep mode whose wake sources and wake time fit the application, enter it with WFI on Cortex-M, and restore clocks after waking. Calculate average current as a time-weighted sum before building, then hunt for hidden drains, because in a duty-cycled design the sleep current usually decides the battery life.