Power Management & Battery Projects
By the end of this lesson you will be able to estimate how long a battery will run your project, cut power use dramatically with ESP32 deep sleep, measure a battery's voltage from your sketch, and choose safe batteries, regulators and charging modules. This is what separates a desk demo from a device that runs for months in a garden.
A plant sensor that needs its batteries changed every two days will be abandoned within a week. Good power design is invisible: the device just keeps working.
Capacity, current and the power budget
Battery capacity is given in milliamp-hours (mAh). A 2,000 mAh battery could, in theory, supply 2,000 mA for one hour, or 100 mA for 20 hours, or 1 mA for 2,000 hours.
battery life in hours ≈ capacity in mAh ÷ average current in mA
In practice you get less: batteries lose capacity in heat and cold, with age, and at high currents, and your circuit stops working before the battery is completely empty. A common rule of thumb is to plan with about 80% of the label capacity.
A power budget lists every part and its current, like a monthly household budget lists rent, groceries and the electricity bill:
| Part | Typical current | Notes |
|---|---|---|
| ESP32 with Wi-Fi transmitting | Often 80 to 250 mA in bursts | The biggest cost; keep Wi-Fi time short |
| ESP32 in deep sleep (chip alone) | Microamps | Only a few parts stay awake |
| Dev board extras (USB chip, power LED, regulator) | Can be several mA, always on | Often more than the sleeping chip itself |
| Capacitive soil sensor | A few mA | Can be switched off between readings |
| OLED display | Varies with how many pixels are lit | Turn it off when nobody is looking |
Check your own parts' datasheets; these figures are only typical ranges.
Step 1: Estimate battery life
The secret of long battery life is duty cycling: be awake for a short time, then sleep for a long time. The average current is then dominated by the sleep current. This Python calculator compares three designs that send one reading every 10 minutes, each awake for 2 seconds at 80 mA, using a 2,000 mAh battery:
# Estimate battery life for a device that wakes, sends, then sleeps
def average_ma(awake_ma, awake_s, sleep_ma, period_s):
sleep_s = period_s - awake_s
return (awake_ma * awake_s + sleep_ma * sleep_s) / period_s
def life_days(capacity_mah, avg_ma, usable=0.8):
return capacity_mah * usable / avg_ma / 24
always_on = 80
bare_chip = average_ma(80, 2, 0.15, 600) # good low-power board asleep
dev_board = average_ma(80, 2, 10, 600) # dev board: USB chip and LED stay on
for name, ma in [("Always awake", always_on), ("Sleeps, low-power board", bare_chip),
("Sleeps, typical dev board", dev_board)]:
print(f"{name:26} {ma:7.2f} mA -> {life_days(2000, ma):6.1f} days")Always awake 80.00 mA -> 0.8 days Sleeps, low-power board 0.42 mA -> 160.2 days Sleeps, typical dev board 10.23 mA -> 6.5 days
Three lessons from three lines. Staying awake drains the battery in under a day. Sleeping makes a huge difference. And the board matters as much as the code: the extra 10 mA of a typical dev board's always-on parts cuts 160 days down to under a week. Boards sold as "low power" remove or switch off those parts.
Step 2: ESP32 deep sleep
In deep sleep, the ESP32 switches off its processors, Wi-Fi and most RAM. A small, low-power part keeps a timer running and wakes the chip later. Waking from deep sleep is like a full restart: setup() runs again from the top and loop() is never reached.
Normal variables are lost in deep sleep. A variable marked RTC_DATA_ATTR is kept in the small memory that stays powered, so it survives:
const unsigned long long SLEEP_US = 10ULL * 1000000ULL; // 10 seconds in microseconds
RTC_DATA_ATTR int bootCount = 0; // survives deep sleep
void setup() {
Serial.begin(115200);
delay(500);
bootCount++;
Serial.printf("Boot number %d\n", bootCount);
// ... read sensors and send the data here, quickly ...
Serial.println("Going to sleep for 10 s");
Serial.flush(); // finish printing before sleeping
esp_sleep_enable_timer_wakeup(SLEEP_US);
esp_deep_sleep_start(); // nothing after this line runs
}
void loop() {
// never reached
}Over the first three wake-ups, the Serial Monitor shows (with the ESP32's own boot messages in between, which you can ignore):
Boot number 1 Going to sleep for 10 s Boot number 2 Going to sleep for 10 s Boot number 3 Going to sleep for 10 s
Real projects sleep for minutes, not seconds. ULL makes the numbers 64-bit (unsigned long long), because the wake-up time is given in microseconds and 10 minutes is 600,000,000 µs. Keep the awake part short: connect to Wi-Fi, send, sleep.
Step 3: Measure the battery voltage
A lithium-ion cell is about 4.2 V when full and is considered empty around 3.0 to 3.3 V, depending on the cell and your circuit. That is above the ESP32's 3.3 V pin limit, so use a voltage divider with two equal resistors (for example 100 kΩ and 100 kΩ) to halve it. High values waste very little current.
| Part | Pin | Connects to |
|---|---|---|
| 100 kΩ resistor (R1) | Leg 1 | Battery + (after the protection board) |
| 100 kΩ resistor (R1) | Leg 2 | ESP32 GPIO 34 (ADC1) |
| 100 kΩ resistor (R2) | Leg 1 | ESP32 GPIO 34 |
| 100 kΩ resistor (R2) | Leg 2 | GND |
On the ESP32, analogReadMilliVolts(pin) returns a calibrated voltage in millivolts. Multiply by 2 to undo the divider, then estimate the charge level. Test the estimate on fixed values first:
int batteryPercent(long millivolts) {
long p = map(millivolts, 3300, 4200, 0, 100); // rough straight-line estimate
return constrain(p, 0, 100);
}
void setup() {
Serial.begin(115200);
delay(500);
const long tests[] = {4200, 3750, 3300, 3000};
for (int i = 0; i < 4; i++) {
Serial.printf("%ld mV -> %d %%\n", tests[i], batteryPercent(tests[i]));
}
// live: long mv = analogReadMilliVolts(34) * 2;
}
void loop() {
}4200 mV -> 100 % 3750 mV -> 50 % 3300 mV -> 0 % 3000 mV -> 0 %
A lithium cell's voltage does not fall in a straight line, so this percentage is only a rough guide. It is still useful for a "low battery" warning sent before the device dies.
Choosing batteries and regulators
| Power source | Good for | Watch out for |
|---|---|---|
| USB power bank | Easy portable projects at 5 V | Many switch off automatically when the current is very low, such as during deep sleep |
| 3 or 4 AA cells | Simple, safe, easy to buy | 4 fresh alkaline cells can exceed 6 V; check what your parts accept |
| Single Li-ion cell (18650 or pouch) with protection | Long-life IoT nodes | Needs a proper charger module and protection circuit |
| 9 V PP3 battery | Very little | Low capacity; most of it is wasted as heat in the regulator |
| Small solar panel + charger | Outdoor nodes | Panel output drops sharply in shade, cloud and the monsoon |
A voltage regulator turns a higher, varying battery voltage into a steady 3.3 V or 5 V. A linear regulator simply burns the extra voltage as heat; a switching (buck) regulator converts it much more efficiently. For battery projects, choose an efficient regulator with a low quiescent current (the current it uses itself, even with no load).
For a single lithium cell, use a charger module made for it, such as the common TP4056-based boards with a protection circuit (often labelled with a DW01 protection chip). These stop overcharging, over-discharging and short circuits. Follow the rules from Electronics Safety First: never charge damaged or swollen cells, never leave them charging unattended, and keep them out of direct sun.
Other ways to save power
- Switch sensors off between readings by powering them through a transistor or MOSFET switch.
- Turn off displays, or show the reading only when a button is pressed.
- Send less often. Every 15 minutes is plenty for soil moisture; it changes slowly.
- Send less data. Short messages mean shorter Wi-Fi time.
- Remove or cut the power LED on boards you own, if you are comfortable soldering.
Common mistakes
- Trusting the dev board for low power. Its USB chip and LED may use more than everything else combined.
- Forgetting that deep sleep restarts the sketch. Code in
loop()never runs; put the work insetup(). - Connecting a 4.2 V battery straight to an ESP32 ADC pin. Use a divider.
- Unprotected lithium cells and improvised charging. Use protected cells and a real charger module.
- Power banks that switch off. Test that yours stays on at very low current.
Key points
- Battery life in hours ≈ usable mAh ÷ average mA; plan with about 80% of the label capacity.
- Duty cycling (short awake, long sleep) makes the sleep current the most important number.
- ESP32 deep sleep restarts
setup()on wake;RTC_DATA_ATTRvariables survive. - Measure a lithium cell with a high-value divider and
analogReadMilliVolts(); percentages are rough. - Use efficient regulators, protected lithium cells and proper charger modules.
Exercise
Ananya's weather node wakes every 15 minutes, stays awake 3 seconds at 120 mA, and sleeps at 0.2 mA on a low-power board. She has a 3,000 mAh lithium cell. Use the Python functions above to calculate the average current and the battery life in days with 80% usable capacity.
Show solution
def average_ma(awake_ma, awake_s, sleep_ma, period_s):
sleep_s = period_s - awake_s
return (awake_ma * awake_s + sleep_ma * sleep_s) / period_s
def life_days(capacity_mah, avg_ma, usable=0.8):
return capacity_mah * usable / avg_ma / 24
avg = average_ma(120, 3, 0.2, 15 * 60)
print(f"Average current: {avg:.3f} mA")
print(f"Battery life: {life_days(3000, avg):.0f} days")Average current: 0.599 mA Battery life: 167 days
The awake part (120 × 3 = 360 mA-seconds) and the sleep part (0.2 × 897 ≈ 179 mA-seconds) are similar, so making either one smaller would noticeably extend the life.
Quick Quiz
1. A device averages 5 mA from a 2,000 mAh battery with 80% usable. About how long will it run?
2,000 × 0.8 = 1,600 mAh, and 1,600 ÷ 5 = 320 hours.
2. What happens when an ESP32 wakes from deep sleep?
Waking from deep sleep is like a restart, which is why the work goes in setup().
3. Which variable keeps its value through deep sleep?
RTC_DATA_ATTR places the variable in memory that stays powered during deep sleep.
4. Why does a typical dev board give poor battery life even with deep sleep?
Always-on extras can draw milliamps while the sleeping chip uses microamps.
Learn IoT & Arduino with a mentor
Free lessons get you started. Our IoT & Embedded Systems training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
