#include <avr/io.h>
#include <avr/interrupt.h>
#include <Wire.h>
#include <EEPROM.h>
#include <LiquidCrystal_I2C.h>
#include <avr/pgmspace.h>
#include <math.h>
#include <avr/wdt.h> // <<< Watchdog include
/* ===================== LCD ===================== */
LiquidCrystal_I2C lcd(0x27,20,4);
bool lcd_ok=false;
String lcd_line_cache[4]={"","","",""};
void lcd_update_line(uint8_t row,const String &raw){
// kalo gak ada LCD, skip aja bro
if(!lcd_ok||row>3) return;
String s=raw; if((int)s.length()>20) s.remove(20);
while((int)s.length()<20) s+=' ';
if(lcd_line_cache[row]!=s){ lcd.setCursor(0,row); lcd.print(s); lcd_line_cache[row]=s; }
}
void lcd_safe_clear(){ if(!lcd_ok) return; for(uint8_t i=0;i<4;i++) lcd_line_cache[i]=""; lcd.clear(); }
void printRight_cached(int row,int rightEdge,const String &txt){
if(!lcd_ok) return;
String line=lcd_line_cache[row]; while(line.length()<20) line+=' ';
int col=rightEdge-(int)txt.length()+1; if(col<0) col=0; if(col>19) col=19;
for(int i=0;i<(int)txt.length() && (col+i)<20;i++) line.setCharAt(col+i,txt[i]);
lcd_update_line(row,line);
}
/* ===================== Pins ===================== */
/* ADC pins */
const uint8_t PIN_VBAT=A0, PIN_VFB=A1, PIN_IFB=A2, PIN_TFB=A3;
/* Proteksi eksternal LM393 -> INT0 (D2 = PIN 4) */
const uint8_t PIN_PROTEK = 2; // PIN 4 / INT0, LM393 output (open-collector) -> use pull-up
/* Tombol: UP dipindah ke PD6 sesuai instruksi */
// Back=pin 11, Down=pin 13, UP=pin 12, SET=pin 6
const uint8_t PIN_BTN_UP = 6, PIN_BTN_DOWN=7, PIN_BTN_SET=4, PIN_BTN_BACK=5;
/* PWM pins */
const uint8_t HO1_50HZ=9, LO1_50HZ=10; //1HO=PIN 15 - 1LO=PIN 16
const uint8_t LO2_20KHZ=3, HO2_20KHZ=11; //2LO=PIN 5 - 2HO=PIN 17
const uint8_t FAN_PIN=12, BUZZER_PIN=13; //FAN=PIN 18 - BUZZER=PIN 19
/* Tambahan LED (menggunakan D0/D1 - RX/TX). Upload via ISP aman */
const uint8_t LED_RUNNING_PIN = 0; // D0 (RX)
const uint8_t LED_PROTEK_PIN = 1; // D1 (TX)
/* ===================== Settings ===================== */
struct Settings{
uint16_t magic;
int16_t setVoltOut,setPowerLimit,setBattHigh,setBattLow;
int16_t setTempLimit,calibTemp,calibI_gx100,calibBatt;
int16_t calVout_scale_gx1000,calVout_offset_tenths;
int16_t pi_kp_gx1000,pi_ki_gx1000;
uint8_t buzzerOn,deadtime_us,battMode,battType,lcdTimeoutSec,fanOnTemp,autoRecSec;
int16_t battRecover_tenths;
int16_t shortSens_Amp;
int16_t calibI_offset_mA; // **BARU** offset kalibrasi IFB dalam mA (-5000..5000)
} S;
const unsigned long RESET_HOLD_MS = 3000UL;
const unsigned long FACTORY_HOLD_MS = 5000UL;
void loadDefaults(){
// default bro, kalo EEPROM ga di-set
S.magic=0xBEEF;
S.setVoltOut=220; S.setPowerLimit=10000;
S.setBattHigh=900; S.setBattLow=120;
S.setTempLimit=65; S.calibTemp=0; S.calibI_gx100=100; // default 100% (scale)
S.calibBatt=0;
S.calVout_scale_gx1000=1000; S.calVout_offset_tenths=0;
S.pi_kp_gx1000=900; S.pi_ki_gx1000=80;
S.buzzerOn=1; S.deadtime_us=2; S.battMode=12; S.battType=0;
S.lcdTimeoutSec=30; S.fanOnTemp=45; S.autoRecSec=5;
S.battRecover_tenths = S.setBattLow + 10;
S.shortSens_Amp = 25;
S.calibI_offset_mA = 0; // default offset 0 mA
}
void loadEEPROM(){
EEPROM.get(0,S);
if(S.magic!=0xBEEF){ loadDefaults(); EEPROM.put(0,S); }
if(S.setVoltOut<110) S.setVoltOut=110; if(S.setVoltOut>300) S.setVoltOut=300;
if(S.setBattHigh<120) S.setBattHigh=120; if(S.setBattHigh>900) S.setBattHigh=900;
if(S.setBattLow<105) S.setBattLow=105; if(S.setBattLow>S.setBattHigh-5) S.setBattLow=S.setBattHigh-5;
if(!(S.battMode==12||S.battMode==24||S.battMode==48||S.battMode==60)) S.battMode=12;
if(S.battType>1) S.battType=0; if(S.fanOnTemp<20) S.fanOnTemp=20; if(S.fanOnTemp>90) S.fanOnTemp=90;
if(S.autoRecSec>120) S.autoRecSec=120; if(S.pi_kp_gx1000<0) S.pi_kp_gx1000=0; if(S.pi_ki_gx1000<0) S.pi_ki_gx1000=0;
if(S.battRecover_tenths < (S.setBattLow+1)) S.battRecover_tenths = S.setBattLow+1;
if(S.battRecover_tenths > S.setBattHigh) S.battRecover_tenths = S.setBattHigh;
if(S.shortSens_Amp < 0) S.shortSens_Amp = 0;
if(S.shortSens_Amp > 100) S.shortSens_Amp = 100;
// clamp new offset mA
if(S.calibI_offset_mA < -5000) S.calibI_offset_mA = -5000;
if(S.calibI_offset_mA > 5000) S.calibI_offset_mA = 5000;
}
void saveEEPROM(){ EEPROM.put(0,S); }
/* ===================== Runtime ===================== */
float vBatt=0,vOut=0,iOut=0,tempReading=0,pOut=0;
volatile bool protekActive=false; unsigned long protekStart=0;
volatile bool outputsEnabled=true;
bool inverterEnabled=true;
unsigned long lastBtn=0; const unsigned long menuTimeout=8000;
unsigned long lastToggle=0; bool showMax=true;
struct ProtCount{ uint16_t ovBatt,uvBatt,overTemp,overPower,ovOut,lvOut,shortC,acTrip; } PCount;
/* display toggles */
unsigned long lastRow0Toggle = 0;
bool showRow0Limit = true;
unsigned long lastRightSwap = 0;
bool showRightAmp = false;
bool showRightFreq = false;
/* scaling */
const float VBAT_k=(5.0/1023.0)*3.0*10.0; // scaling buat ADC A0
const float VOUT_BASE_K=220.0/((3.0/5.0)*1023.0);
/* icons */
byte iconVolt[8]={B00100,B11111,B10101,B00100,B11111,B10101,B00100,B01110};
byte iconPower[8]={B01010,B01010,B11111,B11111,B01110,B00100,B00100,B00100};
byte iconThermo[8]={B01110,B01010,B01010,B01110,B01110,B11111,B11111,B01110};
byte iconBattHead[8]={B01110,B11111,B10001,B10011,B10111,B11111,B11111,B11111};
byte iconBlk[8]={B01110,B11111,B10001,B10011,B10111,B11111,B11111,B11111};
byte iconEmp[8]={B11111,B10001,B10001,B10001,B10001,B10001,B10001,B11111};
/* ===================== Buttons ===================== */
int menuIndex=-1; bool editMode=false;
enum{IDX_UP=0,IDX_DOWN=1,IDX_SET=2,IDX_BACK=3};
struct BtnState { uint8_t pin; bool lastLevel; unsigned long firstDownMs; unsigned long lastFireMs; bool firedOnPress; } BTN[4];
// ---- tuned timings for snappier response (non-blocking) ----
const unsigned long BTN_DEBOUNCE_MS = 20; // was 30
const unsigned long BTN_INITIAL_HOLD_MS = 300; // was 500
const unsigned long BTN_REPEAT_MS = 60; // was 100
void btns_init(){ BTN[IDX_UP] = { PIN_BTN_UP, HIGH,0,0,false }; BTN[IDX_DOWN] = { PIN_BTN_DOWN, HIGH,0,0,false }; BTN[IDX_SET] = { PIN_BTN_SET, HIGH,0,0,false }; BTN[IDX_BACK] = { PIN_BTN_BACK, HIGH,0,0,false }; }
bool read_pin_level(uint8_t pin){ return digitalRead(pin); }
// ---------------- Buzzer non-blocking helper ----------------
bool buzzerActive = false;
unsigned long buzzerStartMs = 0;
const unsigned long BUZZER_SHORT_MS = 80;
void beep_nonblocking() {
if(S.buzzerOn && !buzzerActive){
digitalWrite(BUZZER_PIN, HIGH);
buzzerActive = true;
buzzerStartMs = millis();
}
}
void updateBuzzer() {
if(buzzerActive && (millis() - buzzerStartMs >= BUZZER_SHORT_MS)){
digitalWrite(BUZZER_PIN, LOW);
buzzerActive = false;
}
}
// ------------------------------------------------------------
uint8_t process_buttons(){
uint8_t events = 0;
unsigned long now = millis();
for(int i=0;i<4;i++){
bool level = read_pin_level(BTN[i].pin);
bool pressed = (level == LOW);
if(pressed && !BTN[i].lastLevel){
BTN[i].firstDownMs = now; BTN[i].lastFireMs = now; BTN[i].firedOnPress = true; events |= (1<<i);
} else if(pressed && BTN[i].lastLevel){
unsigned long held = now - BTN[i].firstDownMs;
if(BTN[i].firedOnPress && held >= BTN_INITIAL_HOLD_MS){
if(now - BTN[i].lastFireMs >= BTN_REPEAT_MS){
events |= (1<<i); BTN[i].lastFireMs = now;
}
}
} else if(!pressed && BTN[i].lastLevel){
BTN[i].firedOnPress=false; BTN[i].firstDownMs=BTN[i].lastFireMs=0;
}
BTN[i].lastLevel = level;
}
return events;
}
/* ===================== Baterai helper ===================== */
void battRange(float &vmin,float &vmax){
if(S.battType==0){
if(S.battMode==12){ vmin=10.8; vmax=12.6; } else if(S.battMode==24){ vmin=21.6; vmax=25.2; } else if(S.battMode==48){ vmin=43.2; vmax=50.4; } else { vmin=54.0; vmax=63.0; }
} else {
if(S.battMode==12){ vmin=10.0; vmax=14.6; } else if(S.battMode==24){ vmin=20.0; vmax=29.2; } else if(S.battMode==48){ vmin=40.0; vmax=58.4; } else { vmin=50.0; vmax=73.0; }
}
}
int battPercent(){
float vmin,vmax; battRange(vmin,vmax);
float lo=S.setBattLow/10.0, hi=S.setBattHigh/10.0;
if(lo>vmin) vmin=lo; if(hi<vmax) vmax=hi; if(vmax<=vmin) vmax=vmin+0.1;
long pct=map((long)(vBatt*10),(long)(vmin*10),(long)(vmax*10),0,100);
if(pct<0) pct=0; if(pct>100) pct=100; return (int)pct;
}
#define PIN_ARUS PIN_IFB
// ADC midpoint is dynamic: measured at startup if hardware has no offset
static uint16_t ADC_MID = 512; // initial default; may be updated in autoCalibrateADCmid()
#define K_CT_30A_1V 0.1466f // A per ADC RMS count (calibratable) - keep dulu
#define ARUS_NOISE_FLOOR 0.02f // 20 mA
#define ARUS_FILTER_ALPHA 0.25f // smoothing alpha for display (made a bit faster)
static float iOut_filtered = 0.0f; // used for display & control
static float iOut_display_buf = 0.0f;
static unsigned long last_ct_sample_us = 0;
static uint32_t ct_sumSq = 0;
static uint16_t ct_sampleCount = 0;
static const uint16_t CT_AGG_SAMPLES = 200; // ~0.1s at 2kHz sampling
static const uint16_t CT_SAMPLE_US = 500; // 500us between samples => ~2kHz
// Read ADC channel (0..7 for ADC0..ADC7) using registers (blocking until conversion done)
// Added small delay to allow MUX settle when switching channels
static inline uint16_t adc_read_channel_blocking(uint8_t ch) {
ADMUX = (ADMUX & 0xF0) | (ch & 0x0F); // preserve REFS bits set elsewhere
delayMicroseconds(20); // allow mux/track to settle (helps accuracy)
ADCSRA |= (1<<ADSC);
while (ADCSRA & (1<<ADSC)) { /* busy wait ~100us */ }
uint16_t v = ADC; // read result
return v;
}
// Startup auto-calibration to measure ADC midpoint (useful if no hardware offset/bias)
// IMPORTANT: Requires no load current when called (run at boot with inverter idle)
// It samples ADC on PIN_ARUS for 200 ms and averages to estimate midpoint.
void autoCalibrateADCmid() {
const unsigned long samplePeriodMs = 200;
const unsigned long end = millis() + samplePeriodMs;
unsigned long count = 0;
uint32_t sum = 0;
while (millis() < end) {
uint16_t v = adc_read_channel_blocking(2); // A2
sum += v;
count++;
delay(2);
}
if (count > 0) {
uint16_t avg = (uint16_t)(sum / count);
ADC_MID = avg;
// clamp to safe range
if (ADC_MID < 200) ADC_MID = 200;
if (ADC_MID > 824) ADC_MID = 824;
// store to EEPROM? not doing to preserve original flow; user can save if needed
}
}
// Non-blocking style aggregator called from loop()
void updateCurrent_CT30A1V_nonblocking(){
unsigned long us = micros();
if((unsigned long)(us - last_ct_sample_us) < CT_SAMPLE_US) return;
last_ct_sample_us = us;
// Select channel corresponding to PIN_ARUS (A2 -> ADC2)
const uint8_t adc_ch = 2; // A2 -> ADC2
// Do one conversion to settle ADC after channel change and discard
(void)adc_read_channel_blocking(adc_ch);
// Actual conversion to use
uint16_t a = adc_read_channel_blocking(adc_ch);
// center about dynamic ADC_MID
int16_t centered = (int16_t)a - (int16_t)ADC_MID;
// accumulate square (use 32-bit for safety)
uint32_t sq = (uint32_t)((int32_t)centered * (int32_t)centered);
if (ct_sumSq + sq < ct_sumSq) ct_sumSq = 0; // overflow protection
else ct_sumSq += sq;
ct_sampleCount++;
if(ct_sampleCount >= CT_AGG_SAMPLES){
float meanSq = (float)ct_sumSq / (float)ct_sampleCount;
float vrms_counts = sqrtf(meanSq);
float Irms = vrms_counts * K_CT_30A_1V; // hasil dalam Ampere berdasarkan factor
// apply scale calibration (persen-like)
Irms *= (S.calibI_gx100 / 100.0f);
// apply user-set offset kalibrasi (mA) -> convert to A
Irms += ((float)S.calibI_offset_mA) / 1000.0f;
if(Irms < ARUS_NOISE_FLOOR) Irms = 0.0f;
// exponential smoothing biar tampilanannya cakep
iOut_filtered = (ARUS_FILTER_ALPHA * Irms) + ((1.0f - ARUS_FILTER_ALPHA) * iOut_filtered);
iOut = iOut_filtered; // update global dipake di tempat lain
// reset aggregator
ct_sumSq = 0;
ct_sampleCount = 0;
}
}
// ===================== <<< END REVISED ARUS =====================================================
/* ===================== Sensor & Proteksi ===================== */
void applyProtection(bool on, const String &reason);
String currentProtMsg = "";
bool protekLowBatt = false;
/* ===================== EXTERNAL LM393 PROTEK (INT0) ===================== */
/* Volatile flags changed by ISR (very small ISR body) */
volatile bool ext_protek_flag = false;
volatile unsigned long ext_protek_time_ms = 0; // updated in loop, not in ISR
const unsigned long DEBOUNCE_EXT_MS = 100UL; // debounce window for ext trips (handled in loop)
/* INT0 ISR: very lightweight. Use AVR ISR macro. */
ISR(INT0_vect){
PCount.shortC++;
ext_protek_flag = true;
TCCR1A &= ~((1<<COM1A1)|(1<<COM1B1));
OCR1A = 0; OCR1B = 0;
digitalWrite(HO2_20KHZ, LOW); digitalWrite(LO2_20KHZ, LOW);
digitalWrite(HO1_50HZ, LOW); digitalWrite(LO1_50HZ, LOW);
digitalWrite(LED_PROTEK_PIN, HIGH);
digitalWrite(LED_RUNNING_PIN, LOW);
}
/* ===================== Sensor read ===================== */
/* IMPORTANT: vBatt from A0, vOut from A1, IFB via ADC aggregator on A2. */
void readSensors(){
int aBatt=analogRead(PIN_VBAT); // A0 only for battery
int aVout=analogRead(PIN_VFB); // A1 only for output voltage
int aTemp=analogRead(PIN_TFB); // A3 temperature sensor ADC reading
// VBAT scaling (unchanged style)
vBatt = aBatt * VBAT_k + (S.calibBatt/10.0);
// VOUT scaling (unchanged style)
float scale = S.calVout_scale_gx1000 / 1000.0f;
vOut = (aVout * VOUT_BASE_K) * scale + (S.calVout_offset_tenths/10.0f);
// IFB: aggregator ngupdate iOut via updateCurrent_CT30A1V_nonblocking()
const float Vref = 5.0f;
float Vntc = (float)aTemp * (Vref / 1023.0f);
tempReading = -99.0f;
if (aTemp > 4 && aTemp < 1018) {
const float Rpullup = 10000.0f;
float denom = (Vref - Vntc);
if (fabs(denom) > 0.0001f) {
float R0 = 10000.0f, T0 = 298.15f, Beta = 3950.0f;
float Rntc = (Rpullup * Vntc) / denom;
if (Rntc > 0.0f) {
float tempK = 1.0f / ( (1.0/T0) + (1.0/Beta) * log(Rntc / R0) );
tempReading = (tempK - 273.15f) + S.calibTemp;
}
}
}
const int ADC_TFB_THRESHOLD = 860;
static unsigned long over_start_ms = 0;
if (aTemp >= ADC_TFB_THRESHOLD) {
if (over_start_ms == 0) over_start_ms = millis();
if (millis() - over_start_ms >= 200) {
applyProtection(true, "Suhu Terlalu Tinggi"); over_start_ms = 0;
}
} else {
over_start_ms = 0;
}
if(vOut <= 1) vOut = S.setVoltOut;
pOut = vOut * iOut;
if(!lcd_ok) return;
}
/* ===================== Proteksi & kipas ===================== */
void showProtectScreen(const String &reason){
if(!lcd_ok) return;
lcd_safe_clear();
lcd_update_line(0," PROTEKSI AKTIF ");
lcd_update_line(2, reason);
String l3 = (S.autoRecSec>0)? ("AutoRec:"+String((int)S.autoRecSec)+"s") : ("SET=Reset (hold)");
lcd_update_line(3, l3);
}
void applyProtection(bool on, const String &reason){
if(on){
protekActive = true; protekStart = millis();
currentProtMsg = reason;
outputsEnabled = false;
inverterEnabled = false;
TCCR1A &= ~((1<<COM1A1)|(1<<COM1B1));
OCR1A = 0; OCR1B = 0;
digitalWrite(HO2_20KHZ, LOW); digitalWrite(LO2_20KHZ, LOW);
digitalWrite(HO1_50HZ, LOW); digitalWrite(LO1_50HZ, LOW);
if(S.buzzerOn){ digitalWrite(BUZZER_PIN, HIGH); }
digitalWrite(LED_PROTEK_PIN, HIGH);
digitalWrite(LED_RUNNING_PIN, LOW);
protekLowBatt = (reason.indexOf("LOW BATT") != -1);
showProtectScreen(reason);
} else {
protekActive = false; currentProtMsg = "";
protekLowBatt = false;
outputsEnabled = true;
inverterEnabled = true;
TCCR1A |= (1<<COM1A1)|(1<<COM1B1);
digitalWrite(BUZZER_PIN, LOW);
digitalWrite(LED_PROTEK_PIN, LOW);
digitalWrite(LED_RUNNING_PIN, HIGH);
lcd_safe_clear();
}
}
void checkProtection(){
bool trip=false; String cause="";
if(vBatt < (S.setBattLow/10.0)){ trip=true; cause="PERINGATAN LOW BATT"; PCount.uvBatt++; }
if(vBatt > (S.setBattHigh/10.0)){ trip=true; cause="PERINGATAN OVER VOLTAGE"; PCount.ovBatt++; }
if(tempReading > S.setTempLimit){ trip=true; cause="PERINGATAN OVER TEMPERATURE"; PCount.overTemp++; }
if(pOut > S.setPowerLimit){ trip=true; cause="PERINGATAN OVER DAYA"; PCount.overPower++; }
if(vOut > S.setVoltOut*1.25){ trip=true; cause="PERINGATAN OVER VOLTASE"; PCount.ovOut++; }
if(vOut < S.setVoltOut*0.60){ trip=true; cause="PERINGATAN LOW VOLTASE"; PCount.lvOut++; }
if(vOut >= 300.0f){ trip=true; cause="Periksa Kabel Sensor"; PCount.ovOut++; }
float iEstLimit = (S.setPowerLimit>0 && vOut>10)? (S.setPowerLimit / vOut) : 99999;
float Ithreshold = (S.shortSens_Amp>0)? (float)S.shortSens_Amp : (iEstLimit*1.5f);
static unsigned long shortStart = 0;
if(iOut > Ithreshold + 50.0f){
PCount.shortC++; trip = true; cause = "Protek Arus Berlebih";
} else if(iOut > Ithreshold){
if(shortStart==0) shortStart = millis();
else if(millis() - shortStart >= 200){ PCount.shortC++; trip = true; cause = " Arus Melebihi Batas"; }
} else shortStart = 0;
if(trip){
if(!protekActive){ applyProtection(true, cause); }
else { if(cause != currentProtMsg){ currentProtMsg = cause; showProtectScreen(cause); } }
} else {
if(protekActive){
if(protekLowBatt){
float recoverV = S.battRecover_tenths / 10.0f;
if(vBatt >= recoverV){ applyProtection(false, ""); }
} else if(S.autoRecSec>0){
if(millis() - protekStart >= (unsigned long)S.autoRecSec * 1000UL){
if(currentProtMsg.indexOf("SHORT") != -1){
if(digitalRead(PIN_PROTEK) == HIGH){ applyProtection(false, ""); }
else protekStart = millis();
} else {
bool acStill = true;
if(!acStill) applyProtection(false, ""); else protekStart = millis();
}
}
}
}
}
/* Fan control with hysteresis */
static bool fanOn=false;
if(tempReading >= (float)S.fanOnTemp){
if(!fanOn){ digitalWrite(FAN_PIN, HIGH); fanOn=true; }
}
else if(tempReading <= (float)S.fanOnTemp - 10){
if(fanOn){ digitalWrite(FAN_PIN, LOW); fanOn=false; }
}
}
/* ===================== Tampilan HOME ===================== */
void drawBattBar6_at(int col,int row,int percent){
int blocks = map(constrain(percent,0,100),0,100,0,6);
lcd.setCursor(col,row);
lcd.write((uint8_t)5);
for(int i=0;i<6;i++) lcd.write((uint8_t)(i<blocks?0:1));
}
float getOutputFreq(){
const float prescaler = 1024.0f;
const float top = (float)(OCR2A + 1);
float f = (float)F_CPU / (2.0f * prescaler * top);
return f;
}
void showHome(){
if(!lcd_ok) return;
// Row 0: vOut left, ON:vBatt right
{
String left=""; left+=char(2); left+=" "; left+=String((int)vOut); left+="V";
String right = "ON:" + String(vBatt,1) + "V";
lcd_update_line(0,left); printRight_cached(0,19,right);
}
// Row 1: power left, current right (real-time IFB ditampilin di Home juga)
{
String left=""; left+=char(3); left+=" "; left+=String((int)pOut); left+="W";
String right = String(iOut_filtered,2) + "A"; // tampil 2 decimal, biar cakep
lcd_update_line(1,left); printRight_cached(1,19,right);
}
// Row 2: batt percent + bar
{
int pct = battPercent();
String line=""; while((int)line.length()<20) line+=' ';
int barStart = 13;
String ps = String(pct) + "%";
int psEnd = barStart - 2;
int psStart = psEnd - (int)ps.length() + 1; if(psStart<0) psStart=0;
String left=""; left+=char(5); left+=" "; left+=String(vBatt,1); left+="V";
if((int)left.length()>psStart) left.remove(psStart);
for(int i=0;i<(int)left.length() && i<20;i++) line.setCharAt(i,left[i]);
for(int i=0;i<(int)ps.length() && (psStart+i)<20;i++) line.setCharAt(psStart+i,ps[i]);
lcd_update_line(2,line);
if(lcd_ok){ drawBattBar6_at(barStart,2,pct); }
}
// Row 3: temp left, freq right
{
String left=""; left+=char(4); left+=" ";
if(tempReading < -50.0f){
left += "---";
String right = "ADC:" + String(analogRead(PIN_TFB));
lcd_update_line(3,left); printRight_cached(3,19,right);
} else {
left += String((int)tempReading);
left += (char)223; left += "C";
lcd_update_line(3,left);
String right;
if(showRightFreq){ right = String(getOutputFreq(),1) + "Hz"; }
else { right = "LT:"+String(S.setTempLimit)+String((char)223)+"C"; }
printRight_cached(3,19,right);
}
}
}
/* ===================== Menu ===================== */
enum MenuItem{
MENU_VOUT=0,MENU_LIMIT_DAYA,MENU_BATT_HIGH,MENU_BATT_LOW,MENU_BATT_REC,
MENU_TIPE_BATRAI,MENU_MODE_BATRAI,MENU_AC_AUTO_REC,MENU_DEADTIME,
MENU_KP,MENU_KI,MENU_FAN_ON,MENU_TEMP_LIMIT,MENU_LCD_TIMEOUT,
MENU_CAL_VOUT,MENU_CAL_ARUS,MENU_CAL_BATRAI,MENU_CAL_TEMP,
MENU_SHORT_SENS,MENU_COUNT
};
const char* menuNames[MENU_COUNT]={
"1.Volt AC","2.Limit Daya","3.Batt Batas Atas","4.Batt Batas Bawah","5.Batt Recover",
"6.Type Batrai","7.Mode Batrai","8.Auto ON","9.Deadtime","10.Kp","11.Ki","12.Kipas ON",
"13.Temperatur Limit","14.LCD Timeout","15.Kalibrasi Voltase","16.Kalibrasi Amper",
"17.Kalibrasi VBatt","18.Kalibrasi SUHU","19.Adjus Sensitiv"
};
void showSetting(){
if(!lcd_ok) return;
String header= editMode? "[EDIT] ":"[PENGATURAN] ";
lcd_update_line(0,header);
int idx=(menuIndex<0)?0:menuIndex; if(idx>=MENU_COUNT) idx=MENU_COUNT-1;
lcd_update_line(1,String(menuNames[idx]));
String val="";
switch(idx){
case MENU_VOUT: val=String(S.setVoltOut); break;
case MENU_LIMIT_DAYA: val=String(S.setPowerLimit); break;
case MENU_BATT_HIGH: val=String(S.setBattHigh/10.0,1); break;
case MENU_BATT_LOW: val=String(S.setBattLow/10.0,1); break;
case MENU_BATT_REC: val=String(S.battRecover_tenths/10.0,1); break;
case MENU_TIPE_BATRAI: val=(S.battType==0?"Lead Acid":"LiFePO4"); break;
case MENU_MODE_BATRAI: val=String(S.battMode)+"V"; break;
case MENU_AC_AUTO_REC: val=String(S.autoRecSec)+"s"; break;
case MENU_DEADTIME: val=String(S.deadtime_us)+"us"; break;
case MENU_KP: val=String(S.pi_kp_gx1000/1000.0,3); break;
case MENU_KI: val=String(S.pi_ki_gx1000/1000.0,3); break;
case MENU_FAN_ON: val=String(S.fanOnTemp)+String((char)223)+"C"; break;
case MENU_TEMP_LIMIT: val=String(S.setTempLimit)+String((char)223)+"C"; break;
case MENU_LCD_TIMEOUT: val=String(S.lcdTimeoutSec); break;
/* ==== TAMPILAN KALIBRASI 0–100% ==== */
case MENU_CAL_VOUT: {
int pct = map(S.calVout_scale_gx1000, 800, 1200, 0, 100);
if(pct<0) pct=0; if(pct>100) pct=100;
val = "-->: " + String(pct) + "%";
break;
}
case MENU_CAL_ARUS: {
// tampilkan offset mA (-5000..5000) DAN pembacaan realtime
int off = S.calibI_offset_mA;
val = String(off) + "mA I:" + String(iOut_filtered*1000.0,1) + "mA"; // live reading mA
break;
}
case MENU_CAL_BATRAI: {
int pct = map(S.calibBatt, -50, 50, 0, 100);
if(pct<0) pct=0; if(pct>100) pct=100;
val = "-->: " + String(pct) + "%";
break;
}
case MENU_CAL_TEMP: {
int pct = map(S.calibTemp, -20, 20, 0, 100);
if(pct<0) pct=0; if(pct>100) pct=100;
val = "-->: " + String(pct) + "%";
break;
}
case MENU_SHORT_SENS: { val = String(S.shortSens_Amp) + "A"; break; }
}
lcd_update_line(2,val);
lcd_update_line(3, editMode? "UP/DN=Ubah SET=OK":"UP/DN=Pilih SET=Edit");
}
int constrain_int(int v,int lo,int hi){ if(v<lo) return lo; if(v>hi) return hi; return v; }
void adjustValue(int idx,int dir){
switch(idx){
case MENU_VOUT: S.setVoltOut=constrain_int(S.setVoltOut+dir*1,110,300); break;
case MENU_LIMIT_DAYA: S.setPowerLimit=constrain_int(S.setPowerLimit+dir*50,100,10000); break;
case MENU_BATT_HIGH: S.setBattHigh=constrain_int(S.setBattHigh+dir*1,120,900); if(S.setBattLow>S.setBattHigh-5) S.setBattLow=S.setBattHigh-5; if(S.battRecover_tenths>S.setBattHigh) S.battRecover_tenths=S.setBattHigh; break;
case MENU_BATT_LOW: S.setBattLow=constrain_int(S.setBattLow+dir*1,105,S.setBattHigh-5); if(S.battRecover_tenths<S.setBattLow+1) S.battRecover_tenths=S.setBattLow+1; break;
case MENU_BATT_REC: { int minv=S.setBattLow+1; int maxv=S.setBattHigh; S.battRecover_tenths=constrain_int(S.battRecover_tenths+dir*1,minv,maxv); } break;
case MENU_TIPE_BATRAI: S.battType^=1; break;
case MENU_MODE_BATRAI: { int m=S.battMode; if(dir>0){ if(m==12) m=24; else if(m==24) m=48; else if(m==48) m=60; } else { if(m==60) m=48; else if(m==48) m=24; else if(m==24) m=12; } S.battMode=m; } break;
case MENU_AC_AUTO_REC: S.autoRecSec=constrain_int(S.autoRecSec+dir*1,0,120); break;
case MENU_DEADTIME: { int n=S.deadtime_us+dir*2; if(n<2) n=2; if(n>6) n=6; if(n==3||n==5) n+=1; S.deadtime_us=(uint8_t)n; } break;
case MENU_KP: S.pi_kp_gx1000=constrain_int(S.pi_kp_gx1000+dir*10,0,5000); break;
case MENU_KI: S.pi_ki_gx1000=constrain_int(S.pi_ki_gx1000+dir*2,0,2000); break;
case MENU_FAN_ON: S.fanOnTemp=constrain_int(S.fanOnTemp+dir*1,20,90); break;
case MENU_TEMP_LIMIT: S.setTempLimit=constrain_int(S.setTempLimit+dir*1,30,90); break;
case MENU_LCD_TIMEOUT: S.lcdTimeoutSec=constrain_int(S.lcdTimeoutSec+dir*1,0,255); break;
/* Nilai asli tetap disesuaikan, tapi tampilan diubah ke % */
case MENU_CAL_VOUT: S.calVout_scale_gx1000=constrain_int(S.calVout_scale_gx1000+dir*5,800,1200); break;
case MENU_CAL_ARUS: // **UBAH**: sekarang mengatur offset mA (-5000..5000)
S.calibI_offset_mA = constrain_int(S.calibI_offset_mA + dir*50, -5000, 5000);
break;
case MENU_CAL_BATRAI: S.calibBatt=constrain_int(S.calibBatt+dir*1,-50,50); break;
case MENU_CAL_TEMP: S.calibTemp=constrain_int(S.calibTemp+dir*1,-20,20); break;
case MENU_SHORT_SENS: { S.shortSens_Amp = constrain_int(S.shortSens_Amp + dir*1, 0, 100); break; }
}
saveEEPROM(); showSetting();
}
void handleButtons_events(uint8_t events){
bool any=false;
static bool resetTrig=false;
static bool factoryTrig=false;
static unsigned long factoryStart=0;
bool upEvent = events & (1<<IDX_UP);
bool downEvent = events & (1<<IDX_DOWN);
bool setEvent = events & (1<<IDX_SET);
bool backEvent = events & (1<<IDX_BACK);
static unsigned long setHoldStart = 0;
bool setPressed = (digitalRead(PIN_BTN_SET) == LOW);
if(setPressed){
if(setHoldStart==0) setHoldStart = millis();
else {
unsigned long held = millis() - setHoldStart;
if(protekActive){
if(held>=3000 && !resetTrig){
lcd_safe_clear();
lcd_update_line(0," INFO PROTEKSI ");
lcd_update_line(1,currentProtMsg);
if(S.buzzerOn){ beep_nonblocking(); }
resetTrig=true; lastBtn=millis();
}
} else {
if(held>=RESET_HOLD_MS && !resetTrig){
PCount={0,0,0,0,0,0,0,0};
if(lcd_ok){ lcd_safe_clear(); lcd_update_line(1," Counter Protek "); lcd_update_line(2," DI-RESET "); }
if(S.buzzerOn){ beep_nonblocking(); }
applyProtection(false,""); resetTrig=true; lastBtn=millis();
}
}
}
} else { setHoldStart = 0; resetTrig=false; }
bool backPressed = (digitalRead(PIN_BTN_BACK) == LOW);
if(setPressed && backPressed){
if(factoryStart==0) factoryStart = millis();
if(!factoryTrig && (millis()-factoryStart)>=FACTORY_HOLD_MS){
applyProtection(false,"");
loadDefaults(); saveEEPROM();
if(lcd_ok){
lcd_safe_clear();
lcd_update_line(1," FACTORY RESET ");
lcd_update_line(2," SELESAI (OK) ");
}
if(S.buzzerOn){
beep_nonblocking();
}
factoryTrig=true; lastBtn=millis();
}
} else { factoryStart=0; factoryTrig=false; }
if(upEvent){
if(menuIndex<0){ menuIndex=0; editMode=false; showSetting(); }
else if(editMode){ adjustValue(menuIndex,+1); }
else { menuIndex++; if(menuIndex>=MENU_COUNT) menuIndex=0; showSetting(); }
any=true; lastBtn=millis();
if(S.buzzerOn) beep_nonblocking();
}
if(downEvent){
if(menuIndex<0){ menuIndex=MENU_COUNT-1; editMode=false; showSetting(); }
else if(editMode){ adjustValue(menuIndex,-1); }
else { menuIndex--; if(menuIndex<0) menuIndex=MENU_COUNT-1; showSetting(); }
any=true; lastBtn=millis();
if(S.buzzerOn) beep_nonblocking();
}
if(setEvent){
if(protekActive){ applyProtection(false,""); menuIndex=-1; showHome(); }
else if(menuIndex<0){ menuIndex=0; editMode=false; showSetting(); }
else { editMode=!editMode; showSetting(); }
any=true; lastBtn=millis();
if(S.buzzerOn) beep_nonblocking();
}
if(backEvent){
if(editMode){ editMode=false; showSetting(); }
else { menuIndex=-1; showHome(); }
any=true; lastBtn=millis();
if(S.buzzerOn) beep_nonblocking();
}
if((millis()-lastBtn)>menuTimeout){ if(menuIndex>=0){ menuIndex=-1; showHome(); } }
}
/* ===================== SPWM LUT & PWM handling ===================== */
/* Keep ICR1 = 799 for 20kHz carrier */
#ifndef F_CPU
#define F_CPU 16000000UL
#endif
#define PWM_CARRIER_FREQ 20000UL
#define SINE_FREQ 50UL
#define SINE_SAMPLES 256
const uint16_t ICR1_VAL = 799; // already used across code
// 256-point full-cycle sine LUT stored in PROGMEM (0..255 centered)
const uint8_t sineTable256[SINE_SAMPLES] PROGMEM = {
128,131,134,137,140,143,146,149,
152,156,159,162,165,168,171,174,
177,180,183,186,188,191,194,197,
199,202,205,207,210,212,215,217,
219,222,224,226,228,231,233,235,
237,238,240,242,244,245,247,248,
250,251,252,253,254,254,255,255,
255,255,255,255,255,254,254,253,
252,251,250,248,247,245,244,242,
240,238,237,235,233,231,228,226,
224,222,219,217,215,212,210,207,
205,202,199,197,194,191,188,186,
183,180,177,174,171,168,165,162,
159,156,152,149,146,143,140,137,
134,131,128,124,121,118,115,112,
109,106,103, 99, 96, 93, 90, 87,
84, 81, 78, 75, 72, 70, 67, 64,
61, 59, 56, 53, 51, 48, 46, 43,
41, 39, 36, 34, 32, 30, 27, 25,
23, 21, 20, 18, 16, 14, 13, 11,
10, 8, 7, 6, 5, 5, 4, 4,
4, 4, 4, 4, 4, 4, 5, 5,
6, 7, 8, 10, 11, 13, 14, 16,
18, 20, 21, 23, 25, 27, 30, 32,
34, 36, 39, 41, 43, 46, 48, 51,
53, 56, 59, 61, 64, 67, 70, 72,
75, 78, 81, 84, 87, 90, 93, 96,
99,103,106,109,112,115,118,121,
124
};
// runtime variables for SPWM
volatile uint16_t spwm_index = 0;
volatile bool spwm_toggle_phase = false; // toggled each full-sine cycle
volatile float spwm_amplitude = 0.8f; // 0..1 amplitude controlled by PI
/* Timer2 ISR: drives SPWM LUT index at SINE_FREQ * SINE_SAMPLES */
ISR(TIMER2_COMPA_vect){
if(outputsEnabled && inverterEnabled && !protekActive){
uint8_t sample = pgm_read_byte(&sineTable256[spwm_index]);
uint16_t center = ICR1_VAL / 2;
int16_t delta = (int16_t)sample - 128;
int32_t scaled = center + (int32_t)( (float)delta * ( (float)ICR1_VAL/256.0f ) * spwm_amplitude );
if(scaled < 1) scaled = 1;
if(scaled > (ICR1_VAL-1)) scaled = ICR1_VAL-1;
OCR1A = (uint16_t)scaled;
OCR1B = (uint16_t)(ICR1_VAL - scaled);
spwm_index++;
if(spwm_index >= SINE_SAMPLES){
spwm_index = 0;
spwm_toggle_phase = !spwm_toggle_phase;
if(spwm_toggle_phase){ digitalWrite(HO1_50HZ, HIGH); digitalWrite(LO1_50HZ, LOW); }
else { digitalWrite(HO1_50HZ, LOW); digitalWrite(LO1_50HZ, HIGH); }
}
} else {
OCR1A = 0; OCR1B = 0;
digitalWrite(HO1_50HZ, LOW); digitalWrite(LO1_50HZ, LOW);
}
}
/* ===================== PWM setup (Timer1 & Timer2) ===================== */
volatile bool t2_toggle=false;
void setupPWM(){
pinMode(LO2_20KHZ, OUTPUT); pinMode(HO2_20KHZ, OUTPUT);
TCCR1A = 0; TCCR1B = 0; TCNT1 = 0;
TCCR1A |= (1<<WGM11);
TCCR1B |= (1<<WGM12) | (1<<WGM13);
TCCR1A |= (1<<COM1A1) | (1<<COM1B1);
ICR1 = ICR1_VAL;
OCR1A = ICR1/2;
OCR1B = ICR1/2;
TCCR1B |= (1<<CS10);
pinMode(HO1_50HZ,OUTPUT); pinMode(LO1_50HZ,OUTPUT);
digitalWrite(HO1_50HZ,LOW); digitalWrite(LO1_50HZ,HIGH);
TCCR2A = 0; TCCR2B = 0; TCNT2 = 0;
TCCR2A |= (1<<WGM21);
OCR2A = (uint8_t)((F_CPU / (8UL * (SINE_FREQ * SINE_SAMPLES))) - 1);
TIMSK2 |= (1<<OCIE2A);
TCCR2B |= (1<<CS21);
}
/* ===================== PI control (amplitude control) ===================== */
float pi_int=0.0f;
const float ampMin = 0.05f, ampMax = 0.98f;
void regulateVout(){
float kp = S.pi_kp_gx1000/1000.0f;
float ki = S.pi_ki_gx1000/1000.0f;
float err = (float)S.setVoltOut - vOut;
pi_int += err * ki * 0.02f;
if(pi_int > 0.5f) pi_int = 0.5f;
if(pi_int < -0.5f) pi_int = -0.5f;
float u = (kp * (err / 300.0f)) + pi_int;
spwm_amplitude += u;
if(spwm_amplitude < ampMin) spwm_amplitude = ampMin;
if(spwm_amplitude > ampMax) spwm_amplitude = ampMax;
}
/* ===================== Setup & Loop ===================== */
bool lcd_present_probe(){ Wire.beginTransmission(0x27); return (Wire.endTransmission()==0); }
static bool lastSyntheticSet = false;
static unsigned long lastSyntheticSetMs = 0;
void setup(){
Wire.begin();
Wire.setClock(25000);
lcd_ok = lcd_present_probe();
if(lcd_ok){
lcd.init(); lcd.backlight();
lcd.createChar(0,iconBlk);
lcd.createChar(1,iconEmp);
lcd.createChar(2,iconVolt);
lcd.createChar(3,iconPower);
lcd.createChar(4,iconThermo);
lcd.createChar(5,iconBattHead);
lcd_safe_clear();
}
pinMode(BUZZER_PIN,OUTPUT); digitalWrite(BUZZER_PIN,LOW);
pinMode(FAN_PIN,OUTPUT); digitalWrite(FAN_PIN,LOW);
pinMode(PIN_BTN_UP,INPUT_PULLUP); pinMode(PIN_BTN_DOWN,INPUT_PULLUP);
pinMode(PIN_BTN_SET,INPUT_PULLUP); pinMode(PIN_BTN_BACK,INPUT_PULLUP);
pinMode(LED_RUNNING_PIN, OUTPUT);
pinMode(LED_PROTEK_PIN, OUTPUT);
digitalWrite(LED_RUNNING_PIN, HIGH);
digitalWrite(LED_PROTEK_PIN, LOW);
btns_init(); analogReference(DEFAULT); loadEEPROM(); setupPWM();
// Setup ADC: AVcc reference, enable ADC, prescaler = 128 (125 kHz ADC clock @16MHz)
ADMUX = (1<<REFS0); // AVcc, right adjust
ADCSRA = (1<<ADEN) | (1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0); // enable ADC, prescaler 128
autoCalibrateADCmid(); // samples A2 for ~200ms to estimate ADC_MID (requires no current during this window)
pinMode(PIN_PROTEK, INPUT_PULLUP);
EIMSK &= ~(1<<INT0);
EICRA |= (1<<ISC01); EICRA &= ~(1<<ISC00);
EIFR |= (1<<INTF0);
EIMSK |= (1<<INT0);
wdt_disable();
delay(100);
wdt_enable(WDTO_2S);
sei(); lcd_update_line(0," Welcome SPWM ZIKRI"); delay(300); lcd_safe_clear();
}
void loop(){
wdt_reset();
updateBuzzer();
// <<< call non-blocking CT sampler each loop biar realtime dan MCU gak nge-hang
updateCurrent_CT30A1V_nonblocking();
readSensors();
uint8_t events = process_buttons();
bool setPinIsLow = (digitalRead(PIN_BTN_SET) == LOW);
bool syntheticSet = false;
unsigned long now = millis();
if(setPinIsLow && !(events & (1<<IDX_SET))){
if(!lastSyntheticSet && (now - lastSyntheticSetMs > 120)){
syntheticSet = true;
lastSyntheticSet = true;
lastSyntheticSetMs = now;
events |= (1<<IDX_SET);
}
} else if(!setPinIsLow){
lastSyntheticSet = false;
}
if(events) handleButtons_events(events);
if(ext_protek_flag){
unsigned long now2 = millis();
if( (now2 - ext_protek_time_ms) >= DEBOUNCE_EXT_MS ){
ext_protek_time_ms = now2;
applyProtection(true, " SHORT CIRCUIT");
if(S.buzzerOn){
beep_nonblocking();
}
}
ext_protek_flag = false;
}
if(millis() - lastRightSwap >= 3000){
showRightAmp = !showRightAmp;
showRightFreq = !showRightFreq;
lastRightSwap = millis();
}
if(!protekActive && inverterEnabled && outputsEnabled) regulateVout();
checkProtection();
if(protekActive){
showProtectScreen(currentProtMsg.length()?currentProtMsg:"PERINGATAN PROTEK");
} else {
if(menuIndex<0) showHome(); else showSetting();
}
delay(3);
}
// ========================= (END OF FILE) =========================