/*
* Hybrid Inverter/UPS Firmware - Arduino Nano
* 50Hz Fundamental, 25kHz Carrier, 3-Stage Charging
*
* Architecture: Unipolar SPWM
* H-Bridge: TLP250 Drivers
*
* Full Menu System with 4 Display Pages
* Settings stored in EEPROM
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <math.h>
// --- Configuration ---
#define SPWM_FREQ 25000 // 25kHz
#define SYS_CLOCK 16000000
#define ICR_VAL (SYS_CLOCK / (2 * SPWM_FREQ)) // Phase Correct PWM (Approx 320)
#define PI 3.14159265358979323846
// --- Pin Definitions ---
#define PIN_HI_L 9 // Timer1 OC1A
#define PIN_HI_R 10 // Timer1 OC1B
#define PIN_LO_L 7
#define PIN_LO_R 8
#define PIN_FAN 6
#define PIN_BUZZER 5
#define PIN_RELAY 4 // Relay: HIGH=Grid(Charge), LOW=Inverter
// Zero Crossing Detection Pin (Critical for synchronization)
#define PIN_ZC 13 // Digital pin for zero-crossing detection
#define BTN_PWR 2
#define BTN_ENT 3
#define BTN_UP 11
#define BTN_DWN 12
#define SENS_AC_IN A0
#define SENS_CT A1
#define SENS_BAT A2
#define SENS_NTC A3
#define SENS_FB A6 // AC Out Feedback
// --- Button Press Timing ---
#define LONG_PRESS_TIME 1500 // 1.5 seconds for long press
#define DEBOUNCE_TIME 50 // 50ms debounce time
#define SETTINGS_TIMEOUT 30000 // 30 seconds timeout for settings
#define DISPLAY_CHANGE_TIME 3000 // 3 seconds auto page change
// --- Zero Crossing Variables ---
volatile unsigned long zcTime = 0; // Time of last zero crossing
volatile unsigned long lastZcTime = 0; // Time of previous zero crossing
volatile unsigned long halfPeriod = 10000; // 10ms default for 50Hz (half period in µs)
volatile bool zcDetected = false; // Zero crossing flag
volatile bool zcPolarity = true; // true = positive half, false = negative half
volatile int phaseAdjust = 0; // Phase adjustment for synchronization
volatile unsigned long phaseError = 0; // Phase error in microseconds
bool gridStable = false; // Grid frequency stability flag
// --- Objects ---
LiquidCrystal_I2C lcd(0x27, 20, 4);
// --- Variables & Flags ---
volatile int sineTable[250]; // Half wave table
volatile int spwmIndex = 0;
volatile bool cyclePositive = true;
volatile float modulationIndex = 0.0; // 0.0 to 1.0 (Soft Start)
volatile float targetModulation = 0.85; // Feedback loop adjusts this
// Measurements
float batVolts = 0.0;
float acInVolts = 0.0;
float acOutVolts = 0.0;
float currentAmps = 0.0;
float loadWatts = 0.0;
float loadVA = 0.0;
float powerFactor = 1.0;
int tempC = 0;
float gridFreq = 0.0;
float battPower = 0.0;
float efficiency = 0.0;
unsigned long uptimeHours = 0;
unsigned long uptimeMinutes = 0;
unsigned long inverterRuntime = 0;
unsigned long chargeTime = 0;
float dailyEnergy = 0.0; // kWh
// System Stats
float minBattVoltage = 99.0;
float maxBattVoltage = 0.0;
float maxLoadWatts = 0.0;
float maxTemp = 0.0;
// Settings (Saved to EEPROM)
struct Settings {
float batLowCut;
float batFull;
float chargeCurrent;
int acMin;
int acMax;
int phaseSyncWindow; // Phase synchronization window in microseconds
int gridStableCount; // Number of stable cycles required
float bulkVoltage; // Bulk charging voltage
float floatVoltage; // Float charging voltage
int fanStartTemp; // Temperature to start fan
int fanMaxTemp; // Temperature for max fan speed
int overloadDelay; // Overload delay in milliseconds
int displayTimeout; // Display page auto-change time
float efficiencyOffset; // Efficiency calibration offset
float voltageCalibration; // Voltage calibration factor
float currentCalibration; // Current calibration factor
} settings;
// Default settings
Settings defaultSettings = {
10.5, // batLowCut
14.2, // batFull
10.0, // chargeCurrent
200, // acMin
250, // acMax
200, // phaseSyncWindow
10, // gridStableCount
14.4, // bulkVoltage
13.5, // floatVoltage
45, // fanStartTemp
70, // fanMaxTemp
5000, // overloadDelay (5 seconds)
3000, // displayTimeout (3 seconds)
0.95, // efficiencyOffset
1.0, // voltageCalibration
1.0 // currentCalibration
};
// State Machine
enum Mode { STARTUP, INVERTER, GRID_BYPASS, CHARGING, SYNCHRONIZING, ERROR_STATE, SETTINGS_MODE };
Mode currentMode = STARTUP;
String errorMsg = "";
// Charging State
enum ChargeStage { BULK, ABSORPTION, FLOAT };
ChargeStage chgStage = BULK;
// Display Pages
enum DisplayPage {
PAGE_MAIN, // Main operational data
PAGE_DETAILED, // Detailed measurements
PAGE_STATS, // System statistics
PAGE_SYSTEM // System info and uptime
};
DisplayPage currentPage = PAGE_MAIN;
DisplayPage manualPage = PAGE_MAIN; // For manual navigation
bool autoPageChange = true;
unsigned long lastPageChange = 0;
int totalPages = 4;
// Settings Menu
enum MenuPage {
MAIN_MENU,
BATTERY_SETTINGS,
AC_SETTINGS,
CHARGING_SETTINGS,
TEMP_SETTINGS,
SYSTEM_SETTINGS,
DISPLAY_SETTINGS,
CALIBRATION_SETTINGS,
SAVE_EXIT
};
MenuPage currentMenu = MAIN_MENU;
int menuItem = 0;
int maxMenuItems = 0;
bool editingValue = false;
unsigned long settingsEnterTime = 0;
bool settingsChanged = false;
// Button States
volatile bool btnEntPressed = false;
volatile bool btnEntLongPress = false;
volatile bool btnUpPressed = false;
volatile bool btnDwnPressed = false;
volatile bool btnPwrPressed = false;
unsigned long btnEntPressTime = 0;
unsigned long btnUpPressTime = 0;
unsigned long btnDwnPressTime = 0;
unsigned long btnPwrPressTime = 0;
unsigned long lastUpdate = 0;
unsigned long lastZcCheck = 0;
unsigned long startupTime = 0;
unsigned long lastEnergyUpdate = 0;
bool systemOn = false;
// --- EEPROM Functions ---
void saveSettingsToEEPROM() {
int addr = 0;
EEPROM.put(addr, settings);
// Write checksum
byte checksum = 0;
byte* data = (byte*)&settings;
for (unsigned int i = 0; i < sizeof(settings); i++) {
checksum ^= data[i];
}
EEPROM.write(addr + sizeof(settings), checksum);
// Show saved message
lcd.clear();
lcd.setCursor(0, 1);
lcd.print(" Settings Saved! ");
delay(1000);
}
bool loadSettingsFromEEPROM() {
int addr = 0;
Settings loadedSettings;
EEPROM.get(addr, loadedSettings);
// Verify checksum
byte checksum = 0;
byte* data = (byte*)&loadedSettings;
for (unsigned int i = 0; i < sizeof(loadedSettings); i++) {
checksum ^= data[i];
}
byte storedChecksum = EEPROM.read(addr + sizeof(settings));
if (checksum == storedChecksum) {
settings = loadedSettings;
return true;
} else {
// Load defaults if checksum fails
settings = defaultSettings;
saveSettingsToEEPROM();
return false;
}
}
// --- Zero Crossing Interrupt Service Routine ---
void zeroCrossingISR() {
unsigned long currentTime = micros();
// Calculate half period
if (lastZcTime > 0) {
halfPeriod = currentTime - lastZcTime;
// Check for valid 50Hz range (8ms to 12ms for half period)
if (halfPeriod >= 8000 && halfPeriod <= 12000) {
// Calculate phase error (for synchronization)
unsigned long expectedTime = lastZcTime + 10000; // Expected 10ms for 50Hz
phaseError = (currentTime > expectedTime) ? (currentTime - expectedTime) : (expectedTime - currentTime);
// Update polarity (alternates each zero crossing)
zcPolarity = !zcPolarity;
zcDetected = true;
lastZcTime = currentTime;
zcTime = currentTime;
// Calculate frequency
gridFreq = 500000.0 / halfPeriod; // 0.5 / (halfPeriod/1000000)
// Grid stability check
static int stableCount = 0;
if (phaseError < settings.phaseSyncWindow) {
stableCount++;
if (stableCount > settings.gridStableCount) {
gridStable = true;
}
} else {
stableCount = 0;
gridStable = false;
}
}
} else {
lastZcTime = currentTime;
}
}
// --- Lookup Table Generation ---
void generateSineTable() {
for (int i = 0; i < 250; i++) {
// Generate sine values scaled to Timer1 ICR (320)
// 250 steps per half cycle (50Hz / 25kHz = 500 steps total)
float rads = PI * i / 250.0;
sineTable[i] = (int)(sin(rads) * (float)ICR_VAL);
}
}
// --- Interrupt Service Routine (SPWM Core) ---
ISR(TIMER1_OVF_vect) {
if (currentMode == INVERTER || currentMode == SYNCHRONIZING) {
int duty = (int)(sineTable[spwmIndex] * modulationIndex);
// Unipolar Switching Logic
// Positive Half: Left High (PWM), Right Low (ON)
// Negative Half: Right High (PWM), Left Low (ON)
if (cyclePositive) {
OCR1A = duty; // Drive Left High
OCR1B = 0;
digitalWrite(PIN_LO_L, LOW);
digitalWrite(PIN_LO_R, HIGH);
} else {
OCR1A = 0;
OCR1B = duty; // Drive Right High
digitalWrite(PIN_LO_L, HIGH);
digitalWrite(PIN_LO_R, LOW);
}
spwmIndex++;
if (spwmIndex >= 250) {
spwmIndex = 0;
cyclePositive = !cyclePositive;
}
}
else if (currentMode == CHARGING) {
// Charging with zero-crossing synchronization
// Only start charging cycle at zero crossing
static bool chargingCycle = false;
if (zcDetected && zcPolarity) { // Start on positive zero crossing
chargingCycle = true;
zcDetected = false;
}
if (chargingCycle) {
digitalWrite(PIN_LO_L, LOW);
digitalWrite(PIN_LO_R, LOW);
// Determine charging PWM based on stage
int chargeDuty = (int)(modulationIndex * ICR_VAL);
// Apply PWM synchronized with grid
OCR1A = chargeDuty;
OCR1B = chargeDuty;
// End charging cycle after appropriate delay
static unsigned long chargeStart = 0;
if (chargeStart == 0) chargeStart = micros();
if (micros() - chargeStart > 8000) { // Charge for 8ms
OCR1A = 0;
OCR1B = 0;
chargingCycle = false;
chargeStart = 0;
}
} else {
OCR1A = 0;
OCR1B = 0;
}
}
else {
// Disable outputs
OCR1A = 0;
OCR1B = 0;
digitalWrite(PIN_LO_L, LOW);
digitalWrite(PIN_LO_R, LOW);
}
}
// --- Button Handling Functions ---
void handleButtons() {
static unsigned long lastDebounceTime = 0;
unsigned long currentTime = millis();
// --- Enter Button ---
if (digitalRead(BTN_ENT) == LOW) {
if (!btnEntPressed && (currentTime - lastDebounceTime > DEBOUNCE_TIME)) {
btnEntPressed = true;
btnEntPressTime = currentTime;
lastDebounceTime = currentTime;
// Check for long press after delay
while (digitalRead(BTN_ENT) == LOW && (millis() - btnEntPressTime < LONG_PRESS_TIME)) {
delay(10);
}
if (digitalRead(BTN_ENT) == LOW) {
// Long press detected
btnEntLongPress = true;
// Enter settings mode if not already in settings
if (currentMode != SETTINGS_MODE && currentMode != ERROR_STATE) {
currentMode = SETTINGS_MODE;
currentMenu = MAIN_MENU;
menuItem = 0;
editingValue = false;
settingsEnterTime = millis();
settingsChanged = false;
// Disable outputs while in settings
modulationIndex = 0;
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(" SETTINGS MODE ");
lcd.setCursor(0, 1);
lcd.print("Long press Exit ");
delay(500);
} else if (currentMode == SETTINGS_MODE) {
// Long press in settings mode to exit
exitSettingsMode();
}
// Wait for button release
while (digitalRead(BTN_ENT) == LOW) delay(10);
} else {
// Short press
handleEnterShortPress();
}
btnEntPressed = false;
btnEntLongPress = false;
}
} else {
btnEntPressed = false;
btnEntLongPress = false;
}
// --- Up Button ---
if (digitalRead(BTN_UP) == LOW) {
if (!btnUpPressed && (currentTime - btnUpPressTime > DEBOUNCE_TIME)) {
btnUpPressed = true;
btnUpPressTime = currentTime;
handleUpButton();
}
} else {
btnUpPressed = false;
}
// --- Down Button ---
if (digitalRead(BTN_DWN) == LOW) {
if (!btnDwnPressed && (currentTime - btnDwnPressTime > DEBOUNCE_TIME)) {
btnDwnPressed = true;
btnDwnPressTime = currentTime;
handleDownButton();
}
} else {
btnDwnPressed = false;
}
// --- Power Button ---
if (digitalRead(BTN_PWR) == LOW) {
if (!btnPwrPressed && (currentTime - btnPwrPressTime > DEBOUNCE_TIME)) {
btnPwrPressed = true;
btnPwrPressTime = currentTime;
handlePowerButton();
}
} else {
btnPwrPressed = false;
}
// Settings timeout
if (currentMode == SETTINGS_MODE && (millis() - settingsEnterTime > SETTINGS_TIMEOUT)) {
exitSettingsMode();
}
}
void handleEnterShortPress() {
if (currentMode == SETTINGS_MODE) {
if (!editingValue) {
// Enter edit mode or submenu
switch (currentMenu) {
case MAIN_MENU:
switch (menuItem) {
case 0: currentMenu = BATTERY_SETTINGS; menuItem = 0; break;
case 1: currentMenu = AC_SETTINGS; menuItem = 0; break;
case 2: currentMenu = CHARGING_SETTINGS; menuItem = 0; break;
case 3: currentMenu = TEMP_SETTINGS; menuItem = 0; break;
case 4: currentMenu = SYSTEM_SETTINGS; menuItem = 0; break;
case 5: currentMenu = DISPLAY_SETTINGS; menuItem = 0; break;
case 6: currentMenu = CALIBRATION_SETTINGS; menuItem = 0; break;
case 7: currentMenu = SAVE_EXIT; menuItem = 0; break;
}
break;
case SAVE_EXIT:
if (menuItem == 0) {
// Save and exit
if (settingsChanged) {
saveSettingsToEEPROM();
}
exitSettingsMode();
} else {
// Exit without saving
loadSettingsFromEEPROM(); // Reload original settings
exitSettingsMode();
}
break;
default:
// Enter edit mode for the current setting
editingValue = true;
break;
}
} else {
// Exit edit mode
editingValue = false;
settingsChanged = true;
}
} else {
// Toggle auto/manual page change
autoPageChange = !autoPageChange;
if (!autoPageChange) {
currentPage = manualPage;
}
lastPageChange = millis();
}
}
void handleUpButton() {
if (currentMode == SETTINGS_MODE) {
if (!editingValue) {
// Navigate menu
menuItem--;
if (menuItem < 0) menuItem = maxMenuItems - 1;
} else {
// Edit value
editValue(1); // Increase value
}
} else if (!autoPageChange) {
// Manual page navigation
manualPage = (DisplayPage)((manualPage + 1) % totalPages);
currentPage = manualPage;
lastPageChange = millis();
}
}
void handleDownButton() {
if (currentMode == SETTINGS_MODE) {
if (!editingValue) {
// Navigate menu
menuItem++;
if (menuItem >= maxMenuItems) menuItem = 0;
} else {
// Edit value
editValue(-1); // Decrease value
}
} else if (!autoPageChange) {
// Manual page navigation
manualPage = (DisplayPage)((manualPage - 1 + totalPages) % totalPages);
currentPage = manualPage;
lastPageChange = millis();
}
}
void handlePowerButton() {
if (currentMode == SETTINGS_MODE) {
// Quick exit from settings
exitSettingsMode();
} else if (currentMode == ERROR_STATE) {
// Reset from error state
currentMode = STARTUP;
systemOn = false;
errorMsg = "";
} else {
// Power toggle
systemOn = !systemOn;
if (systemOn) {
currentMode = INVERTER;
digitalWrite(PIN_BUZZER, HIGH);
delay(100);
digitalWrite(PIN_BUZZER, LOW);
} else {
currentMode = STARTUP;
modulationIndex = 0;
targetModulation = 0.85;
}
delay(300); // Debounce
}
}
void exitSettingsMode() {
currentMode = STARTUP;
systemOn = false;
lcd.clear();
lcd.setCursor(0, 1);
lcd.print(" Exiting Settings ");
delay(1000);
}
void editValue(int direction) {
switch (currentMenu) {
case BATTERY_SETTINGS:
switch (menuItem) {
case 0:
settings.batLowCut += (direction * 0.1);
if (settings.batLowCut < 8.0) settings.batLowCut = 8.0;
if (settings.batLowCut > 12.0) settings.batLowCut = 12.0;
break;
case 1:
settings.batFull += (direction * 0.1);
if (settings.batFull < 12.0) settings.batFull = 12.0;
if (settings.batFull > 15.0) settings.batFull = 15.0;
break;
}
break;
case AC_SETTINGS:
switch (menuItem) {
case 0:
settings.acMin += direction;
if (settings.acMin < 180) settings.acMin = 180;
if (settings.acMin > 230) settings.acMin = 230;
break;
case 1:
settings.acMax += direction;
if (settings.acMax < 230) settings.acMax = 230;
if (settings.acMax > 270) settings.acMax = 270;
break;
}
break;
case CHARGING_SETTINGS:
switch (menuItem) {
case 0:
settings.chargeCurrent += (direction * 0.5);
if (settings.chargeCurrent < 2.0) settings.chargeCurrent = 2.0;
if (settings.chargeCurrent > 20.0) settings.chargeCurrent = 20.0;
break;
case 1:
settings.bulkVoltage += (direction * 0.1);
if (settings.bulkVoltage < 13.0) settings.bulkVoltage = 13.0;
if (settings.bulkVoltage > 15.0) settings.bulkVoltage = 15.0;
break;
case 2:
settings.floatVoltage += (direction * 0.1);
if (settings.floatVoltage < 12.5) settings.floatVoltage = 12.5;
if (settings.floatVoltage > 14.5) settings.floatVoltage = 14.5;
break;
}
break;
case TEMP_SETTINGS:
switch (menuItem) {
case 0:
settings.fanStartTemp += direction;
if (settings.fanStartTemp < 30) settings.fanStartTemp = 30;
if (settings.fanStartTemp > 60) settings.fanStartTemp = 60;
break;
case 1:
settings.fanMaxTemp += direction;
if (settings.fanMaxTemp < 50) settings.fanMaxTemp = 50;
if (settings.fanMaxTemp > 90) settings.fanMaxTemp = 90;
break;
}
break;
case SYSTEM_SETTINGS:
switch (menuItem) {
case 0:
settings.phaseSyncWindow += (direction * 10);
if (settings.phaseSyncWindow < 50) settings.phaseSyncWindow = 50;
if (settings.phaseSyncWindow > 500) settings.phaseSyncWindow = 500;
break;
case 1:
settings.gridStableCount += direction;
if (settings.gridStableCount < 3) settings.gridStableCount = 3;
if (settings.gridStableCount > 20) settings.gridStableCount = 20;
break;
case 2:
settings.overloadDelay += (direction * 100);
if (settings.overloadDelay < 1000) settings.overloadDelay = 1000;
if (settings.overloadDelay > 10000) settings.overloadDelay = 10000;
break;
case 3:
settings.efficiencyOffset += (direction * 0.01);
if (settings.efficiencyOffset < 0.70) settings.efficiencyOffset = 0.70;
if (settings.efficiencyOffset > 1.0) settings.efficiencyOffset = 1.0;
break;
}
break;
case DISPLAY_SETTINGS:
if (menuItem == 0) {
settings.displayTimeout += (direction * 500);
if (settings.displayTimeout < 1000) settings.displayTimeout = 1000;
if (settings.displayTimeout > 10000) settings.displayTimeout = 10000;
}
break;
case CALIBRATION_SETTINGS:
switch (menuItem) {
case 0:
settings.voltageCalibration += (direction * 0.01);
if (settings.voltageCalibration < 0.80) settings.voltageCalibration = 0.80;
if (settings.voltageCalibration > 1.20) settings.voltageCalibration = 1.20;
break;
case 1:
settings.currentCalibration += (direction * 0.01);
if (settings.currentCalibration < 0.80) settings.currentCalibration = 0.80;
if (settings.currentCalibration > 1.20) settings.currentCalibration = 1.20;
break;
}
break;
}
}
// --- Display Settings Menu ---
void displaySettingsMenu() {
lcd.clear();
switch (currentMenu) {
case MAIN_MENU:
maxMenuItems = 8;
lcd.setCursor(0, 0);
lcd.print(">>MAIN MENU<< ");
lcd.setCursor(0, 1);
lcd.print(menuItem == 0 ? "> " : " ");
lcd.print("Battery Settings");
lcd.setCursor(0, 2);
lcd.print(menuItem == 1 ? "> " : " ");
lcd.print("AC Settings ");
lcd.setCursor(0, 3);
if (menuItem == 2) {
lcd.print("> Charging Settings");
} else {
lcd.print(" Charging Settings");
}
break;
case BATTERY_SETTINGS:
maxMenuItems = 2;
lcd.setCursor(0, 0);
lcd.print(">>BATTERY SETTINGS<<");
lcd.setCursor(0, 1);
lcd.print(menuItem == 0 ? "> " : " ");
lcd.print("Low Cut: ");
lcd.print(settings.batLowCut, 1);
lcd.print("V ");
lcd.setCursor(0, 2);
lcd.print(menuItem == 1 ? "> " : " ");
lcd.print("Full: ");
lcd.print(settings.batFull, 1);
lcd.print("V ");
lcd.setCursor(0, 3);
lcd.print(editingValue ? "Edit Mode - Use U/D" : "Press ENT to edit ");
break;
case AC_SETTINGS:
maxMenuItems = 2;
lcd.setCursor(0, 0);
lcd.print(">>AC SETTINGS<< ");
lcd.setCursor(0, 1);
lcd.print(menuItem == 0 ? "> " : " ");
lcd.print("AC Min: ");
lcd.print(settings.acMin);
lcd.print("V ");
lcd.setCursor(0, 2);
lcd.print(menuItem == 1 ? "> " : " ");
lcd.print("AC Max: ");
lcd.print(settings.acMax);
lcd.print("V ");
lcd.setCursor(0, 3);
lcd.print(editingValue ? "Edit Mode - Use U/D" : "Press ENT to edit ");
break;
case CHARGING_SETTINGS:
maxMenuItems = 3;
lcd.setCursor(0, 0);
lcd.print(">>CHARGING SETTINGS<");
lcd.setCursor(0, 1);
lcd.print(menuItem == 0 ? "> " : " ");
lcd.print("Current: ");
lcd.print(settings.chargeCurrent, 1);
lcd.print("A ");
lcd.setCursor(0, 2);
lcd.print(menuItem == 1 ? "> " : " ");
lcd.print("Bulk V: ");
lcd.print(settings.bulkVoltage, 1);
lcd.print("V ");
lcd.setCursor(0, 3);
if (menuItem == 2) {
lcd.print("> Float V: ");
lcd.print(settings.floatVoltage, 1);
lcd.print("V ");
} else {
lcd.print(" Float V: ");
lcd.print(settings.floatVoltage, 1);
lcd.print("V ");
}
break;
case TEMP_SETTINGS:
maxMenuItems = 2;
lcd.setCursor(0, 0);
lcd.print(">>TEMP SETTINGS<< ");
lcd.setCursor(0, 1);
lcd.print(menuItem == 0 ? "> " : " ");
lcd.print("Fan Start: ");
lcd.print(settings.fanStartTemp);
lcd.print("C ");
lcd.setCursor(0, 2);
lcd.print(menuItem == 1 ? "> " : " ");
lcd.print("Fan Max: ");
lcd.print(settings.fanMaxTemp);
lcd.print("C ");
lcd.setCursor(0, 3);
lcd.print(editingValue ? "Edit Mode - Use U/D" : "Press ENT to edit ");
break;
case SYSTEM_SETTINGS:
maxMenuItems = 4;
lcd.setCursor(0, 0);
lcd.print(">>SYSTEM SETTINGS<<");
lcd.setCursor(0, 1);
lcd.print(menuItem == 0 ? "> " : " ");
lcd.print("Sync Window: ");
lcd.print(settings.phaseSyncWindow);
lcd.print("uS");
lcd.setCursor(0, 2);
lcd.print(menuItem == 1 ? "> " : " ");
lcd.print("Stable Count: ");
lcd.print(settings.gridStableCount);
lcd.print(" ");
lcd.setCursor(0, 3);
if (menuItem == 2) {
lcd.print("> O/L Delay: ");
lcd.print(settings.overloadDelay / 1000);
lcd.print("s ");
} else if (menuItem == 3) {
lcd.print("> Efficiency: ");
lcd.print(settings.efficiencyOffset * 100, 0);
lcd.print("% ");
} else {
lcd.print(" O/L Delay: ");
lcd.print(settings.overloadDelay / 1000);
lcd.print("s ");
}
break;
case DISPLAY_SETTINGS:
maxMenuItems = 1;
lcd.setCursor(0, 0);
lcd.print(">>DISPLAY SETTINGS<");
lcd.setCursor(0, 1);
lcd.print("Page Time: ");
lcd.print(settings.displayTimeout / 1000);
lcd.print("s ");
lcd.setCursor(0, 2);
lcd.print("Auto: ");
lcd.print(autoPageChange ? "ON " : "OFF");
lcd.print(" Manual: ");
lcd.print(!autoPageChange ? "ON" : "OFF");
lcd.setCursor(0, 3);
lcd.print(editingValue ? "Edit: Use UP/DOWN " : "ENT:Toggle Auto/M");
break;
case CALIBRATION_SETTINGS:
maxMenuItems = 2;
lcd.setCursor(0, 0);
lcd.print(">>CALIBRATION<< ");
lcd.setCursor(0, 1);
lcd.print(menuItem == 0 ? "> " : " ");
lcd.print("Voltage: ");
lcd.print(settings.voltageCalibration, 2);
lcd.print(" ");
lcd.setCursor(0, 2);
lcd.print(menuItem == 1 ? "> " : " ");
lcd.print("Current: ");
lcd.print(settings.currentCalibration, 2);
lcd.print(" ");
lcd.setCursor(0, 3);
lcd.print(editingValue ? "Edit Mode - Use U/D" : "Press ENT to edit ");
break;
case SAVE_EXIT:
maxMenuItems = 2;
lcd.setCursor(0, 0);
lcd.print(">>SAVE & EXIT<< ");
lcd.setCursor(0, 1);
lcd.print(menuItem == 0 ? "> " : " ");
lcd.print("Save & Exit ");
lcd.setCursor(0, 2);
lcd.print(menuItem == 1 ? "> " : " ");
lcd.print("Exit Without Save ");
lcd.setCursor(0, 3);
lcd.print("Long press to exit");
break;
}
}
// --- Update Display Pages ---
void updateDisplayPage() {
lcd.clear();
switch (currentPage) {
case PAGE_MAIN:
// Page 1: Main Operational Data
lcd.setCursor(0, 0);
if (currentMode == INVERTER) {
lcd.print("INV:ON ");
lcd.print("MOD:");
lcd.print(modulationIndex * 100, 0);
lcd.print("% ");
} else if (currentMode == CHARGING) {
lcd.print("CHG:");
if (chgStage == BULK) lcd.print("BULK ");
else if (chgStage == ABSORPTION) lcd.print("ABS ");
else lcd.print("FLT ");
lcd.print("D:");
lcd.print(modulationIndex * 100, 0);
lcd.print("% ");
} else if (currentMode == GRID_BYPASS) {
lcd.print("GRID:BYPASS ");
} else if (currentMode == SYNCHRONIZING) {
lcd.print("SYNC:GRID ");
lcd.print(phaseError);
lcd.print("uS ");
} else if (currentMode == ERROR_STATE) {
lcd.print("ERR:");
lcd.print(errorMsg);
} else {
lcd.print("STANDBY ");
}
lcd.setCursor(0, 1);
lcd.print("OUT:");
lcd.print(acOutVolts, 0);
lcd.print("V ");
lcd.print(loadWatts, 0);
lcd.print("W ");
lcd.print(currentAmps, 1);
lcd.print("A");
lcd.setCursor(0, 2);
lcd.print("BAT:");
lcd.print(batVolts, 1);
lcd.print("V ");
if (currentMode == INVERTER) {
battPower = batVolts * currentAmps;
lcd.print(battPower, 0);
lcd.print("W ");
} else if (currentMode == CHARGING) {
lcd.print("CHG:");
lcd.print(currentAmps, 1);
lcd.print("A");
} else {
lcd.print("---");
}
lcd.setCursor(0, 3);
lcd.print("TEMP:");
lcd.print(tempC);
lcd.print("C IN:");
lcd.print(acInVolts, 0);
lcd.print("V ");
if (!autoPageChange) {
lcd.print("M1");
}
break;
case PAGE_DETAILED:
// Page 2: Detailed Measurements
lcd.setCursor(0, 0);
lcd.print("DETAILED MEASURE ");
lcd.setCursor(0, 1);
lcd.print("Vrms:");
lcd.print(acOutVolts, 1);
lcd.print("V ");
lcd.print("PF:");
lcd.print(powerFactor, 2);
lcd.setCursor(0, 2);
lcd.print("VA:");
lcd.print(loadVA, 0);
lcd.print(" Freq:");
lcd.print(gridFreq, 1);
lcd.print("Hz");
lcd.setCursor(0, 3);
lcd.print("EFF:");
lcd.print(efficiency * 100, 0);
lcd.print("% ");
if (gridStable) {
lcd.print("GRID:OK");
} else {
lcd.print("GRID:--");
}
if (!autoPageChange) {
lcd.print(" M2");
}
break;
case PAGE_STATS:
// Page 3: System Statistics
lcd.setCursor(0, 0);
lcd.print("SYSTEM STATISTICS ");
lcd.setCursor(0, 1);
lcd.print("BAT:");
lcd.print(minBattVoltage, 1);
lcd.print("-");
lcd.print(maxBattVoltage, 1);
lcd.print("V");
lcd.setCursor(0, 2);
lcd.print("LOAD:");
lcd.print(maxLoadWatts, 0);
lcd.print("W TEMP:");
lcd.print(maxTemp, 0);
lcd.print("C");
lcd.setCursor(0, 3);
lcd.print("ENERGY:");
lcd.print(dailyEnergy, 1);
lcd.print("kWh");
if (!autoPageChange) {
lcd.print(" M3");
}
break;
case PAGE_SYSTEM:
// Page 4: System Info
lcd.setCursor(0, 0);
lcd.print("SYSTEM INFORMATION");
lcd.setCursor(0, 1);
lcd.print("UPTIME:");
if (uptimeHours > 0) {
lcd.print(uptimeHours);
lcd.print("h ");
}
lcd.print(uptimeMinutes);
lcd.print("m");
lcd.setCursor(0, 2);
lcd.print("INV RT:");
lcd.print(inverterRuntime / 1000 / 60);
lcd.print("m CHG:");
lcd.print(chargeTime / 1000 / 60);
lcd.print("m");
lcd.setCursor(0, 3);
lcd.print("PAGE:");
lcd.print(currentPage + 1);
lcd.print("/");
lcd.print(totalPages);
lcd.print(" MOD:");
lcd.print(modulationIndex * 100, 0);
lcd.print("%");
if (!autoPageChange) {
lcd.print(" M4");
}
break;
}
}
// --- Update Statistics ---
void updateStatistics() {
static unsigned long lastStatUpdate = 0;
if (millis() - lastStatUpdate > 1000) {
lastStatUpdate = millis();
// Update battery stats
if (batVolts < minBattVoltage) minBattVoltage = batVolts;
if (batVolts > maxBattVoltage) maxBattVoltage = batVolts;
// Update load stats
if (loadWatts > maxLoadWatts) maxLoadWatts = loadWatts;
// Update temperature stats
if (tempC > maxTemp) maxTemp = tempC;
// Update uptime
if (startupTime > 0) {
unsigned long uptimeSeconds = (millis() - startupTime) / 1000;
uptimeHours = uptimeSeconds / 3600;
uptimeMinutes = (uptimeSeconds % 3600) / 60;
// Update inverter runtime
if (currentMode == INVERTER) {
inverterRuntime += 1000;
}
// Update charging time
if (currentMode == CHARGING) {
chargeTime += 1000;
}
}
// Update energy consumption
if (lastEnergyUpdate > 0) {
float hoursPassed = (millis() - lastEnergyUpdate) / 3600000.0; // Convert to hours
dailyEnergy += (loadWatts * hoursPassed) / 1000.0; // Convert to kWh
}
lastEnergyUpdate = millis();
// Calculate power factor and efficiency
loadVA = acOutVolts * currentAmps;
if (loadVA > 0) {
powerFactor = loadWatts / loadVA;
if (powerFactor > 1.0) powerFactor = 1.0;
}
if (currentMode == INVERTER && loadWatts > 0) {
efficiency = (loadWatts / (batVolts * currentAmps)) * settings.efficiencyOffset;
if (efficiency > 1.0) efficiency = 1.0;
}
}
}
void setup() {
Serial.begin(9600);
// Pin Setup
pinMode(PIN_HI_L, OUTPUT);
pinMode(PIN_HI_R, OUTPUT);
pinMode(PIN_LO_L, OUTPUT);
pinMode(PIN_LO_R, OUTPUT);
pinMode(PIN_RELAY, OUTPUT);
pinMode(PIN_FAN, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
// Zero Crossing Pin Setup
pinMode(PIN_ZC, INPUT_PULLUP); // Assuming active LOW signal
pinMode(BTN_PWR, INPUT_PULLUP);
pinMode(BTN_ENT, INPUT_PULLUP);
pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DWN, INPUT_PULLUP);
// Attach Zero Crossing Interrupt
attachInterrupt(digitalPinToInterrupt(PIN_ZC), zeroCrossingISR, FALLING);
// Load settings from EEPROM
if (!loadSettingsFromEEPROM()) {
Serial.println("Loaded default settings");
}
// Initialize statistics
startupTime = millis();
lastEnergyUpdate = millis();
// LCD Init
lcd.begin(20,4);
lcd.backlight();
lcd.setCursor(0,0);
lcd.print(" MNG proSystems ");
lcd.setCursor(0,1);
lcd.print(" Inverter UPS v2.0" );
lcd.setCursor(0,2);
lcd.print("4-Page Display Sys ");
lcd.setCursor(0,3);
lcd.print("U/D:Pages ENT:Auto/M");
generateSineTable();
// Timer1 Setup for Phase Correct PWM 25kHz
TCCR1A = 0;
TCCR1B = 0;
TCCR1A = (1 << COM1A1) | (1 << COM1B1) | (1 << WGM11); // Non-inverted PWM
TCCR1B = (1 << WGM13) | (1 << CS10); // Phase Correct, ICR1 Top, No Prescaling
ICR1 = ICR_VAL;
TIMSK1 |= (1 << TOIE1); // Enable Overflow Interrupt
delay(2000);
lcd.clear();
}
void loop() {
readSensors();
handleButtons();
updateStatistics();
if (currentMode == SETTINGS_MODE) {
// Update settings menu display
static unsigned long lastMenuUpdate = 0;
if (millis() - lastMenuUpdate > 200) {
displaySettingsMenu();
lastMenuUpdate = millis();
}
} else {
handleFan();
stateMachine();
// Update Display with auto page change
if (millis() - lastUpdate > 500) {
if (autoPageChange) {
// Auto cycle through pages
if (millis() - lastPageChange > settings.displayTimeout) {
currentPage = (DisplayPage)((currentPage + 1) % totalPages);
lastPageChange = millis();
}
}
updateDisplayPage();
lastUpdate = millis();
}
}
checkGridSync();
}
// --- Grid Synchronization Check ---
void checkGridSync() {
static unsigned long lastCheck = 0;
if (millis() - lastCheck > 1000 && currentMode != SETTINGS_MODE) {
lastCheck = millis();
if (gridStable && halfPeriod > 0) {
// Grid frequency is stable
if (currentMode == SYNCHRONIZING) {
// Try to synchronize phase
synchronizePhase();
}
} else {
gridStable = false;
}
}
}
// --- Phase Synchronization Function ---
void synchronizePhase() {
// Adjust SPWM index to match grid phase
if (phaseError > 100) { // If error > 100µs
if (micros() - zcTime < halfPeriod / 2) {
// We're in first half of cycle
int targetIndex = map(micros() - zcTime, 0, halfPeriod, 0, 250);
if (abs(spwmIndex - targetIndex) > 2) {
// Adjust index gradually
if (spwmIndex < targetIndex) spwmIndex++;
else if (spwmIndex > targetIndex) spwmIndex--;
// Also adjust polarity
cyclePositive = zcPolarity;
}
}
// If synchronized within tolerance
if (phaseError < 50) { // 50µs tolerance
currentMode = GRID_BYPASS;
digitalWrite(PIN_RELAY, HIGH); // Switch to grid
modulationIndex = 0; // Stop SPWM
}
}
}
// --- Sensor Reading & Math ---
void readSensors() {
static unsigned long lastSensorRead = 0;
if (millis() - lastSensorRead > 100) {
lastSensorRead = millis();
// Battery with moving average
static float batVoltsSum = 0;
static int batReadCount = 0;
batVoltsSum += analogRead(SENS_BAT) * (20.0 / 1023.0);
batReadCount++;
if (batReadCount >= 5) {
batVolts = (batVoltsSum / 5) * settings.voltageCalibration;
batVoltsSum = 0;
batReadCount = 0;
}
// Temp
int tempRaw = analogRead(SENS_NTC);
tempC = map(tempRaw, 0, 1023, 0, 100);
// AC Input Voltage
acInVolts = analogRead(SENS_AC_IN) * (350.0 / 1023.0) * settings.voltageCalibration;
// AC Output Voltage Feedback
acOutVolts = analogRead(SENS_FB) * (350.0 / 1023.0) * settings.voltageCalibration;
// Current (Offset 2.5V = 512)
int iRaw = analogRead(SENS_CT) - 512;
currentAmps = (abs(iRaw) * (50.0 / 512.0)) * 0.707 * settings.currentCalibration; // Approx RMS
loadWatts = acOutVolts * currentAmps;
}
}
// --- Core Logic ---
void stateMachine() {
// 1. Safety Checks always active
if (tempC > 75) {
currentMode = ERROR_STATE;
errorMsg = "OVER HEAT";
}
static unsigned long overloadStart = 0;
if (loadWatts > 6100) {
if (overloadStart == 0) {
overloadStart = millis();
} else if (millis() - overloadStart > settings.overloadDelay) {
currentMode = ERROR_STATE;
errorMsg = "OVERLOAD";
overloadStart = 0;
}
} else {
overloadStart = 0;
}
switch (currentMode) {
case STARTUP:
digitalWrite(PIN_RELAY, LOW); // Default Inverter
if (systemOn) currentMode = INVERTER;
break;
case INVERTER:
digitalWrite(PIN_RELAY, LOW); // Relay OFF (Inverter Mode)
// Mains Check
if (acInVolts >= settings.acMin && acInVolts <= settings.acMax && gridStable) {
// Grid is good and stable, prepare for synchronization
modulationIndex = targetModulation; // Maintain voltage
currentMode = SYNCHRONIZING;
return;
}
// Battery Check
if (batVolts < settings.batLowCut) {
currentMode = ERROR_STATE;
errorMsg = "LOW BATT";
return;
}
// Soft Start & Feedback Logic
if (modulationIndex < targetModulation) modulationIndex += 0.01;
// Voltage Feedback Regulator (Simple P-Control)
if (acOutVolts < 230) targetModulation += 0.001;
if (acOutVolts > 232) targetModulation -= 0.001;
if (targetModulation > 0.95) targetModulation = 0.95;
if (targetModulation < 0.70) targetModulation = 0.70;
break;
case SYNCHRONIZING:
// Inverter continues running while synchronizing phase
// checkGridSync() will handle the synchronization
// Timeout after 5 seconds
static unsigned long syncStart = 0;
if (syncStart == 0) syncStart = millis();
if (millis() - syncStart > 5000) {
// Failed to sync, stay in inverter mode
currentMode = INVERTER;
syncStart = 0;
}
break;
case GRID_BYPASS:
digitalWrite(PIN_RELAY, HIGH); // Relay ON (Grid Pass-through)
if (!gridStable || acInVolts < settings.acMin || acInVolts > settings.acMax) {
// Grid lost or unstable
digitalWrite(PIN_RELAY, LOW);
currentMode = INVERTER;
modulationIndex = 0; // Reset for soft start
return;
}
// Check if charging needed
if (batVolts < settings.batFull) {
currentMode = CHARGING;
}
break;
case CHARGING:
digitalWrite(PIN_RELAY, HIGH);
// Ensure grid is still stable
if (!gridStable) {
currentMode = INVERTER;
chgStage = BULK;
return;
}
// 3 Stage Logic using settings
if (chgStage == BULK) {
if (batVolts >= settings.bulkVoltage) chgStage = ABSORPTION;
if (currentAmps < settings.chargeCurrent) modulationIndex += 0.001;
else modulationIndex -= 0.001;
}
else if (chgStage == ABSORPTION) {
if (currentAmps < 1.0) chgStage = FLOAT;
if (batVolts > settings.batFull) modulationIndex -= 0.001;
else modulationIndex += 0.001;
}
else {
if (batVolts > settings.floatVoltage) modulationIndex -= 0.001;
else modulationIndex += 0.001;
}
// Cap duty cycle
if (modulationIndex > 0.4) modulationIndex = 0.4;
if (modulationIndex < 0) modulationIndex = 0;
// Exit Charging if Mains bad
if (!gridStable) currentMode = INVERTER;
break;
case ERROR_STATE:
modulationIndex = 0;
digitalWrite(PIN_RELAY, LOW);
static unsigned long lastBeep = 0;
if (millis() - lastBeep > 1000) {
digitalWrite(PIN_BUZZER, HIGH);
delay(100);
digitalWrite(PIN_BUZZER, LOW);
lastBeep = millis();
}
break;
}
}
void handleFan() {
if (tempC > settings.fanStartTemp) {
int fanSpeed = map(tempC, settings.fanStartTemp, settings.fanMaxTemp, 100, 255);
if (fanSpeed > 255) fanSpeed = 255;
if (fanSpeed < 0) fanSpeed = 0;
analogWrite(PIN_FAN, fanSpeed);
} else {
analogWrite(PIN_FAN, 0);
}
}