/*
* Arduino-Nano 12 V UPS / INVERTER
* 50 Hz SPWM 23.4 kHz carrier – 3-stage charger – 20×4 I²C LCD
* ONE button ON/OFF – NTC + FAN – single CT – mains detect relay
*
* License: MIT – use on your own risk
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <math.h>
LiquidCrystal_I2C lcd(0x27, 20, 4); // I²C address change if required
//------------------------- USER CONSTANTS ---------------------------
const float VBULK = 14.4;
const float VFLOAT = 13.5;
const float VBAT_LOW = 10.8; // inverter shutdown
const float VBAT_HIGH = 15.0; // over-voltage shutdown
const float TEMP_MAX = 70.0; // °C on heatsink
const float CT_BURDEN = 33.0; // ohm
const float CT_RATIO = 1000.0; // 1000:1 CT
const float ACSENS = (5.0/1023.0);
//------------------------ PIN DEFINITIONS ---------------------------
#define H1 3
#define L1 4
#define H2 5
#define L2 6
#define FAN 7
#define RELAY 8
#define CHG_PWM 10 // charger buck
#define BTN 2
#define BEEP 9
//------------------------ GLOBALS ----------------------------------
volatile bool systemON = false;
volatile bool mainsPresent = false;
volatile bool chargerMode = false;
const uint8_t sineTable[256] PROGMEM = {
128,131,134,137,140,143,146,149,152,155,158,161,164,167,170,173,
176,179,182,184,187,190,193,195,198,201,203,206,208,211,213,216,
218,220,223,225,227,229,231,233,235,237,239,241,242,244,246,247,
249,250,251,253,254,255,255,256,257,258,258,259,259,260,260,260,
260,260,260,260,259,259,258,258,257,256,255,255,254,253,251,250,
249,247,246,244,242,241,239,237,235,233,231,229,227,225,223,220,
218,216,213,211,208,206,203,201,198,195,193,190,187,184,182,179,
176,173,170,167,164,161,158,155,152,149,146,143,140,137,134,131,
128,125,122,119,116,113,110,107,104,101, 98, 95, 92, 89, 86, 83,
80, 77, 74, 72, 69, 66, 63, 61, 58, 55, 53, 50, 48, 45, 43, 40,
38, 36, 33, 31, 29, 27, 25, 23, 21, 19, 17, 15, 14, 12, 10, 9,
7, 6, 5, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3,
5, 6, 7, 9,10,12,14,15,17,19,21,23,25,27,29,31,
33,36,38,40,43,45,48,50,53,55,58,61,63,66,69,72,
74,77,80,83,86,89,92,95, 98,101,104,107,110,113,116,119,122,125
};
volatile uint16_t spwmIndex = 0;
//------------------------ PROTOTYPES -------------------------------
void timer2Init();
float readVbat();
float readTemp();
float readAmps();
bool readMains();
void updateLCD(float vbat, float amp, float temp);
void beep(uint8_t n);
void shutdown();
void updateCharger(float vbat);
//------------------------ SETUP ------------------------------------
void setup() {
// Initialize pins
pinMode(H1, OUTPUT);
pinMode(L1, OUTPUT);
pinMode(H2, OUTPUT);
pinMode(L2, OUTPUT);
pinMode(FAN, OUTPUT);
pinMode(RELAY, OUTPUT);
pinMode(CHG_PWM, OUTPUT);
pinMode(BEEP, OUTPUT);
pinMode(BTN, INPUT_PULLUP);
// Ensure all outputs are OFF
digitalWrite(H1, LOW);
digitalWrite(L1, LOW);
digitalWrite(H2, LOW);
digitalWrite(L2, LOW);
digitalWrite(FAN, LOW);
digitalWrite(RELAY, LOW);
digitalWrite(CHG_PWM, LOW);
digitalWrite(BEEP, LOW);
// Initialize LCD
Wire.begin();
lcd.init();
lcd.backlight();
lcd.setCursor(0,0); lcd.print("Nano-UPS 50Hz");
lcd.setCursor(0,1); lcd.print(" 23.4k SPWM");
delay(1500);
timer2Init(); // start SPWM
ADC_ENABLE();
beep(2);
}
//------------------------ MAIN LOOP --------------------------------
void loop() {
static uint32_t tmr = 0;
static uint32_t chargerTmr = 0;
if (millis() - tmr >= 200) { // 5 Hz refresh
tmr = millis();
float vbat = readVbat();
float temp = readTemp();
float amp = readAmps();
mainsPresent = readMains();
//----- Button ON/OFF (long press >700 ms) ----------------------
static bool lastBtn = HIGH;
static uint32_t btnT = 0;
bool b = digitalRead(BTN);
if (!b && lastBtn) { // Button pressed
btnT = millis();
}
if (b && !lastBtn && (millis() - btnT > 700)) { // Released after long press
systemON = !systemON;
if (systemON) beep(1);
}
lastBtn = b;
//----- Protection checks ---------------------------------------
if (vbat < VBAT_LOW || vbat > VBAT_HIGH || temp > TEMP_MAX) {
shutdown();
}
//----- Mode selection ------------------------------------------
if (mainsPresent) {
digitalWrite(RELAY, HIGH); // mains route
chargerMode = true;
} else {
digitalWrite(RELAY, LOW); // inverter route
chargerMode = false;
digitalWrite(CHG_PWM, LOW); // Turn off charger
}
//----- Charger control -----------------------------------------
if (chargerMode && systemON) {
if (millis() - chargerTmr >= 100) { // 10 Hz charger update
chargerTmr = millis();
updateCharger(vbat);
}
} else {
digitalWrite(CHG_PWM, LOW);
}
//----- FAN control ---------------------------------------------
digitalWrite(FAN, temp > 45);
//----- LCD update ----------------------------------------------
updateLCD(vbat, amp, temp);
}
}
//------------------------ ADC READINGS -----------------------------
float readVbat() {
uint16_t a = analogRead(A0);
return a * ACSENS * 4.0 * (16.0 / 15.0); // divider 1/4 + calib
}
float readTemp() {
uint16_t a = analogRead(A1);
if (a == 0) return 25.0; // Avoid division by zero
float r = 10000.0 * (1023.0 / a - 1.0); // NTC 10k @25°C β=3950
float t = 1.0 / (log(r/10000.0)/3950.0 + 1.0/298.15) - 273.15;
return t;
}
float readAmps() {
uint16_t a = analogRead(A2);
float v = (a - 512) * ACSENS;
return abs(v) * CT_RATIO / CT_BURDEN; // amps
}
bool readMains() {
uint16_t a = analogRead(A3);
return (a > 400); // ~1 V AC detected
}
//------------------------ CHARGER CONTROL --------------------------
void updateCharger(float vbat) {
static uint8_t pwmValue = 0;
if (vbat < VBULK) {
// Bulk charging
pwmValue = 255; // Full power
} else if (vbat < VFLOAT) {
// Absorption charging
pwmValue = 128; // Half power
} else {
// Float charging
pwmValue = 64; // Low power
}
analogWrite(CHG_PWM, pwmValue);
}
//------------------------ LCD --------------------------------------
void updateLCD(float vbat, float amp, float temp) {
lcd.setCursor(0,0);
lcd.print("Vbat "); lcd.print(vbat,1); lcd.print("V ");
lcd.setCursor(0,1);
lcd.print("Amp "); lcd.print(amp,1); lcd.print("A ");
lcd.setCursor(0,2);
lcd.print("Temp "); lcd.print(temp,0); lcd.print("C ");
lcd.setCursor(0,3);
if (systemON) {
lcd.print(mainsPresent ? "UPS " : "INV ");
} else {
lcd.print("OFF ");
}
// Calculate load percentage (assuming max 10A = 100%)
int loadPercent = constrain((int)(amp * 10), 0, 100);
lcd.print("Load "); lcd.print(loadPercent); lcd.print("% ");
}
//------------------------ BEEP / SHUTDOWN ---------------------------
void beep(uint8_t n) {
for(uint8_t i = 0; i < n; i++) {
digitalWrite(BEEP, HIGH);
delay(80);
digitalWrite(BEEP, LOW);
delay(80);
}
}
void shutdown() {
systemON = false;
digitalWrite(H1, LOW);
digitalWrite(L1, LOW);
digitalWrite(H2, LOW);
digitalWrite(L2, LOW);
digitalWrite(CHG_PWM, LOW);
digitalWrite(RELAY, LOW);
lcd.clear();
lcd.setCursor(0,0);
lcd.print("SYSTEM SHUTDOWN!");
lcd.setCursor(0,1);
lcd.print("Check battery/temp");
beep(6);
while(1) {
// Flash LED or provide status indication
digitalWrite(BEEP, HIGH);
delay(100);
digitalWrite(BEEP, LOW);
delay(2000);
}
}
//------------------------ TIMER-2 SPWM ------------------------------
ISR(TIMER2_OVF_vect) {
if (!systemON || chargerMode) { // mute bridge when charging or off
OCR2A = 0;
OCR2B = 0;
digitalWrite(H1, LOW);
digitalWrite(L1, LOW);
digitalWrite(H2, LOW);
digitalWrite(L2, LOW);
return;
}
uint8_t sa = pgm_read_byte(&sineTable[spwmIndex]);
uint8_t sb = pgm_read_byte(&sineTable[(spwmIndex + 128) & 255]);
OCR2A = sa; // PWM 1
OCR2B = sb; // PWM 2
// H-bridge control with dead time
if (sa > 5) {
digitalWrite(L1, LOW);
digitalWrite(H1, HIGH);
} else {
digitalWrite(H1, LOW);
digitalWrite(L1, HIGH);
}
if (sb > 5) {
digitalWrite(L2, LOW);
digitalWrite(H2, HIGH);
} else {
digitalWrite(H2, LOW);
digitalWrite(L2, HIGH);
}
spwmIndex = (spwmIndex + 1) & 255; // Wrap around
}
void timer2Init() {
// Timer-2 fast-pwm 23.4 kHz non-inverting
TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM20) | _BV(WGM21);
TCCR2B = _BV(WGM22) | _BV(CS20); // prescaler = 1
OCR2A = 0; // Start with PWM off
OCR2B = 0;
TIMSK2 = _BV(TOIE2); // enable overflow interrupt
}
// MACROS (moved to end to avoid potential issues)
#define ADC_ENABLE() (ADCSRA |= _BV(ADEN))
#define ADC_START() (ADCSRA |= _BV(ADSC))