#include <Arduino.h>
// Radmor A-5512B – firmware ESP32-S3
// VFD (SN75518N)
#define PIN_DIN 11
#define PIN_CLK 12
#define PIN_LE 10
#define PIN_STROBE 13
// IR
#define PIN_IR 14
// LED
#define LED_STBY 46
#define LED_SPATIAL 45
#define LED_PSEUDO 48
#define LED_LOUD 47
#define LED_FLAT 21
#define LED_EQ 20
#define LED_SLEEP 19
// Przyciski
#define BTN_STBY 35
#define BTN_SPATIAL 36
#define BTN_LOUDNESS 37
#define BTN_MUTING 38
#define BTN_TUNER 39
#define BTN_PHONO 40
#define BTN_CD 41
#define BTN_AUX 42
#define BTN_TAPE 2
#define BTN_EQ 1
// Enkodery (wspólny CLK)
#define ENC_CLK 4
#define ENC_BAL_DT 5
#define ENC_BAL_SW 16
#define ENC_TRE_DT 6
#define ENC_TRE_SW 17
#define ENC_BAS_DT 7
#define ENC_BAS_SW 18
#define ENC_VOL_DT 15
#define ENC_VOL_SW 8
// Przekaźnik głośników
#define PIN_RELAY 9
// VFD
const uint8_t segment_bits[17] = {
14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 15, 0
};
const uint8_t grid_bits[9] = {
28, 27, 26, 25, 24, 23, 22, 21, 20
};
typedef struct {
char ch;
uint8_t seg[17];
} vfd_char_t;
const vfd_char_t vfdFont[] = {
// A, J, H, K, B, F, G, M, C, E, V, N, P, D, C1,C2,dp
{ '!', { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 } },
{ ' ', { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ '0', { 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0 } },
{ '1', { 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ '2', { 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0 } },
{ '3', { 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0 } },
{ '4', { 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0 } },
{ '5', { 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0 } },
{ '6', { 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0 } },
{ '7', { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 } },
{ '8', { 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0 } },
{ '9', { 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0 } },
{ 'A', { 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0 } },
{ 'B', { 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0 } },
{ 'C', { 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 } },
{ 'D', { 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0 } },
{ 'E', { 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0 } },
{ 'F', { 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 } },
{ 'G', { 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0 } },
{ 'H', { 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0 } },
{ 'I', { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0 } },
{ 'J', { 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0 } },
{ 'K', { 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0 } },
{ 'L', { 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0 } },
{ 'M', { 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0 } },
{ 'N', { 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0 } },
{ 'O', { 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0 } },
{ 'P', { 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0 } },
{ 'Q', { 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0 } },
{ 'R', { 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0 } },
{ 'S', { 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0 } },
{ 'T', { 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ 'U', { 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0 } },
{ 'V', { 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0 } },
{ 'W', { 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0 } },
{ 'X', { 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0 } },
{ 'Y', { 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
{ 'Z', { 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0 } },
{ '-', { 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0 } },
{ '+', { 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0 } },
{ '_', { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 } },
{ '/', { 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 } },
{ '.', { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0 } },
{ ',', { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0 } },
{ ':', { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 } },
};
const int FONT_SIZE = sizeof(vfdFont) / sizeof(vfdFont[0]);
volatile uint32_t seg_masks[9] = {0};
volatile uint8_t current_pos = 0;
volatile bool need_update = false;
volatile uint8_t next_pos = 0;
hw_timer_t *vfd_timer = NULL;
uint32_t get_char_mask(char c) {
for (int i = 0; i < FONT_SIZE; i++) {
if (vfdFont[i].ch == c) {
uint32_t mask = 0;
for (int s = 0; s < 17; s++)
if (vfdFont[i].seg[s]) mask |= (1UL << segment_bits[s]);
return mask;
}
}
return 0;
}
void send_data(uint32_t data) {
for (int i = 31; i >= 0; i--) {
digitalWrite(PIN_CLK, LOW);
digitalWrite(PIN_DIN, (data >> i) & 1);
delayMicroseconds(1);
digitalWrite(PIN_CLK, HIGH);
delayMicroseconds(1);
}
}
void write_vfd(uint32_t seg_mask, uint8_t pos) {
uint32_t data = seg_mask | (1UL << grid_bits[pos]);
digitalWrite(PIN_STROBE, HIGH);
digitalWrite(PIN_LE, LOW);
send_data(data);
digitalWrite(PIN_LE, HIGH);
delayMicroseconds(1);
digitalWrite(PIN_LE, LOW);
digitalWrite(PIN_STROBE, LOW);
}
void IRAM_ATTR on_vfd_timer() {
next_pos = current_pos;
current_pos = (current_pos + 1) % 9;
need_update = true;
}
void vfd_set_text(const char *str) {
for (int i = 0; i < 9; i++)
seg_masks[i] = get_char_mask(str[i] ? str[i] : ' ');
}
// Stan systemu
enum Source { SRC_TUNER, SRC_PHONO, SRC_CD, SRC_AUX, SRC_TAPE };
enum SpatialMode { SPATIAL_OFF, SPATIAL_ON, PSEUDO_ON };
struct {
bool power;
Source source;
bool phono_filter;
bool mute;
bool loudness;
SpatialMode spatial;
bool eq;
int volume; // 0-40
int bass; // -4..+5
int treble; // -4..+4
int balance; // -7..+7
bool sleep_active;
int sleep_minutes;
} state = {
false, SRC_TUNER, false, false, false, SPATIAL_OFF, false,
20, 0, 0, 0, false, 0
};
enum Disp18Mode { D18_SOURCE, D18_VOLUME, D18_BASS, D18_TREBLE, D18_TEMP };
Disp18Mode disp18_mode = D18_SOURCE;
char disp18_temp[10] = "";
unsigned long disp18_temp_until = 0;
void show_temp(const char *msg, int ms = 3000) {
strncpy(disp18_temp, msg, 9);
disp18_temp_until = millis() + ms;
disp18_mode = D18_TEMP;
vfd_set_text(msg);
}
void update_display() {
if (!state.power) {
vfd_set_text(" ");
return;
}
if (disp18_mode == D18_TEMP) {
vfd_set_text(disp18_temp);
return;
}
char buf[10];
if (state.mute) {
int vl = state.volume - max(0, -state.balance);
int vr = state.volume - max(0, state.balance);
snprintf(buf, 10, "MUTE%2d%2d ", vl, vr);
vfd_set_text(buf);
return;
}
switch (disp18_mode) {
case D18_VOLUME: {
int vl = constrain(state.volume + state.balance, 0, 40);
int vr = constrain(state.volume - state.balance, 0, 40);
snprintf(buf, 10, "VOL %2d %2d", vl, vr);
vfd_set_text(buf);
break;
}
case D18_BASS:
if (state.bass == 0)
vfd_set_text("BASS 0 ");
else {
snprintf(buf, 10, "BASS %+2d", state.bass);
vfd_set_text(buf);
}
break;
case D18_TREBLE:
if (state.treble == 0)
vfd_set_text("TREBLE 0 ");
else {
snprintf(buf, 10, "TREBLE%+2d", state.treble);
vfd_set_text(buf);
}
break;
default: {
const char *src_names[] = {
"TUNER ", "PHONO ", "CD ", "AUX ", "TAPE "
};
if (state.source == SRC_PHONO && state.phono_filter)
vfd_set_text("PHONO- ");
else
vfd_set_text(src_names[state.source]);
break;
}
}
}
// LED
unsigned long mute_blink_ms = 0;
bool mute_blink_state = false;
void update_leds() {
digitalWrite(LED_STBY, !state.power ? HIGH : LOW);
digitalWrite(LED_SPATIAL, state.spatial == SPATIAL_ON ? HIGH : LOW);
digitalWrite(LED_PSEUDO, state.spatial == PSEUDO_ON ? HIGH : LOW);
digitalWrite(LED_LOUD, state.loudness ? HIGH : LOW);
bool flat = state.power && (state.bass == 0 && state.treble == 0 && !state.eq);
digitalWrite(LED_FLAT, flat ? HIGH : LOW);
digitalWrite(LED_EQ, state.eq ? HIGH : LOW);
digitalWrite(LED_SLEEP, state.sleep_active ? HIGH : LOW);
}
// Przyciski
struct Button {
uint8_t pin;
bool last;
unsigned long last_ms;
};
#define DEBOUNCE_MS 50
Button buttons[] = {
{ BTN_STBY, HIGH, 0 },
{ BTN_SPATIAL, HIGH, 0 },
{ BTN_LOUDNESS, HIGH, 0 },
{ BTN_MUTING, HIGH, 0 },
{ BTN_TUNER, HIGH, 0 },
{ BTN_PHONO, HIGH, 0 },
{ BTN_CD, HIGH, 0 },
{ BTN_AUX, HIGH, 0 },
{ BTN_TAPE, HIGH, 0 },
{ BTN_EQ, HIGH, 0 },
};
const int BTN_COUNT = sizeof(buttons) / sizeof(buttons[0]);
enum BtnId {
BID_STBY = 0, BID_SPATIAL, BID_LOUDNESS, BID_MUTING,
BID_TUNER, BID_PHONO, BID_CD, BID_AUX, BID_TAPE, BID_EQ
};
unsigned long relay_on_at = 0;
bool relay_pending = false;
// forward declaration
void update_display();
void on_button_press(int id) {
switch (id) {
case BID_STBY:
state.power = !state.power;
if (state.power) {
relay_pending = true;
relay_on_at = millis() + 4000;
vfd_set_text("RADMOR ");
Serial.println("POWER ON – relay in 4s");
} else {
digitalWrite(PIN_RELAY, LOW);
relay_pending = false;
relay_on_at = 0;
Serial.println("STAND-BY");
}
break;
case BID_SPATIAL:
if (state.spatial == SPATIAL_OFF) {
state.spatial = SPATIAL_ON;
show_temp("SPATIAL ");
} else if (state.spatial == SPATIAL_ON) {
state.spatial = PSEUDO_ON;
show_temp("PSEUDO ");
} else {
state.spatial = SPATIAL_OFF;
}
break;
case BID_LOUDNESS:
state.loudness = !state.loudness;
if (state.loudness) show_temp("LOUDNESS ");
break;
case BID_MUTING:
state.mute = !state.mute;
Serial.println(state.mute ? "MUTE ON" : "MUTE OFF");
break;
case BID_TUNER:
state.source = SRC_TUNER;
state.phono_filter = false;
break;
case BID_PHONO:
if (state.source == SRC_PHONO)
state.phono_filter = !state.phono_filter;
else {
state.source = SRC_PHONO;
state.phono_filter = false;
}
break;
case BID_CD:
state.source = SRC_CD;
state.phono_filter = false;
break;
case BID_AUX:
state.source = SRC_AUX;
state.phono_filter = false;
break;
case BID_TAPE:
state.source = SRC_TAPE;
state.phono_filter = false;
break;
case BID_EQ:
state.eq = !state.eq;
if (state.eq) show_temp("EQ ");
break;
}
update_leds();
if (disp18_mode != D18_TEMP) {
disp18_mode = D18_SOURCE;
update_display();
}
}
void check_buttons() {
unsigned long now = millis();
for (int i = 0; i < BTN_COUNT; i++) {
bool cur = digitalRead(buttons[i].pin);
if (cur != buttons[i].last && (now - buttons[i].last_ms) > DEBOUNCE_MS) {
buttons[i].last = cur;
buttons[i].last_ms = now;
if (cur == LOW) on_button_press(i);
}
}
}
// Enkodery
void on_volume_change(int delta) {
if (state.mute) {
state.mute = false;
if (delta < 0 && state.volume > 15)
state.volume = 15;
else
state.volume = constrain(state.volume + delta, 0, 40);
} else {
state.volume = constrain(state.volume + delta, 0, 40);
}
disp18_mode = D18_VOLUME;
disp18_temp_until = millis() + 2500;
update_display();
}
void on_bass_change(int delta) {
state.bass = constrain(state.bass + delta, -4, 5);
disp18_mode = D18_BASS;
disp18_temp_until = millis() + 2000;
update_leds();
update_display();
}
void on_treble_change(int delta) {
state.treble = constrain(state.treble + delta, -4, 4);
disp18_mode = D18_TREBLE;
disp18_temp_until = millis() + 2000;
update_leds();
update_display();
}
void on_balance_change(int delta) {
state.balance = constrain(state.balance + delta, -7, 7);
disp18_mode = D18_VOLUME;
disp18_temp_until = millis() + 2500;
update_display();
}
// Enkodery – wspólny CLK, reaguje tylko ten enkoider którego DT się zmieniło
uint8_t enc_dt_pins[4] = {ENC_BAL_DT, ENC_TRE_DT, ENC_BAS_DT, ENC_VOL_DT};
uint8_t enc_sw_pins[4] = {ENC_BAL_SW, ENC_TRE_SW, ENC_BAS_SW, ENC_VOL_SW};
bool enc_dt_last[4] = {HIGH, HIGH, HIGH, HIGH};
bool enc_sw_last[4] = {HIGH, HIGH, HIGH, HIGH};
void check_encoders() {
static bool clk_last = HIGH;
bool clk = digitalRead(ENC_CLK);
if (clk != clk_last) {
clk_last = clk;
if (clk == LOW) {
for (int i = 0; i < 4; i++) {
bool dt = digitalRead(enc_dt_pins[i]);
if (dt != enc_dt_last[i]) {
enc_dt_last[i] = dt;
int delta = (dt == LOW) ? +1 : -1;
switch (i) {
case 0: on_balance_change(delta); break;
case 1: on_treble_change(delta); break;
case 2: on_bass_change(delta); break;
case 3: on_volume_change(delta); break;
}
}
}
}
}
// Przyciski enkoderów
for (int i = 0; i < 4; i++) {
bool sw = digitalRead(enc_sw_pins[i]);
if (sw == LOW && enc_sw_last[i] == HIGH) {
switch (i) {
case 0: state.balance = 0; disp18_mode = D18_VOLUME;
disp18_temp_until = millis() + 2500;
update_display();
break;
case 1: state.treble = 0; disp18_mode = D18_TREBLE; disp18_temp_until = millis() + 2000; update_leds(); update_display();
break;
case 2: state.bass = 0; disp18_mode = D18_BASS; disp18_temp_until = millis() + 2000; update_leds(); update_display();
break;
case 3: on_button_press(BID_MUTING); break;
}
}
enc_sw_last[i] = sw;
}
}
// Setup & Loop
void setup() {
Serial.begin(115200);
pinMode(PIN_DIN, OUTPUT); digitalWrite(PIN_DIN, LOW);
pinMode(PIN_CLK, OUTPUT); digitalWrite(PIN_CLK, LOW);
pinMode(PIN_LE, OUTPUT); digitalWrite(PIN_LE, LOW);
pinMode(PIN_STROBE, OUTPUT); digitalWrite(PIN_STROBE, HIGH);
pinMode(LED_STBY, OUTPUT);
pinMode(LED_SPATIAL, OUTPUT);
pinMode(LED_PSEUDO, OUTPUT);
pinMode(LED_LOUD, OUTPUT);
pinMode(LED_FLAT, OUTPUT);
pinMode(LED_EQ, OUTPUT);
pinMode(LED_SLEEP, OUTPUT);
for (int i = 0; i < BTN_COUNT; i++)
pinMode(buttons[i].pin, INPUT_PULLUP);
pinMode(ENC_CLK, INPUT_PULLUP);
pinMode(ENC_BAL_DT, INPUT_PULLUP); pinMode(ENC_BAL_SW, INPUT_PULLUP);
pinMode(ENC_TRE_DT, INPUT_PULLUP); pinMode(ENC_TRE_SW, INPUT_PULLUP);
pinMode(ENC_BAS_DT, INPUT_PULLUP); pinMode(ENC_BAS_SW, INPUT_PULLUP);
pinMode(ENC_VOL_DT, INPUT_PULLUP); pinMode(ENC_VOL_SW, INPUT_PULLUP);
pinMode(PIN_RELAY, OUTPUT);
digitalWrite(PIN_RELAY, LOW);
vfd_timer = timerBegin(1000000);
timerAttachInterrupt(vfd_timer, &on_vfd_timer);
timerAlarm(vfd_timer, 2000, true, 0);
vfd_set_text("RADMOR ");
update_leds();
Serial.println("Radmor A-5512B ready");
}
void loop() {
if (need_update) {
need_update = false;
write_vfd(seg_masks[next_pos], next_pos);
}
unsigned long now = millis();
if (relay_pending && now >= relay_on_at) {
relay_pending = false;
digitalWrite(PIN_RELAY, HIGH);
Serial.println("RELAY ON – speakers connected");
disp18_mode = D18_SOURCE;
update_display(); // teraz przejdź do normalnego wyświetlania
}
if ((disp18_mode == D18_TEMP || disp18_mode == D18_VOLUME ||
disp18_mode == D18_BASS || disp18_mode == D18_TREBLE) &&
now >= disp18_temp_until) {
disp18_mode = D18_SOURCE;
update_display();
}
if (state.mute && state.power && (now - mute_blink_ms) > 500) {
mute_blink_ms = now;
mute_blink_state = !mute_blink_state;
if (mute_blink_state) {
char buf[10];
int vl = constrain(state.volume + state.balance, 0, 40);
int vr = constrain(state.volume - state.balance, 0, 40);
snprintf(buf, 10, "MUTE%2d%2d ", vl, vr);
vfd_set_text(buf);
} else {
vfd_set_text(" ");
}
}
check_buttons();
check_encoders();
}BALANCE
TREBLE
BASS
VOLUME