Lithium Battery Load Tester

 

October 2014

Components

ATmega328 with Arduino bootloader Hobbytronics, eBay suppliers
FT232RL Sparkfun breakout board Hobbytronics
20 x 4 LCD with HD44780 ControllereBay
Adafruit INA219 Current sensor oomlout
IRL2203N MOSFETeBay
Enclosure 200 x 150 x 80 EnclosureeBay (discount-devices-electronics-shop)
Mains Transformer. Secondary 6-0-6V. 6VA (3VA per winding)Various suppliers

 

Downloads

Windows Program: BatteryTester.zip.

Circuit Wizard PCB Layout: battery-tester.cwz

PCB Layout PDF: battery-tester.pdf

Arduino Sketch: battery-tester-sketch.zip

 

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 end_pin = 11;                            // Output pin for end of test LED
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)
unsigned long millis_PC_wait;                 // Timer for samples to PC
unsigned long millisCalc_mAh;                 // Timer for nexr mAh calc. and LCD write.

float hours;                                  // Working variables for time and mAh
float last_hours = 0.0;
int mins, secs, tMins;
float mAh_soFar = 0.0;

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;

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);                    // MAN/RUN switch. Grounded in MAN position.
  digitalWrite(manual_pin, HIGH);                // Enable internal pullup resistor.
  
  pinMode(end_pin, OUTPUT);                      // Output to LD to signal end of test.
  digitalWrite(end_pin, LOW);
  
  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(10, 1);
     lcd.print("      ");
     lcd.setCursor(10, 1);
     lcd.print(target_mA); 
     time_limit = 480;                               // 8 hours. Test relies on the..
     cutoff_voltage = 3.0;                           // 3.0 volts cutoff voltage.
     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 and light the
    digitalWrite(end_pin, HIGH);                             // End LED.
    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() > millisCalc_mAh + 1000) {
    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 (millis() > millis_PC_wait + sampleTime) {               // If the Sample-to-PC time has
      write_to_pc();                                          // elapsed, send data to the PC
      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((int)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() {
  
  Serial.print("STARTC");  Serial.print(current_mA); Serial.println(" mAENDC");                                    

  
  /* 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.  */
  Serial.print("STARTT"); Serial.print((int) hours); Serial.print(":");
  if (mins < 10) Serial.print("0"); Serial.print(mins); Serial.print(":");
  if (secs < 10) Serial.print("0"); Serial.print(secs); Serial.println("ENDT");
  
  /* Send mAh used so far, this sample's current and voltage to PC */
  Serial.print("STARTMAH"); Serial.print(mAh_soFar); Serial.println("ENDMAH");
  Serial.print("GRAPHCS"); Serial.print(current_mA); Serial.println("GRAPHCEND");
  Serial.print("GRAPHVS");  Serial.print(loadvoltage); Serial.println("GRAPHVEND");
  
  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() {         
  char buffer[20];
  unsigned long nTime;
  unsigned long milli =  millis() - startMillisec;

  nTime = milli / 1000;
  tMins = nTime / 60;
  nTime = nTime % (24*3600);
  hours = nTime / 3600;
  nTime = nTime % 3600;
  mins  = nTime / 60;
  secs  = nTime % 60;
}




 

Back to Index | Page 1 | Page 2 | Page 3

 


This site and its contents are © Copyright 2005 - All Rights Reserved.