An Arduino Battery Charger: How to save the planet one battery at a time!
Did you know that all those single use batteries (alkaline ones) that have a
label on them saying "Do Not Recharge" are in fact chargeable!
Of course some will not be and some will charge a little bit better than
others. But even getting on re-use out of a throw-away battery seems like a
better way to go.
The criteria for whether or not the
battery is re-chargeable or not is first of all give it a visual inspection. If
it has powdery residue on it then it is dead so re-cycle it in your local
battery re-cycling place.
The next thing to determine re-chargeability is whether or not the battery
voltage has fallen below 0.8V. If it is too low then you are unlikely to get
anywhere. You could measure this with a volt meter or just use the project
below that does this automatically for you (I set the level to 0.75 since the
voltage measurement is not absolutely accurate (Voltage regs usually have a
±10% regulation so you don't get exactly 5V out - also the reference voltage I
used is using two divider resistors so that's another 10%). It is not
absolutely critical for charging alkaline batteries.
Note: This alkaline battery charger project is not
suitable for re-charging NimH, NiCd or Li based batteries. Lithium
batteries (Li) require a voltage regulation of at least 1% and an absolute
exact cut off voltage, otherwise there is a risk of explosion. Just use the
project as it stands for alkaline batteries (standard single-use batteries of
nominal voltage 1.5V).
Warning: Some batteries will leak, so inspect them first
(preferably wearing gloves). if you see white powder on them dispose of them at
a battery re-cycling center.
Arduino Battery Charger
Schematic
Note: If you don't have a serial LCD just use the parallel
connections shown here and
adjust the code accordingly. You will only need to adjust the initialiser (for
pins) as the libraries are based on identical code and will therefore operate
using the same instructions e.g. for lcd.print etc.
Note: You can use any general purpose low volt PNP transistor
for this circuit.
Arduino Battery
Charger Circuit operation
First of all when CHRG and HCHRG are floating then R1 pulls the transistor
base high so no current flows = OFF.
Pulling CHRG low results in a constant current of 21.3mA and then pulling
HCRG low results in an increased charging current of 33mA.
Note: These are nominal values and will vary with the
tolerance of the resistors used and the ambient temperature. For these dry
cells the exact values are not important.
The basic operation is to start charging the battery at low current for a
period, and then charge at a high current for another period. If the battery
voltage increases, during high charge, then carry on charging, if not, then
charging is finished (as long as the battery is within the acceptable voltage
limits).
Arduino Battery
Charger charging current
When CHRG is pulled low R1, R2 and R3 are enabled forming a resistive
divider that sets the base voltage at
3.28V : 5*((1e3+560)/(1e3+560+820))
So the voltage across the base of the transistor, i.e. across R1, is:
1.722 : 5 - 3.26
The base emitter diode drop is 0.7V so that leaves 1V dropped across R4.
This therefore allows you to define the charging current i.e. for the 47R
resistor shown the charging current will be
21.3mA : 1.0/47
Arduino Battery
Charger high current Charging
When HCHRG is pulled low (regardless of CHRG) then resistor R3 is shorted
out to ground. So the voltage now dropped across (only) R2 is:
2.74V : 5*((1e3)/(1e3+820))
This the voltage across the base of the transistor, i.e. across R1, is:
2.25V : 5 - 2.74
Now the voltage drop across R1 is:
1.55 : 2.25 - 0.7
Which results in a charging current of:
33mA : 1.55/47
AREF External voltage
The voltage divider formed by R8 and R9 halves the voltage used for the ADC
reference so the maximum that the ADC can read is 2.5V which is plenty since we
only really need to measure up to about 1.6V. Having a lower AREF voltage also
means that each bit of the ADC has a lower value i.e. a finer resolution -
instead of 4.88mV it is 2.44mV per bit so the ADC can see smaller voltage
changes at the battery.
Displays
You can use only the LCD display or only the LEDs. You will get a green
(pass) or red (fail) light at the end of charging. The LCD displays more
information such as the current voltage level at the battery, the start voltage
of the battery and the elapsed time. It also shows the state variable (really
for debug) that lets you know where the code is at. In addition the serial port
outputs text messages showing you what happened. So the LCD is not really
essential for operation but is useful if you make a stand alone unit.
Serial Monitor
You can use the serial monitor (in the Arduino IDE) to observe operation as
serial data is also reported showing state machine operation, adc values and
voltages.
Software Library and versions
Arduino IDE Version
Version : 1.8.3
Library
The normal library used for the Hitachi HD44780 display is called
liquidCrystal and the process below will replace it with different code
so that you can create a serial driven LCD using an 74HC595 serial to parallel chip.
In the original library you need the following code to initialise an instantiation of the LiquidCrystal Class:
This is a 4 bit interface which is commonly used as the default
parallel interface (it is possible to use an 8 bit interface but does
not give much advantage). So in that interface you need a total of 6
pins to drive the chip.
When you use the SPI version of the LiquidCrystal interface then the initialiser is:
LiquidCrystal
lcd(SPI_SS_PIN);
This looks like it uses only one pin but in fact it uses the standard SPI pins:
MOSI
SCK
SPI_SS_PIN is simply the user defined Slave Select pin while the
others are set at fixed pins on the Arduino board. So by adding a
74HC595 to drive those original pins, seen in the first initialiser, you
can save three pins. Not only that, these SPI pins can be used to drive
other SPI devices (with a different SS signal).
TIP: You can use other SPI devices with a different SS control pin.
If you don't have an 74HC595 then you can use the same sketch as below but wire up the LCD as shown here
(4 bit parallel interface) - and don't change the library (below). The
SPI library uses the same functions as the normal library so the code
sketch does not change.
Note: Use the LiquidCyrstal SPI library if you are
using a serial SPI LCD.
Replacing the liquidCrystal Library
The liquidCrystal library is replaced with one that allows SPI operation:
You can more or less read these from left to right and see what the code is
doing.
IDLE
First of all in the IDLE state the system is set up to charge.
BATFND
Then in the BATFND state the voltage is checked (during low charge) to see
if a valid battery is present.
If out of range then the state is set to FAIL.
If in range then the state is set to CHARGE.
CHARGE
In the CHARGE state low charge current continues for 30 seconds. At the end
the ADC reading is stored in the variable lowChrgADC.
After 30 seconds the state is set to HIGH_CHARGE and the high current output
activated.
The state changes to HIGH_CHARGE after 30s.
Two other tests are made (all the time).
Battery reaches max voltage
1. The state is set to FINISHED.
Battery is out of range
2. The state is set to FAIL.
HIGH_CHARGE
After 30 seconds the state is set to TEST
The state changes to TEST after 30s.
Two other tests are made (all the time).
Battery reaches max voltage
1. The state is set to FINISHED.
Battery is out of range
2. The state is set to FAIL.
TEST
Battery Voltage
Increase for High Charge
This is where the decision is made either to terminate the process or
continue. First of all, if the current ADC value (at the end of high charge
compared to the ADC value at the end of the low current charge period) is not
high enough (<9.77mV) then the battery is considered to be charged and the
state is set to FINISHED.
The state of the battery (out of range) is not tested here since it has been
continuously tested in states CHARGE and HIGH_CHARGE.
Detecting Final Charge : test_avg()
Some batteries hover around a final value but change their voltage output
enough during High charge so that the algorithm can't stop. To detect this
state (over several charge and high-charge cycles) requires storing ADC values
so a rolling shift register containing the last 10 low charging ADC values is
used for detection.
In the function test_avg() the array prevADC[] is tested to see if the
values are all within 1 value of the average of those 10 readings. If they are,
then the state machine goes to the FIINISHED state i.e. this shows there has
been no 'real' change in battery voltage over the last few charging cycles.
After each test phase the array is shifted and the latest low charge ADC
value is inserted.
If neither of the above cases is true then the state machine goes back to
IDLE.
The next state is :
FINISHED (if battery voltage is high enough) or
FINISHED for no average battery change or
IDLE (if the others are not true).
FINISHED
The controls and LEDs are set: (green LED is lit to indicate success) and
charging is turned off.
The next state is WAIT_START.
FAIL
The controls and LEDs are set: (red LED is lit to indicate failure) and
charging is turned off.
The next state is WAIT_START.
WAIT_START
Here is the only state that allows restart detecting a button press on the
input pin. When pressed, various variables are initialised for a complete
restart.
When pressed the state is set to IDLE.
Arduino Battery Charger
Sketch
// AA and AAA charger
// Could use for others with different charge currents
// i.e. different emitter resistors.
//
// LiquidCrystal library used here is not the original
// download an enhanced version from
// https://playground.arduino.cc/Main/LiquidCrystal
// LiquidCrystal_1.zip allows SPI operation.
//
// V1.00
// Copyright John Main
// Free for non-commercial use.
//
#include<SPI.h>
#include<LiquidCrystal.h>// Enhanced for SPI operation.
#defineSTART_PIN12
#defineRLED_PINA4
#defineGLED_PINA5
#defineVBATT_PINA0
#defineCHRG_PIN2
#defineHIGH_PIN3
#defineSPI_SS_PIN10
#defineCHARGE_ONpinMode(CHRG_PIN,OUTPUT);digitalWrite(CHRG_PIN,LOW);
#defineCHARGE_OFFpinMode(CHRG_PIN,INPUT);
#defineHIGH_ONpinMode(HIGH_PIN,OUTPUT);digitalWrite(HIGH_PIN,LOW);
#defineHIGH_OFFpinMode(HIGH_PIN,INPUT);
#defineRLED_ONdigitalWrite(RLED_PIN,HIGH);
#defineRLED_OFFdigitalWrite(RLED_PIN,LOW);
#defineGLED_ONdigitalWrite(GLED_PIN,HIGH);
#defineGLED_OFFdigitalWrite(GLED_PIN,LOW);
#defineSPCSerial.print(' ');
#defineVBATMAX1.60// Maximum and minimum acceptable battery.
#defineVBATMIN0.75// voltage to allow charging to start.
#defineVREFANA2.50// Reference voltage (AREF). Measure this with DMM.
#defineCHRG_ms30000// Time period of normal charging time (ms).
#defineHIGH_ms30000// Time period of normal higher power charging time (ms).
#definePREV_ADC10// Number of nearly same ADC readings to declare done.
typedefenum{IDLE,BATFND,CHARGE,HIGH_CHARGE,TEST,
FINISHED,FAIL,WAIT_START}state_t;
enum{BATTFULL,BATTAVGSTABLE,BATTBAD,BATTNORISE};
// Initialize for SPI with sspin, also known as RCLK or LATCH.
// For the lcd other pins are the standard SPI pins.
LiquidCrystallcd(SPI_SS_PIN);
///////////////////////////////////////////////////
voidsetup(){
pinMode(VBATT_PIN,INPUT);
pinMode(START_PIN,INPUT_PULLUP);
pinMode(CHRG_PIN,OUTPUT);
pinMode(HIGH_PIN,OUTPUT);
pinMode(GLED_PIN,OUTPUT);
pinMode(RLED_PIN,OUTPUT);
CHARGE_OFF
HIGH_OFF
RLED_OFF
GLED_OFF
analogReference(EXTERNAL);
// set up the LCD's number of columns and rows:
lcd.begin(16,2);
Serial.begin(57600);
Serial.println("Battery Charger ");
showStartVolts();
}
///////////////////////////////////////////////////
// Print start volts to LCD & serial(for reference).
voidshowStartVolts(void){
floatv=getVana(analogRead(VBATT_PIN));
lcd.setCursor(11,0);lcd.print(v,3);
Serial.print("\nStart volts: ");
Serial.println(v,3);
}
///////////////////////////////////////////////////
voidreason(uint8_tc){
Serial.print("End reason: ");
switch(c){
caseBATTFULL:Serial.println("Max volts.");break;
caseBATTAVGSTABLE:Serial.println("No avg change.");break;
caseBATTBAD:Serial.println("Out of range.");
caseBATTNORISE:Serial.println("No Hchrg rise.");
}
}
///////////////////////////////////////////////////
voidloop(){
staticuint32_tloop_time=0;
if(millis()-loop_time>250){// No need execute too fast
action();
loop_time=millis();}
}
///////////////////////////////////////////////////
floatgetVana(uint16_tadc){returnadc*VREFANA/1024;
}
///////////////////////////////////////////////////
uint8_tcheckBattBad(floatVana){if(Vana<VBATMIN||Vana>VBATMAX)return1;
return0;
}
///////////////////////////////////////////////////
uint8_tcheckBattFinished(floatVana){if(Vana>=VBATMAX-0.1)return1;
return0;
}
///////////////////////////////////////////////////
voidshowSerialElapsedTime(uint32_tr_time){Serial.print("Time:");
Serial.print(r_time/1000/60);Serial.println(" mins.");}
///////////////////////////////////////////////////
voidshowState(state_tstate){
// This state has no text, to leave FINISHED or FAIL on LCD.
if(state==WAIT_START)return;// Show current state
lcd.setCursor(0,0);
lcd.print(" ");// 11 chars clear debug area.
lcd.setCursor(0,0);switch(state){
caseIDLE:lcd.print("IDLE");break;
caseBATFND:lcd.print("BATFND");break;
caseCHARGE:lcd.print("CHRG");break;
caseHIGH_CHARGE:lcd.print("H-CHRG");break;
caseTEST:lcd.print("TEST");break;
caseFINISHED:lcd.print("FINSHD");break;
caseFAIL:lcd.print("FAIL");break;
// case WAIT_START : lcd.print("WAIT");break;
default:break;
}
}
///////////////////////////////////////////////////
voidshowTime(uint32_ttimeVal_ms){// could use sprinf - this = less memory.
uint8_tlcdpos;
uint16_ttmin,tsec;
uint32_tt;
t=timeVal_ms;
tmin=(t/1000/60);// Could use sprintf but smaller code using:
if(tmin>=1){// Print minutes since reset
lcdpos=12;
if(tmin>99)lcdpos=10;
elseif(tmin>9)lcdpos=11;lcd.setCursor(lcdpos,1);lcd.print(tmin);
lcd.print(':');
}
// Print seconds since reset.
tsec=(t/1000)%60;
lcd.setCursor(14,1);
if(tsec<10)lcd.print('0');lcd.print(tsec);
}
///////////////////////////////////////////////////
// -1 resets the store
//
uint8_ttest_avg(uint16_tadc){
uint8_ti,match,allmatch;
staticuint16_tprevADC[PREV_ADC];
uint16_tavg;
if(adc==-1){
for(i=0;i<PREV_ADC;i++)prevADC[i]=0;
return;
}
// Get average value
avg=0;
Serial.print("AVG:");for(i=0;i<PREV_ADC;i++){
Serial.print(prevADC[i]);SPC;
avg+=prevADC[i];}
avg+=5;// =0.5 after division by 10
avg/=10;
Serial.print("Avg ");Serial.println(avg);
// If any are zero then not filled: set avg zero to stop err. match
for(i=0;i<PREV_ADC;i++){
if(prevADC[i]==0){avg=0;
Serial.println("Avg zet ZERO");
break;
}
}
// Check if has not increased over last n readings - if so exit.
allmatch=1;for(i=0;i<PREV_ADC;i++){
match=0;
// Detect close matches: lowChrgADC+/-2 (+/-4.88mV)
if(avg-1<=prevADC[i]&&prevADC[i]<=avg+1)match=1;
// Here if the value is one of 3: lowChrgADC+/-1, then match is high
if(!match){allmatch=0;break;}
}
// Update rolling store. // Array indices 0 ~ (n-1) shift.
for(i=0;i<PREV_ADC-1;i++)prevADC[i]=prevADC[i+1];prevADC[PREV_ADC-1]=adc;
// allmatch is 1 if all have matched for all elements.
if(allmatch&&avg!=0){// Zero is a special case.
reason(BATTAVGSTABLE);
return1;// All the same so indicate finished.
}
return0;
}
///////////////////////////////////////////////////
voidaction(void){
staticuint32_ts_time=millis();
staticuint32_tr_time=s_time;// Relative, start times.
staticuint32_ttimewas=0,timewas_state=r_time;
staticstate_tstate=IDLE;staticuint16_tlowChrgADC=0;
staticuint8_ttimer_on=1;
uint16_tadc=0;
floatVana;
staticuint8_tdone_once=0;
if(!done_once){
test_avg(-1);// Reset store.
done_once=1;
}
r_time=millis()-s_time;// Use time relative to start
if(timer_on)showTime(r_time);// Time since start: to LCD.
adc=analogRead(VBATT_PIN);
lcd.setCursor(0,1);
lcd.print("Vb:");
lcd.print(Vana=getVana(adc),3);
switch(state){
caseIDLE:state=BATFND;
Serial.println("IDLE>BATFND");CHARGE_ON// Start charge to detect battery.
HIGH_OFF
RLED_OFF
GLED_OFF
showSerialElapsedTime(r_time);
break;
caseBATFND:// Battery ok?
Serial.println("BATFND>CHARGE");
if(checkBattBad(Vana)){
state=FAIL;// Battery is dead or not present.
Serial.println("FAIL in BATFND");}elseif(Vana>=VBATMIN&&Vana<=VBATMAX){
state=CHARGE;
}break;
caseCHARGE:if(checkBattBad(Vana)){reason(BATTBAD);state=FAIL;}if(checkBattFinished(Vana)){reason(BATTFULL);state=FINISHED;}
elseif(r_time-timewas_state>CHRG_ms){Serial.println("CHARGE>HCHARGE");lowChrgADC=adc;// Store value just before high charge time.
HIGH_ON
state=HIGH_CHARGE;timewas_state=r_time;}break;
caseHIGH_CHARGE:if(checkBattBad(Vana)){reason(BATTBAD);state=FAIL;}if(checkBattFinished(Vana)){reason(BATTFULL);state=FINISHED;}elseif(r_time-timewas_state>HIGH_ms){
Serial.println("HCHARGE>TEST");state=TEST;
timewas_state=r_time;}
break;caseTEST:Serial.println("TEST>IDLE/FINISHED");
Serial.println("Cur adc,lowChrgADC");
Serial.print(adc);SPC;Serial.println(getVana(adc),3);Serial.print(lowChrgADC);SPC;Serial.println(getVana(lowChrgADC),3);if(adc>(lowChrgADC+4)){// Gone up by > (2.5/1024)*4=9.77mV
state=IDLE;}else{// No rise so batery charge maxed out.
reason(BATTNORISE);state=FINISHED;
}if(test_avg(lowChrgADC))state=FINISHED;
break;caseFAIL:// Error condition so stop charging battery.
Serial.println("FAIL");CHARGE_OFF
HIGH_OFF
timer_on=0;
RLED_ON
GLED_OFF
state=WAIT_START;
break;
caseFINISHED:Serial.print("FINISHED:");Serial.println(Vana);showSerialElapsedTime(r_time);
CHARGE_OFF
HIGH_OFF
RLED_OFFGLED_ONtimer_on=0;
state=WAIT_START;
break;
caseWAIT_START:
if(digitalRead(START_PIN)==0){
state=IDLE;
s_time=millis();
r_time=millis()-s_time;
timewas_state=r_time;
timer_on=1;
test_avg(-1);// Reset store.
lcd.clear();
showStartVolts();}
break;}
showState(state);
// Show raw adc value
lcd.setCursor(7,0);lcd.print(adc);lcd.print(' ');
}
The definition VREFANA is
nominally 2.5V but I measured it using a Digital Multimeter and placed
the value into the code, to give a more accurate reading. If you don't
want to do that just leave the value as 2.5.
Unlock the secrets of Arduino scrolling displays! This beginner-friendly guide shows you how to create real-time, dynamic graphics using an SSD1306 OLED, perfect for tracking sensor data and building…
How to get accurate DHT22 digital humidity sensor readings with an Arduino. Did you know it also measures temperature as Well? Find out why, in this page...
A PIR sensor lets your Arduino sense movement without contact. This tutorial covers PIR sensor basics, connecting one to an Arduino board and coding a motion detector.
Arduino Hall Effect Sensor: Add magnetic sensing superpowers to your Arduino projects with an easy-to-use hall effect sensor. With full code and layout...
Comments
Have your say about what you just read! Leave me a comment in the box below.
Don’t see the comments box? Log in to your Facebook account, give Facebook consent, then return to this page and refresh it.