| Lithium Battery Load Tester | |||||||||||||||
|
October 2014
Components
DownloadsWindows Program: BatteryTester v2.2c.Circuit Wizard PCB Layout: battery-tester.cwz PCB Layout PDF: battery-tester.pdf
Arduino Sketch
/* Lithium Battery Tester - works with hardware shown at vwlowen.co.uk/arduino/battery-tester/batery-tester.htm */
#include <TimerOne.h> //https://github.com/PaulStoffregen/TimerOne
#include <Wire.h>
#include <Adafruit_INA219.h> //https://github.com/adafruit/Adafruit_INA219
#include <LiquidCrystal.h>
int end_sounder = A2; // Digital output for sounder
boolean sounded = false;
int manual_volts_pin = 11; // Push button to cycle through battery cutoff volts when in manual.
int manual_pin = 12; // Manual/Run switch. Grounded in Manual.
int manual_value = A3; // Set target discharge current with potentiometer
int lcdRS = 2; // Define output pins for LCD
int lcdE = 4;
int lcdD4 = 5;
int lcdD5 = 6;
int lcdD6 = 7;
int lcdD7 = 8;
LiquidCrystal lcd(lcdRS, lcdE, lcdD4, lcdD5, lcdD6, lcdD7);
byte bell[8] = {0, 4, 14, 14, 14, 31, 4, 0}; // Data for 'bell' character for LCD.
const byte pwm_pin = 9; // PWM DAC, only pins 9 and 10 are allowed
const byte period = 128; // for 10 bit DAC
unsigned long startMillisec; // Variables for discharge timer.
unsigned long sampleTime = 10000; // Default samples to PC time (ms)
int pc_SampleCount = 0;
unsigned long pc_SampleTime = 5000;
unsigned long millis_PC_wait; // Timer for samples to PC
unsigned long millisCalc_mAh; // Timer for nexr mAh calc. and LCD write.
float last_hours = 0.0; // Working variables for time and mAh
float mAh_soFar = 0.0;
int days, hours;
int mins, secs;
int tMins;
Adafruit_INA219 ina219; // Create instance for INA219 sensor library
int current_mA = 0; // Variables for INA219 readings
float busvoltage = 0.0;
float shuntvoltage = 0.0;
float loadvoltage = 0.0;
float battery_volts = 0.0;
float vR; // Volt drop in circuit
int target_mA = 0; // Default 'set point' variables
float cutoff_voltage = 3.0;
int time_limit = 180;
float offset = 0.0;
float error;
char* batt[6] = {"Li-ion 3.7v", "Li-ion 7.2v", "Ni-MH", "Ni-Cd", "Lead 6v", "Lead 12v"};
float voltage[6] = {3.0, 5.8, 0.9, 1.1, 5.8, 11.6};
int battCount = 0;
int cancel = 0; // Variables and flags to terminate test
boolean timed_out = false;
boolean high_current = false;
boolean cutoff_voltage_reached = false;
boolean manual = false;
int pwm = 0; // Starting PWM value for 10-bit DAC
float kP = 30; // Simple 'Proportional' control term
int tolerance = 1; // Deadband to stop 'hunting' around target value
int beep = 1; // Sounder on/off (0 = off, 1 = on)
/* === Set up - normal setup parameters and initialises values for the contol loop ===== */
void setup() {
lcd.createChar(1, bell); // Bell char for LCD. Displayed when sounder is enabled.
pinMode(manual_pin, INPUT_PULLUP); // MAN/RUN switch. Grounded in MAN position.
pinMode(manual_volts_pin, INPUT_PULLUP);
pinMode(end_sounder, OUTPUT); // Output to sounder.
digitalWrite(end_sounder, LOW);
pinMode(pwm_pin, OUTPUT); // Set up 10-bit DAC pin as output
Timer1.initialize(period); //
Timer1.pwm(pwm_pin, pwm); // and set zero PWM out
ina219.begin(); // Initialise INA219 Current Sensor
Serial.begin(9600); // Initialise Arduino to PC Com Port
lcd.begin(20, 4); // Initialize the LCD.
lcd.clear();
delay(100);
lcd.setCursor(0, 1);
lcd.print("Initialising..."); // Print something on the display to show
delay(1000); // it's working.
lcd.clear(); // Clear display
delay(100);
while (digitalRead(manual_pin) == LOW) { // Loop here to adjust desired load mA manually
lcd.setCursor(0, 1);
lcd.print("Set mA: ");
manual = true;
target_mA = map(analogRead(manual_value), 0, 1023, 0, 1500);
lcd.setCursor(9, 1);
lcd.print(" ");
lcd.setCursor(9, 1);
lcd.print(target_mA);
if (digitalRead(manual_volts_pin) == LOW) {
battCount++;
if (battCount > 5) battCount = 0;
delay(250);
}
lcd.setCursor(0, 2);
lcd.print("Battery: ");
lcd.setCursor(9, 2);
lcd.print(" ");
lcd.setCursor(9, 2);
lcd.print(batt[battCount]);
time_limit = 480; // 8 hours. Test relies on the..
cutoff_voltage = voltage[battCount]; // Cutoff voltage is determined by battery type.
kP = 30; // Default values are used in Manual Mode.
offset = 0;
tolerance = 1;
beep = 1;
cancel = 0;
delay(200);
}
if (!manual) { // If not manual, must be under PC control.
lcd.setCursor(0, 1);
lcd.print("Waiting for settings"); // Prompt that we're waiting to receive
lcd.setCursor(6, 2); // the load settings from the application
lcd.print("from PC..."); // that's running on the PC.
while (Serial.available() == 0 ) ; // Wait for settings params from PC
if (Serial.available() > 0) { // Data available on serial port from PC
target_mA = Serial.parseInt(); // so read each one in turn into variables.
cutoff_voltage = Serial.parseFloat(); // Minimum battery voltage to end test
time_limit = Serial.parseInt(); // maximum time allowed for the test
sampleTime = Serial.parseInt() * 1000; // Interval to send data to PC
kP = Serial.parseInt(); // kP - control loop Proportional value
offset = Serial.parseFloat(); // To reduce offset between target and actual mA
tolerance = Serial.parseInt();
beep = Serial.parseInt(); // Sounder on/off (0=off, 1=on)
cancel = Serial.parseInt(); // Will = 0. Clear Cancel flag
}
Serial.flush(); // Make sure the serial port is empty to avoid
// false 'Cancel' messages in the control loop.
}
/* These lines echo the received values to the LCD and display for 5 seconds */
lcd.clear();
delay(200);
lcd.print("Target mA "); lcd.print(target_mA);
lcd.setCursor(0, 1);
lcd.print("Cutoff v "); lcd.print(cutoff_voltage);
if (beep == 1) {
lcd.setCursor(19, 0);
lcd.write(1);
}
lcd.setCursor(0, 2);
lcd.print("Time (min) "); lcd.print(time_limit);
lcd.setCursor(0, 3); lcd.print("S="); lcd.print(sampleTime/1000);
lcd.print(" t="); lcd.print(tolerance); lcd.print(" kP="); lcd.print((int)kP);
lcd.print(" i="); lcd.print(offset, 1);
delay(8000);
lcd.clear();
startMillisec = millis(); // get millisec time at start
if (beep == 1) {
digitalWrite(end_sounder, HIGH); // Bleep once to indicate test starting.
delay(50);
digitalWrite(end_sounder, LOW);
}
}
/* =========== Get Current and Voltage from Adafruin INA219 breakout board ============ */
void readINA219() { // Function to read curent and voltage from INA219.
float R = 0.12; // "Tweaking" value to compensate for circuit resistance.
float temp_mA = 0.0;
float temp_V = 0.0;
shuntvoltage = ina219.getShuntVoltage_mV(); // Get values for the INA219 sensor.
for (int i = 0; i< 10; i++) {
temp_V = temp_V + ina219.getBusVoltage_V(); // Take average of 10 readings for
delayMicroseconds(600); // Bus Voltage.......
}
busvoltage = temp_V / 10;
for (int i = 0; i< 20; i++) {
temp_mA = temp_mA + ina219.getCurrent_mA(); //...... and 20 reading for current.
delayMicroseconds(600);
}
current_mA = temp_mA / 20;
vR = R * current_mA / 1000; // Total load voltage has to factor in the
loadvoltage = busvoltage + (shuntvoltage/1000) + vR; // volt drop across the 0.1R shunt & circuit resistance.
}
/* ========== Main Loop ============================================================= */
void loop() {
readINA219(); // Get current and voltage values.
/* Calculate error between target_mA and actual mA and apply to PWM value */
error = abs(target_mA - current_mA);
error = (error / target_mA) * 100;
if ((error > tolerance) ) { // If out of tolerance (deadband to stop 'hunting')
error = error - offset; // Bias (long term error compensation)
error = (error * kP) / 100; // 'proportional' factor reduces impact of 'raw' error.
error = constrain(error, 0.0, 50.0); // 25
if (current_mA > target_mA) error = -error; // Determine if it's a pos or neg error.
pwm = abs(pwm + round(error));
pwm = constrain(pwm, 0, 1023);
}
if (current_mA > (target_mA * 2.0) ) { // Check if measured current has
pwm = 0; // overshot the target value by more than
Timer1.pwm(pwm_pin, pwm); // 100% and abort if it has.
high_current = true;
target_mA = 0;
lcd.setCursor(3, 1);
lcd.print("ERROR - Hi mA");
Serial.print("MSGSTError - High mAMSGEND");
}
if (loadvoltage < cutoff_voltage) { // Monitor battery voltage and finish
delay(3000); // the load test when it reaches the
readINA219(); // target value. Allow 3 seconds for
if (loadvoltage < cutoff_voltage) { // short dip in battery voltage when
pwm = 0; // load comes on.
Timer1.pwm(pwm_pin, pwm);
cutoff_voltage_reached = true;
target_mA = 0;
lcd.setCursor(5, 1);
lcd.print("FINISHED");
Serial.print("MSGSTTest FinishedMSGEND");
}
}
if ((!cutoff_voltage_reached) && (tMins > time_limit)) { // A time limit can also be set to
pwm = 0; // abort the test if it appears to
Timer1.pwm(pwm_pin, pwm); // be taking too long.
timed_out = true;
target_mA = 0;
lcd.setCursor(2, 1);
lcd.print("TIMED OUT");
Serial.print("MSGSTTime ExceededMSGEND");
}
if (Serial.available() > 0) { // Data available on serial port from PC
cancel = Serial.parseInt(); // 999 will calcel the test. 0 will clear Cancel flag
if (cancel == 999) { // 999 from the PC means 'Cancel' the test.
pwm = 0;
Timer1.pwm(pwm_pin, pwm);
target_mA = 0;
lcd.setCursor(3, 1);
lcd.print("CANCELLED");
Serial.print("MSGSTUser cancelledMSGEND");
}
}
Serial.flush();
/* If all is ok, outout the PWM value to adjust the current. */
if ((cancel == 0) && (!timed_out) && (!high_current) && (!cutoff_voltage_reached)) {
Timer1.pwm(pwm_pin, pwm); // Adjust PWM to calculated value.
}
else { // otherwise sound the beep
if ((!sounded) && (beep == 1)) {
sounded = true;
for (int i = 0; i< 10; i++) {
digitalWrite(end_sounder, HIGH);
delay(50);
digitalWrite(end_sounder, LOW);
delay(50);
}
}
}
/* Calculate the elapsed time and the mA / hr used each second round the loop. Send to LCD and PC */
getTime();
if ((millis() - 1000) >= millisCalc_mAh) {
float this_hours = (millis() - startMillisec) / (1000.0 * 3600.0); // Calculate mA used in this time sample
mAh_soFar = mAh_soFar + ((this_hours - last_hours) * current_mA); //
last_hours = this_hours;
write_to_lcd(); // Write data to LCD
millisCalc_mAh = millis();
}
if (pc_SampleCount < 100) {
pc_SampleTime = 5000;
} else
pc_SampleTime = sampleTime;
if (millis() > millis_PC_wait + pc_SampleTime) { // If the Sample-to-PC time has
write_to_pc(); // elapsed, send data to the PC
pc_SampleCount++;
millis_PC_wait = millis(); // and reset the elapsed time
} // counter.
}
/* ============== Write values to LCD ========================================== */
void write_to_lcd() {
lcd.setCursor(0, 0);
if (current_mA < 1000) lcd.print(" "); if (current_mA < 100) lcd.print(" ");
if (current_mA < 10) lcd.print(" ");
lcd.print(current_mA); lcd.print(" mA");
lcd.setCursor(9, 0); //
lcd.print(loadvoltage); lcd.print(" v ");
if (beep == 1) {
lcd.setCursor(19, 0);
lcd.write(1);
}
if ((cancel == 0) && (!timed_out) && (!high_current) && (!cutoff_voltage_reached)) {
lcd.setCursor(0, 2);
lcd.print("Time: ");
lcd.print(days); lcd.print(":"); if (hours < 10) lcd.print("0"); lcd.print(hours); lcd.print(":");
if (mins < 10) lcd.print("0"); lcd.print(mins); lcd.print(":");
if (secs < 10) lcd.print("0"); lcd.print(secs);
lcd.setCursor(0, 3);
lcd.print("Used: "); lcd.print(mAh_soFar); lcd.print(" mAh");
}
}
/* =================== Send values to PC =================================== */
void write_to_pc() {
/* Send time elapsed to PC. Formatting is done this end as it's easier than
having to parse/unparse the string twice at the other end. */
String message = "GRAPHVS";
message += days;
message += ":";
if (hours < 10) message += "0"; message += hours; message += ":";
if (mins < 10) message += "0"; message += mins; message += ":";
if (secs < 10) message += "0"; message += secs; message += "!";
message += mAh_soFar; message += "!";
message += (int) current_mA; message += "!";
message += loadvoltage; message += "GRAPHVEND";
Serial.print(message);
Serial.flush(); // Flush serial port before it returns to the main loop.
}
/* === Generic routine for hours, minutes and seconds between two millis() values.===*/
void getTime() {
long day = 86400000; // 86400000 milliseconds in a day
long hour = 3600000; // 3600000 milliseconds in an hour
long minute = 60000; // 60000 milliseconds in a minute
long second = 1000; // 1000 milliseconds in a second
unsigned long timeNow = millis() - startMillisec;
tMins = timeNow / minute;
days = timeNow / day ;
hours = (timeNow % day) / hour;
mins = ((timeNow % day) % hour) / minute ;
secs = (((timeNow % day) % hour) % minute) / second;
}
| |||||||||||||||