The single hardest constraint I’ve ever designed against wasn’t timing or memory — it was a customer requirement that a sensor node run for five years on two AA batteries. That number forces a completely different way of thinking about firmware and hardware, where every microamp and every millisecond spent awake becomes a line item in a budget. In this article I’ll walk through the practical techniques, both hardware and firmware, that make multi-year battery life achievable in embedded systems.
Building a Power Budget
Before writing a line of firmware, I build a power budget: a table of every operating state the device will enter, its current draw, and the fraction of time spent there. Average current draw over a duty cycle directly determines battery life.
graph LR
A[Deep Sleep - 0.5uA] -->|99.9% of time| E[Weighted Average Current]
B[Sensor Read - 2mA] -->|0.05% of time| E
C[Radio TX - 40mA] -->|0.02% of time| E
D[MCU Active/Processing - 5mA] -->|0.03% of time| E
Battery life (hours) ≈ Battery capacity (mAh) / Average current draw (mA)
For example, a device averaging 5µA (0.005mA) on a 2000mAh battery gives roughly 2000/0.005 = 400,000 hours (~45 years) theoretically — though real batteries also have self-discharge, so the practical figure is lower but still enormous. This math is exactly why even a small increase in sleep-mode current can devastate battery life far more than optimizing the “busy” states.
MCU Power States
Every modern MCU offers a hierarchy of power states, and the deepest one usable at any given moment is the one I want the firmware defaulting to.
| State (typical ARM Cortex-M naming) | CPU | RAM | Peripherals | Wake Source | Typical Current |
|---|---|---|---|---|---|
| Run | Active | Retained | Active | N/A | mA range |
| Sleep | Stopped | Retained | Active | Any interrupt | Sub-mA |
| Stop | Stopped | Retained | Mostly off | RTC, external pin, some peripherals | µA range |
| Standby/Shutdown | Off | Lost (mostly) | Off | RTC, wake pin, reset | Sub-µA to nA |
/* STM32 example: entering STOP mode, waking via RTC alarm */
void Enter_Low_Power_Sleep(void)
{
HAL_SuspendTick(); /* prevent SysTick from waking us immediately */
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
/* --- execution resumes here after wake interrupt --- */
SystemClock_Config(); /* clocks must be reconfigured after STOP */
HAL_ResumeTick();
}
Firmware Strategies for Low Power
1. Sleep Aggressively, Wake Only When Necessary
The golden rule of low-power embedded firmware: the CPU should be asleep by default and only wake in response to a real event (timer, interrupt, or external trigger) — never poll in a busy loop.
int main(void)
{
System_Init();
RTC_SetWakeupAlarm_Periodic(60); /* wake every 60 seconds */
for (;;) {
Enter_Low_Power_Sleep(); /* CPU parked here almost all the time */
Read_Sensors();
Process_And_Store();
if (Should_Transmit()) {
Radio_TransmitBuffer();
}
}
}
2. Minimize Time in Active/Run Mode
Every millisecond spent at full clock speed costs orders of magnitude more current than the same millisecond in sleep. I optimize hot code paths not just for correctness but for cycle count, specifically to shrink the active window.
gantt
title Duty Cycle: Sleep vs Active Time
dateFormat X
axisFormat %s
section Cycle (60s period)
Deep Sleep :done, s1, 0, 59800
Wake + Sensor Read :active, s2, 59800, 100
Process + Radio TX :crit, s3, 59900, 100
3. Dynamic Voltage and Frequency Scaling (DVFS)
Some MCUs allow scaling clock speed and core voltage down when full performance isn’t needed, since power roughly scales with P ∝ C × V² × f — meaning even a modest voltage reduction has an outsized effect on power draw.
void Set_Low_Power_Clock_Profile(void)
{
/* Drop from 168MHz/high-voltage to 16MHz/low-voltage range
for tasks that don't need peak performance */
HAL_RCC_OscConfig(&low_power_osc_config);
HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE3);
}
4. Peripheral Clock Gating
Unused peripherals still draw current if their clocks are left enabled. I explicitly disable clocks to any peripheral not currently in use.
void Disable_Unused_Peripherals(void)
{
__HAL_RCC_SPI2_CLK_DISABLE();
__HAL_RCC_USART3_CLK_DISABLE();
__HAL_RCC_TIM6_CLK_DISABLE();
}
5. Radio Duty Cycling
Radio transmission is almost always the single largest current draw in a wireless embedded device (often 10-100x the MCU’s active current). I minimize both transmission duration and frequency:
- Batch multiple sensor readings into a single transmission instead of transmitting each individually
- Use the lowest transmit power that still achieves reliable link margin
- Choose low-power wireless protocols (BLE, LoRa) appropriate to the range/data-rate need, as covered in the communication article of this series
#define BATCH_SIZE 10
static float reading_batch[BATCH_SIZE];
static uint8_t batch_index = 0;
void Add_Reading(float value)
{
reading_batch[batch_index++] = value;
if (batch_index >= BATCH_SIZE) {
Radio_TransmitBatch(reading_batch, BATCH_SIZE); /* one radio-on event instead of ten */
batch_index = 0;
}
}
6. Event-Driven Sensor Reading Instead of Polling
Many modern sensors support interrupt-on-threshold or interrupt-on-motion features, letting the MCU stay asleep until the sensor itself has something worth reporting, rather than waking on a fixed timer just to check.
sequenceDiagram
participant Sensor as Accelerometer (motion-interrupt capable)
participant MCU
Note over MCU: Deep sleep, sensor monitors autonomously
Sensor->>MCU: Interrupt pin toggles (motion detected)
MCU->>MCU: Wake up
MCU->>Sensor: Read motion data over I2C
MCU->>MCU: Process, transmit if needed
MCU->>MCU: Return to deep sleep
Hardware-Level Power Optimization
Firmware can only do so much — hardware choices set the ceiling:
- Low-quiescent-current voltage regulators (some modern LDOs draw under 1µA quiescent current)
- Load switches to fully power off sensor subsystems (not just put them in low-power mode) when not in use
- Choosing MCUs with genuinely low sleep-current specs — datasheet numbers vary hugely between vendors and families for supposedly similar “stop mode” states
- Minimizing pull-up/pull-down resistor current leakage on GPIO pins
- Battery chemistry selection — primary lithium cells (like LiSOCl2) offer very low self-discharge for multi-year deployments, versus rechargeable chemistries suited to frequent-cycling applications
Measuring Real-World Power Consumption
I never trust datasheet numbers alone — I measure actual current draw on the bench using a precision current measurement tool (like a Nordic Power Profiler Kit or an oscilloscope with a shunt resistor) across the full duty cycle, since real firmware behavior often draws more current than the idealized datasheet scenario.
graph LR
A[Precision Shunt Resistor] --> B[Current Measurement Amplifier]
B --> C[Oscilloscope / Power Profiler]
C --> D[Visualize sleep/active current transitions over time]
Battery Chemistry Considerations
The choice of battery chemistry itself is a power management decision, not just a mechanical/BOM one. Different chemistries have very different discharge curves, temperature tolerance, and self-discharge rates, all of which affect how I design the firmware’s low-battery detection and behavior:
| Chemistry | Nominal Voltage | Self-Discharge | Discharge Curve | Typical Use |
|---|---|---|---|---|
| LiSOCl2 (Lithium Thionyl Chloride) | 3.6V | Very low (<1%/year) | Flat until sudden drop | Multi-year remote sensors, meters |
| LiMnO2 (Lithium Manganese Dioxide) | 3.0V | Low | Gradual, more predictable | Medical devices, consumer electronics |
| Alkaline | 1.5V/cell | Moderate | Gradual slope | Low-cost, replaceable-battery consumer products |
| Li-ion/LiPo (rechargeable) | 3.7V nominal | Moderate, needs protection circuit | Gradual with steep tail | Wearables, rechargeable IoT devices |
A flat discharge curve (like LiSOCl2) makes “battery percentage remaining” estimation difficult from voltage alone, since voltage barely changes until the battery is nearly depleted — I often use coulomb counting (tracking cumulative current draw against known capacity) instead of voltage-based estimation for these chemistries.
/* Simple coulomb counter for flat-discharge-curve batteries,
where voltage alone can't reliably indicate remaining capacity */
typedef struct {
float capacity_mah;
float consumed_mah;
} coulomb_counter_t;
void Coulomb_Counter_Update(coulomb_counter_t *cc, float current_ma, float elapsed_hours)
{
cc->consumed_mah += current_ma * elapsed_hours;
}
float Coulomb_Counter_RemainingPercent(coulomb_counter_t *cc)
{
float remaining = cc->capacity_mah - cc->consumed_mah;
return (remaining / cc->capacity_mah) * 100.0f;
}
Energy Harvesting as a Power Supplement
For some designs, I’ve supplemented or entirely replaced battery power with energy harvesting — small solar cells, thermoelectric generators, or piezoelectric vibration harvesters — paired with a supercapacitor or thin-film rechargeable battery for energy storage. This fundamentally changes the firmware’s power management philosophy: instead of budgeting a fixed energy reserve to last a target lifetime, the system manages an intermittent, variable energy income and must gracefully suspend operation (and resume correctly) whenever harvested energy runs short.
graph LR
A[Solar Cell / Piezoelectric Harvester] --> B[Energy Harvesting IC]
B --> C[Supercapacitor / Thin-Film Battery]
C --> D[MCU + Sensors]
D -->|monitors stored energy level| E{Enough Energy?}
E -->|Yes| F[Perform Measurement + Transmit]
E -->|No| G[Stay in Deep Sleep, Wait for More Harvested Energy]
Firmware-Level Battery Monitoring and Graceful Degradation
Beyond simply reporting battery level, I design firmware to actively change its own behavior as battery capacity declines, extending useful device life even as available energy shrinks:
typedef enum { PWR_NORMAL, PWR_CONSERVE, PWR_CRITICAL } power_mode_t;
power_mode_t Determine_Power_Mode(float battery_percent)
{
if (battery_percent > 30.0f) return PWR_NORMAL;
if (battery_percent > 10.0f) return PWR_CONSERVE;
return PWR_CRITICAL;
}
void Apply_Power_Mode(power_mode_t mode)
{
switch (mode) {
case PWR_NORMAL: Set_Sample_Interval(60); Set_Radio_Power(FULL); break;
case PWR_CONSERVE: Set_Sample_Interval(300); Set_Radio_Power(REDUCED); break;
case PWR_CRITICAL: Set_Sample_Interval(3600); Send_LowBattery_Alert_Once(); break;
}
}
Validating Battery Life Estimates Before Shipping
Theoretical power budget calculations always need real-world validation before I’m confident quoting a battery life figure to a customer. My typical validation process:
- Bench current profiling across the full duty cycle using a precision power analyzer, capturing sleep current, wake transients, active processing current, and radio transmission current as separate measured values rather than trusting datasheet typical values alone
- Accelerated life testing — running the actual production firmware continuously on a bench power supply while logging cumulative energy consumption, extrapolating to the target deployment duration
- Field pilot deployment — a small batch of units in real operating conditions (which often include temperature extremes and RF environments the bench doesn’t replicate) with periodic battery voltage/capacity checks against the model’s predictions
- Worst-case scenario testing — deliberately triggering the highest-power scenarios back-to-back (maximum sensor activity, repeated radio retries due to poor signal) to validate the power budget still holds under conditions less favorable than the “typical” case used in the original calculation
Datasheet current figures are typically measured under ideal lab conditions at a specific voltage and temperature; real deployed current draw is often meaningfully higher, which is exactly why I build margin into every battery life estimate rather than quoting the theoretical best case as the expected outcome.
Environmental and Temperature Effects on Power Budgets
Battery capacity and leakage current are both temperature-dependent in ways that firmware power budgets need to account for, not just idealize away. Lithium battery capacity typically drops noticeably in cold conditions (a battery rated for 2000mAh at room temperature might deliver meaningfully less at sub-zero temperatures), while MCU leakage current in sleep states tends to increase with higher temperatures. For products deployed outdoors or in industrial environments spanning a wide temperature range, I validate the power budget across the full rated operating temperature range, not just at typical room-temperature bench conditions, and build in extra margin for whichever direction — hot or cold — the deployment environment is expected to skew.
Voltage Regulator Selection and Efficiency
The power path between the battery and the MCU is itself a significant design decision. A linear regulator (LDO) is simple and low-noise but wastes the voltage difference between input and output as heat — inefficient whenever the battery voltage is meaningfully higher than the MCU’s required supply voltage. A switching regulator (buck converter) is far more efficient across a wide input voltage range but adds switching noise that can couple into sensitive analog circuitry, tying directly back to the signal conditioning and PCB layout considerations discussed earlier in this series.
graph TD
A[Battery] --> B{Voltage Difference to MCU Supply}
B -->|Small difference| C[LDO - simple, low noise, acceptable efficiency loss]
B -->|Large difference| D[Buck Converter - higher efficiency, more design complexity]
C --> E[MCU Supply Rail]
D --> E
For many of my battery-powered designs, I use a switching regulator for the bulk of the power path (maximizing runtime from the battery’s full discharge curve) but add a small LDO stage specifically for noise-sensitive analog circuitry, getting both efficiency where it matters most and clean power where noise would otherwise compromise measurement accuracy.
Real-World Applications
- Environmental sensor nodes — years of battery life on coin cells using RTC-triggered wake and batched LoRa transmission
- Wearables — balancing continuous sensing (heart rate, motion) against daily charge cycle constraints using aggressive sleep states between samples
- Asset tracking tags — GPS/cellular modules duty-cycled to seconds of activity per hour to stretch battery life to months
- Smart water/gas meters — decade-plus battery life requirements driving extremely conservative wake schedules and minimal radio usage
Frequently Asked Questions
What’s the biggest single factor in extending battery life? In almost every design I’ve worked on, it’s minimizing time spent in active/radio-on states and maximizing time in the deepest available sleep mode — the sleep current times the (very long) sleep duration usually dominates the power budget more than any single active-mode optimization.
Does a higher-capacity battery always mean longer battery life? Not necessarily — a poorly optimized firmware duty cycle can drain even a large battery quickly, while good power management can make a small battery last years; capacity matters, but average current draw usually matters more.
Should I use a rechargeable or primary (single-use) battery? Primary lithium batteries (like LiSOCl2 or LiMnO2) typically offer better energy density and much lower self-discharge for long-life, low-power devices; rechargeable batteries make sense when the device is used frequently enough to justify regular charging infrastructure.
How do I know if my sleep mode is actually working? Measure actual current draw with a precision meter across a full duty cycle — it’s common to assume the device is sleeping correctly when, in reality, an unintended peripheral or GPIO configuration is preventing the MCU from reaching its lowest power state.
Summary
Managing power in battery-powered embedded systems is a discipline that spans hardware selection and firmware architecture together: building an honest power budget, using the deepest available sleep states by default, minimizing and batching radio use, gating unused peripherals, and choosing low-quiescent-current hardware. None of these techniques alone is usually enough — it’s the combination, applied consistently across every part of the system, that turns a device that lasts days on a battery into one that lasts years.