/*###########################################
SMART CHARGER MULTI BATTERY TESTER
Version 0.2 Beta
Project: Constantin PowerWall
by 81ROI, ROMANIA
Slave UNIT - This software will automatic charge, discharge and mesure capacity of 16 cells 18650 Li-Ion Battery
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY!
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Grbl. If not, see <http://www.gnu.org/licenses/>.
###########################################*/
/*###########pinout#########################
Arduino MEGA
Charge MOSFET D22-D37
Discharge Mosfet D2-D17
Complet Charge LED D38-D53
MUX1 is used to read Vbat and Vres for 1-8 cells is connected C0 = Vbat1 C1 = Vres1 , C2 = Vbat2 C3 = Vres2, ... , C14 = Vbat8 C15 = Vres8
MUX1 S0-S3 A0-A3
MUX1 Enable A4
MUX1 Sig A14
MUX1 is used to read Vbat and Vres for 9-16 cells is connected C0 = Vbat9 C1 = Vres9 , C2 = Vbat10 C3 = Vres10, ... , C14 = Vbat16 C15 = Vres16
MUX2 S0-S3 A5-A8
MUX2 Enable A9
MUX2 Sig A15
SDA SCL
RELAY1 D20 21
RELAY2 D20 21
Serial Output:
<Battery]Status|Capacity|Voltage>
<Battery]Status|Capacity|Voltage|chargeTime|dischargeTime|rechargeTime>
Times are in minutes
##########################################*/
#include <Wire.h>
#define STATUS_EMPTY 0
#define STATUS_READY 1
#define STATUS_CHARGE 2
#define STATUS_HIGH 3
#define STATUS_DISCHARGE 4
#define STATUS_LOW 5
#define STATUS_RECHARGE 6
#define STATUS_IDLE 7
#define STATUS_WAIT 8
#define CHARGE_ON 0 //mosfet control signal nedit to open 0=GND 1=5V
#define CHARGE_OFF 1
#define DISCHARGE_ON 1
#define DISCHARGE_OFF 0
#define RELAY_P0 1
#define RELAY_P1 0
#define MOSFET_CHARGE_PIN 22 //pin of the first charge MOSFET
#define MOSFET_DISCHARGE_PIN 2 //pin of the first discharge MOSFET
#define CHARGING_COMPLETE_PIN 38 //pin of the first charging complete LED
#define BATTERY8_VOLTAGE_PIN A4 //pin of the first battery voltage sensor
#define BATTERY16_VOLTAGE_PIN A9 //pin of the first resistor voltage sensor
#define MUX1_S0 A0 //mux1 s0 pin
#define MUX2_S0 A5 //mux2 s0 pin
#define MUX1_EN A14 //mux1 enable pin
#define MUX2_EN A15 //mux2 enable pin
#define RES_VALUE 10 //value of resistor in ohm
#define BATTERY_NUMBER 16 //number of charge/discharge battery
#define BATTERY_ROWS 2
bool mosfetChargeMode[BATTERY_NUMBER]; //store the mosfet working status 0 - off 1 - on
bool mosfetDischargeMode[BATTERY_NUMBER]; //store the mosfet working status 0 - off 1 - on
bool relayMode[BATTERY_NUMBER]; //store relay pozition 0 or 1
uint8_t chargerStatus[BATTERY_NUMBER]; //store status of the slots
long lastRead[BATTERY_NUMBER]; //store the last time when the voltage sensor was readed
bool autoMode[BATTERY_NUMBER]; //used for charger auto start CYCLE: Charge - Discharge - Recharge
uint8_t batteryStatus[BATTERY_NUMBER*BATTERY_ROWS];
bool batteryComplete[BATTERY_NUMBER*BATTERY_ROWS];
float batteryVoltage[BATTERY_NUMBER*BATTERY_ROWS];
float batteryCapacity[BATTERY_NUMBER*BATTERY_ROWS]; //store the batterys capacity
long chargeTimeStart[BATTERY_NUMBER*BATTERY_ROWS]; //time of charging
long chargeTimeStop[BATTERY_NUMBER*BATTERY_ROWS]; //time of charging
long dischargeTimeStart[BATTERY_NUMBER*BATTERY_ROWS]; //time to discharge
long dischargeTimeStop[BATTERY_NUMBER*BATTERY_ROWS]; //time to discharge
long rechargeTimeStart[BATTERY_NUMBER*BATTERY_ROWS]; //time to reacharge - this is the full recharge from 2.7 to full
long rechargeTimeStop[BATTERY_NUMBER*BATTERY_ROWS]; //time to reacharge - this is the full recharge from 2.7 to full
float batteryHigh=4.2; //battery high when is charged
float batteryLow=2.7; //battery cut of discharge
float Vcc=5; //voltage of the 5v pin from arduino tested with multimeter
uint8_t readSample=100; //number of samples to read from an analog pin
#define RAPORT_TIME 10000 //raport once at 30 sec
long lastTimeRaport=0; //time of last raport;
bool autoRaport=true; //autoraport
bool Serial1Raport=true;
#define STATUS_TIME 5000 //check status one at 3 sec
long lastTimeStatus;
#define SERIAL_BUFFER 63 //max data to send
bool string1Available=false; //used to see when we have an command on serial
String string1Rx=""; //serial data
String string1Tx=""; //serial1 string need it to send
uint8_t serial1Sendit=0; //serial1 sendit
bool serial1Confirmed=true; //serial1 confirmed
bool serial1Connected=true; //if serial 1 is connected to controller
#define RELAY_1 0x20 //address of first i2c relay
#define RELAY_2 0x39 //address of second 12c relay
void setup() {
Serial.begin(9600); //used for usb serial
Serial1.begin(9600); //used for inter module communication
Wire.begin();
// put your setup code here, to run once:
// Set pin INPUT / OUTPUT
for (uint8_t i=0;i<BATTERY_NUMBER;i++){
pinMode(MOSFET_CHARGE_PIN+i,OUTPUT);
digitalWrite(MOSFET_CHARGE_PIN+i,CHARGE_OFF);
mosfetChargeMode[i]=false;
pinMode(MOSFET_DISCHARGE_PIN+i,OUTPUT);
digitalWrite(MOSFET_DISCHARGE_PIN+i,DISCHARGE_OFF);
mosfetDischargeMode[i]=false;
pinMode(CHARGING_COMPLETE_PIN+i,INPUT);
digitalWrite(CHARGING_COMPLETE_PIN+i,HIGH);
}
//set analog inputs
for(uint8_t i=0;i<4;i++){
pinMode(MUX1_S0+i,OUTPUT);
digitalWrite(MUX1_S0+i,LOW);
pinMode(MUX2_S0+i,OUTPUT);
digitalWrite(MUX2_S0+i,LOW);
}
//disable mux
pinMode(MUX1_EN,OUTPUT);
pinMode(MUX2_EN,OUTPUT);
digitalWrite(MUX1_EN,HIGH);
digitalWrite(MUX2_EN,HIGH);
//all relay in position 0
IOexpanderWrite(RELAY_1,255 );
IOexpanderWrite(RELAY_2,255 );
//init charge
initCharger();
}
void loop() {
// put your main code here, to run repeatedly:
checkStatus(); //check status of charges
raport(); //send raport if is needit
serial1Event(); //check data from buffer
Serial1Tx(); //senddata from serial buffer
}
void IOexpanderWrite(byte address, byte data )
{
Wire.beginTransmission(address);
Wire.write(data);
Wire.endTransmission();
delay(10);
}
void setRelay(uint8_t n, bool mode){
uint8_t relayAddress;
uint8_t r;
int data=0;
if(mode)
relayMode[n]=RELAY_P1;
else
relayMode[n]=RELAY_P0;
if(n<8){
relayAddress=RELAY_1;
r=0;
}
else{
relayAddress=RELAY_2;
r=8;
}
for(uint8_t i=0;i<8;i++){
if(relayMode[r+i]){
data+=0.5+pow(2,i);
}
}
IOexpanderWrite(relayAddress,data);
delay(10);
}
void initCharger(){
for(uint8_t i=0;i<BATTERY_NUMBER;i++){ //set all charger sokets as empty for be ready to take new battery.
chargerStatus[i]=STATUS_EMPTY;
autoMode[i]=true;
relayMode[i]=RELAY_P0;
}
Serial.println("Multi Charger Ready!");
}
void setMux(uint8_t mux,uint8_t channel){
int controlPin[] = {mux, mux+1, mux+2, mux+3};
int muxChannel[16][4]={ {0,0,0,0}, //channel 0
{1,0,0,0}, //channel 1
{0,1,0,0}, //channel 2
{1,1,0,0}, //channel 3
{0,0,1,0}, //channel 4
{1,0,1,0}, //channel 5
{0,1,1,0}, //channel 6
{1,1,1,0}, //channel 7
{0,0,0,1}, //channel 8
{1,0,0,1}, //channel 9
{0,1,0,1}, //channel 10
{1,1,0,1}, //channel 11
{0,0,1,1}, //channel 12
{1,0,1,1}, //channel 13
{0,1,1,1}, //channel 14
{1,1,1,1} //channel 15
};
for(uint8_t i = 0; i < 4; i ++){
digitalWrite(controlPin[i], muxChannel[channel][i]);
}
delay(10);
}
void enableMux(uint8_t n){
if(n<8)
digitalWrite(MUX1_EN,LOW);
else
digitalWrite(MUX2_EN,LOW);
delay(10);
}
void disableMux(uint8_t n){
if(n<8)
digitalWrite(MUX1_EN,HIGH);
else
digitalWrite(MUX2_EN,HIGH);
delay(10);
}
float getVoltage(uint8_t pin){ //read voltage from analog pins
float sample=0.0;
for(uint8_t i=0;i<readSample;i++){
sample+=analogRead(pin);
delay(1);
}
sample=sample/readSample;
return (float)sample*Vcc*2/1024;
}
float readBatVoltage(uint8_t bat){
uint8_t voltagePin;
uint8_t muxPin;
uint8_t muxChannel;
if(bat<8){
voltagePin=BATTERY8_VOLTAGE_PIN;
muxPin=MUX1_S0;
}
else{
voltagePin=BATTERY16_VOLTAGE_PIN;
muxPin=MUX2_S0;
}
enableMux(bat);
muxChannel=bat%8*2;
setMux(muxPin,muxChannel);
float v= getVoltage(voltagePin);
disableMux(bat);
return v;
}
float readResVoltage(uint8_t bat){
uint8_t voltagePin;
uint8_t muxPin;
uint8_t muxChannel;
if(bat<8){
voltagePin=BATTERY8_VOLTAGE_PIN;
muxPin=MUX1_S0;
}
else{
voltagePin=BATTERY16_VOLTAGE_PIN;
muxPin=MUX2_S0;
}
enableMux(bat);
muxChannel=bat%8*2+1;
setMux(muxPin,muxChannel);
float v= getVoltage(voltagePin);
disableMux(bat);
return v;
}
void setCharge(uint8_t n, bool mode){ //change mosfet workning mode
if(mode){
digitalWrite(MOSFET_CHARGE_PIN+n,CHARGE_ON);
}
else{
digitalWrite(MOSFET_CHARGE_PIN+n,CHARGE_OFF);
}
mosfetChargeMode[n]=mode;
delay(10);
}
void setDischarge(uint8_t n, bool mode){ //change mosfet workning mode
if(mode){
digitalWrite(MOSFET_DISCHARGE_PIN+n,DISCHARGE_ON);
}
else{
digitalWrite(MOSFET_DISCHARGE_PIN+n,DISCHARGE_OFF);
}
mosfetDischargeMode[n]=mode;
delay(10);
}
bool isChargeingComplete(uint8_t n){ //check if battery is charging
if(digitalRead(CHARGING_COMPLETE_PIN+n)==LOW)
return true;
return false;
}
void checkStatus(){
if(millis()-lastTimeStatus<STATUS_TIME) return;
lastTimeStatus=millis();
for(uint8_t i=0;i<BATTERY_NUMBER;i++){
uint8_t bat;
if(relayMode[i]==RELAY_P0){
bat=i;
}
else{
bat=16+i;
}
if(chargerStatus[i]==STATUS_EMPTY){
setCharge(i,true);
setCharge(i,false);
float bat_Voltage=readBatVoltage(i);
if(bat_Voltage>2){
chargerStatus[i]=STATUS_READY;
batteryStatus[bat]=chargerStatus[i];
}
}
if(chargerStatus[i]==STATUS_READY){
float bat_Voltage=readBatVoltage(i);
batteryVoltage[bat]=bat_Voltage;
batteryCapacity[bat]=0;
chargeTimeStart[bat]=0;
chargeTimeStop[bat]=0;
dischargeTimeStart[bat]=0;
dischargeTimeStop[bat]=0;
rechargeTimeStart[bat]=0;
rechargeTimeStop[bat]=0;
if(Serial1Raport)
Serial1Send("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(batteryVoltage[bat])+">");
//Serial.println("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(batteryVoltage[bat])+">");
if(autoMode[i]){
setCharge(i,true);
chargeTimeStart[bat]=millis();
chargerStatus[i]=STATUS_CHARGE;
batteryStatus[bat]=chargerStatus[i];
}
else{
if(bat_Voltage<0.3){
chargerStatus[i]=STATUS_EMPTY;
batteryStatus[bat]=chargerStatus[i];
if(Serial1Raport)
Serial1Send("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(bat_Voltage)+">");
// Serial.println("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(bat_Voltage)+">");
}
}
}
if(chargerStatus[i]==STATUS_CHARGE){
chargeTimeStop[bat]=millis();
if(isChargeingComplete(i)){
setCharge(i,false);
float bat_Voltage=readBatVoltage(i);
batteryVoltage[bat]=bat_Voltage;
chargerStatus[i]=STATUS_HIGH;
batteryStatus[bat]=chargerStatus[i];
}
}
if(chargerStatus[i]==STATUS_HIGH){
if(autoMode){
batteryCapacity[bat]=0.0;
lastRead[i]=millis();
dischargeTimeStart[bat]=millis();
chargerStatus[i]=STATUS_DISCHARGE;
batteryStatus[bat]=chargerStatus[i];
setDischarge(i,true);
}
else{
float bat_Voltage=readBatVoltage(i);
if(bat_Voltage<1){
chargerStatus[i]=STATUS_EMPTY;
batteryStatus[bat]=chargerStatus[i];
if(Serial1Raport)
Serial1Send("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(bat_Voltage)+">");
//Serial.println("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(bat_Voltage)+">");
}
}
}
if(chargerStatus[i]==STATUS_DISCHARGE){
float bat_Voltage=readBatVoltage(i);
float res_Voltage=readResVoltage(i);
long now=millis();
long time_Passed=now-lastRead[i];
lastRead[i]=now;
dischargeTimeStop[bat]=now;
float current=(bat_Voltage-res_Voltage)/RES_VALUE *1000; //in mA
batteryCapacity[bat]+= current * (time_Passed / 3600000.0); // one Hour = 3600000ms
batteryVoltage[bat]=bat_Voltage;
if(bat_Voltage<batteryLow){
setDischarge(i,false);
chargerStatus[i]=STATUS_LOW;
batteryStatus[bat]=chargerStatus[i];
}
}
if(chargerStatus[i]==STATUS_LOW){
if(autoMode){
setCharge(i,true);
rechargeTimeStart[bat]=millis();
chargerStatus[i]=STATUS_RECHARGE;
batteryStatus[bat]=chargerStatus[i];
}
else{
float bat_Voltage=readBatVoltage(i);
if(bat_Voltage<1){
chargerStatus[i]=STATUS_EMPTY;
batteryStatus[bat]=chargerStatus[i];
if(Serial1Raport)
Serial1Send("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(bat_Voltage)+">");
//Serial.println("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(bat_Voltage)+">");
}
}
}
if(chargerStatus[i]==STATUS_RECHARGE){
rechargeTimeStop[bat]=millis();
if(isChargeingComplete(i)){
setCharge(i,false);
float bat_Voltage=readBatVoltage(i);
batteryVoltage[bat]=bat_Voltage;
batteryComplete[bat]=true;
chargerStatus[i]=STATUS_IDLE;
batteryStatus[bat]=chargerStatus[i];
if(Serial1Raport)
Serial1Send("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(batteryVoltage[bat])+"|"+String((chargeTimeStop[bat]-chargeTimeStart[bat])/60000)+"|"+String((dischargeTimeStop[bat]-dischargeTimeStart[bat])/60000)+"|"+String((rechargeTimeStop[bat]-rechargeTimeStart[bat])/60000)+">");
//Serial.println("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(batteryVoltage[bat])+"|"+String((chargeTimeStop[bat]-chargeTimeStart[bat])/60000)+"|"+String((dischargeTimeStop[bat]-dischargeTimeStart[bat])/60000)+"|"+String((rechargeTimeStop[bat]-rechargeTimeStart[bat])/60000)+">");
}
}
if(chargerStatus[i]==STATUS_IDLE){
float bat_Voltage=readBatVoltage(i);
if(bat_Voltage<1){
chargerStatus[i]=STATUS_EMPTY;
batteryStatus[bat]=chargerStatus[i];
batteryComplete[bat]=false;
if(Serial1Raport)
Serial1Send("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(bat_Voltage)+">");
//Serial.println("<"+String(bat)+"|"+String(chargerStatus[i])+"|"+String(batteryCapacity[bat])+"|"+String(bat_Voltage)+">");
}
else{
if(relayMode[i]==RELAY_P0 && batteryComplete[i+BATTERY_NUMBER]==false){
setRelay(i,true);
chargerStatus[i]=STATUS_EMPTY;
}
if(relayMode[i]==RELAY_P1 && batteryComplete[i+BATTERY_NUMBER]){
setRelay(i,false);
}
}
}
//raport
}
}
//###########Serial#################
void rx_empty(void)
{
while(Serial1.available() > 0) {
Serial1.read();
}
}
void serial1Event()
{
while (Serial1.available())
{
char MinChar = (char)Serial1.read(); // Char received from Serial1
if (MinChar == '\n')
{
string1Available = true;
evaluateRx(string1Rx);
string1Rx="";
}
else
{
if (MinChar >= ' ') {string1Rx += MinChar;} // >= ' ' to avoid not wanted ctrl char.
}
}
}
void evaluateRx(String s){
uint8_t _index1,_index2;
String buff;
uint8_t n,i;
switch(s.charAt(0)){
case 'R':
buff=s.substring(1,s.length());
n=buff.toInt();
if(n>=0 && n<BATTERY_NUMBER)
raportDetails(n);
//Serial1.println("[R"+String(n)+":OK]");
break;
case 'S':
buff=s.substring(1,s.length());
n=buff.toInt();
if(n==0){
serial1Sendit=0;
string1Tx="";
serial1Confirmed=true;
Serial.println("[S0:OK]");
}else{
serial1Sendit-=n;
}
break;
case 'C':
buff=s.substring(1,s.length());
n=buff.toInt();
if(n>=0 && n<BATTERY_NUMBER*BATTERY_ROWS)
batteryComplete[n]=false;
else
if(n==33)
for(i=0;i<BATTERY_NUMBER*BATTERY_ROWS;i++)
if(batteryComplete[i]) batteryComplete[i]=false;
break;
default:
if(s=="[OK]") serial1Confirmed=true;
break;
}
}
void raport(){
if(millis()-lastTimeRaport<RAPORT_TIME || autoRaport==false) return;
Serial.println();
for(uint8_t i=0;i<BATTERY_NUMBER*BATTERY_ROWS;i++){
if(i%4==0)
Serial.println();
Serial.println("<"+String(i)+"|"+String(batteryStatus[i])+"|"+String(batteryCapacity[i])+"|"+String(batteryVoltage[i])+"|"+String((chargeTimeStop[i]-chargeTimeStart[i])/60000)+"|"+String((dischargeTimeStop[i]-dischargeTimeStart[i])/60000)+"|"+String((rechargeTimeStop[i]-rechargeTimeStart[i])/60000)+">");
Serial1Send("<"+String(i)+"|"+String(batteryStatus[i])+"|"+String(batteryCapacity[i])+"|"+String(batteryVoltage[i])+"|"+String((chargeTimeStop[i]-chargeTimeStart[i])/60000)+"|"+String((dischargeTimeStop[i]-dischargeTimeStart[i])/60000)+"|"+String((rechargeTimeStop[i]-rechargeTimeStart[i])/60000)+">");
}
Serial.println();
lastTimeRaport=millis();
}
void raportDetails(uint8_t i){
Serial1.println("<"+String(i)+"|"+String(batteryStatus[i])+"|"+String(batteryCapacity[i])+"|"+String(batteryVoltage[i])+"|"+String((chargeTimeStop[i]-chargeTimeStart[i])/60000)+"|"+String((dischargeTimeStop[i]-dischargeTimeStart[i])/60000)+"|"+String((rechargeTimeStop[i]-rechargeTimeStart[i])/60000)+">");
}
void Serial1Send(String s){
if(Serial1Raport && serial1Connected){
string1Tx+=s;
}
else
string1Tx="";
}
void Serial1Tx(){
uint8_t len=Serial1.availableForWrite()-2;
String buff;
if(len>0 && string1Tx.length()>0 && serial1Confirmed){
if(len>string1Tx.length())
len=string1Tx.length();
buff=string1Tx.substring(0,len);
string1Tx.remove(0,len);
Serial1.print("["+buff+"]");
serial1Sendit=len;
Serial1.flush();
serial1Confirmed=false;
}
}