B.A.T.T.E.R.Y. - the DIY Desktop Battery Tester
by The Uncertified Engineer in Circuits > Tools
985 Views, 1 Favorites, 0 Comments
B.A.T.T.E.R.Y. - the DIY Desktop Battery Tester
BATTERY — Basically A Toy That Embodies Rechargeable Yet-Again-Batteries
We've all been there. You grab a remote, a torch, or a toy and it's completely dead. You rummage through the junk drawer, find a handful of AA batteries, and have absolutely no way to know which ones still have juice and which ones are empty imposters. You end up doing the old "shake and pray" method or just swapping them one by one until something works.
BATTERY is the answer to that frustration — and it's also one of the most fun little builds you can put together in an afternoon.
BATTERY stands for Basically A Toy That Embodies Rechargeable Yet-Again-Batteries, and it does exactly what the name implies. It's a AA battery health tester that looks like a miniature car battery — built from cardboard, colored paper, and a handful of electronic components, with a cartoon-style design that makes it look more like a desk toy than a test instrument.
At the heart of it is an ESP32 microcontroller, reading the voltage of whatever AA battery you plug into its terminals. A 128×64 OLED display shows you the result with a charging battery animation, a live voltage readout in volts, and a percentage indicator — all rendered with a clean, polished UI. The whole thing is powered by a humble 9V battery tucked inside the enclosure.
What makes this project more than just a voltage divider and a display is the attention to accuracy. The ESP32's ADC is notoriously noisy, so under the hood BATTERY uses 32× oversampling, min/max rejection, a 10-sample moving average, and the ESP32's own eFuse-based ADC calibration to give you a reading you can actually trust. It even has a software trim knob if your specific ESP32 board reads slightly off.
And the irony of this project it that,
"Its a battery, powered by a Battery, made to test other Batteries"
Supplies
Electronics
- ESP32 Development Board
- 128×64 I2C OLED Display (SSD1306)
- 9V Battery + 9V Battery Connector Clip
- Jumper Wires
Enclosure Materials
- Corrugated Cardboard — 3 to 4 sheets
- Colored Paper or Construction Paper
- White School Glue (PVA) or a Glue Gun
- Ruler, Pencil, Scissors, Box Cutter
- Black Marker or Acrylic Paint
Software & Libraries
- Arduino IDE (v1.8.x or v2.x)
- U8g2 Library
How It Works — the Idea Behind BATTERY
Before you wire anything up or cut a single piece of cardboard, it's worth spending five minutes understanding what's actually happening inside BATTERY. This isn't just good practice — it means that if something goes wrong during your build, you'll know exactly where to look. It also means you can confidently modify the project to suit your own ideas later.
Let's break it down into three layers: the measurement problem, the solution, and the display pipeline.
The Measurement Problem
A fresh AA alkaline battery sits at around 1.5V to 1.6V. A dead one reads somewhere around 1.0V or lower. The entire useful life of a AA battery happens inside that narrow 0.6V window. That might seem like a simple thing to measure, but there's a catch.
The ESP32's ADC (Analog-to-Digital Converter) is designed to read voltages between 0V and 3.3V, which means a 1.5V battery signal lands comfortably within range — no voltage divider needed, which simplifies the circuit enormously. You can connect the positive terminal of the AA cell directly to GPIO 34 and the negative terminal to GND.
However, the ESP32's ADC has a well-known problem in the maker community: it's noisy and non-linear, especially at the top of its range. A fresh 1.5V battery can easily read as 1.65V or 1.7V on a bare ADC read, which would make it look like it's overcharged — which is obviously wrong and useless. This is the core engineering challenge that BATTERY solves
The Solution — Layers of Accuracy
BATTERY tackles the noisy ADC problem with techniques stacked on top of each other. Each one on its own helps a little. Together, they give you a reading accurate enough to actually trust.
Layer 1 — eFuse Calibration
The ESP32 stores factory calibration data burned into its eFuse memory at the time of manufacture. The esp_adc_cal library reads this data and uses it to correct the ADC's known non-linearities before we even start averaging. Not all ESP32 boards have this data written, but most modern ones do. If yours doesn't, BATTERY falls back to a sensible default and tells you via the Serial monitor.
Layer 2 — 10-Sample Moving Average
Even after oversampling, a single measurement can still jump around slightly between readings. BATTERY maintains a rolling window of the last 10 measurements and always displays their average. This means the number on screen changes smoothly and gradually rather than flickering every half second, giving you a much more readable and trustworthy result.
Layer 3 — Software Trim
Every individual ESP32 board is slightly different. Even with eFuse calibration, one board might consistently read 2% high compared to a multimeter. BATTERY has a single constant called VOLTAGE_CALIBRATION in the code. If you own a multimeter and want maximum accuracy, you measure the true battery voltage, compare it to what BATTERY displays, and adjust this one number. No code restructuring needed — just change that value.
The Display Pipeline
Once BATTERY has a clean, stable voltage reading, the rest is straightforward.
The voltage is passed through two simple math functions. The first maps it onto a 0–100% scale using the known voltage range of a AA alkaline cell — 1.0V being empty and 1.6V being full. The second function classifies the battery into one of four states: Healthy, Low, Dead, or No Battery (for when nothing is plugged in at all, reading below 0.2V).
These values — voltage in volts, percentage, and status label — are then handed off to the display. The OLED runs on the U8g2 library in page-buffer mode, which means the screen is redrawn in small horizontal slices rather than all at once. This uses very little RAM, which matters on a microcontroller, and is the rendering mode that Lopaka — the browser-based UI designer used to create this project's layout — specifically targets.
The charging battery animation is a 28-frame sprite sequence, pre-generated in Lopaka and stored as a header file. The ESP32 cycles through these frames to create the effect of a battery filling up on screen, while the live voltage and percentage numbers update alongside it in real time.
What GPIO 34 Is and Why It Matters
GPIO 34 on the ESP32 is an input-only pin that belongs to ADC1. This is important for two reasons. First, ADC1 works reliably even when Wi-Fi is active — ADC2 does not, because the Wi-Fi radio shares its circuitry. Second, being input-only means there's no risk of accidentally driving voltage back out of this pin toward your battery. It can only ever listen, never transmit, which makes it safe for direct battery connection.
The Circuit & Wiring
The circuit for BATTERY is genuinely one of the simplest you'll find in any ESP32 project. There are only three connections that carry actual signals — everything else is just power and ground. Let's go through it methodically, one section at a time.
That's the entire circuit. No resistors, no transistors, no level shifters, no voltage dividers. Straightforward enough to wire in under ten minutes.
Powering the ESP32
The ESP32 needs a stable power source. In BATTERY, that source is a 9V alkaline battery connected through the VIN pin.
The VIN pin on most ESP32 dev boards feeds directly into the onboard AMS1117 voltage regulator, which steps the 9V down to a clean 3.3V that the ESP32 and the OLED both run on. This means you never have to worry about powering the OLED separately — the 3.3V regulated rail handles it all.
Connect the red wire of your 9V battery clip to the VIN pin of the ESP32. Connect the black wire to any GND pin on the ESP32. Most dev boards have multiple GND pins — any of them will work.
One important note: Do not connect the 9V battery while the ESP32 is also connected to your computer via USB. Either power it through USB during programming, or power it through VIN during use. Both at the same time won't destroy anything immediately, but it puts unnecessary stress on the onboard regulator and can cause unstable readings.
Setting Up the Software
Before uploading anything, you need three things in place — the ESP32 board package, the U8g2 library, and the animation header file. Let's get all three sorted quickly.
Installing the ESP32 Board Package
If you've never used an ESP32 with the Arduino IDE before, the IDE doesn't know the board exists yet. Here's how to add it:
- Open Arduino IDE and go to File → Preferences
- Find the field labelled "Additional Boards Manager URLs" and paste in this URL:
- Click OK, then go to Tools → Board → Boards Manager
- Search for "esp32" and install the package by Espressif Systems
- Once installed, go to Tools → Board → ESP32 Arduino and select "ESP32 Dev Module"
- Set Tools → Upload Speed to 115200 and Tools → Port to whichever COM port your ESP32 shows up on
Installing the U8g2 Library
This is the only library you need to install manually. Everything else — esp_adc_cal.h, driver/adc.h, Wire.h — comes bundled with the ESP32 core automatically.
- Go to Sketch → Include Library → Manage Libraries
- Search for "U8g2"
- Find the entry by Oliver Kraus and click Install
- When it asks about dependencies, click Install All
That's it. The library is large but installs cleanly and is extremely well maintained.
The Animation Header File
The charging battery animation on the OLED is not stored in the main .ino file. It lives in a separate header file called:
This file contains a 28-frame sprite animation generated using Lopaka — a free browser-based UI designer built specifically for small embedded displays. The frames are stored as a const uint8_t array in program memory (PROGMEM), so they don't eat into the ESP32's RAM.
You must place this file in the same folder as your .ino sketch file. If it's anywhere else, the code will fail to compile with a "file not found" error.The easiest way to do this correctly:
- In Arduino IDE, go to Sketch → Show Sketch Folder
- Place charging_battery_64_64_28f.h directly inside that folder
- Return to the IDE — it will appear as a new tab alongside your main sketch automatically
Once everythingis done, connect your ESP32 via USB, select the correct port, and hit Upload. The IDE will compile again, then flash the board. You'll see a progress bar and then:
That means it worked. The OLED should light up within two seconds.
The Code Explained
The full sketch is provided in the next step to download and copy. This step is purely about understanding what each part does and why it was written that way. Read this once before uploading and the whole project will make much more sense.
The Configuration Block — Your Control Panel
Right at the top of the sketch, before any functions, sits a block of #define constants. This is intentional — everything you might ever want to tweak is in one place so you never have to dig through the code.
BAT_PIN is the GPIO that reads the AA battery voltage. ADC_SAMPLES controls how many raw readings are taken and averaged per measurement cycle — higher means more accurate but slightly slower. MA_SAMPLES is the size of the smoothing window. READ_INTERVAL_MS sets how often a new measurement is taken.
VOLTAGE_CALIBRATION is the one you'll touch most. It defaults to 1.0 meaning no correction. If your multimeter shows a AA battery at 1.48V but BATTERY displays 1.52V, set this to 1.48 / 1.52 = 0.973f and your readings will match your multimeter exactly.
The voltage threshold block just below it defines what counts as a full, empty, low, or missing battery:
These are based on real alkaline AA discharge curves. A resting voltage above 1.6V on a non-rechargeable AA is physically unusual, so anything at or above that is treated as 100%. Anything at or below 1.0V has essentially nothing left. The 0.20V threshold for "No Battery" handles the case where nothing is plugged in at all — GPIO 34 will float near zero rather than reading a true voltage.
ADC Calibration — calibrateADC()
This runs once in setup() and never again. It asks the ESP32 to read its own eFuse memory for factory calibration data and loads it into the adcChars struct that the voltage conversion function uses later.
The ADC_ATTEN_DB_11 setting is important — it sets the ADC's input attenuation to 11dB, which extends its readable range from the default 1.1V up to approximately 3.1V. Without this, a fresh 1.5V battery would be outside the measurable range and read incorrectly.
If the ESP32 finds valid eFuse calibration data, adcCalibrated is set to true and the conversion uses the precise factory model. If not, it falls back to a simple linear formula using 3.3V as the reference. Either way the code runs — it just tells you via Serial which path it took.
The Oversampling Function — readCalibratedVoltage()
This is the most important function in the whole project. Rather than calling analogRead() once and using that number directly, it does this:
It takes 32 readings with a 150 microsecond gap between each one, tracks the highest and lowest values seen, then subtracts both before averaging. That delayMicroseconds(150) gap is not arbitrary — it gives the ADC's internal sample-and-hold capacitor time to fully charge between readings, which is a genuine hardware requirement for accurate results on the ESP32.
The result is a single averaged raw ADC value with the two worst outliers removed. This gets passed to esp_adc_cal_raw_to_voltage() which converts it to millivolts using the calibration data, and we divide by 1000 to get volts.
The Moving Average — updateMovingAverage()
This function maintains a circular buffer — a fixed-size array where new values overwrite the oldest ones as it fills:
Every time a new voltage reading arrives, it gets written into the next slot in the buffer. Once the buffer is full, it wraps back around to the beginning. The function then averages all values currently in the buffer and returns that as the display value.
The practical effect is that the number on the OLED never jumps suddenly. If you touch a battery to the terminals, the reading rises smoothly to the true value over roughly half a second rather than flickering all over the place on contact.
Battery Math — calcPercent() and calcStatus()
These two small functions handle all the decision-making about what the readings mean:
This is a simple linear interpolation between the empty and full voltage thresholds. It's not a perfect model of battery discharge — real alkaline cells discharge on a curve, not a straight line — but it's accurate enough to be genuinely useful and gives clean, readable percentage values.
calcStatus() is even simpler — it just checks the voltage against the four thresholds and returns the appropriate enum value: STATUS_HEALTHY, STATUS_LOW, STATUS_DEAD, or STATUS_NO_BATTERY. These feed the status label string shown on the OLED.
The Display — drawScreen_1()
All the UI rendering lives in a single function. This is deliberate — if you ever want to redesign the screen layout, you only need to touch one place in the code. The function runs inside U8g2's firstPage()/nextPage() loop:
This is how page-buffer mode works in U8g2. The display is divided into horizontal bands, and the loop redraws each band in turn until the full screen is refreshed. It uses very little RAM compared to keeping the entire frame in memory at once, which matters on a microcontroller where RAM is a limited resource.
Inside drawScreen_1(), the animation frame is drawn first, then the voltage string, percentage string, and status label are drawn on top using different font sizes. The animation frame index advances by one each time the screen redraws, cycling back to zero after frame 27, which creates the looping charging animation effect.
The Code
The full code is hosted on GitHub and consists of two files that must both be present in the same Arduino sketch folder:
- BATTERY.ino — the main sketch
- charging_battery_64_64_28f.h — the Lopaka-generated 28-frame charging animation
Download both files, place them in the same folder, open BATTERY.ino in Arduino IDE and the header file will automatically appear as a second tab.
If you haven't set up the Arduino IDE for ESP32 yet, go back to Step 3 first — the libraries and board package need to be installed before this will compile.
Building the Enclosure
This is the most enjoyable part of the build and also the part that determines whether BATTERY looks like a finished product or a homework project. The goal is a chunky, cartoon-style miniature car battery — the kind you'd see in an animated movie. Bold colors, clean edges, slightly exaggerated proportions. It doesn't need to be perfect. It needs to have personality.
Step 6.1 — Plan Your Dimensions
Before cutting anything, figure out your box size based on the components going inside. The two things that drive the dimensions are the ESP32 board and the 9V battery.
A typical ESP32 dev board is about 52mm × 28mm. A 9V battery is about 48mm × 26mm × 17mm. They'll sit side by side or stacked depending on how tall you want your enclosure. A good starting size that fits everything comfortably is:
This gives you breathing room for wires, the OLED mounted on the front face, and the two terminal posts on top. Scale it up if you want a chunkier, more dramatic look — a bigger enclosure is actually easier to work with and photographs better.
Sketch the six faces of your box on paper first — front, back, left, right, top, and bottom — with their measurements written on each one. This takes two minutes and saves you from cutting the wrong size twice.
Step 6.2 — Cut and Assemble the Box
Transfer your measurements onto the cardboard and cut out all six panels. A ruler and box cutter gives cleaner edges than scissors, though scissors work fine for the outer covering paper later.
Assemble the box with the top panel left unglued for now — you'll need to place your electronics inside before sealing it. Use a glue gun for speed or PVA for cleaner seams. Reinforce the inside corners with small strips of extra cardboard if the box feels flimsy — a sturdy enclosure makes the whole project feel much more solid.
Step 6.3 — Cut the OLED Window
On the front face of the box, measure and cut a rectangular window for the OLED display. The visible area of a standard 128×64 SSD1306 OLED is approximately 26mm × 14mm, but cut the hole very slightly smaller — about 24mm × 12mm — so the display's PCB edge has a small lip to rest against from the inside and doesn't fall through.
Test the fit before gluing anything. The OLED should sit flush against the inside of the front panel with its screen perfectly centered in the window. A tiny strip of foam tape or a dab of hot glue around its edges on the inside will hold it in place permanently once you're happy with the position.
Step 6.4 — Make the Terminal Posts
The two terminal posts are the most recognisable feature of a car battery silhouette and they're what make BATTERY immediately readable as what it is from across the room.
Cut two short tubes from cardboard — roughly 20mm tall and 15–20mm in diameter. Toilet roll tubes cut down to size work perfectly. If you don't have tubes handy, roll strips of cardboard tightly and secure with glue until you have a solid cylinder of the right size.
On the top panel of the box, mark two positions — one toward the left side, one toward the right, slightly offset like a real car battery. Cut a small hole through the center of each terminal position, just large enough for your female Dupont jumper wire to pass through snugly.
Thread one wire up through each hole before gluing the terminals down. The female end pokes out from the top of the terminal post — this is where the AA battery being tested will connect. Glue the terminal tubes over the holes firmly so they stand upright and don't wobble.
Step 6.5 — Cover With Colored Paper
This is where the cartoon personality comes in. Cut pieces of your colored paper to fit each face of the box and glue them on cleanly, folding the edges neatly at corners like wrapping a gift.
A color scheme that works really well for that bold cartoon car battery look:
- Main body — bright yellow
- Top and bottom panel — black rectangular strips covering the top and bottom
- Terminal posts — bright red for positive, black for negative
- Top panel — yellow with the terminal holes cut through cleanly
Take your time on this step. Smooth paper with no bubbles or lifted edges is what separates a great-looking build from a rough one. A flat tool like a ruler edge pressed firmly along each piece as it dries helps enormously.
Step 6.6 — Add the Details
This is the finishing touch that takes the enclosure from colored box to actual prop. Using a black marker, acrylic paint, or cut paper labels, add:
- A large "+" near the red terminal and "−" near the black one
Step 6.7 — Final Assembly
Place the ESP32, OLED (already glued in the window), and 9V battery inside the enclosure. Arrange the wires so nothing is pinched. Route the two terminal post wires down through the top panel holes as planned.
Once everything fits and the OLED is correctly seated, glue the top panel shut. Leave a small gap or a removable flap at the back or bottom if you want to be able to change the 9V battery without destroying the enclosure — a strip of velcro or a small flap held with a single dab of removable adhesive works well for this.
Using BATTERY
Once the code is uploaded and the enclosure is built, using BATTERY is about as simple as it gets.
Powering It On
Snap a fresh 9V battery into the clip inside the enclosure. The OLED will light up within two seconds and the charging animation will begin playing immediately. If nothing appears on the display, check that the OLED's VCC is on 3.3V and not accidentally on 5V — that's the most common reason for a blank screen on first boot.
With nothing plugged into the terminals, the display will read somewhere near 0.00V, 0%, and show "No Battery" as the status. This is correct behaviour — GPIO 34 is floating near zero with no battery connected.
Testing a AA Battery
Take the AA battery you want to test. Touch its positive end to the red terminal wire and its negative end to the black terminal wire. Within about half a second — the time it takes for the moving average to settle — the display will update with three pieces of information:
The voltage — shown to two decimal places, like 1.48V. This is the resting open-circuit voltage of the cell, which is the standard way to assess alkaline battery health without a load tester.
The percentage — a 0–100% value mapped linearly across the 1.0V to 1.6V range. A brand new battery fresh out of the packet will read close to 100%. One that still works but is getting tired will be somewhere in the 40–70% range. Anything below 20% is on borrowed time.
The status label — one of four plain-English assessments:
A Note on Open-Circuit Voltage
BATTERY measures open-circuit voltage, meaning the battery is not under any load while being tested. A battery that reads 1.35V at rest can sometimes still struggle to power a high-drain device like a camera flash or a motorised toy, because its internal resistance causes the voltage to sag under load.
For remote controls, clocks, low-drain LED torches, and most everyday devices, open-circuit voltage is a perfectly reliable indicator of remaining life. For high-drain applications, a proper load tester is more accurate. BATTERY is honest about what it is — a quick, convenient, low-drain health check, not a laboratory instrument.
Checking Accuracy
If you own a multimeter, this is worth doing once just to build confidence in your readings. Set your multimeter to DC voltage, touch its probes to a AA battery, and note the reading. Then test the same battery in BATTERY immediately after.
On most ESP32 boards with eFuse calibration, the readings will match your multimeter within ±0.03V, which is well within the margin needed to meaningfully sort good batteries from dead ones. If yours reads consistently higher or lower than the multimeter, adjust the VOLTAGE_CALIBRATION constant in the code as described in Step 4 and re-upload.
Conclusion & What's Next
BATTERY started as a simple idea — a AA battery tester that looks more interesting than a bare circuit on a breadboard — and turned into something that's actually genuinely useful to have on a desk. It reads accurately, responds quickly, looks immediately recognisable for what it is, and cost almost nothing to build.
The cardboard enclosure keeps the build accessible to anyone. No 3D printer, no laser cutter, no special tools. Just cardboard, paper, glue, and a bit of patience. That was always the point — the DIY feel is part of the personality, not a compromise.
Things You Might Have Learned Along the Way
If this was your first time working with the ESP32's ADC, you now know why a raw analogRead() isn't good enough for precision work and what to do about it. If it was your first time using U8g2, you've worked with one of the most capable and well-supported display libraries in the Arduino ecosystem. And if it was your first time building a cardboard enclosure, you've discovered that with a little care it can look surprisingly good.
These are skills that carry forward into every project you build after this one.
What Could Come Next
BATTERY v5.0 is a complete, working project as it stands. But if you find yourself wanting to push it further, here are some directions worth exploring:
Support for other battery types — the voltage thresholds in the config block are easy to change. With a few extra #define lines and a mode-select button, BATTERY could test AAA, C, D, and even NiMH rechargeable cells, each with their own discharge curves and percentage scales.
A load test mode — adding a known resistor across the terminals when testing would give a loaded voltage reading, making BATTERY more accurate for high-drain applications as discussed in the previous step. A MOSFET switched by another GPIO would let the ESP32 control when the load is applied.
Sound feedback — a small passive buzzer connected to a GPIO could play a short tone pattern depending on the result. One clean beep for a good battery, two short beeps for low, a descending tone for dead. Simple to add and makes the device feel much more complete.
A rechargeable version — swapping the 9V battery for a small LiPo cell with a TP4056 charging module would make BATTERY itself rechargeable. There's an irony there that feels very on-brand for a project with this name.
Better enclosure materials — if you later get access to a 3D printer, the cardboard design translates directly into a printed shell. The same proportions, the same terminal post positions, the same OLED window — just in PLA instead of cardboard, with cleaner edges and more durability.
A Final Note
The name came first, honestly. Basically A Toy That Embodies Rechargeable Yet-Again-Batteries was always going to be the project — it just took a while to figure out what the project actually was. Sometimes that's how the best builds start. You pick a name you like and then build something worthy of it.
And that's BATTERY. Go build it.