Welcome to buchanan1.net

J.R. Buchanan

http://www.buchanan1.net

Battery Test

Testing the amp hour capacity of AA cells

Mostly Alkaline, one Heavy Duty Carbon Zinc cell

June 2015

arduino battery tester

Introduction

Years ago when I worked at a small factory in Westfield, Indiana, we made products that shipped with D Cell batteries. Customers were complaining that the batteries we supplied didn't last very long. So I was assigned to test the commonly available batteries to select a better brand of battery for our products. After a lot of testing, I determined that the Duracell D Cell "Coppertop" was the longest lasting at the load current our equipment drew. So, of course management went ahead and selected the least expensive battery they could find. Which lasted for an even shorter period of time than the ones we had been using.

That was 30 years ago, and since then I have wondered if Duracell Coppertop batteries are still the longest lasting. We don't use many D cells around our house, so I was more interested in AA cells which we go through in great quantities. (We tried rechargeables, but the kids (we are a foster home as well as having our own kids) just did not "get" them. I got tired of them mixing batteries at different levels of charge (reverse charging one of them) and mixing them with primary cells. We had to give up on rechargeables.)

If you are only interested in the results, and not the details of obtaining them, skip down to the results below.

Methods

Recently I started back into electronics as a hobby, and have been playing around with Arduinos. I came across a Seeed brand SD card shield at Radio Shack (R.I.P.) and grabbed it, thinking, "Data Logger". The first thing I decided to try was battery testing.

The first decision was, "how much current should I draw during this testing?" Some applications run a battery down in a few hours, in other applications, they last years. I don't have the patience or the resources to test multiple batteries under very low current draw, and nothing we use them for runs them down really fast. So I picked an intermediate current, about 100mA. I figured that they should last about 10 to 20 hours at this load. Instead of using a constant current load, I decided to use a resistor as a load for simplicity, I selected 15 ohms for a nominal current of 100mA.

The next decision was, "at what voltage should I consider the battery dead?" I somewhat arbitrarily, chose to test to two different levels, 1.25V and 1.0V. Since I made that decision, I read an article about battery life extension. The author claimed that the average voltage that devices failed at was 1.3V. If so, my 1.25V measurements are the most useful. Also since then, I measured the under load voltage of the batteries in my electronic thermostat after it failed due to low batteries. It failed at 1.36V.

The most obvious way to measure the battery voltage was to read an analog input and divide by 1023 to get the voltage. This would give .5 mV resolution, way more than good enough. But what about the accuracy? This is totally dependent on the accuracy of the 5V regulator on the Arduino board. I measured this, with the Arduino powered by a 9V wall wart and got 5.00V on my DVM. Good. When I powered the Arduino from the USB cable connected to my computer, I got 4.92V. Not good, but not really important, as I never intended to run the tests plugged into the computer. If you duplicate this circuit and you don't get 5.00V from the Arduino regulator, simply change the 5V in the voltage calculation in the Arduino program to the value you measure.

I had been using a 16x2 character LCD display in some experimentation with the Arduino, I decided to use it to display information during the test. I set it up to display the elapsed time in minutes and seconds on the top line and the current voltage on the bottom line. I added an alert LED that could be used to indicate the end of test at a glance.

I decided to log the data on the SD card in a Comma Separated Value (CSV) file which could be analyzed on a desktop computer, and maybe imported into a spreadsheet for graphing.. I logged the voltage every minute and put the elapsed time in the first column and the voltage in the second column. The program (I have trouble with the term "sketch") shuts the test down at 1.0V, displays a message on the LCD (the voltage is still displayed) and blinks the alert LED.

Here's a diagram:

Diagram

Here's the program for the Arduino:

/*

This program is used to measure the life of batteries in amp hours. It
produces a .csv (Comma Separated Value) file on an SD card that is
analyzed with a Perl script to report the amp hour capacity of the
battery tested. It uses a Seeed brand SD card shield and an Arduino Uno

The format of the .csv file, the first column is the elapsed time in
minutes, the second column is voltage, in volts:

0,1.52
1,1.55
2,1.54
3,1.54
4,1.54
5,1.53
6,1.53
7,1.53
8,1.52
[...]  <-- lots of elided lines
1547,1.01
1548,1.01
1549,1.01
1550,1.00
1551,1.00
1552,1.00
1553,1.00
1554,1.00
1555,1.00
1556,1.00
1557,1.00
1558,1.00
1559,1.00
1560,1.00

The circuit:

* SD card attached to SPI (Serial Peripheral Interface) bus as follows:
** MOSI - pin 11 (Master Out Slave In) - The Master line for sending data to
                  the peripherals
** MISO - pin 12 (Master In Slave Out) - The Slave line for sending data to
                  the master
** CLK - pin 13  or SCK (Serial Clock) - The clock pulses which synchronize
                 data transmission generated by the master 
** CS - pin 4    or SS (Slave Select) - the pin on each device that the
                 master can use to enable and disable specific devices, this
                 is usually pin 10, but the Seeed SD card uses 4, even though
                 the Seeed card does not use pin 10, it must remain an unused
                 I/O for the SD card library to function
This is all taken care of by simply plugging the SD shield onto the Arduino

The pins we use for communication with LCD board:
RS, Enable, D4, D5, D6, D7     <- names of lcd pins
13, 11,     6,  5,  4,  3      <- pin number on lcd board
 8,  7,      6,  5,  3,  2      <- pin numbers on Arduino (digital I/O)
These are all connected with wires to the LCD board

The alert LED is in series with a 560 ohm resistor from digital 0 to
ground
 
The battery being tested is connected to A0 with load resistor across
battery
 
 */
 
#include <SPI.h>           // needed for other libraries to work as of IDE 1.6
#include <SD.h>
#include <LiquidCrystal.h>

File myFile; // handle of log file

// Tell the LCD library Which pins are used on the Arduino and the order
// they're hooked to LCD
// Next two lines duplicates of circuit description, here for clarity
// RS, Enable, D4, D5, D6, D7             <- names of lcd pins
// 13, 11,     6,  5,  4,  3              <- pin number on lcd board
LiquidCrystal lcd(8, 7, 6, 5, 3, 2);   // <- matching pins on Arduino (digital)

// A locally written clear function
// line param of 0 for first line, 1 for second line of LCD
// no_char is the number of characters your display supports per line
void lcd_clear (int line, int no_char);

#define NO_CHAR 16             // number of characters per line of your display
#define LOG_FILE "LOG.TXT"     // Name of log file created on SD card
#define TEST_INTERVAL 60000    // Frequency of voltage test in ms, one minute
#define REGULATOR_VOLTAGE 5.00 // Set to the value of your Arduino's 5V line
#define START_VOLTAGE 1.2      // Voltage that is needed to start test
#define END_VOLTAGE 1.0        // Voltage that indicates end of test

// Initial part of program, executed before the loop() function
void setup()
{
  int sense_value; // voltage read with analogRead(), a value of 1023 is 5V
                   // This is referenced to the on-board voltage regulator when
                   // running on a DC supply, it's from the USB if running from
                   // USB cord, not to be trusted in either case, unless you
                   // test it first. Mine was 5.00V running from a
                   // DC supply, 4.92V when plugged into the computer
                   // via a USB cable.
  float voltage;   // Voltage measured, sense_value*REGULATOR_VOLTAGE/1023
    
  // init lcd, number of char per line and 2 lines
  lcd.begin(NO_CHAR, 2);
  
  // On the SD Shield, CS is pin 4. It's set as an output by default.
  // Note that even if it's not used as the CS pin, the hardware SS pin
  // (10 on most Arduino boards, 53 on the Mega) must be left as an output
  // or the SD library functions will not work.
  pinMode(10, OUTPUT);

  // for the end-of-test blinking LED
  pinMode(0, OUTPUT);

  // Init SD card library, print error message on failure
  // failure is most likely a missing SD card in the shield
  if (!SD.begin(4)) {
    lcd_clear (0, NO_CHAR);
    lcd.print("SD init failed");
    // Blink the status LED until Arduino is powered off
    // (indicating failed init)
    while (1){
      digitalWrite(0, HIGH);
      delay(1000);
      digitalWrite(0, LOW);
      delay(1000); 
    }
  }

  // If log file exists, delete it
  if (SD.exists(LOG_FILE)) {
    SD.remove(LOG_FILE);
  }

  // Create log file
  myFile = SD.open(LOG_FILE, FILE_WRITE);

  // Display a ready for battery insertion message and display voltage
  // until it exceeds START_VOLTAGE In actual use, the voltage won't be visibly
  // displayed, as it rises *way* too fast. It's here for testing with a slowly
  // varying input voltage. When START_VOLTAGE is exceeded, the loop is broken
  // and the main part of the program starts in the loop() function
  while (1){
    sense_value = analogRead(A0);
    voltage = sense_value * (REGULATOR_VOLTAGE / 1023);
    if (voltage > START_VOLTAGE){
      break;
    }
    lcd_clear (0, NO_CHAR);
    lcd.print ("Rdy for batt");
    lcd_clear (1, NO_CHAR);
    lcd.print (voltage);
    lcd.print ("V");
    delay (50);
  }
  
}

// main part of program, loops forever  
void loop()
{
  int sense_value; // voltage read with analogRead(), a value of 1023 is
                   // REGULATOR_VOLTAGE (nominally 5V)
                   // This is referenced to the on-board voltage regulator when
                   // running on a DC supply, it's from the USB if running from
                   // USB cord, not to be trusted in either case, unless you
                   // test it first. Mine was close enough running from a
                   // DC supply
  float voltage;   // Voltage measured, sense_value*REGULATOR_VOLTAGE/1023
  int hours;       // Hours to be displayed before the ":" while test is
                   // progressing
  int minutes;     // Minutes to be displayed after the ":" while test
                   // is progressing
  static unsigned int no_minutes = 0; // number of minutes passed since loop
                                      // first invoked. static so that it's
                                      // not reinitialized every time the
                                      // loop repeats

  hours = no_minutes / 60;            // calculate hours for display
  minutes = no_minutes - hours * 60;  // calculate minutes for display
  lcd_clear (0, NO_CHAR);             // clear LCD and display elapsed time
  lcd.print (hours);
  lcd.print (":");
  if (minutes < 10){                  // add leading 0 to minutes if under 10
    lcd.print ("0");
  }
  lcd.print (minutes);
  
  sense_value = analogRead(A0);                         // read battery voltage
  voltage = sense_value * (REGULATOR_VOLTAGE / 1023.0); // convert to volts
  lcd_clear (1, NO_CHAR);                               // print voltage on
                                                        // LCD 2nd line
  lcd.print (voltage);
  lcd.print ("V");

  // Write data to log file on SD card in .csv format
  myFile.print (no_minutes);
  myFile.print (",");
  myFile.print (voltage);
  myFile.print ("\n");

  // if voltage is END_VOLTAGE or lower, end test
  if (voltage <= END_VOLTAGE){
   while (1){         // loop forever
    myFile.close();   // close log file on SD card

    // continue to display voltage
    sense_value = analogRead(A0);
    voltage = sense_value * (REGULATOR_VOLTAGE / 1023.0);
    lcd_clear (1, NO_CHAR);
    lcd.print (voltage);
    lcd.print ("V");

    // blink status LED to draw attention to test being over
    digitalWrite(0, HIGH);
    delay(1000);
    digitalWrite(0, LOW);
    delay(1000); 
   } 
  }

  // Still in main loop here, we haven't hit 1V yet
  // delay one minute until next reading
  delay (TEST_INTERVAL);

  // increment number of minutes before loop starts over
  no_minutes++;
  
}

// clears specified line on lcd display
void lcd_clear (int line, int no_char)
{
  char *printable_string;
  int ktr;
 
  // displays typically have 2 lines, if a number other than 0 or 1
  // (first or second line), set it to 0 or 1
  line = constrain (line, 0, 1);

  // allocate memory for a string with no_char spaces to display on LCD,
  // clearing that line
  printable_string = (char *) malloc (no_char + 1); // dangerous assumption
                                                    // that malloc succeeds
                                                    // The + 1 is to leave room
                                                    // for a terminating NULL

  // assign blank space to each character of allocated string
  for (ktr = 0; ktr < no_char; ktr++)
  {
    printable_string[ktr] = ' ';
  }  
  printable_string[no_char] = '\0';  // terminate string with a NULL
  
  // set cursor and print blanking string
  lcd.setCursor (0, line); // position cursor to begining of specified line
  lcd.print (printable_string);  // print string to blank line
  lcd.setCursor (0, line); // position cursor to begining of specified line

  // free allocated memory for later use, run out of RAM w/o this
  free (printable_string);

}

When I first wrote it, I put very verbose messages in to print via the LCD during startup. I had to take them out because of memory restrictions of the Arduino Uno. It has 2K of static RAM which had never been a problem for me before this. However, when I first tested it out, the Arduino would crash after about 2 hours. I immediately suspected the SD card library as I'd never had a crash before. So I commented all of the SD functionality out. It would run as long as I wanted. I put it back in, crash in about 2 hours. I had been opening and closing the file every time I wrote to it, as this is what the examples I had seen did. I modified the program to open the file once, and closed it when the test was done. It ran for about 10 hours before a crash after this. At this point, I turned to Google. My first searches came up empty, but when I found the right keywords, I discovered that this was a common problem. It seems that the SD card library uses a lot of static RAM, and if not a memory leak, it at least uses more up to a point as time goes by. So I minimized the amount of RAM my section of the code used by removing a lot of the verbose messages displayed on the LCD. That was it! It would run as long as I wanted again.

In addition to the program that runs on the Arduino, I wrote a Perl script to analyze the .csv files stored on the SD card when the tests are run. The user enters the end-of-test voltage, the parallel resistance, and the name of the .csv file. The program prints the number of amp hours provided. I did this on a Linux machine, but Perl is available for Windows (One of them: http://www.activestate.com/activeperl/downloads) and Mac as well (I don't use a Mac, but it looks like Perl should already be installed with OS X, at least according to this document: http://www.macinstruct.com/node/437)

Here it is:

#!/usr/bin/perl

use strict;
use warnings;

my ($threshhold);
my ($file_name);
my ($low_t);
my ($high_t);
my ($in_file);
my ($current_line);
my ($time);
my ($voltage);
my ($prev_time);
my ($amp_minutes);
my ($amp_hours);
my ($resistance);

# voltage threshold limits
$low_t = 0;
$high_t = 1.7;

# get threshold voltage
print "Enter threshhold voltage (1.25): ";
$threshhold = <STDIN>;
chomp ($threshhold);
if ($threshhold eq "")
   {
   $threshhold = 1.25;
   }
if (($threshhold < $low_t) || ($threshhold > $high_t))
   {
   die ("***ERROR*** Threshhold voltage must be between $low_t and $high_t\n");
   }

# get resistive load in ohms
print "Enter resistive load (15): ";
$resistance = <STDIN>;
chomp ($resistance);
if ($resistance eq "")
   {
   $resistance = 15;
   }
if ($resistance <= 0)
   {
   die ("***ERROR*** Resistance must be above 0\n");
   }

# get file name
print "Enter file name: ";
$file_name = <STDIN>;
chomp ($file_name);
unless (-e $file_name)
       {
       die ("***ERROR*** '$file_name' does not exist.\n");
       }

# open csv file
unless (open ($in_file, "<", $file_name))
       {
       die ("***ERROR*** Could not open '$file_name' for read, $!\n");
       }

# loop through and calculate amp minutes
$prev_time = 0;
$amp_minutes = 0;
while ($current_line = <$in_file>)
      {
      chomp ($current_line);

      ($time, $voltage) = split (/,/, $current_line);

      $amp_minutes += ($time - $prev_time) * ($voltage / $resistance);

      if ($voltage <= $threshhold)
         {
         last;
         }

      $prev_time = $time;
      }  

# close file
close ($in_file);

# calculate amp hours
$amp_hours = $amp_minutes / 60;

# print result
printf ("\nAmp hours: %.2f\n", $amp_hours);

Results

By now, I suspect that you are wondering which brand of battery lasts the longest. Here are the result. I tested 7 brands of alkaline cells, and for fun, one brand of "Heavy Duty" carbon zinc cell. I didn't expect much from the carbon zinc cell, and was not surprised.

                      1.25V     1.00V
Tenergy Heavy Duty    0.25      0.69
AC Delco              0.71      2.09
Energizer             0.72      2.17
Sony Platinum         0.94      2.06
Rayovac               0.96      2.37
Duracell Coppertop    0.99      2.43
Rayovac Advanced      1.09      2.51
Duracell Quantum      1.18      2.66

The results are shown in amp hours. Quite a range, as you can see. Duracell won again, 30 years later, both in the "standard" alkaline cells and in the "Advanced" & "Quantum" extra life versions.

Here are the best prices that I found on Amazon, except for the Tenergy, I couldn't find them on Amazon, but frys.com had them. I bought mine at a brick and mortar Fry's. I tried for as close as I could to 48 count packages. Pricing done on 5/20/15:

Tenergy Heavy Duty $0.17 ea. (4 pack on frys.com)
AC Delco           $0.21 ea. (100 pack)
Rayovac            $0.38 ea. (48 pack)
Duracell Coppertop $0.42 ea. (40 pack)
Energizer          $0.45 ea. (50 pack)
Sony Platinum      $0.58 ea. (12 pack)
Duracell Quantum   $0.65 ea. (32 pack)
Rayovac Advanced   $0.82 ea. (6 pack, the biggest I saw locally as well)

Cost per amp hour at 1.25V

                   $/Amp Hour
AC Delco           $0.30
Rayovac            $0.40
Duracell Coppertop $0.42
Sony Platinum      $0.62
Energizer          $0.63
Duracell Quantum   $0.55
Tenergy Heavy Duty $0.68
Rayovac Advanced   $0.75

Cost per amp hour at 1.0V

                   $/Amp Hour
AC Delco           $0.10
Duracell Coppertop $0.12
Rayovac            $0.16
Energizer          $0.21
Duracell Quantum   $0.24
Tenergy Heavy Duty $0.25
Sony Platinum      $0.28
Rayovac Advanced   $0.33

Generated .csv files for each type of battery:

Discharge curves

First I'll provide a graph of the four batteries I found most interesting, all eight would have made for a messy graph. Below that, I provided a graph for each type of battery tested.

Green, Tenergy Heavy Duty
Yellow, Energizer
Red, Duracell Coppertop
Blue, Duracell Quantum

Graph of 4 battery discharge

Now individual graphs.

AC Delco Duracell Duracell Quantum Energizer Rayovac Advanced Rayovac Ready Power Sony Platinum Tenergy