My first Arduino project was to build a battery capacity tester. I’ve got a box of rechargeable AA batteries, and it seams they’ve been less and less effective. Since most applications require 4 batteries, invariably one problem battery makes the rest of them look bad.
The Atmel ATMega328 microcontroller has 6 analog inputs with 10-bit A-to-D converters and a external AREF that allows you to define what voltage 0x3FF represents. In other words, it’ll give you ~1.4mV precision measuring 0-1.5V when given a 1.5V analog reference. Plenty accurate for a battery capacity measurement.
The principle is fairly simple. Apply a known load to a battery, record the voltage periodically while the battery discharges, stop recording at some point, and integrate to arrive at the area under the curve in order to derive the amp-hours delivered from the battery.
Enough theory, let’s see how it works. The UI starts with a helpful message:
Insert Battery

NIKON D70, ISO 500, ƒ/2.0, 1/20sec, 50mm focal L.
While the battery discharges, the UI prints the voltage, real-time capacity measured thus far, and the duration that the measurement has been taking place.
Battery In

NIKON D70, ISO 500, ƒ/2.0, 1/25sec, 50mm focal L.
When the cut-off voltage of 0.9V has been reached, the “usable” capacity is saved at the top. The real-time data continues so the capacity below 0.9V is measured. For the NiMH batteries I’ve tested, the capacity below 0.9V is minimal (~100mAH).
After the cut-off is reached, an LED starts blinking to attract attention that the test is effectively over.
Done

NIKON D70, ISO 500, ƒ/2.0, 1/20sec, 50mm focal L.
/* Battery Characterization Tool
11/7/2009 John Terry
Compiled on Aurdino 0017 in 4930 bytes.
This uses the analog in to measure the voltage of a battery under a known load and
integrates the area under the curve to arrive at the useful capacity of the
batterin in mAH.
*/
#include <LiquidCrystal.h>
// Set some constants -- these will need to be adjusted for your setup
//
// Connect a ~10K Ω (Rr) resistor from 3.3V supply pin to the AREF pin,
// creating a voltage divder with the internal 32KΩ resistor on AREF.
// aRefVoltage = supply_voltage * 32K / Rr
// Alternatively, measure the actual AREF voltage applied with a good DMM:
// my measured aRefVoltage = 2.62;
const float aRefVoltage = 2.62;
// Connect a load resistor (Rl) to the battery. ~2.2 Ω restistor gives ~500mA drain which
// is about right for a battery rated at 2500mAH. Note, this should be a >=1W resistor!
// Dont trust your DMM to meausre such a low resistance accurately. I measured the current
// and back-calculated the resistance. Best just to trust rated resistance.
// my resistance = 2.18;
//
// The integrator works by accumulating the sampled voltage values from the start until
// hitting a pre-determined low-voltage threshold. Since they are sampled at a
// known period, the number of samples taken cancels out and the accumulation of all samples
// simply needs to be scalled by a factor
//
// Let's make some definitions to show the derivation of how this is so:
// I = current
// V = load voltage
// Rl = load resistance
// samples = number of voltage samples made
// sensor = value read out of analog A->D
// rate = number of samples taken per second
//
// ave(V)
// capacity = ∫I ~= ave(I) * time = -------- * time
// Rl
//
// Where:
// ∑(V) ∑(sensor) * aRefVoltage/1024
// ave(V) = ------- = ---------------------------------
// samples samples
//
// time = samples/rate
//
// Thus:
// ∑(sensor) * aRefVoltage samples
// capacity = ------------------------- * ---------
// samples * 1024 * Rl rate
//
// ∑(sensor) * aRefVoltage
// capacity = ------------------------- = ∑(sensor) * quanta
// 1024 * Rl * rate
//
// quanta = aRefVoltage/(1024 * Rl * rate ) * 1000/3600 // scaled for mA Hours
const double quanta = 0.00030179; // 2.62/1024/2.355/3.6
// Define low voltage threshold where any remaining capacity is "unusable"
// lowThreshold = 0.9V * 1024/aRefVoltage
const int lowThreshold = 354;
int sensorPin = 0; // select the analog input pin for the voltage measurement
int ledPin = 13; // select the pin for the LED
int fetGatePin = 8; // select the pin for the LED
int sensorValue = 0; // unscaled sensor output
float voltage = 0; // measured voltage
double mAH = 0; // Calculated current
long accumulator = 0; // sum of all unscalled sensor values sampled
int epoch = 0; // seconds since battery was inserted
int lowVolts = 0; // debounce the low voltage threshold
boolean done = false;// Voltage has dropped below threshold
boolean batteryIn = false;// Battery present
// initialize the the LCD library with the numbers of the interface pins
// LCD Pins -- RS, EN, D4, D5, D6, D7
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
void setup() {
// declare the ledPin and fetGatePin as an OUTPUT:
pinMode(ledPin, OUTPUT);
pinMode(fetGatePin, OUTPUT);
// set analog reference to external
analogReference(EXTERNAL);
// set up the LCD's number of rows and columns:
lcd.begin(16, 2);
// Print a helpful start-up message to the LCD.
lcd.print("Insert battery");
// DEBUG initialize serial communications at 9600 bps:
// Serial.begin(9600);
}
void loop() {
sensorValue = analogRead(sensorPin);
if (!batteryIn && (sensorValue > 100)) {
// Initialize upon the insertion of a "fresh" battery
batteryIn = true;
done = false;
epoch = 0;
accumulator = 0;
lowVolts = 0;
digitalWrite(ledPin, LOW);
digitalWrite(fetGatePin, HIGH);
// clear out when LCD when starting over
lcd.setCursor(0, 0);
lcd.print("vlts mAH Time ");
lcd.setCursor(0, 1);
lcd.print(" ");
}
else if (batteryIn && done && (sensorValue <= 20)) {
// consider the battery removed only after finishing the last measurement to
// debounce a glitchy concact
batteryIn=false;
}
else if (batteryIn) {
// Running state during discharge state
voltage = sensorValue*aRefVoltage/1024.0;
// print voltage
lcd.setCursor(0, 1);
lcd.print(voltage);
if (!done) {
accumulator += sensorValue;
// print mAH
mAH = accumulator*quanta;
if (mAH < 10) { lcd.setCursor(8, 1); } // adjust to make it perdy
else if (mAH < 100) { lcd.setCursor(7, 1); }
else if (mAH < 1000) { lcd.setCursor(6, 1); }
else { lcd.setCursor(5, 1); }
lcd.print(int(mAH));
}
// print current time
lcd.setCursor(11, 1);
lcd.print(epoch/60.0);
lcd.setCursor(15, 1);
lcd.print("m");
if (!done) {
// lowVolts requires 10 seconds in the last 20 before being done
if (sensorValue < lowThreshold) { lowVolts++;}
else if (lowVolts > 0) { lowVolts--;}
// If it's below threshold for 10 of 20 samples, bail out
if (lowVolts > 10) {
done=true;
digitalWrite(fetGatePin, LOW);
lcd.setCursor(0, 0);
lcd.print(" mAH in ");
lcd.setCursor(1, 0);
lcd.print(int(mAH));
// put the time, in minutes in the upper right
lcd.setCursor(11, 0);
lcd.print(epoch/60.0);
lcd.setCursor(15, 0);
lcd.print("m");
// Clear out the bottom line
lcd.setCursor(4, 1);
lcd.print("v ");
}
}
epoch++;
} // batteryIn -- main routine
if (done) {
// When done, flash the LED to get attention
digitalWrite(ledPin, HIGH);
delay(500);
digitalWrite(ledPin, LOW);
delay(499);
} else {
// Since the processing takes some time prior to the delay, we'll assume 1mS
// This could stand to be improved with an interrupt routine that is kicked off
// before all the processing starts for each loop
delay(999);
}
/* DEBUG
Serial.print("\nsensor = " );
Serial.print(sensorValue);
Serial.print("\t lowvolrs = " );
Serial.print(lowVolts);
*/
}
Compiles into 4930 bytes.
Schematics, excluding the LCD display (pin-out is listed in the code).

Thanks for the info!
I am wondering about your code. It seems to have errors such as the #include not listing any files. The compiler gives me a few more errors as well. Is this your actual code?
Okay. The two things that you need to change to make the code run is to change “#include” to “#include ” and to change “else if (batteryIn && done && (sensorValue < = 20)) {" to "else if (batteryIn && done && (sensorValue <= 20)) {" (i.e. change "< =" to "<=" by removing the space.)
Thanks for posting this project.
After the #include you need to list the LiquidCrystal.h file. This site won’t print the code for some reason.
Thanks Michael — Didn’t even notice that the cut-paste of code was partially interpreted as HTML.
Fixed.
In any case, the “Here’s the code” has always been a link to the text — much better than cutting/pasting from the browser
I’ve constructed a version of this. I noticed your comments about not trusting the VOM on the low R (ohm) readings and to measure the current and calculate backwards. You may want to reconsider. I assume you said this because you had problems making the calibration numbers balance (V/I=R). My calculation seemed to indicate an additional .7R to .8R in the system (in addition to the 2.1R resistors). I finally decided to measure the VOM which was measuring current with another VOM for any R that the (first) VOM might be adding. Sure enough, the VOM measuring current added .7R to .8R. While .7R doesn’t mean much in a high impedence system, it makes a big difference when added to 2R of the load resistors.
Thanks again for posting your example. I had independently thought of building this gadget. Your code saved me a ton of time.
Thank you for this exciting project.
I have to test the batteries of 16V or 12V.
I could indicate how you made the connections or a Schematic of tester?
Hi John:
I was looking over your code/project. You put a lot of thought into the features … and its a good application for an ARDUINO.
I’m currently working on a project that is similar to yours but is acting as a chart recorder. I am testing Li-Ion charge/discharge characteristics for another project. I had thought about using a simple resistor load but in using that method, the actual load current diminishes as the cell voltage decreases. In my humble opinion, sinking constant current increases the accuracy of your measurments and is a “more correct” way to determine discharge ratings of a battery.
In my application, I used a constant current sink. I chose a TL431 “Adjustable Precision Shunt Regulator”. On page 29 of the Texas Instruments data sheet, there is a schematic for a “Precision Constant Current Sink”. You can power the TL431 from the ARDUINO 5V supply rail and attach the collector of the sink transistor to your battery. Since the TL431′s “REF” pin has a constant 2.495V (+/-%), it could also be used as your A/D reference instead of the resistor divider you used. The “Standard Grade” part has a +/- 2% tolerance, while the “B Grade” part sports +/- 0.5% tolerance.
Thanks for posting your work.
Best regards,
Scott
Hi Scott;
I too am interesting in creating a constant-current discharger/logger. Would the TL431-based constant current sink work with 1.2v nmh batteries? Would using a MosFet in place of the BJT shown on the TI page work better?
@Scott: It’s good point, especially for LION where the discharge profile has the voltage steadily declining throughout the discharge cycle. NiMH has a fairly stable voltage profile that falls off a “cliff” quite rapidly upon depletion. Thus, I suspect you’d see minimal difference in the measured capacity for the two methods.
Constant power discharge would probably be the best method for applications that use nonlinear voltage regulation.
Thanks for posting this.
Did I miss the link to the parts list and schematic?
Updated!
The usage of the FET to stop discharging probably isn’t entirely obvious to everyone. Thanks for motivating me to actually draw it out.
Thanks for the schematic.
What did you use for Q1? What are the required specs for Q1?
I didn’t exactly engineer it and just pieced it together to get the discharge current (drain-source ON resistance) I was looking for. I had a hand-full of unspecified low-current transistors sitting around and I just put them all in parallel to minimize the resistance.
Really, most any N-channel FET that’s solidly saturated when Vgs>=4 V should work with a continuous current rating of >500mA. You’ll want to account for the resistance of the FET and adjust accordingly to get an accurate measurement.
Perhaps this one would work fine for you.
Another way to do it is to use a relay, but that’s more costly. Alternatively, you could use nothing and just be ready to pull the battery out when it hits the low-voltage threshold.
Thanks for the info. I do hobby electronics (passives, transistors, op-amps, etc.) , but I’m not really up on MosFets. I want to take your idea (and thank you for posting it) and modify it to make the load constant-current by measuring the voltage drop across the resistor and varying the gate voltage to keep the current constant, regardless of the voltage. And of course, shut off when the voltage on the battery gets to the safe minimum.
Saw a circuit using an Op-Amp to drive the gate and keep the current constant, but not sure if I need the op-amp since I can use 2 of the A/D inputs to measure the voltage drop directly and calculate the current draw. I’d use one of the PWM outputs and a cap to send a voltage to the gate.
Any suggestions on how I would go about choosing a suitable MosFet?
Thanks for your help.