Battery and power budget
The battery is rarely the problem. The regulator between it and the board is - a linear one throws away a third of the pack as heat, and a dev board draws milliamps while the chip sleeps at ten microamps.
What the regulator costs
Two numbers, not one
Average current decides how long the battery lasts. Peak current decides whether the board resets. They are hundreds of times apart on this chip — 10 µA asleep, 350 mA in a transmit burst — and a design that only considers the average browns out.
The peak is handled with capacitance: 100 µF close to the module, plus whatever the board already has.
A checklist for a battery build
- A cell whose voltage stays above the regulator's dropout for most of its life.
- A low-dropout or buck regulator, not the AMS1117 on the dev board.
- A bare module, or a dev board with the power LED removed.
- A divider for battery sense that is switched, not permanent.
- Everything else on the board switched off in sleep — sensors included.
The code
Two resistors and an ADC1 pin. The MOSFET is what makes it a battery design rather than a demonstration - a permanently connected divider drains the pack for years.
const int BATT = 35; // ADC1, so Wi-Fi does not disturb it
const int ENABLE = 25; // gate of a MOSFET in series with the divider
float readBattery() {
digitalWrite(ENABLE, HIGH);
delay(10); // let it settle
long sum = 0;
for (int i = 0; i < 16; i++) sum += analogReadMilliVolts(BATT);
digitalWrite(ENABLE, LOW); // stop draining the pack
return (sum / 16) * 2.0 / 1000.0; // two equal resistors = /2
}
void setup() {
Serial.begin(115200);
pinMode(ENABLE, OUTPUT);
analogSetPinAttenuation(BATT, ADC_11db);
Serial.printf("%.2f V\n", readBattery());
}
void loop() {}Use an ADC1 pin. On the classic ESP32 the ADC2 pins stop working the moment Wi-Fi starts, and a battery monitor that reads zero once connected is a confusing bug.
The same divider read from MicroPython. Turning volts into a percentage needs the cell's discharge curve, not a straight line - lithium sits near 3.7 V for most of its life.
from machine import ADC, Pin
import time
adc = ADC(Pin(35))
adc.atten(ADC.ATTN_11DB)
enable = Pin(25, Pin.OUT, value=0)
def volts():
enable.value(1)
time.sleep_ms(10)
v = sum(adc.read_uv() for _ in range(16)) / 16 / 1e6 * 2
enable.value(0)
return v
v = volts()
pct = max(0, min(100, (v - 3.0) / (4.2 - 3.0) * 100))
print('{:.2f} V, roughly {:.0f}%'.format(v, pct))A lithium cell below 3.0 V should be treated as empty. Going lower damages it, and most protection boards cut out around there anyway.
When it does not work
The rail sags under a 350 mA burst. A 100 µF capacitor across 3.3 V close to the module fixes most of these, and a thicker cable or a better regulator fixes the rest.
The regulator ran out of headroom. An AMS1117 needs about 4.4 V in to give 3.3 V out, so it abandons a lithium cell at half charge. Use a low-dropout part or a buck-boost.
The dev board, not the chip. Power LED, USB-serial chip, regulator quiescent current. For a real battery product, use a bare module and your own regulator.
You are measuring against a reference derived from the supply. analogReadMilliVolts uses the internal reference and is far more stable.
Every number on this page is a guess until you measure it, and measuring microamps is harder than it looks.
Measuring current draw →Edit this page — content/esp32/battery-and-power-budget.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.