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 0×3FF 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.

































Recent Comments