/*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~350 W Pure Sine Wave Inverter with CC CV Charging, Automatic Changeover [Rev. 2]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
****[The author will NOT take any liability if you receive any kind of damage using this code. Use at your own risk!]****
Pure_Sine_Wave_Inverter_NANO_V.3.1 [NOT compatible with version 1.x or 2.x boards]
Change log:
16x2 LCD added instead of LED pannel
pin# changed: HS2 "D11 --> D10", LS1 "D10 --> D11"
MOSFET driver shutdown (SD) pin disconnected and grounded (i.e. drivers are now always ON)
UPS_ON/OFF switch added at D13
Fuse Blown signal added at D8 [Not implemented so far. Kept for future upgradability]
Sayantan Sinha: 06/06/2021
sPWM on the atMega328P for the arduino NANO. H-bridge output with deadtime.
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
#define FB 0 // Fuse Blown (D8 i.e. bit-0 in PORTB) (LOW: OK, HIGH: iether DC fuse is blown or any-or-both of the low-side MOSFETs are not working)
#define HS1 1 // High side drive pin of Half Bridge #1 (D9 i.e. bit-1 in PORTB)
#define HS2 2 // Low side drive pin of Half Bridge #1 (D10 i.e. bit-2 in PORTB)
#define LS1 3 // High side drive pin of Half Bridge #2 (D11 i.e. bit-3 in PORTB)
#define LS2 4 // Low side drive pin of Half Bridge #2 (D12 i.e. bit-4 in PORTB)
#define SW 5 // UPS ON/OFF switch (D13 i.e. bit-5 in PORTB) (HIGH: UPS OFF, LOW: UPS ON)
#define PIN_ZC (PIND & (1 << PD3)) // Read pinZc
#define PIN_MNS (PIND & (1 << PD2)) // Read pinMns
#define PIN_SW (PINB & (1 << PB5)) // Read pinSw
#define W_CNG_RLY_HIGH (PORTD |= 0b10000000) // Make change over relay HIGH
#define W_CNG_RLY_LOW (PORTD &= 0b01111111) // Make change over relay LOW
// A4 & A5 are used in I2C communication
#define SW_FREQ 16000 // sPWM switching frequency in Hz
#define MAX_COUNT 1000 // (16 MHz / SW_FREQ) / 2
#define MAX_COUNT_CHG 400 // Max count for charging which corresponds to a max duty cycle = (MAX_COUNT_CHG / MAX_COUNT) * 100% during charging
#define NP 160 // Number of Pulses in one half cycle = (16 kHz / 50 Hz) / 2 = 160
#define Q_CYCLE 80 // Pulse counter at Querter of cycle
#define DT 100 // Deadtime between low-side & high-side switching
#define THC 200 // Threshold count = the min count needs to execute the compare match int reliably
#define MOD_INDX_MAX 95 // Max modulation index of sPWM
#define MOD_INDX_MIN 40 // Min modulation index of sPWM
#define VO_LO 537 // ADC reading equivalent for the lower threshold output voltage = ((vtx / c) / ADC res); e.g. vtx = 10.95 V, voltage devider fact. c = 2.660, ADC res = 4.887 mV
#define VO_HI 562 // ADC reading equivalent for the upper threshold output voltage = ((vtx / c) / ADC res); e.g. vtx = 12.40 V, voltage devider fact. c = 2.660, ADC res = 4.887 mV
#define VB_MAX 950 // ADC reading equivalent for the max bat voltage during cc-cv charge = ((vb / k) / ADC res); e.g. vb = 14.20 V, voltage devider fact. k = 3.122, ADC res = 4.887 mV
#define VB_TOP 920 // ADC reading equivalent for the max bat voltage during topping charge = ((vb / k) / ADC res); e.g. vb = 13.80 V, voltage devider fact. k = 3.122, ADC res = 4.887 mV
#define VC_MAX 85 // ADC reading equivalent for the max chg current during cc-cv charge = ((ic * a) / ADC res); e.g. ic = 6.0 A, voltage amplification fact. a = 0.07, ADC res = 4.887 mV
#define VC_TOP 25 // ADC reading equivalent for the max chg current during topping charge = ((ic * a) / ADC res); e.g. ic = 0.5 A, voltage amplification fact. a = 0.22, ADC res = 4.887 mV
#define VB_LOW 750 // ADC reading equivalent for the low bat voltage during running the inverter = ((vb / k) / ADC res); e.g. vb = 11.50 V, voltage devider fact. k = 3.122, ADC res = 4.887 mV
#define VB_CRIT 690 // ADC reading equivalent for the critical bat voltage during running the inverter = ((vb / k) / ADC res); e.g. vb = 10.50 V, voltage devider fact. k = 3.122, ADC res = 4.887 mV
#define VL_MAX 560 // ADC reading equivalent for the max load current during inverter running = ((il * b) / ADC res); e.g. il = 50 A, voltage amplification fact. b = 0.0545, ADC res = 4.887 mV
#define MNS_WAIT_TIME 3000 // Wait time (ms) from detecting mains voltage to setting the mnsOn flag
#define CNG_OVER_DLY 8 // Delay time (ms) after zero-crossing to start the change over procedure
#define CNG_OVER true
#define MNS_ON 0b10000000 // Mask to read MNS_ON bit from the LED Status Reg
#define INV_ON 0b01000000
#define CHG_ON 0b00100000
#define BAT_LO 0b00010000
#define OVR_LD 0b00001000
#define FUS_BL 0b00000100
const int pinVo = A0, pinVt = A1, pinVb = A2, pinMd = A3, pinVl = A6, pinVc = A7; // Analog pin declearations
const int pinFan = 1, pinMns = 2, pinZc = 3, pinBuz = 4, pinChRng = 5, pinChRly = 6, pinCngRly = 7, pinFb = 8, pinSw = 13; // Digital pin declerations
byte ledsr = 0; // LED Status Register [MNS_ON INV_ON CHG_ON BAT_LO OVR_LD FUS_BL 0 0]
byte erCode = 0; // Error code for inverter shutdown
volatile byte i = 0; // The counter for the lookUp array
volatile bool pcyl = false; // Flag for positive half cycle
bool invOn = false; // Inverter on flag
bool mnsOn = false; // Mains on flag
bool chgOn = false; // Charging on flag
bool cngOverRequest = false; // Change over to mains request flag
bool zcPrev = true; // Stores the status of pinZc
bool topChg = false;
byte dispStatus = 0; // LCD display status register [MNS_ON INV_ON CHG_ON BAT_LO OVR_LD FUS_BL 0 0]
unsigned int modIndx = 50; // Start the inverter with this modulation index
unsigned int vo; // Stores output voltage feedback
unsigned int vb; // Stores bat voltage
unsigned int vl; // Stores load current
unsigned int vc; // Stores charging current
unsigned long timePrev = 0; // General purpose time recorder
unsigned long timeInvOff = 0; // Inverter off time recorder
unsigned long timeMnsOn = 0; // Mains on time recorder
unsigned long zcTime = 0; // Records zero crossing time of mains voltage
unsigned long mnsTime = 0; // Records the mains voltage detection time
unsigned long tRefresh = 0; // Records the time of display refresh
//Look up tables with 160 entries each, normalised to have max value of 500 which is the period of the PWM loaded into register ICR1.(D:\Project Files\MATLAB\SPWM_PhaseFreqCorrect_LookUp_Table_Gen_for_V_3_0.m)
unsigned int lookUpInverseHalf[NP] = {0, 10, 20, 29, 39, 49, 59, 69, 78, 88, 98, 107, 117, 126, 136, 145, 155, 164, 173, 182, 191, 200, 209, 218, 227, 236, 244, 253, 261, 270, 278, 286, 294, 302, 310, 317, 325, 332, 339, 347, 354, 360, 367, 374, 380, 387, 393, 399, 405, 410, 416, 421, 426, 431, 436, 441, 446, 450, 454, 458, 462, 466, 469, 472, 476, 478, 481, 484, 486, 488, 490, 492, 494, 495, 497, 498, 498, 499, 500, 500, 500, 500, 500, 499, 498, 498, 497, 495, 494, 492, 490, 488, 486, 484, 481, 478, 476, 472, 469, 466, 462, 458, 454, 450, 446, 441, 436, 431, 426, 421, 416, 410, 405, 399, 393, 387, 380, 374, 367, 360, 354, 347, 339, 332, 325, 317, 310, 302, 294, 286, 278, 270, 261, 253, 244, 236, 227, 218, 209, 200, 191, 182, 173, 164, 155, 145, 136, 126, 117, 107, 98, 88, 78, 69, 59, 49, 39, 29, 20, 10};
unsigned int lookUp[NP] = {1000, 980, 961, 941, 922, 902, 882, 863, 844, 824, 805, 786, 767, 748, 729, 710, 691, 672, 654, 636, 617, 599, 581, 564, 546, 529, 511, 494, 478, 461, 444, 428, 412, 396, 381, 366, 351, 336, 321, 307, 293, 279, 266, 252, 240, 227, 215, 203, 191, 180, 169, 158, 147, 137, 128, 118, 109, 100, 92, 84, 76, 69, 62, 55, 49, 43, 38, 32, 28, 23, 19, 16, 12, 9, 7, 5, 3, 2, 1, 0, 0, 0, 1, 2, 3, 5, 7, 9, 12, 16, 19, 23, 28, 32, 38, 43, 49, 55, 62, 69, 76, 84, 92, 100, 109, 118, 128, 137, 147, 158, 169, 180, 191, 203, 215, 227, 240, 252, 266, 279, 293, 307, 321, 336, 351, 366, 381, 396, 412, 428, 444, 461, 478, 494, 511, 529, 546, 564, 581, 599, 617, 636, 654, 672, 691, 710, 729, 748, 767, 786, 805, 824, 844, 863, 882, 902, 922, 941, 961, 980};
void changeDuty(void);
void fireInv(void);
void shutdownInv(bool coEnable = false);
void startChg(void);
void stopChg(void);
void refreshDisp(const byte oneByte);
void beepErr(void);
void setup()
{
DDRB = 0b00011110; // Set pin D12 to D9 as output and D13 & D8 as input. D13 = PB5 (PB7 & PB6 are unusable). PORTB = [XTAL2 XTAL1 D13 D12 D11 D10 D9 D8]]
pinMode(pinFan, OUTPUT);
pinMode(pinMns, INPUT_PULLUP);
pinMode(pinZc, INPUT_PULLUP);
pinMode(pinBuz, OUTPUT);
pinMode(pinChRng, INPUT_PULLUP);
pinMode(pinChRly, OUTPUT);
pinMode(pinCngRly, OUTPUT);
//pinMode(SCL, INPUT_PULLUP);
//pinMode(SDA, INPUT_PULLUP);
lcd.init(); // Initialize the LCD
lcd.backlight(); // Turn on the backlight
changeDuty();
refreshDisp(MNS_ON | INV_ON | CHG_ON | BAT_LO | OVR_LD | FUS_BL); // Show welcome msg ("INVERTER ON / CHARGING")
digitalWrite(pinBuz, HIGH); // Start up beep!
delay(500);
digitalWrite(pinBuz, LOW);
delay(2000);
refreshDisp(0); // Clear LCD
mnsOn = !PIN_MNS; // Mains On flag will be cleared if pinMns is high
if (PIN_SW)
if (!mnsOn) // No mains power detected
fireInv();
}
void loop()
{
if (invOn) { // Inverter Mode routine
if (i == 1) { // Zero-crossing of the inverter
if (cngOverRequest) // If there is a change over to mains request pending
shutdownInv(); // Shutdown inverter but without changing over to the mains
else { // If there is no shutdown request pending
vo = analogRead(pinVo); // Read output voltage feedback at (+Ve & -Ve peaks)
vb = analogRead(pinVb); // Read bat volatage
vl = analogRead(pinVl); // Read load current
if (vo < VO_LO) { // If output voltage is lower than the VO_LO
if (modIndx < MOD_INDX_MAX) {
modIndx++; // Increase modulation index by 1%
changeDuty();
}
}
else if (vo > VO_HI) { // If output voltage is higher than the VO_HI
if (modIndx > MOD_INDX_MIN) {
modIndx--; // Decrease modulation index by 1%
changeDuty();
}
}
erCode = vb < VB_CRIT ? BAT_LO : vl > VL_MAX ? OVR_LD : erCode;
if (erCode) { // If either bat voltage is below critical or load current is higher that max rating
shutdownInv(CNG_OVER); // Shut down inverter with change over to mains enable
beepErr(); // Gives error notification by beeping and makes an 1 s delay
}
if (!PIN_SW) // If inverter on/off switch is turned off
shutdownInv(CNG_OVER); // Shut down inverter with change over to mains enable
}
}
if (mnsOn) { // If mains voltage is on but inverter is still running
if (cngOverRequest) { // If there is a change over to mains request pending
if (millis() - zcTime > CNG_OVER_DLY) // The change over to mains process starts after CNG_OVER_DLY
shutdownInv(CNG_OVER); // Shut down inverter with change over to mains enable
}
else if (PIN_ZC && !zcPrev) { // When the mains voltage is going from -Ve half to +Ve half. (pinZc = HIGH: Mains voltage is at +Ve hallf cycle)
cngOverRequest = pcyl; // A change over request to mains voltage is issued if inverter and mains both are at +Ve half
zcTime = millis(); // Record the time of zero crossing
}
zcPrev = PIN_ZC; // Save the state of zero-crossing pin
mnsOn = !PIN_MNS; // Mains On flag will be cleared if pinMns goes high
}
else {
if (!PIN_MNS) { // Mains voltage detected
if (!mnsTime)
mnsTime = millis(); // Record the time when the mains voltage is detected
else if (millis() - mnsTime > MNS_WAIT_TIME) {
mnsOn = !PIN_MNS; // Mains On flag will be set if pinMns is still low after MNS_WAIT_TIME is elapsed
mnsTime = 0; // Clear the mnsTime
zcPrev = true;
cngOverRequest = false; // Clear any pending change over to mains request
}
}
else // Mains power failed before the mns wait time
mnsTime = 0; // Clear the mnsTime
}
}
else { // Mains Mode routine (will be executed even if mains is off, if the inverter on/off switch is at "off" position)
if (mnsOn) { // If mains voltage is on
if (!chgOn) {
if (millis() - timeMnsOn > 2500) // If 2.5 s has been elapsed after mains voltage on
startChg(); // Start charging
}
else { // Charging is on
if (millis() - timePrev > 9) { // Actuate charging current in every 10 ms interval
unsigned int vbatMax = topChg ? VB_TOP : VB_MAX; // If topping charge is on max charging voltage is VB_TOP equiv. else VB_MAX equiv.
vb = analogRead(pinVb);
vc = analogRead(pinVc);
if (vc < VC_MAX && vb < vbatMax) {
if (OCR1B < MAX_COUNT_CHG) // Maximum duty cycle = (MAX_COUNT_CHG / MAX_COUNT) * 100 %;
OCR1B = OCR1B + 1; // Increase duty-cycle slowly
}
else if (vb > vbatMax || vc > VC_MAX) {
if (OCR1B > 0) // Minimum possible duty cycle 0%
OCR1B = OCR1B - 1; // Decrease duty-cycle slowly
}
topChg = topChg ? vc <= VC_TOP : (vb >= VB_MAX && vc < VC_TOP); // If battery voltage reaches VB_MAX equiv. and charging current drops below VC_TOP equiv. then topping charge is started. If topping chg is already set then it will be reset if chg current goes above VC_TOP equiv.
if ((ledsr & CHG_ON) && topChg) // If cc-cv charging ended and topping charge has been started but charge on led is still on
refreshDisp(ledsr & ~CHG_ON); // Clear charging status on LCD
else if (!(ledsr & CHG_ON) && !topChg) // If cc-cv charging is on but, chg on LED is off
refreshDisp(CHG_ON); // Show charging status on LCD
timePrev = millis(); // Record the charging current actuation time
}
}
if (!(ledsr & MNS_ON)) // If mains is on but, mains on LED is off
refreshDisp(MNS_ON);
pcyl = !PIN_ZC; // If mains voltage is at +Ve half, the inverter will start at -Ve half in case of mains failure
}
else { // If mains voltage fails
if (PIN_SW) { // Check if inverter on/off switch is at "on" position
fireInv(); // Start inverter and switch to inverter mode
}
if (!invOn) { // If last fireInv call did not turn on the inverter
timeMnsOn = millis(); // Mains on timing is reset to present time
if (chgOn) // Mains off, Inverter off but charging is still on
stopChg(); // Stop chargig
if (ledsr & MNS_ON) // If mains failed but mains on LED is still on
refreshDisp(ledsr & ~MNS_ON); // Turn off mains on LED
}
}
mnsOn = !PIN_MNS; // Mains On flag will be cleared if pinMns goes high
}
}
ISR(TIMER1_OVF_vect)
{
if (i >= NP) {
i = 0;
pcyl = !pcyl;
}
if (i == 1) {
TCCR1A = pcyl ? 0b11000010 : 0b00110010; // +Ve half cycle: Set OC1A (pin D9) on Compare Match; Disconnect OC1B (pin D10); -Ve half cycle: Opposite
TIFR1 |= 0b00000110; // Clears the Output compare match A&B flags. [Ref. Datasheet p. 136]
TIMSK1 = pcyl ? 0b00000101 : 0b00000011; // +ve half cycle: INT on comp match B, -ve half cycle: INT on comp match A, both halves: INT on timer overflow
}
if (pcyl) {
OCR1A = lookUp[i];
OCR1B = lookUp[i] - DT;
}
else {
OCR1B = lookUp[i];
OCR1A = lookUp[i] - DT;
}
if (lookUp[i] > THC)
PORTB |= 0b00011000;
else if (pcyl)
PORTB &= 0b11110111;
else
PORTB &= 0b11101111;
i++;
}
ISR(TIMER1_COMPA_vect)
{
if (invOn)
PORTB &= 0b11101111; // Turn off LS2
else if (OCR1B > 0)
PORTB |= 0b00011000; // Turn on LS1 & LS2 in charging mode
}
ISR(TIMER1_COMPB_vect)
{
if (invOn)
PORTB &= 0b11110111; // Turn off LS1
else
PORTB &= 0b11100111; // Turn off LS1 & LS2 in charging mode
}
void changeDuty(void)
{
modIndx = modIndx > MOD_INDX_MAX ? MOD_INDX_MAX : modIndx < MOD_INDX_MIN ? MOD_INDX_MIN : modIndx; // Above 92% the minimum count will be below dead-time (DT)
lookUp[0] = MAX_COUNT;
for (int i = 1; i <= Q_CYCLE; i++) {
lookUp[i] = MAX_COUNT - ((lookUpInverseHalf[i] * modIndx) / 50); // Calculate new duty cycle for quarter cycle
}
int j = Q_CYCLE - 1;
for (int i = Q_CYCLE + 1; i < NP; i++) { // Copy the duty cycle for next quarter cycle
lookUp[i] = lookUp[j];
j--;
}
}
void fireInv(void)
{
TIMSK1 = 0; // Disable Timer1 interrupts
PORTB &= 0b11100001; // Pull down LS2, LS1, HS2, & HS1.
vb = analogRead(pinVb);
if ( vb > VB_LOW) {
W_CNG_RLY_HIGH; // Engage change over relay
refreshDisp(INV_ON); // Show the inverter on status on LCD
TCCR1A = pcyl ? 0b11000010 : 0b00110010; // To start with: +Ve half cycle-> OC1A (pin D9) = Set on Compare Match; OC1B (pin D10) = Disconnect; -Ve half cycle-> Opposite
TCCR1B = 0b00011001; // Fast PWM (Mode 14); Clock Select = System clock (No Prescaling) [Ref. Data Sheet, p. 132]
ICR1 = MAX_COUNT; // Switching frequency = 16 kHz (1 / (TIMER1_TOP * 62.5e-9))
i = 0; // Clear counter for lookup table
if (pcyl) {
OCR1A = lookUp[i];
OCR1B = lookUp[i] - DT;
}
else {
OCR1B = lookUp[i];
OCR1A = lookUp[i] - DT;
}
PORTB &= 0b11100001; // Pull down LS2, LS1, HS2, & HS1.
TIFR1 |= 0b00000111; // Clears the Output compare match A&B flags. Clear timer1 overflow flag [Ref. Datasheet pp. 136-137]
TIMSK1 = pcyl ? 0b00000101 : 0b00000011; // +ve half cycle: INT on comp match B, -ve half cycle: INT on comp match A, both halves: INT on timer overflow
sei(); // Enable global interrupt
invOn = true; // Inverter on flag set
mnsOn = false; // Clear the mains on status flag
chgOn = false; // Clear the charge on status flag
erCode = 0; // Inverter shutdown error code. 0 implies no error, inv shut down due to mains on
}
else {
if (!(ledsr & BAT_LO)) { // If battery low indicator is not glowing
refreshDisp(BAT_LO); // Indicate battery low
digitalWrite(pinBuz, HIGH); // Put a long beep
}
delay(800);
digitalWrite(pinBuz, LOW);
delay(200);
}
}
void shutdownInv(bool coEnable = false) // If coEnable is true, change over to mains will occur
{
TIMSK1 = 0; // No more interrupt!
TCCR1A = 0b00000010; // OC1A (D9) & OC1B (D10) = Disconnect;
PORTB &= 0b11100001; // Pull down LS2, LS1, HS2, & HS1.
timeInvOff = millis(); // Record the time when the inverter is turned off
if (coEnable) { // If change over is enabled
delay(1);
W_CNG_RLY_LOW; // Disengage change over relay to switch to the mains power
invOn = false; // Clear inverter on flag only after a successfull changeover to mains
refreshDisp(!erCode ? MNS_ON : erCode); // Indicate mains on if error code is zero else indicate the specific error
mnsOn = !PIN_MNS; // Mains On flag will be cleared if pinMns goes high
timeMnsOn = millis(); // Cng over to mains time is recorded
cngOverRequest = false; // Clear change over request pending status flag
modIndx = 70;
changeDuty();
}
chgOn = false; // Clear the charge on status flag
}
void startChg(void)
{
refreshDisp(ledsr | CHG_ON); // Show the charging on status on display
TCCR1A = 0b00000010; // OC1A (pin D9) & OC1B (pin D10) = Disconnect
TCCR1B = 0b00011001; // Fast PWM (Mode 14); Clock Select = System clock (No Prescaling) [Ref. Data Sheet, p. 132]
ICR1 = MAX_COUNT; // Switching frequency = 16 kHz (1 / (TIMER1_TOP * 62.5e-9))
OCR1A = 0; // Turn on LS1 & LS2 @ count 0 of timer1
OCR1B = 0; // This dictates the duty cycle. 0 = 0%, MAX_COUNT = 100%
PORTB &= 0b11100001; // Pull down LS2, LS1, HS2, & HS1.
TIFR1 |= 0b00000111; // Clears the Output compare match A&B flags. Clear timer1 overflow flag [Ref. Datasheet pp. 136-137]
TIMSK1 = 0b00000110; // INT on output comp match A & B
chgOn = true; // Inverter on flag set
topChg = false; // Clear topping charge flag
}
void stopChg(void)
{
OCR1A = 0; // Turn on LS1 & LS2 @ count 0 of timer1
OCR1B = 0; // This dictates the duty cycle. 0 = 0%, MAX_COUNT = 100%
TIMSK1 = 0b00000000; // Disable INT on output comp match A & B
PORTB &= 0b11100001; // Pull down LS2, LS1, HS2, & HS1.
chgOn = false; // Clear the charge on status flag
refreshDisp(ledsr & ~CHG_ON); // Turn off the charging on indicator
}
void refreshDisp(const byte oneByte)
{
ledsr = oneByte; // update LED status register
lcd.clear();
if (oneByte & INV_ON)
lcd.print("INVERTER ON");
else if (oneByte & MNS_ON)
lcd.print("MAINS ON");
if (oneByte & 0x3F) { // If any flag other than INV_ON or MNS_ON is(are) set
lcd.setCursor(0, 1);
if (oneByte & CHG_ON)
lcd.print("CHARGING...");
else if (oneByte & BAT_LO)
lcd.print("BATTERY LOW!");
else if (oneByte & OVR_LD)
lcd.print("OVER LOAD!");
else if (oneByte & FUS_BL)
lcd.print("FUSE BLOWN!");
}
}
void beepErr(void)
{
digitalWrite(pinBuz, HIGH);
delay(800);
digitalWrite(pinBuz, LOW);
delay(3000);
}