Hardware Fundamentals

Power Management for IoT

Learn how to power IoT devices with batteries and solar panels, use deep sleep modes, and calculate battery life to build long-running edge nodes.

Power Management for IoT

Battery-powered IoT devices live or die by their power efficiency. An ESP32 running full-time drains a 2000 mAh battery in about 3 days. With deep sleep, the same battery lasts over 2 years. Understanding power management is what separates prototypes from production devices.


Power Consumption Basics

python
# Battery life calculation formula
def battery_life_days(capacity_mah, avg_current_ma):
    """
    Estimate battery life.

    capacity_mah  — battery capacity in milliamp-hours
    avg_current_ma — device average current draw in milliamperes
    """
    hours = capacity_mah / avg_current_ma
    days  = hours / 24
    return round(days, 1)

# ESP32 scenarios
scenarios = [
    ("Always on (WiFi active)",         240,   "mA"),
    ("Active, WiFi off",                30,    "mA"),
    ("Modem sleep",                     20,    "mA"),
    ("Light sleep",                     0.8,   "mA"),
    ("Deep sleep",                      0.010, "mA"),  # 10 µA
]

battery_mah = 2000  # Common 18650 cell

print(f"Battery: {battery_mah} mAh\n")
print(f"{'Scenario':<30} {'Current':>10} {'Battery Life':>15}")
print("-" * 60)
for name, current_ma, unit in scenarios:
    life = battery_life_days(battery_mah, current_ma)
    print(f"{name:<30} {current_ma:>8.3f} mA  {life:>12.1f} days")
Battery: 2000 mAh

Scenario                       Current    Battery Life
────────────────────────────────────────────────────────
Always on (WiFi active)        240.000 mA          0.3 days
Active, WiFi off                30.000 mA          2.8 days
Modem sleep                     20.000 mA          4.2 days
Light sleep                      0.800 mA        104.2 days
Deep sleep                       0.010 mA      8333.3 days

ESP32 Sleep Modes

ModeCPUWiFi/BTGPIO IRQRTCCurrent
ActiveRunningActiveYesYes240 mA
Modem SleepRunningOffYesYes20 mA
Light SleepPausedOffYesYes0.8 mA
Deep SleepOffOffWakeup onlyYes10 µA
HibernationOffOffLimitedPartial5 µA

Deep Sleep with Timer Wakeup

The most common pattern: wake up, read sensor, send data, go back to sleep.

python
# MicroPython ESP32 — deep sleep with timer wakeup
import machine
import esp32
import dht
import network
import urequests
import time

# Wakeup reason tells us why we woke from sleep
cause = machine.reset_cause()
WAKE_CAUSES = {
    machine.PWRON_RESET:     "Power-on reset",
    machine.HARD_RESET:      "Hard reset",
    machine.WDT_RESET:       "Watchdog reset",
    machine.DEEPSLEEP_RESET: "Wake from deep sleep",
    machine.SOFT_RESET:      "Soft reset",
}
print(f"Wake cause: {WAKE_CAUSES.get(cause, 'Unknown')}")

# ── Read sensor ─────────────────────────────────
sensor = dht.DHT22(machine.Pin(4))
sensor.measure()
temperature = sensor.temperature()
humidity    = sensor.humidity()
print(f"T={temperature}°C  H={humidity}%")

# ── Connect WiFi and send data ──────────────────
def connect_wifi(ssid, password, timeout=10):
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(ssid, password)
        deadline = time.time() + timeout
        while not wlan.isconnected() and time.time() < deadline:
            time.sleep_ms(200)
    return wlan.isconnected()

if connect_wifi("MySSID", "MyPassword"):
    try:
        url = "https://api.thingspeak.com/update"
        response = urequests.get(
            url,
            params={"api_key": "WRITE_KEY",
                    "field1": temperature,
                    "field2": humidity}
        )
        print(f"Sent: entry_id={response.text}")
        response.close()
    except Exception as e:
        print(f"Send failed: {e}")

# ── Enter deep sleep for 5 minutes ─────────────
SLEEP_SECONDS = 300  # 5 minutes
print(f"Sleeping for {SLEEP_SECONDS}s...")
machine.deepsleep(SLEEP_SECONDS * 1000)  # Argument is milliseconds
# Execution stops here. Next line never runs.

Deep Sleep with External Wakeup (PIR Sensor)

python
# Wake from deep sleep when PIR detects motion
from machine import Pin, deepsleep
import esp32

PIR_PIN = 33  # Must use RTC GPIO (0, 2, 4, 12–15, 25–27, 32–39)

# Configure wakeup source before sleeping
esp32.wake_on_ext0(pin=Pin(PIR_PIN, Pin.IN), level=esp32.WAKEUP_ANY_HIGH)

print("Going to deep sleep. Will wake on motion...")
deepsleep()
# Device wakes when PIR goes HIGH, then restarts from top of file

Light Sleep — Preserve RAM and State

Light sleep is useful when you need to retain RAM contents (unlike deep sleep which clears RAM).

python
# MicroPython — light sleep, wake on pin change
from machine import Pin, lightsleep

# Light sleep preserves RAM — great for keeping state between wakeups
button = Pin(0, Pin.IN, Pin.PULL_UP)

sensor_readings = []  # This list survives light sleep

for i in range(10):
    print(f"Reading #{i+1}")
    # Simulate sensor reading
    sensor_readings.append(24.5 + i * 0.1)

    # Light sleep for 60 seconds (or wake on button press)
    lightsleep(60_000)  # 60,000 ms = 60 seconds

    if button.value() == 0:
        print("Button woke us up!")
        break

print(f"All readings: {sensor_readings}")
# RAM was preserved through all light sleeps

Power Sources

Battery Options

Battery TypeVoltageTypical CapacityRechargeableNotes
AA Alkaline1.5 V2500–3000 mAhNoCheap, widely available
18650 Li-ion3.7 V2000–3500 mAhYesMost common for IoT
LiPo Pouch3.7 V100–10000 mAhYesThin, flexible shape
LiFePO43.2 V1000–6000 mAhYesSafer, longer cycle life
AAA Alkaline1.5 V1000–1200 mAhNoSmaller, less capacity
CR2032 Coin3 V200–240 mAhNoBLE beacons, tiny sensors
python
# Voltage monitoring to detect low battery
from machine import ADC, Pin

# Use a resistor divider to measure battery voltage
# If battery is 3.7V max and ADC range is 0-3.3V:
# Use 100kΩ + 100kΩ divider → 1.85V max into ADC
DIVIDER_RATIO = 2.0  # Adjust based on your resistor values
VREF = 3.3           # ESP32 ADC reference

batt_adc = ADC(Pin(35))
batt_adc.atten(ADC.ATTN_11DB)

def read_battery_voltage():
    raw = batt_adc.read()
    voltage = raw / 4095 * VREF * DIVIDER_RATIO
    return round(voltage, 2)

def battery_percent(voltage, v_min=3.3, v_max=4.2):
    """Rough SoC estimate for Li-ion (non-linear in reality)."""
    pct = (voltage - v_min) / (v_max - v_min) * 100
    return max(0, min(100, round(pct)))

v = read_battery_voltage()
pct = battery_percent(v)
print(f"Battery: {v} V ({pct}%)")

if pct < 10:
    print("LOW BATTERY — sending alert!")

Solar Charging

Solar Panel → Charge Controller → Battery → Power Regulator → MCU

Typical setup for outdoor sensor node:
├── 5 V / 1 W solar panel (small, ~65 × 65 mm)
├── TP4056 LiPo charge controller module
├── 3.7 V / 2000 mAh 18650 battery
└── AMS1117-3.3 or ME6211 LDO regulator → ESP32

Energy budget (daily):
  Solar generation (5h sun × 1W panel × 80% efficiency): 4 Wh
  ESP32 consumption (5s awake @ 240mA + 295s sleep @ 10µA):
    = 5 × 0.240 + 295 × 0.000010 = 1.2 + 0.003 = 1.203 Wh
  Net positive: system self-sustains with surplus energy.

Practical Power Optimization Tips

python
# Tip 1: Turn off WiFi when not needed
import network

wlan = network.WLAN(network.STA_IF)
# ... do your WiFi work ...
wlan.disconnect()
wlan.active(False)  # Power down WiFi radio (~170 mA saved)

# Tip 2: Lower CPU frequency when doing simple tasks
import machine
machine.freq(80_000_000)   # 80 MHz (vs default 240 MHz)
# Use 80 MHz for ADC reading, 240 MHz only when doing WiFi/BT

# Tip 3: Batch your data — one WiFi connection for multiple readings
buffer = []

for _ in range(10):
    sensor.measure()
    buffer.append({
        't': sensor.temperature(),
        'h': sensor.humidity()
    })
    machine.deepsleep(60_000)  # Sleep 1 min between readings

# Connect once and send all buffered data at once
if len(buffer) == 10:
    send_batch(buffer)
    buffer.clear()

# Tip 4: Use RTC memory to persist small state across deep sleeps
import esp32

rtc_mem = esp32.NVS('myapp')  # Non-volatile storage in RTC domain
rtc_mem.set_i32('count', rtc_mem.get_i32('count') + 1)

Summary

  • A ESP32 in deep sleep draws only ~10 µA vs 240 mA when active — a 24,000× difference
  • Deep sleep clears RAM but is most power-efficient; light sleep preserves RAM
  • Use a timer wakeup for periodic sensing and ext0/ext1 wakeup for event-driven operation
  • 18650 Li-ion batteries + TP4056 charger is the most common IoT power setup
  • For outdoor, unattended devices use solar + battery with a proper energy budget calculation
  • Batch your WiFi connections — connect once, send all buffered readings, then sleep

Next, we'll explore the networking protocols that connect your IoT devices to the cloud.