/*An audio amplifier dummy load is a crucial test bench tool,
that simulates a loudspeaker's electrical resistance (typically 4Ω, 8Ω, or 16Ω).
This code control electronic speaker protection and speaker/dummy load combinations */
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <math.h>
/* ===================== FORWARD DECLARATIONS ===================== */
void relaysAllOff();
void applyRelaysSafe();
int8_t readTemperature();
void updateFanAndSafety(int8_t t);
const char* getStatusMsg();
void rebuildScroll();
void rebuildScrollIfChanged();
void updateScroll();
void drawSplash();
void drawHome(int8_t t);
void drawMenu(int8_t t);
void handleEncoderButton(int8_t t);
unsigned long blTimeoutMs();
void backlightUpdate();
const char* blToStrFrom(uint8_t b);
void applySigOutput();
/* ===================== LCD ===================== */
LiquidCrystal_I2C lcd(0x27, 20, 4);
//LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);
byte DEGREE_CHAR[8] = {
B00111,
B00101,
B00111,
B00000,
B00000,
B00000,
B00000,
B00000
};
byte OMEGA_CHAR[8] = {
B00000,
B01110,
B10001,
B10001,
B10001,
B01010,
B11011
};
byte SIG_POS[8] = {
B00100,
B01010,
B10001,
B10001,
B00000,
B00000,
B00000,
B00000
};
byte SIG_NEG[8] = {
B00000,
B00000,
B00000,
B00000,
B10001,
B10001,
B01010,
B00100
};
void lcdPrintOhm(){lcd.write((uint8_t)1);}
void lcdPrintDegC(){ lcd.write((uint8_t)0); lcd.print('C'); }
/* ===================== PINOUT ===================== */
const uint8_t PIN_ENC_CLK = 2;
const uint8_t PIN_ENC_DT = 3;
const uint8_t PIN_ENC_SW = 4;
const uint8_t PIN_RY1 = 7;
const uint8_t PIN_RY2 = 8;
const uint8_t PIN_RY3 = 9;
const uint8_t PIN_RY4 = 10;
const uint8_t PIN_RY5 = 11;
const uint8_t PIN_RY6 = 12;
const uint8_t PIN_NTC = A0;
const uint8_t PIN_FAN = 5;
const uint8_t PIN_SIG = 6;
const uint8_t PIN_PROT_L = A1;
const uint8_t PIN_PROT_R = A2;
bool protL = false;
bool protR = false;
/* ===================== RELAY ===================== */
const uint8_t RELAY_ON = HIGH;
const uint8_t RELAY_OFF = LOW;
const uint16_t RELAY_ALL_OFF_MS = 120;
const uint16_t RELAY_SEQ_MS = 120;
/* ===================== TEMP ===================== */
const int8_t TEMP_LIMIT_C = 80;
const int8_t TEMP_RESET_C = 40;
const int8_t FAN_ON_C = 60;
const int8_t FAN_OFF_C = 40; // mala histereza za ventilator
bool ntcFault=false;
bool tooHot=false;
bool relaysSuppressed = false; // true kad smo zbog zaštite ugasili releje
/* ===================== SETTINGS ===================== */
enum Mode:uint8_t{MODE_SPEAKER,MODE_RESISTOR};
enum Config:uint8_t{CFG_LR_8,CFG_BRIDGE_R4};
Mode currentMode=MODE_SPEAKER;
Config currentConfig=CFG_LR_8;
void requestRelayApply(Mode m, Config c);
void relayFsmTick();
/* ===================== UI ===================== */
enum UiState:uint8_t{UI_HOME,UI_MENU};
UiState ui=UI_HOME;
uint8_t menuIndex=0;
bool menuEdit=false;
bool sigEnabled = false; // primenjeno stanje
bool pendingSig = false; // menjanje u meniju
enum BacklightMode:uint8_t{BL_ON,BL_5MIN,BL_10MIN,BL_15MIN,BL_OFF};
BacklightMode blMode=BL_ON;
// Pending (apply tek po izlasku iz menija)
Mode pendingMode = MODE_SPEAKER;
Config pendingConfig = CFG_LR_8;
BacklightMode pendingBL = BL_ON;
bool pendingDirty = false;
// Blink for edit mode cursor
const uint16_t EDIT_BLINK_MS = 350;
unsigned long lastBlinkMs = 0;
bool blinkOn = true;
uint8_t menuTop = 0;
uint8_t cursorLastRow = 255;
char cursorLastCh = 0;
const uint16_t SIG_SWAP_MS = 220; // brzina animacije
unsigned long sigSwapMs = 0;
bool sigSwap = false;
bool sigWasOn = false;
unsigned long lastActivityMs=0;
static inline void markActivity(){ lastActivityMs=millis(); }
unsigned long blTimeoutMs(){
switch(blMode){
case BL_5MIN:return 300000UL;
case BL_10MIN:return 600000UL;
case BL_15MIN:return 900000UL;
default:return 0;
}
}
void backlightUpdate(){
if(blMode==BL_ON){ lcd.backlight(); return; }
if(blMode==BL_OFF){ lcd.noBacklight(); return; }
unsigned long tmo=blTimeoutMs();
if(tmo==0){ lcd.backlight(); return; }
if(millis()-lastActivityMs>=tmo) lcd.noBacklight();
else lcd.backlight();
}
void applySigOutput(){
// Arduino samo ukljucuje/iskljucuje enable preko N-MOSFET low-side
digitalWrite(PIN_SIG, sigEnabled ? HIGH : LOW);
}
void updateSigSwapOnHome(){
// animacija samo na HOME i samo kad je SIG ukljucen
if(ui != UI_HOME){
sigWasOn = false;
return;
}
if(!sigEnabled){
// ako je bio ON pa sad OFF, obrisi 2 celije
if(sigWasOn){
lcd.setCursor(18,0);
lcd.print(" ");
sigWasOn = false;
}
return;
}
sigWasOn = true;
unsigned long now = millis();
if(now - sigSwapMs < SIG_SWAP_MS) return;
sigSwapMs = now;
sigSwap = !sigSwap;
lcd.setCursor(18,0); // gde hoces da bude ikonica (desno gore)
if(!sigSwap){
lcd.write((uint8_t)2); // POS levo
lcd.write((uint8_t)3); // NEG desno
} else {
lcd.write((uint8_t)3); // NEG levo
lcd.write((uint8_t)2); // POS desno
}
}
const char* blToStrFrom(uint8_t b){
switch((BacklightMode)b){
case BL_ON:return "ON";
case BL_5MIN:return "5 MIN";
case BL_10MIN:return "10 MIN";
case BL_15MIN:return "15 MIN";
case BL_OFF:return "OFF";
}
return "ON";
}
/* ===================== ENCODER ===================== */
volatile int16_t encDelta=0;
bool encWasDown=false;
unsigned long encDownMs=0;
bool longDone=false;
void isrEnc(){
static unsigned long lastUs=0;
unsigned long nowUs=micros();
if(nowUs-lastUs<10000) return;
lastUs=nowUs;
if(digitalRead(PIN_ENC_CLK)==HIGH) encDelta--;
else encDelta++;
}
void updateProtectInputs(){
// HIGH = PC817 ne provodi -> PROTECT (relej otpustio)
bool rawL = (digitalRead(PIN_PROT_L) == HIGH);
bool rawR = (digitalRead(PIN_PROT_R) == HIGH);
static bool lastL=false, lastR=false;
static unsigned long tL=0, tR=0;
unsigned long now = millis();
if(rawL != lastL){ lastL = rawL; tL = now; }
if(rawR != lastR){ lastR = rawR; tR = now; }
if(now - tL > 200) protL = lastL; // 200ms stabilno
if(now - tR > 200) protR = lastR;
}
/* ===================== SCROLL ===================== */
const uint16_t SCROLL_PERIOD_MS=180;
unsigned long lastScrollMs=0;
int scrollPos=0;
int scrollLen=0;
char scrollVirt[80];
char scrollMsg[32]={0};
const char* getStatusMsg(){
if(ntcFault) return "Sensor Error";
if(tooHot) return "Resistors Very Hot";
if(protL && protR) return "Protect L+R Active (!)";
if(protL) return "Protect L Active (!)";
if(protR) return "Protect R Active (!)";
return "Test In Progress (!)";
}
void rebuildScroll(){
const char* m=getStatusMsg();
strncpy(scrollMsg,m,sizeof(scrollMsg)-1);
scrollMsg[sizeof(scrollMsg)-1]='\0';
int idx=0;
for(int i=0;i<20;i++) scrollVirt[idx++]=' ';
for(int i=0;i<strlen(scrollMsg);i++) scrollVirt[idx++]=scrollMsg[i];
for(int i=0;i<20;i++) scrollVirt[idx++]=' ';
scrollVirt[idx]='\0';
scrollLen=idx;
scrollPos=0;
}
void rebuildScrollIfChanged(){
const char* m=getStatusMsg();
if(strcmp(scrollMsg,m)==0) return;
rebuildScroll();
}
void updateScroll(){
rebuildScrollIfChanged();
if(millis()-lastScrollMs<SCROLL_PERIOD_MS) return;
lastScrollMs=millis();
char win[21];
for(int i=0;i<20;i++){
int p=scrollPos+i;
win[i]=(p<scrollLen)?scrollVirt[p]:' ';
}
win[20]='\0';
lcd.setCursor(0,3);
lcd.print(win);
scrollPos++;
if(scrollPos>scrollLen-20) scrollPos=0;
}
/* ===================== RELAYS ===================== */
void relaysAllOff(){
digitalWrite(PIN_RY1,RELAY_OFF);
digitalWrite(PIN_RY2,RELAY_OFF);
digitalWrite(PIN_RY3,RELAY_OFF);
digitalWrite(PIN_RY4,RELAY_OFF);
digitalWrite(PIN_RY5,RELAY_OFF);
digitalWrite(PIN_RY6,RELAY_OFF);
}
/* ===================== RELAY FSM (NON-BLOCKING) ===================== */
enum RelayStep : uint8_t {
RS_IDLE,
RS_ALL_OFF,
RS_WAIT_OFF,
RS_MASTER_ON,
RS_WAIT_2S,
RS_PAIR_ON
};
RelayStep relayStep = RS_IDLE;
unsigned long relayT0 = 0;
Mode targetMode = MODE_SPEAKER;
Config targetCfg = CFG_LR_8;
static inline bool relayBusy(){ return relayStep != RS_IDLE; }
void requestRelayApply(Mode m, Config c){
// ne startuj sekvencu ako je safety aktivan
if(ntcFault || tooHot) return;
targetMode = m;
targetCfg = c;
// restart FSM
relayStep = RS_ALL_OFF;
relayT0 = millis();
}
void relayFsmTick(){
unsigned long now = millis();
// Safety uvek ima prioritet
if(ntcFault || tooHot){
relaysAllOff();
relayStep = RS_IDLE;
return;
}
switch(relayStep){
case RS_IDLE:
return;
case RS_ALL_OFF:
relaysAllOff();
relayT0 = now;
relayStep = RS_WAIT_OFF;
break;
case RS_WAIT_OFF:
if(now - relayT0 >= RELAY_ALL_OFF_MS){
// Ako je L+R -> odmah palimo par
// Ako je R ch (bridge) -> prvo master, pa čekamo 2s
if(targetCfg == CFG_LR_8){
relayStep = RS_PAIR_ON;
} else {
relayStep = RS_MASTER_ON;
}
}
break;
case RS_MASTER_ON:
// Master relej zavisi od moda
if(targetMode == MODE_RESISTOR){
digitalWrite(PIN_RY1, RELAY_ON); // master
} else {
digitalWrite(PIN_RY4, RELAY_ON); // master
}
relayT0 = now;
relayStep = RS_WAIT_2S;
break;
case RS_WAIT_2S:
if(now - relayT0 >= 2000){
relayStep = RS_PAIR_ON;
}
break;
case RS_PAIR_ON:
if(targetMode == MODE_RESISTOR){
// Par releja za RESISTOR: RY2 + RY3 zajedno
digitalWrite(PIN_RY2, RELAY_ON);
digitalWrite(PIN_RY3, RELAY_ON);
} else {
// Par releja za SPEAKER: RY5 + RY6 zajedno
digitalWrite(PIN_RY5, RELAY_ON);
digitalWrite(PIN_RY6, RELAY_ON);
}
relayStep = RS_IDLE; // gotovo
break;
}
}
void applyRelaysSafe(){
requestRelayApply(currentMode, currentConfig);
}
/* ===================== NTC ===================== */
int8_t readTemperature(){
auto readADC=[](){return analogRead(PIN_NTC);};
int r[7];
for(int i=0;i<7;i++){r[i]=readADC();delayMicroseconds(200);}
for(int i=1;i<7;i++){int x=r[i],j=i-1;while(j>=0&&r[j]>x){r[j+1]=r[j];j--;}r[j+1]=x;}
int raw=r[3];
if(raw<8||raw>1015) return INT8_MIN;
float R=(raw*10000.0)/(1023.0-raw);
float t=log(R);
t=1.0/(0.001129148+(0.000234125*t)+(0.0000000876741*t*t*t));
t-=273.15;
return (int8_t)(t+0.5);
}
void updateFanAndSafety(int8_t t){
static bool stopLatched = false; // za test stop (80C / 40C)
static bool fanLatched = false; // za ventilator (60C / 55C)
ntcFault = (t == INT8_MIN);
// ====== SENSOR FAULT ======
if(ntcFault){
stopLatched = false;
fanLatched = true; // fan neka radi kod greške
tooHot = false;
digitalWrite(PIN_FAN, HIGH);
relaysAllOff();
relaysSuppressed = true;
return;
}
// ====== FAN CONTROL (60C ON / 55C OFF) ======
if(!fanLatched && t >= FAN_ON_C) fanLatched = true;
if(fanLatched && t <= FAN_OFF_C) fanLatched = false;
digitalWrite(PIN_FAN, fanLatched ? HIGH : LOW);
// ====== HARD STOP (80C ON / 40C OFF) ======
if(!stopLatched && t >= TEMP_LIMIT_C) stopLatched = true;
if(stopLatched && t <= (TEMP_LIMIT_C - TEMP_RESET_C)) stopLatched = false;
tooHot = stopLatched;
if(tooHot){
relaysAllOff();
relaysSuppressed = true;
return;
}
// ====== AUTO RESTORE ======
if(relaysSuppressed){
relaysSuppressed = false;
if(ui != UI_MENU){
requestRelayApply(currentMode, currentConfig);
}
}
}
/* ===================== UI DRAW ===================== */
void drawSplash(){
lcd.clear();
lcd.setCursor(0,1); lcd.print(" * AUDIO AMP TEST * ");
lcd.setCursor(0,2); lcd.print(" * MANAGER * ");
}
void drawHome(int8_t t){
lcd.setCursor(0,0);
lcd.print("Mode: ");
lcd.print(currentMode==MODE_SPEAKER?"Speaker":"Resistor");
lcd.setCursor(0,1);
lcd.print("Config: ");
if(currentConfig==CFG_LR_8){
lcd.print("L+R 8");
lcdPrintOhm();
lcd.print(" ");
} else {
lcd.print("R ch 4");
lcdPrintOhm();
lcd.print(" ");
}
lcd.setCursor(0,2);
lcd.print("Res.Temp: ");
if(t==INT8_MIN) lcd.print("---");
else lcd.print((int)t);
lcdPrintDegC();
lcd.print(" ");
}
void drawMenuCursor(){
// koji red u prozoru je selektovan (0..2)
uint8_t rowInWin = menuIndex - menuTop; // 0,1,2
uint8_t row = 1 + rowInWin; // LCD row 1..3
char want = menuEdit ? (blinkOn ? '*' : ' ') : '*';
if(row == cursorLastRow && want == cursorLastCh) return;
// obriši staru poziciju
if(cursorLastRow >= 1 && cursorLastRow <= 3){
lcd.setCursor(19, cursorLastRow);
lcd.print(' ');
}
// upiši novu
lcd.setCursor(19, row);
lcd.print(want);
cursorLastRow = row;
cursorLastCh = want;
}
void drawMenu(int8_t t){
lcd.clear();
cursorLastRow = 255;
cursorLastCh = 0;
lcd.setCursor(5,0);
lcd.print("- MENU -");
// --- LINE 1 (row=1) item = top+0 ---
uint8_t it = menuTop + 0;
lcd.setCursor(0,1);
if(it==0){
lcd.print("Mode: ");
lcd.print(pendingMode==MODE_SPEAKER?"Speaker ":"Resistor");
lcd.print(" ");
} else if(it==1){
lcd.print("Config: ");
if(pendingConfig==CFG_LR_8){
lcd.print("L+R 8 "); lcdPrintOhm(); lcd.print(" ");
} else {
lcd.print("R ch 4 "); lcdPrintOhm(); lcd.print(" ");
}
lcd.print(" ");
} else if(it==2){
lcd.print("Backlight: ");
lcd.print(blToStrFrom((uint8_t)pendingBL));
lcd.print(" ");
} else if(it==3){
lcd.print("Sig.Gen: ");
lcd.print(pendingSig ? "ON " : "OFF");
lcd.print(" ");
}
// --- LINE 2 (row=2) item = top+1 ---
it = menuTop + 1;
lcd.setCursor(0,2);
if(it==0){
lcd.print("Mode: ");
lcd.print(pendingMode==MODE_SPEAKER?"Speaker ":"Resistor");
lcd.print(" ");
} else if(it==1){
lcd.print("Config: ");
if(pendingConfig==CFG_LR_8){
lcd.print("L+R 8 "); lcdPrintOhm(); lcd.print(" ");
} else {
lcd.print("R ch 4 "); lcdPrintOhm(); lcd.print(" ");
}
lcd.print(" ");
} else if(it==2){
lcd.print("Backlight: ");
lcd.print(blToStrFrom((uint8_t)pendingBL));
lcd.print(" ");
} else if(it==3){
lcd.print("Sig.Gen: ");
lcd.print(pendingSig ? "ON " : "OFF");
lcd.print(" ");
}
// --- LINE 3 (row=3) item = top+2 ---
it = menuTop + 2;
lcd.setCursor(0,3);
if(it==0){
lcd.print("Mode: ");
lcd.print(pendingMode==MODE_SPEAKER?"Speaker ":"Resistor");
lcd.print(" ");
} else if(it==1){
lcd.print("Config: ");
if(pendingConfig==CFG_LR_8){
lcd.print("L+R 8 "); lcdPrintOhm(); lcd.print(" ");
} else {
lcd.print("R ch 4 "); lcdPrintOhm(); lcd.print(" ");
}
lcd.print(" ");
} else if(it==2){
lcd.print("Backlight: ");
lcd.print(blToStrFrom((uint8_t)pendingBL));
lcd.print(" ");
} else if(it==3){
lcd.print("Sig.Gen: ");
lcd.print(pendingSig ? "ON " : "OFF");
lcd.print(" ");
}
}
void updateEditBlink(){
if(ui != UI_MENU || !menuEdit){
blinkOn = true; // steady when not editing
lastBlinkMs = millis();
return;
}
unsigned long now = millis();
if(now - lastBlinkMs >= EDIT_BLINK_MS){
lastBlinkMs = now;
blinkOn = !blinkOn;
}
}
void handleEncoderButton(int8_t t){
bool down=(digitalRead(PIN_ENC_SW)==LOW);
if(!encWasDown&&down){ encWasDown=true; encDownMs=millis(); longDone=false; markActivity(); }
if(encWasDown&&down&&!longDone){
if(millis()-encDownMs>=2000){
longDone=true;
menuEdit=false;
if(ui==UI_HOME){
ui = UI_MENU;
menuIndex = 0;
noInterrupts();
encDelta = 0;
interrupts();
menuTop = 0;
// start editing snapshot
pendingMode = currentMode;
pendingConfig = currentConfig;
pendingBL = blMode;
pendingDirty = false;
pendingSig = sigEnabled;
drawMenu(t);
}
else{
// exiting MENU -> commit pending to current
if(pendingDirty){
currentMode = pendingMode;
currentConfig = pendingConfig;
blMode = pendingBL;
sigEnabled = pendingSig;
applySigOutput();
// primeni releje samo ako nema greške i nije pregrejano
if(!ntcFault && !tooHot){
requestRelayApply(currentMode, currentConfig);
} else {
relaysAllOff();
relaysSuppressed = true; // ako koristiš auto-restore iz prethodne poruke
}
}
ui = UI_HOME;
lcd.clear();
}
}
}
if(encWasDown&&!down){
encWasDown=false;
if(!longDone&&ui==UI_MENU){
markActivity();
menuEdit = !menuEdit; // enter/exit edit
drawMenu(t);
}
}
}
/* ===================== SETUP ===================== */
void setup(){
pinMode(PIN_ENC_CLK,INPUT);
pinMode(PIN_ENC_DT,INPUT);
pinMode(PIN_ENC_SW,INPUT_PULLUP);
pinMode(PIN_RY1,OUTPUT);
pinMode(PIN_RY2,OUTPUT);
pinMode(PIN_RY3,OUTPUT);
pinMode(PIN_RY4,OUTPUT);
pinMode(PIN_RY5,OUTPUT);
pinMode(PIN_RY6,OUTPUT);
pinMode(PIN_FAN,OUTPUT);
pinMode(PIN_PROT_L, INPUT_PULLUP);
pinMode(PIN_PROT_R, INPUT_PULLUP);
pinMode(PIN_SIG, OUTPUT);
digitalWrite(PIN_FAN,LOW);
sigEnabled = false;
applySigOutput();
relaysAllOff();
lcd.begin(20, 4);
lcd.createChar(0,DEGREE_CHAR);
lcd.createChar(1, OMEGA_CHAR);
lcd.createChar(2, SIG_POS);
lcd.createChar(3, SIG_NEG);
lcd.backlight();
attachInterrupt(digitalPinToInterrupt(PIN_ENC_DT),isrEnc,RISING);
rebuildScroll();
drawSplash();
delay(1500);
lcd.clear();
}
/* ===================== LOOP ===================== */
/* ===================== LOOP ===================== */
void loop(){
int8_t t = readTemperature();
updateProtectInputs();
updateFanAndSafety(t);
relayFsmTick();
backlightUpdate();
handleEncoderButton(t);
updateEditBlink();
if(ui == UI_MENU){
if(encDelta != 0){
int16_t step;
noInterrupts();
step = encDelta;
encDelta = 0;
interrupts();
markActivity();
if(!menuEdit){
// --- NAVIGACIJA (bez redraw svaki put) ---
uint8_t oldIndex = menuIndex;
if(step > 0) menuIndex = (menuIndex + 1) % 4;
else menuIndex = (menuIndex + 3) % 4;
// prozor: 0 za 0-1-2, 1 za 1-2-3
uint8_t newTop = (menuIndex <= 2) ? 0 : 1;
// redraw menija SAMO ako se promeni prozor
if(newTop != menuTop){
menuTop = newTop;
drawMenu(t);
}
} else {
// --- EDIT MODE ---
if(menuIndex == 0){
pendingMode = (pendingMode == MODE_SPEAKER) ? MODE_RESISTOR : MODE_SPEAKER;
pendingDirty = true;
}
else if(menuIndex == 1){
pendingConfig = (pendingConfig == CFG_LR_8) ? CFG_BRIDGE_R4 : CFG_LR_8;
pendingDirty = true;
}
else if(menuIndex == 2){
int v = (int)pendingBL + ((step>0)?1:-1);
if(v < 0) v = 4;
if(v > 4) v = 0;
pendingBL = (BacklightMode)v;
pendingDirty = true;
}
else if(menuIndex == 3){
pendingSig = !pendingSig;
pendingDirty = true;
}
// u edit modu redraw je ok
drawMenu(t);
}
}
// kursor/blink uvek
drawMenuCursor();
}
if(ui == UI_HOME){
drawHome(t);
updateSigSwapOnHome();
updateScroll();
}
}