ESP32-C3 OLED/Off the bench/08. Battery level, on the screen
Off the bench · 08 of 9

Battery level, on the screen

Bridge one solder pad and the board can read its own cell on GPIO 2. Then three conversions in a row — halve, count, double back — and a calibration constant that is honest only if it is yours.

Bridge the pad first

Nothing in this article works until the solder jumper marked BATTERY MONITOR and IO2 on the expansion base is bridged. It ships open on purpose — the divider sits across the cell and draws from it once connected — so this is a decision, not a step you skipped.

Bridging it also spends GPIO 2. That pin is one of the five with an ADC behind it, and from here on it belongs to the battery.

Halve, count, double back

Halve it, count it, double it back
3.90 V shown
Battery3.90 V
Calibration factor
ADC counts
2420
Printed
3.90 V
Off by
+0.00 V
The arithmetic is exact; the hardware is not. Two equal resistors halve the cell, the ADC counts it, and multiplying back by two recovers the number you started with — on paper, which is what this figure draws. On a real board the ADC has an error of its own, and the printed value lands a few hundredths out. That is the gap a calibration factor closes.

Three conversions, and each of them is a place to be wrong by a factor of two:

  1. The divider halves the cell. Two equal resistors, so the pin sees about 2.05 V when the cell is at 4.1 V. That is deliberate — a full lithium cell is 4.2 V and the ADC's input range tops out below that, so measuring it directly would clip.
  2. The ADC turns that into a number. analogReadMilliVolts() is the call to use rather than analogRead(), because it applies the per-chip calibration that Espressif burns into the efuses at manufacture and hands you millivolts instead of counts out of 4095. It removes most of the error before you write any arithmetic of your own.
  3. You double it back. Multiply by the divider ratio and you have the cell.

The calibration constant is yours

Sketches for this board circulate with a hard-coded correction factor in them — 0.911 is the common one. It is a real measurement: somebody put a meter on their cell, compared it to what their board printed, and divided. It corrects their board's ADC.

Applied to yours it is a guess with a decimal point on it. Start at 1.0, measure the cell across its own terminals, divide by what the sketch printed, and use that. If the two agree to a few hundredths, leave it at 1.0 and stop.

What the number is worth

A lithium cell's voltage is a poor gauge and a good alarm. Most of its capacity lives between about 3.9 and 3.6 V, which is a range narrow enough that ±0.05 V of measurement error moves your "percentage remaining" by a quarter. Then it falls off a cliff below 3.5 V.

So print volts rather than a percentage, and use it for the thing it is genuinely good at: noticing 3.4 V and shutting something down before the cell's own protection circuit does it for you, less gracefully.

The code

battery_meter.ino

Reads the divider on GPIO 2, averages the noise out, and prints the result on the screen and over serial. Only works once the BATTERY MONITOR pad on the expansion base is bridged.

/*  Lonely Binary ESP32-C3 OLED — battery meter
    Tools > Board:            ESP32C3 Dev Module
    Tools > USB CDC On Boot:  Enabled
    Needs: BATTERY MONITOR pad on the base bridged     */

#include <U8g2lib.h>

U8G2_SH1106_72X40_WISE_F_SW_I2C u8g2(U8G2_R0, 6, 5);

const int  ADC_PIN = 2;     // the pad routes the divider here
const float DIVIDER = 2.0;  // two equal resistors: half the cell
const float CAL     = 1.0;  // your board's correction. See the note.
const int  SAMPLES  = 16;

void setup() {
  Serial.begin(115200);
  u8g2.begin();
  u8g2.setFont(u8g2_font_6x10_tf);
  analogSetAttenuation(ADC_11db);   // full 0-3.3 V input range
}

float readBattery() {
  uint32_t sum = 0;
  for (int i = 0; i < SAMPLES; i++) {
    sum += analogReadMilliVolts(ADC_PIN);   // calibrated mV, not counts
    delay(5);
  }
  return (sum / SAMPLES) / 1000.0f * DIVIDER * CAL;
}

void loop() {
  float v = readBattery();

  char line[16];
  snprintf(line, sizeof line, "%.2f V", v);

  u8g2.clearBuffer();
  u8g2.drawStr(0, 12, "Battery");
  u8g2.drawStr(0, 28, line);
  u8g2.sendBuffer();

  Serial.printf("%.3f V\n", v);
  delay(1000);
}

CAL is the number to change. 1.0 is the honest starting point — run the sketch, measure the cell with a meter, and set CAL to (what the meter says) divided by (what the sketch printed). Anyone else's value is a correction for their board's ADC, not yours.

When it does not work

The reading is 0.00 V with a healthy cell connected

The pad is not bridged. It ships open, so GPIO 2 is floating until you make the connection, and a floating ADC input reads whatever it feels like — often near zero. Look for the BATTERY MONITOR block beside the connector and check the solder joint under a light.

The reading is about half what the meter says

The DIVIDER constant is missing or set to 1. The pad connects GPIO 2 to the midpoint of two equal resistors, so the pin only ever sees half the cell voltage and the sketch has to double it back.

The reading is stable but consistently 0.1 to 0.3 V out

That is what CAL is for, and it is per-board. Measure the cell across its own terminals with a meter, divide that by what the sketch prints, and put the result in CAL. Copying a factor from someone else's sketch corrects their ADC's error using your ADC, which just moves the error.

The reading jumps around by a tenth of a volt

Increase SAMPLES, and take the readings while the radio is quiet. Averaging sixteen samples costs 80 ms and removes most of it; a reading taken during a Wi-Fi transmission is measuring the supply sagging as much as the cell.

The voltage reads high while USB is plugged in

It is not wrong, it is charging. The charger holds the cell at up to 4.2 V while USB is connected, so any reading taken then tells you about the charger rather than the cell. Take battery readings on battery.

Where this goes next

The last thing between this board and a project: an antenna a few millimetres long, and a radio that will happily overdrive it.

Wi-Fi the antenna can carry

Edit this page — content/boards/c3-oled/battery-level-on-the-screen.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

ESP32-C3 OLED Development Board with Expansion Base

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse ESP32-C3 OLED on the forum