Charge controller for 4S4P Li-ion battery

Wichit Sirichote, wichit.sirichote@gmail.com

Build your own charge controller for Li-ion battery


This charge controller has been designed for charging the Li-ion battery pack, 4S4P made with 18650 cell. The controller is MEGA328 based MCU. The firmware was developed with Arduino IDE. The power source is 24Voc, PV panel. Analog display shows battery charging voltage. Optional load is 14V LED street lamp.
Figure 1: Charge controller

Figure 2: Sample 4S4P, 18650 Li-ion battery pack

The PWM charge controller connects PV panel to the battery directly. On stage 1, the charging current will depend on the insolation. PWM will be used for stage 2 voltage regulation.
Figure 3: Basic circuit of PWM charge controller

The MCU detects PV voltage, turns on Q1, connects the PV to Li-ion battery. PWM signal varies from 0-100% for current regulation. On stage 1, it will be 100%. On stage 2 it will be 0-100% to retain constant voltage. Q2 is optional for controlling LED street lamp. All control functions are running by MCU firmware.
Figure 4: Block diagram.


Hardware

The MCU is MEGA328, 32-pin TQFP flash microcontroller. The oscillator is 16MHz. ADC channel 0 detects battery voltage, ADC channel 1 detects PV voltage. PWM signal controls Q1, to turn on P-channel FET, IRF4905. The optional D12 is for controlling the LED street lamp. U1 provides +5V for MCU chip.

The analog display with D2-D7 is for battery charging voltage indicator. LED D9 is for tick signal.

Figure 5: Schematic (click to enlarge). 

Firmware

The firmware was c++ code, developed by Arduino IDE. Every 2s, the MCU reads PV and battery voltages. PI control is for voltage regulation for stage 2.
Figure 6: state diagram

 

The source code is shown below.

// Solar Charger for Li-ion battery 4s +16.8V 
       // created Mar, 2018
       // by Wichit Sirichote
       //
       // 14 May 2018 add AUX lamp and switching code from head lamp to aux lamp 
       // 18 May 2018 add serial command to reset sample number to zero       
#include <TimerOne.h>
         #include <avr/wdt.h>
#define ledPin 13
         #define load 7
         #define TEST 2 // TEST KEY
         #define LED7 4
#define LED8 5
#define LED1 8
         #define LED2 9
         #define LED3 10
         #define LED4 11
         #define LED5 12
#define PWM 6
         #define AUX 3 // aux lamp for failed safe lighting
 
unsigned int n,p,I,t, vbatt, pv;
         unsigned int temp;
char command;
         char f0=0;
         long sample=0;
         char load_status=0;
         char state=0; // charge state
   // 0 no charge
   // 1 bulk charge
   // 2 saturation charge
   // 3 fully charged
 
         char night_flag=0, fail_flag=0, aux_flag=0;
unsigned char Pout=50;
int error;
         int PB=20; // test with 20% proportional band
         float pTerm, Kp;
         int j;
         int duty_cycle;
         unsigned int saturation_time=0; 
       
void bulk_charging()
         {
         
   if((pv > 170) && (state==0))
   {
   state = 1;
   // digitalWrite(charge,1); // connect pv to battery
   duty_cycle=255;
   analogWrite(PWM,255);
   digitalWrite(LED7,1); // connect pv to battery
   fail_flag =0; // clear fail_flag
 }
         }
void state_change()
         { 
   if((vbatt >=168) && (state==1) )
   {
   state = 2; // enter to saturation charge
   
   
   
   //digitalWrite(charge,0); // disconnect pv to battery
   // digitalWrite(LED7,0); // disconnect pv to battery
   
   // digitalWrite(LED8, 1); // indicates fully charged
 }
}
// detect charging current insted
         // regulate at 168 V
 
 void saturation_charge()
   {
 if(state==2)
   {
 error = 168 - vbatt;
           
   Kp=100/PB;
   pTerm=Kp*error;
   
   j=(int) pTerm;
 if(j<0) j=0; 
   if(j>100) j=100;
 if(j>=0 && j <=100)
   {
   duty_cycle=j; // for status display
   j= (j*255)/100; // convert to 0-255 for 0-100%
   analogWrite(PWM,j);
   }
 
 if( saturation_time++ > 3000) // test 30 ticks or one minute actual is 3000 ticks or 1.6Hrs
   {
   saturation_time=0;
   state=3;
   // digitalWrite(charge,0); // disconnect pv to battery terminate charging
   // digitalWrite(LED7,0); // disconnect pv to battery
   analogWrite(PWM,0); // disconnect PV
   digitalWrite(LED8, 1); // indicates fully charged
   
   }
 
   } 
   }
void enter_night_mode()
         {
   if(night_flag==0 && fail_flag==0)
   {
 if(pv <90)
   {
   digitalWrite(load,1); // turn load on
   digitalWrite(AUX,0); // turn aux load off
   
   night_flag=1;
   state =0;
   load_status=1;
   digitalWrite(LED8, 0); // turn off fully charged LED
 // digitalWrite(charge,0); // disconnect pv to battery
   // analogWrite(PWM,0);
   digitalWrite(LED7,0); // disconnect pv to battery
 }
 }
         }
void morning_off()
         {
   if(pv>100 && night_flag==1)
   {
   night_flag=0;
   load_status=0;
   digitalWrite(load,0); // turn load off
   digitalWrite(AUX,0); // turn aux lamp off
   }
}
// low voltage disconnect is 2.8V/cell
void low_voltage_disconnect()
         {
 // if(vbatt<112 && night_flag==1)
   if(vbatt<120 && night_flag==1) //12.0-11.2 
   {
   digitalWrite(load,0); // turn load off
   load_status=0;
   fail_flag=1;
   night_flag=0;
   // turn off meter to save power
   digitalWrite(LED1, 0);
   digitalWrite(LED2, 0);
   digitalWrite(LED3, 0);
   digitalWrite(LED4, 0);
   digitalWrite(LED5, 0);
       
 
   }
}
//????????????????????????????????????????????
void very_low_voltage_disconnect()
         {
   if(vbatt<112 && aux_flag==1)
   {
   night_flag=0;
   fail_flag=0;
   aux_flag=0;
   digitalWrite(AUX,0); // turn aux lamp off
 
         }
         
         }
       
// X Y
         //165 6
         //155 5
         //145 4
         //135 3
         //125 2
         //115 1
         // Y = 0.1X - 10.5
         float g;
         int x;
void batt_volt_meter()
         {
         
         // display volt only if fail_flag == 0
 if(fail_flag==0)
   {
 g = vbatt;
   g = g/10 - 10.5;
   x = (int) g;
   
   
   if (x<1)
   {
 // digitalWrite(LED8, 0);
   digitalWrite(LED1, 0);
   digitalWrite(LED2, 0);
   digitalWrite(LED3, 0);
   digitalWrite(LED4, 0);
   digitalWrite(LED5, 0);
   
   }
 if (x== 1)
   {
   // digitalWrite(LED8, 0);
   digitalWrite(LED1, 0);
   digitalWrite(LED2, 0);
   digitalWrite(LED3, 0);
   digitalWrite(LED4, 0);
   digitalWrite(LED5, 1);
   
   }
   if(x==2)
   {
 // digitalWrite(LED8, 0);
   digitalWrite(LED1, 0);
   digitalWrite(LED2, 0);
   digitalWrite(LED3, 0);
   digitalWrite(LED4, 1);
   digitalWrite(LED5, 1);
   }
   if(x==3)
   {
   // digitalWrite(LED8, 0);
   digitalWrite(LED1, 0);
   digitalWrite(LED2, 0);
   digitalWrite(LED3, 1);
   digitalWrite(LED4, 1);
   digitalWrite(LED5, 1);
   }
   if(x==4)
   {
   // digitalWrite(LED8, 0);
   digitalWrite(LED1, 0);
   digitalWrite(LED2, 1);
   digitalWrite(LED3, 1);
   digitalWrite(LED4, 1);
   digitalWrite(LED5, 1);
   }
   if(x==5)
   {
   // digitalWrite(LED8, 0);
   digitalWrite(LED1, 1);
   digitalWrite(LED2, 1);
   digitalWrite(LED3, 1);
   digitalWrite(LED4, 1);
   digitalWrite(LED5, 1);
   }
   if(x>5)
   {
   // digitalWrite(LED8, 0);
   digitalWrite(LED1, 1);
   digitalWrite(LED2, 1);
   digitalWrite(LED3, 1);
   digitalWrite(LED4, 1);
   digitalWrite(LED5, 1);
   }
 }
       
}
       
// enter interrupt service every 2s 
void isrCallback() // callback function when interrupt is asserted
         {
   sample++;
   // disconnect PV before read Vbatt
   // digitalWrite(charge,0); 
   analogWrite(PWM,0);
   delay(10);
   
   n = analogRead(A0);
   temp = n;
   vbatt= (n*168)/305; // battery voltage x0.1
   // vbatt =n;
   
   // connect PC before read PV in bulk charging or saturation charge
   if(state==1)
   {
   // digitalWrite(charge,1);
   analogWrite(PWM,255); 
   delay(10);
   } 
       
 
 
   p = analogRead(A1); // pv voltage x0.1
   pv= (p*168)/305;
   
   // pv = p;
   
   t = analogRead(A2); // lm35 temperature sensor
   I = analogRead(A3); // hall sensor
 bulk_charging();
   state_change();
   saturation_charge();
   enter_night_mode();
   morning_off();
   low_voltage_disconnect(); // turn head lamp off. aux lamp on
   // very_low_voltage_disconnect(); // turn aux lamp off
 batt_volt_meter();
 if(digitalRead(TEST)==0)
   {
   load_status^=1;
   digitalWrite(load, load_status); // manually turn on/off for both lamps
   digitalWrite(AUX,load_status); 
 }
 if(f0) 
   {Serial.print(sample);
   Serial.write(",");
   Serial.print(vbatt);
   Serial.write(",");
   // Serial.print(temp);
   // Serial.write(",");
   
   Serial.print(pv);
   Serial.write(",");
   Serial.print(t);
   Serial.write(",");
   Serial.print(I); // enable print data on serial
   Serial.write(",");
   Serial.print(load_status,DEC);
   Serial.write(",");
   Serial.print(state,DEC); // charging state
   Serial.write(",");
   Serial.print(duty_cycle,DEC); // duty cycle
   
   Serial.print("\r\n");
 }
 digitalWrite(ledPin, HIGH);
   delay(10);
   digitalWrite(ledPin, LOW);
 if(state==2)
   {
   digitalWrite(LED7,1); // saturation mode
   delay(20); 
   digitalWrite(LED7,0); 
 
   }
   
   }
void wdtSetup() {
   cli();
   MCUSR = 0;
   // WDTCSR |= B00101000;
   WDTCSR = B00101000; // 4 seconds timeout
   sei();
   }
       
void setup() {
   // put your setup code here, to run once:
   Serial.begin(9600);
   
   
   // prints title with ending line break
   Serial.println("Li-ion Solar Charger v1.0");
 pinMode(load, OUTPUT); // load connect pin
   digitalWrite(load, 0); // power on disconnect first
   pinMode(AUX, OUTPUT); 
   digitalWrite(AUX, 0); // power on disconnect first
   
   pinMode(TEST, INPUT_PULLUP); // test button for manually turn on/off load
       
 pinMode(13, OUTPUT); // tick LED
   pinMode(12, OUTPUT); 
   pinMode(11, OUTPUT); 
   pinMode(10, OUTPUT); 
   pinMode(9, OUTPUT); 
   pinMode(8, OUTPUT);
   pinMode(4, OUTPUT); 
   pinMode(5, OUTPUT); 
   pinMode(PWM, OUTPUT); 
   
       
 // analogWrite(PWM, (255*Pout)/100);
           
   Timer1.initialize(2000000); // initialize timer1, and set a 2 second period
   Timer1.attachInterrupt(isrCallback);
}
void loop() {
           
   while (Serial.available() > 0)
   {
 command= Serial.read();
           
   switch(command)
   {
 case 0x0d : Serial.println("Li-ion Solar Charger v1.0"); break;
   case 'b' : Serial.println(vbatt); break;
   case 'l' : Serial.println("light ON"); load_status=1; digitalWrite(load, load_status); break;
   case 'o' : Serial.println("light OFF"); load_status=0;digitalWrite(load, load_status); break;
   case 'p' : Serial.println(analogRead(A3)); break;
   case ' ' : f0^=1; break;
   // case 'c' : Serial.println("test charge"); digitalWrite(charge,1); digitalWrite(LED7,1); break;
   case 'r' : sample = 0; break;
   default : Serial.write("What?");
       
 }
           
   }
   }
       
Figure 7: The source code. 

Testing result was charging profile, by recording the PV, battery and LOAD voltage.

Figure 8: Charging profile.  

 

Figure 9: Prototype testing with 20W LED street lamp. 

 

PARTS LIST

Semiconductors

D1 1N4742, +12V zener diode
D2,D3,D4,D5,D6,D7,D8,D9 LED
D10 1N5227
D12,D11 IRF4905/TO, P-channel FET
D13 MBR1645
D14,D16,D17 P6KE36CA
D15 LOAD LED
IC1 ATmega328TQ32, Atmel Flash MCU
J1 CON6AP
J4 CON6
Q1,Q2 BC337, NPN transistor
U1 NCP1117DT50RKG, +5V voltage regulator

Resistors (all resistors are 1/4W +/-5%)

R1 100/1W
R2,R4,R6,R7,R8 10k
R5,R10,R11,R12 1k
R9 4k7
R13,R14 680

Capacitors
C1,C3,C4,C5 100nF
C2 10uF
C6 22uF
C7,C8 20pF

Additional parts

SW1,SW2 SW TACT-SPST/SM
Y1 16MHz

More information please contact wichit.sirichote@gmail.com

Download Schematic, Firmware HEX file and Source code, presentation file , Technical documentation


<

20 July 2019