how to measure AA battery capacity

Poundland in the UK sell batteries branded by Kodak, but the cheapest ones are zinc chloride. I thought ZnCl has gone out of use decades ago, but it is surprising how often people are penny-wise and pound foolish. Particularly in pound shops 😉

Cheap Poundland batteries are in fact twice as dear to use!
Cheap Poundland batteries are in fact twice as dear to use…

So I am looking to make something to track the discharge of a battery. This is also useful to qualify NiMH and NiCd batteries – these fade with time and it is good to know what sort of capacity is left.

...as the expensive ones!
…as the expensive ones!

Every minute it reports the voltage and current from the batteries running through a 2.5V torch bulb, the third bulb is maintained at 2.5V to provide a reference. It transmits the signal using radio to a datalogger. I got a camera to take a picture every 15 minutes, as a video the results are reasonably clear. A picture tells a thousand words – the Zinc Chloride batteries are crap

The left-hand bulb is powered by the ‘cheap’ battery that Poundland sell for 9p, the middle is powered by the ‘dearer’ alkalines they sell at about 17p.

Bulb measurement schematic
Bulb measurement schematic

I have a Ciseco OpenKontrol Gateway as a datalogger. It stores data to a SD card, but there is no law saying you couldn’t send it to some kind of IoT host like Xively. So all I need is the interface to the batteries to track the voltage and current; I used a 2.5V torch bulb for the load because it does not seem unreasonable to me to expect batteries to be used in a torch, I use two batteries in series, which seems to be the most common configuration in a torch. On the way to ground I use a 1 ohm resistor so I can measure between ground and the top of the bulb the battery voltage and between the resistor and ground the voltage corresponding to the current. Using 1 ohm makes the calculation easy, and also drops about 0.3V so the bulb is run at 2.7V at the start.

Now if you are looking to do this in a big way then these guys at Battery Showdown are doing this is a more rigorous way but their hardware is dearer. I’m also not absolutely sure that a constant current discharge is typical of many loads. something like a digital camera with a switch mode PSU actually increases its peak load with decreasing battery voltage because it is more a constant power device and needs to draw more current at a lower voltage to run the focusing bits. Something with a linear voltage regulator is a constant current to the dropout voltage of the regulator, but you don’t often see linear regulators in battery-powered kit.

The bulb isn’t ideal either, with a varying resistance with temperature, so I’d probably favour using resistive loads, and getting four/five battery channels instead of two. But the visuals were great for the video.

If you have a PC that can take RS232 then simply wire the output of the PIC to the PC serial port via a Max232 chip or single transistor inverter if you don’t need the radio connection. Since I have the datalogger logging other sensors it seems easy enough to use it as is.

I used a PIC16F870 that has five A/D channels – this gives me two battery channels. Each battery channel has one A/D port connected to A in the diagram, another one to B and 0V is connected to C. The voltage at B is the same as the current since R is 1Ω and the voltage at A is the battery voltage itself. The 10-bit A/D converter goes up to 1024, by setting the reference voltage to be the PIC power supply at the high end and PIC ground at the low end I made the maths easier by setting the PIC supply voltage to 5.12 V (this is within the PIC operational voltage spec of 4 to 5.5V) in which case a straight divide by two gives V in * 100, and matches the requirements acceptably.

Doing this for the Poundland batteries shows the difference between the alkaline and the ZnCl batteries

alkalines give about three times the runtime than ZnCl
alkalines give about three times the runtime than ZnCl

If we take the service life as down to 2V from 3V initially, then you get 1.7 hours from the cheap ones and 5.8 hours from the alkalines.Alkaline cost/hr is 17*2/5.8=5.9p/hr and the cheap batteries cost 9*2/1.7=10.6p/hr.

As a result you pay about twice as much to run something using the cheap Poundland batteries, and you get to replace the batteries three times as often. Saving money therefore costs you a fortune in aggravation and generates more waste. You also have this sort of problem if you don’t pay attention and leave the used batteries in too long.

cheap batteries consume the zinc outer casing, leading to this
cheap batteries consume the zinc outer casing, leading to this

The device offers an insight into the capacity of rechargeable batteries which is the more useful feature of it.

a very old pair of 500mAh nicads and a pair of Uniross 2300mAh NiMh
a very old pair of 500mAh nicads and a pair of Uniross 2300mAh NiMh

I am not really sure I understand the pathology behind the second wind the NiMH cells seem to have. I’d discard it as a fault in reading except that – the current shows the bulb did not blow or become loose, and the A/D and voltage reference is multiplexed, so the error should have shown elsewhere.  will repeat this and see if it happens again – probably not as this battery has been totally flattened so it is not the same battery.

For rechargeables I will modify this rig to take four individual cells and discharge through four resistors. That way I don’t back-charge any weak cells, and I don’t need to measure the current as the terminal voltage and Ohms Law will tell me the current. If I run 2.7Ω I will run about 440mA.

 

 Schematic

PIC battery tester schematic

JAL code

The JAL code for the PIC is 1311_datalogger_v06.jal

-- JAL 2.4i
-- derived from univ-sensor_v06
-- Datalogger
-- adjust power supply to 5.12V using the LM317 regulator.
-- internal Vref = VCC = 5.12V
-- Can just divide ADC by 2 and shift dec point by two places
-- v06 12 Nov 2013 RM
-- CC BY-SA 3.0
-- http://creativecommons.org/licenses/by-sa/3.0/
-- serial port designed for use with a Ciseco XBBO board and radio ( Google it)
-- can also use w/o radio via a Max232 or a FTDI adapter instead
-- radio is powered down while ADC measurement made to avoid loading regulator voltage

include 16f870
pragma target clock       4_000_000
pragma target osc    HS   ; use a 4MHz crystal or resonator
pragma target WDT    ENABLED
pragma target PWRTE  ENABLED
pragma target BROWNOUT    ENABLED
pragma target LVP    DISABLED
pragma target CP    DISABLED
pragma target CPD    DISABLED
-- HS, -BOD, ,-LVP, -WDT, -CP  = 0x3F22
;pragma target fuses       0x2007

include pic_general
include print
include format

-- set all interrupt sources disabled
;INTCON = 0

-- ADC includes delays so don't include this myself

-- V2 delay's
-- include delay_any_mc

-- more delay functions
-- include extradelay

-- the analogue pins
pin_a0_direction = input
pin_a1_direction = input
pin_a2_direction = input
pin_a3_direction = input
-- there is no a4 analogue channel
pin_a5_direction = input

pin_c7_direction = input
pin_c5_direction = output
pin_c6_direction = output

pin_A4_direction = output          ; this is open drain so 3V3 pullup comes from XRF
alias xrf_sleep is pin_A4          ; pull this low to start the XRF, let go and float up to enter sleep
xrf_sleep=low                      ; must enable with ATSM2

-- now configure ADC  (this is a 10-bit ADC)

const bit ADC_HIGH_RESOLUTION = true
const byte ADC_NCHANNEL = 1
const byte ADC_NVREF = ADC_NO_EXT_VREF
include adc
adc_init()

-- ok, now setup serial, we'll use this
-- to get ADC measures

-- all this is because the 16f870.inc file is not right for serial hardware  8/5/13
alias RCIF IS PIR1_RCIF
alias TXIF IS PIR1_TXIF
alias BRGH IS TXSTA_BRGH
alias RCIE IS PIE1_RCIE
alias TXIE IS PIE1_TXIE
alias TXEN IS TXSTA_TXEN
alias TRMT IS TXSTA_TRMT
alias SPEN IS RCSTA_SPEN
alias OERR IS RCSTA_OERR
alias CREN IS RCSTA_CREN
-- end of
-- all this is because the 16f870.inc file is not right for serial hardware

alias PEIE IS INTCON_PEIE
alias TMR1IE IS PIE1_TMR1IE
alias CCP1IF IS PIR1_CCP1IF

OPTION_REG = 0b00001111 ; PS asllocated to WDT and slowest rate selected (20ms*128)
PEIE = high
TMR1IE = high
T1CON=0b00110001        ; set timer 1 running internal osc 8xprescaled
CCP1CON=0b00001011      ; reset tmr1 on match, kicks off an A/D conversion
                        ; these next values seem to call a period of 10s from previous project
CCPR1H=244              ; decimal
CCPR1L=36               ; decimal
-- so count is 244*256+36 = 62500 At 4MHz each tick is 1uS, prescsaled by 8 to 8uS ticks.
--
const byte cyclesmax=60*2   -- how many times to count 0.5s ticks

var word mincounter = 0
;var byte lastTMR0=0
;TMR0=0   -- clear at start

-- ok, now setup serial;
const serial_hw_baudrate = 9600
const bit usart_hw_serial = TRUE
const bit lowspeed=true
include print
include serial_hardware
serial_hw_init()

const byte booted[] = "aVVSTARTED--"
const byte prefix[] = "aVV"
const byte line1[] = "+++"
const byte line2[] = "ATSM2r"
const byte line3[] = "ATACr"
const byte line4[] = "ATDNr"
const byte line5[] = "ATID0AFFr"
const byte vpfx[]= "VLT"

procedure WaitOnOk() IS
-- state machine - aim is to hang and
-- if this doesn't get to 2 the WDT will start things over after 2 sec
-- if it gets to 2 it clrwdt anbd returns

var byte state=0
asm clrwdt
while state !=2 loop
if serial_hw_data_available then
   if state==0 & serial_hw_data == "O" then
      state=1                        ; this is the only way to get to state 1
   end if
   if (state == 1) & (serial_hw_data == "K") then
      state=2
      else
      state=0                        ; because O was followed by something that wasn't K
   end if
end if
asm clrwdt
end loop

end procedure

procedure justify(word IN myval)  IS
-- value is going to be 0..1024 (in reality 0..512)
-- hundreds are integers, ie 2 d.p.
-- assumes radio is up and running
  format_word_dec(serial_hw_data,myval,3,2)  -- 0   .23
  serial_hw_data="-"
end procedure

function get_volts() return bit  is
  var word reading1
  var word reading2
  var word reading3
  var word reading4
  var word reading5
  asm clrwdt
  -- get ADC result, high resolution
  reading1 = adc_read_high_res(0)/2
  asm clrwdt
  -- get ADC result, high resolution
  reading2 = adc_read_high_res(1)/2
  asm clrwdt
  -- get ADC result, high resolution
  reading3 = adc_read_high_res(2)/2
  asm clrwdt
  -- get ADC result, high resolution
  reading4 = adc_read_high_res(3)/2
  asm clrwdt
  -- get ADC result, high resolution
  reading5 = adc_read_high_res(4)/2
  asm clrwdt
  -- send it back through serial
  xrf_sleep=low                                    -- start the radio a little before the data will be sent.
                                                   -- may want to change the LLAP prefix and comma del if not using radio
  delay_100ms(2)
  asm clrwdt
  print_string(serial_hw_data,prefix)
  print_string(serial_hw_data,vpfx)
  serial_hw_data="A"
  justify(reading1)
  asm clrwdt
  delay_100ms(5)                                   -- no need for this dela if going straight out on serial
  print_string(serial_hw_data,prefix)
  print_string(serial_hw_data,vpfx)
  serial_hw_data="B"
  justify(reading2)
  asm clrwdt
  delay_100ms(5)                                  -- allows the LLAP data to be received properly
  asm clrwdt
  print_string(serial_hw_data,prefix)
  print_string(serial_hw_data,vpfx)
  serial_hw_data="C"
  justify(reading3)
  asm clrwdt
  print_string(serial_hw_data,prefix)
  print_string(serial_hw_data,vpfx)
  serial_hw_data="D"
  justify(reading4)
  asm clrwdt
  delay_100ms(5)
  asm clrwdt
  print_string(serial_hw_data,prefix)
  print_string(serial_hw_data,vpfx)
  serial_hw_data="E"
  justify(reading5)
  asm clrwdt
  delay_100ms(2)                                         -- let the signal be transmitted and buffer empty
  xrf_sleep=high                                   -- stop the radio ftre the original data sent
  return true

end function

-- MAIN PROGRAM START

asm clrwdt
asm sleep                           ; use the WDT to delay about 2s to
                                    ; boot the sensor  and radio
asm clrwdt

-- now initialise the XRF to the system network ID and enable sleep mode
-- clear out any pre-existing RX buffer
var byte rxd
while serial_hw_data_available loop -- check if data is ready for us
  rxd = serial_hw_data          -- get the data
end loop
rxd=" "

if true then                        ; loop is simply to enable/disable
                                    ; if you have no radio then use if false then
asm clrwdt                          ; this setup routine for testing
xrf_sleep=low
                                    ; setup the radio using AT commands
print_string(serial_hw_data, line1) ; send +++
WaitOnOk
print_string(serial_hw_data, line5) ; change network with ATID0AFF
delay_100ms(1)
print_string(serial_hw_data, line2) ; enable sleep mode with ATSM2
delay_100ms(1)
print_string(serial_hw_data, line3) ; and use it - send commit
delay_100ms(1)
print_string(serial_hw_data, line4) ; ATDN we are done configuring
delay_100ms(10)
print_string(serial_hw_data,booted)
delay_100ms(2)
xrf_sleep=high                                    -- power down the radio
end if

forever loop

asm clrwdt
if CCP1IF then
   CCP1IF=false;
   mincounter=mincounter+1
end if

if mincounter >= cyclesmax-1 then
   mincounter=0
   var bit b = get_volts()
end if

end loop