Power Consumption Testing for Embedded Systems and IoT Devices
A firmware update that doubles your device's current draw in sleep mode will kill battery life and generate customer support tickets months after release. Power consumption bugs are invisible in functional testing and only appear in the field — unless you test for them explicitly.
Power testing is a first-class requirement for any battery-powered embedded system or IoT device.
Why Power Testing Gets Skipped
Power consumption testing is skipped more often than it should be because:
It requires hardware. You can't measure real current draw in a simulator or emulator. You need the target hardware and a current measurement instrument.
It's slow. Battery life tests that run devices for hours or days don't fit in a CI pipeline.
The tooling is less mature. Functional test frameworks are standardized; power test frameworks are custom to most organizations.
These are real constraints, but not blockers. You can build a practical power testing strategy that catches most regressions without week-long test runs.
Understanding Power Modes
Embedded systems and IoT devices operate in multiple power states. Each needs to be tested independently:
Active mode: the device is executing code, communicating, or processing data. This is the highest-current state.
Sleep/standby modes: the device is idle but retains RAM state. Most microcontrollers have multiple sleep depths (light sleep, deep sleep, hibernate) with different current draw and wake latency tradeoffs.
Off/shutdown: minimum power draw with state lost. The device is essentially powered down.
Transmission bursts: wireless transmission (WiFi, BLE, LTE-M, LoRa) causes current spikes that are often the dominant power consumer in IoT devices that communicate infrequently.
Your power test suite should verify the expected current draw and timing for each state, and verify correct transitions between states.
Measurement Hardware
For accurate current measurement, you need hardware in the loop:
Nordic PPK2 (Power Profiler Kit II): $65, measures 0.1 µA to 1 A with 100k samples/second. Excellent for BLE and Nordic nRF52/nRF91 platforms. Software is open source with a timestamped CSV export that's easy to analyze programmatically.
Otii Arc: $600-$1000, higher accuracy, supports battery emulation, has a REST API for automation. Good for CI integration.
Joulescope: $500, real-time current measurement with good PC software. No REST API but has a Python API for automation.
Bench power supply with current measurement: a Rigol DP800 or similar provides current measurement to ~1 mA resolution. Sufficient for active mode testing; not sensitive enough for deep sleep testing (µA range).
DIY shunt resistor: a 1Ω shunt resistor and an ADC can measure current with any microcontroller. Resolution depends on your ADC, but this works for fast regression testing at the system level.
For automated CI testing, the Otii Arc and Nordic PPK2 have the best automation support.
Defining Power Budgets
Before you can test power consumption, you need a target budget. This is a product decision, not a testing decision — but engineering needs to quantify it.
A simple power budget for a battery-powered IoT sensor:
Battery: 2000 mAh Li-Ion @ 3.7V
Target lifetime: 2 years standby
Duty cycle: wake every 60 seconds, transmit sensor reading, sleep
Active (1 second): 15 mA
Transmission burst (0.3 seconds): 50 mA
Deep sleep (58.7 seconds): 10 µA
Average current = (15 mA × 1s + 50 mA × 0.3s + 0.01 mA × 58.7s) / 60s
= (15 + 15 + 0.587) / 60
= 0.511 mA
Battery life = 2000 mAh / 0.511 mA = 3913 hours = 163 daysThis doesn't meet the 2-year target. The design needs revision — lower active current, fewer transmissions, or better duty cycling.
Document this budget explicitly. Your test suite verifies that actual measurements match the budget. A regression that doubles deep sleep current from 10 µA to 20 µA reduces battery life from 163 days to 162 days — trivial. A regression that keeps the radio on during sleep and draws 15 mA instead of 10 µA reduces battery life from 163 days to 12 days — catastrophic.
Test Levels for Power Testing
Unit Level: Per-Peripheral Current
Test current draw for individual peripherals in isolation. This catches bugs in peripheral initialization (leaving a sensor powered on when it should be off) and configuration (leaving a clock domain enabled when it's not needed).
void test_sensor_power_off(void) {
sensor_init();
sensor_power_on();
float reading = sensor_read();
sensor_power_off();
// Measure current after power_off
float current_ma = measure_current_ma();
// Sensor should draw < 1 µA when powered off
TEST_ASSERT_FLOAT_WITHIN(0.001, 0.0, current_ma);
}Integration Level: Mode Transitions
Test that current draw is correct after entering each power mode, and that it returns to the expected level after waking.
def test_deep_sleep_current():
device.reset()
device.wait_for_idle()
device.enter_deep_sleep()
time.sleep(1) # Let current settle
current_ua = ppk2.measure_average_current(duration_ms=5000)
assert current_ua < 15, f"Deep sleep current {current_ua} µA exceeds 15 µA budget"
def test_wake_from_deep_sleep():
device.enter_deep_sleep()
time.sleep(1)
device.trigger_wake_interrupt()
# Measure current profile during wake sequence
profile = ppk2.capture_current_profile(duration_ms=500)
# Verify current returns to active range within 100ms
active_current_reached = any(
sample > 5.0 # mA
for sample in profile[100:] # After first 100ms
)
assert active_current_reached, "Device did not wake from deep sleep"System Level: Battery Life Estimation
Capture a complete duty cycle — one full wake-transmit-sleep cycle — and calculate projected battery life from the charge consumed.
def test_duty_cycle_charge(device, ppk2):
ppk2.start_capture()
device.trigger_duty_cycle()
device.wait_for_sleep()
profile = ppk2.stop_capture()
charge_nah = calculate_charge(profile) # Nanocoulombs
# Budget: 1800 nAh per cycle
assert charge_nah < 1800, f"Duty cycle consumed {charge_nah} nAh, exceeds 1800 nAh budget"
# Extrapolate to battery life
cycles_per_day = 86400 / CYCLE_INTERVAL_SECONDS
daily_charge_mah = (charge_nah / 1e6) * cycles_per_day
battery_life_days = BATTERY_CAPACITY_MAH / daily_charge_mah
assert battery_life_days > 365, f"Projected battery life {battery_life_days:.0f} days below 1-year target"Regression Testing in CI
Full battery life tests are too slow for CI. But you can run shorter regression tests that catch the common categories of power regressions:
Sleep current regression test: verify deep sleep current is within budget. This runs in 10-15 seconds.
Peripheral power-off test: verify each peripheral draws < threshold when powered off. Runs per peripheral in 5 seconds each.
Wake time regression test: verify device wakes within budget time. Catches regressions in wake-up code that block on unnecessary initialization.
Transmission current profile: verify radio current profile matches expected shape. A transmission that never ends (radio stuck on) is immediately visible in a current profile.
These tests run in under 5 minutes total on hardware and can be triggered by CI on every firmware change. They won't catch slow drifts in battery life over time, but they'll catch the common single-PR regressions that are most likely to ship.
Tooling for Automation
Nordic PPK2 + ppk2-api Python library: open source Python library that supports programmatic control of the PPK2. Good for BLE/nRF devices.
from ppk2_api.ppk2_api import PPK2_API
ppk2 = PPK2_API('/dev/ttyACM0')
ppk2.get_modifiers()
ppk2.set_source_voltage(3300) # 3.3V
ppk2.use_source_meter()
ppk2.start_measuring()
time.sleep(5)
samples, _ = ppk2.get_data()
ppk2.stop_measuring()
avg_current_ua = sum(samples) / len(samples) / 1000 # Convert to µAOtii REST API: Otii Arc provides a REST API for scripted control from any language.
Custom UART logging: for coarser measurements, add UART log lines at each power state transition and measure current manually during characterization. Log timestamps and use them to calculate time-in-state in post-processing.
Interpreting Power Profiles
Current over time profiles tell you more than average current. Learn to read them:
Flat baseline with spikes: normal for communication-heavy IoT devices. Spikes are transmission events. Long spikes indicate long transmissions or retries.
Elevated baseline that never drops: device is not entering sleep mode as expected. Check sleep mode entry code, verify clocks are being gated, check for GPIO pins held high that prevent sleep.
Periodic unexpected spikes: a timer or interrupt is waking the device more frequently than intended. Profile the wake sources.
Slow current increase over time: memory leak causing increased heap pressure, or a background task consuming increasing CPU. Not always visible in power profiles, but sometimes correlates.
Current that never returns to sleep level: device is waking and not returning to sleep after the expected task. Check for indefinite polling loops or blocking calls.
Connecting Power Testing to Your Release Process
Add power testing gates to your release checklist:
- Deep sleep current < budget (automated, CI)
- Active mode current within 20% of baseline (automated, CI)
- Projected battery life > minimum specification (automated, weekly)
- Power profile reviewed for any new peripherals (manual, PR review)
- Field telemetry battery life trend (manual, monthly)
Field telemetry is the ultimate validation. Devices that report battery SOC over time give you real-world battery life data that no bench test can replicate. If your bench tests pass but field battery life is declining, your test conditions don't match field conditions.
HelpMeTest monitors your IoT devices and embedded systems in production — tracking uptime, response times, and health metrics continuously so you know when your devices stop checking in as expected. Start free.