5- S/Meter com escala completa numéricas S 1, 3, 5, 7, 9, + 30d dentro do espectro.
/*
* DDS VFO Transceiver - 3Band
* =========================================================================
* Fitur :
* - Tampilan Callsign saat startup
* - Band: 80m, 40m, 11m
* - Menu setting : IF (step 10Hz), LSB Offset, USB Offset, AM Offset, Kalibrasi Kristal, TX Offset
* - Gain S-Meter disesuaikan untuk 0.7V = Full Scale
* - Semua setting tersimpan di EEPROM
* - Tampilan UI dengan garis pemisah
* - Auto-save setelah 5 detik idle
* =========================================================================
*/
//---------------------------------------------------------------------------------
// Function
// 1.CLK0 RX VFO 34 MHZ AM,LSB,USB, MODE Cobra 148 GTL OTHER TRANSCEIVERS
// 2.CLK0 TX FREQUENCY VFO - IF = FREQUENCY DISPLAY AM,LSB,USB,OFFSET MODE
// 3.CLK1 BFO RX,TX FI 7800KHZ AM,LSB,USB,OFFSET MODE
// 4.BAND (9) 80M,40M,30M,20M,15M,12M,11M,10M,6M, HF,VHF
// 5.STEP 10Hz,100Hz,1KHz,10KHz,100KHz,1MHz
// 6.MENU CALIBRATION VFO,IF AM,LSB,USB,OFFSET MODE
// 7.S-METER FULL SCALE S 1 3 5 7 9 + 30d
// 8.EEPROM INCLUDED FOR MEMORY
// Sketch modified by Waldir October 2025. Blog: https://projetosetransceptores.blogspot.com/
//////////////////////////////////////////////////////////////////////
// ================== 1. PUSTAKA (LIBRARIES) ==================
#include <EEPROM.h>
#include <Wire.h>
#include <si5351.h>
#include <RotaryEncoder.h>
#include <U8g2lib.h>
// ================== 2. DEFINISI & PENGATURAN INTI ==================
// ----- -----
#define CALLSIGN "JZ13NGF"
#define IF_FREQUENCY_DEFAULT 7800000UL // Enter the transceiver IF 7800000,9998500,10000000,10240000,10700000
#define BFO_LSB_OFFSET_DEFAULT -1500L // LSB
#define BFO_USB_OFFSET_DEFAULT 1500L // USB
#define AM_RX_OFFSET_DEFAULT 0L // Initialize to zero for Menu correction
#define SI5351_CORRECTION_DEFAULT 18000L // Si5351 crystal frequency correction
#define TX_OFFSET_FREQ_DEFAULT 0L // Adjust IF 7800000L or -7800000L TX VFO frequency display
// Definisi Pin
#define PIN_ENCODER_CLK 2
#define PIN_ENCODER_DT 3
#define PIN_ENCODER_SW_BAND 4
#define PIN_STEP_BUTTON A0
#define PIN_TX_BUTTON A1
#define PIN_SET_BUTTON A2
#define PIN_S_METER_IN A3
#define PIN_PTT_OUT 6
// Pengaturan S-Meter
#define S_METER_MAX_ADC 143
// Pengaturan OLED
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// Deklarasi Objek Lainnya
Si5351 si5351;
RotaryEncoder encoder(PIN_ENCODER_DT, PIN_ENCODER_CLK);
// Variabel & Konstanta Program
long bfo_frequency;
const char* mode_names[] = {"LSB","USB","AM"};
const long step_values[] = {10,100,1000,10000,100000,1000000};
const char* step_display_names[] = {"10Hz","100Hz","1KHz","10KHz","100KHz","1MHz"};
byte current_step_index = 3;
// Struktur EEPROM
struct EepromData {
uint32_t magic_key;
long last_freq[9];
int last_band;
int last_mode;
long if_frequency;
long tx_offset_freq;
long bfo_lsb_offset;
long bfo_usb_offset;
long am_rx_offset;
long si5351_correction;
};
// Definisi Band
const char* band_names[] = {"80M","40M","30M","20M","15M","12M","11M","10M","6M"};
const long band_defaults[] = {3500000,7000000,10100000,14000000,21000000,24890000,26965000,28000000,50000000 };
long band_frequencies[9];
// ================== 3. VARIABEL GLOBAL ==================
long current_frequency;
int current_band = 0;
int current_mode = 1;
bool is_tx = false;
bool force_display_update = true;
long last_encoder_pos = 0;
// Menu setting sekarang ada 6 item
int setting_menu_state = 0; // 0:Normal, 1:IF, 2:LSB, 3:USB, 4:AM, 5:Cal, 6:TX
// Variabel Pengaturan
long if_frequency;
long tx_offset_freq;
long bfo_lsb_offset;
long bfo_usb_offset;
long am_rx_offset;
long si5351_correction_val;
// Tombol & Durasi
unsigned long set_button_press_start_time = 0;
const long long_press_duration = 1000;
// Variabel untuk optimasi display
long last_displayed_freq = 0;
bool last_tx_state = false;
int last_bar_level = -1;
long last_if_frequency = 0;
long last_tx_offset_freq = 0;
long last_bfo_lsb_offset = 0;
long last_bfo_usb_offset = 0;
long last_am_rx_offset = 0;
long last_si5351_correction = 0;
// Variabel untuk state change detection
bool band_button_active = false;
bool step_button_active = false;
// Variabel untuk Auto-Save (setelah idle 5 detik)
unsigned long last_activity_time = 0;
bool settings_changed = false;
const unsigned long auto_save_delay = 5000; // 5 detik dalam milidetik
// ===================================================================
// FUNGSI INTERRUPT UNTUK ENCODER
// ===================================================================
void isrEncoder() {
encoder.tick();
}
// ===================================================================
// FUNGSI SETUP
// ===================================================================
void setup() {
pinMode(PIN_ENCODER_SW_BAND, INPUT_PULLUP);
pinMode(PIN_STEP_BUTTON, INPUT_PULLUP);
pinMode(PIN_TX_BUTTON, INPUT_PULLUP);
pinMode(PIN_SET_BUTTON, INPUT_PULLUP);
pinMode(PIN_PTT_OUT, OUTPUT);
digitalWrite(PIN_PTT_OUT, LOW);
pinMode(PIN_ENCODER_CLK, INPUT_PULLUP);
pinMode(PIN_ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_CLK), isrEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_DT), isrEncoder, CHANGE);
u8g2.begin();
// =================== KODE STARTUP SCREEN ===================
u8g2.firstPage();
do {
// Tampilkan Callsign dengan font besar di tengah
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawStr((128 - u8g2.getStrWidth(CALLSIGN)) / 2, 30, CALLSIGN);
// Tampilkan nama perangkat di bawahnya
u8g2.setFont(u8g2_font_6x10_tf);
const char* subtitle = "DDS VFO 9BAND HF VHF";
u8g2.drawStr((128 - u8g2.getStrWidth(subtitle)) / 2, 50, subtitle);
} while (u8g2.nextPage());
delay(2500); // Tahan tampilan startup selama 2.5 detik
// =================== AKHIR KODE STARTUP ====================
load_settings();
if (!si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, si5351_correction_val)) {
si5351.drive_strength(SI5351_CLK0,SI5351_DRIVE_6MA);//you can set this to 2MA, 4MA, 6MA or 8MA
si5351.drive_strength(SI5351_CLK1,SI5351_DRIVE_6MA);//you can set this to 2MA, 4MA, 6MA or 8MA
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(10, 35, "Si5351 GAGAL!");
} while (u8g2.nextPage());
while (1);
}
set_frequency(current_frequency);
}
// ===================================================================
// FUNGSI LOOP
// ===================================================================
void loop() {
check_encoder();
check_buttons();
check_auto_save(); // Panggil fungsi auto-save
update_display();
}
// ===================================================================
// FUNGSI-FUNGSI UTAMA
// ===================================================================
void check_encoder() {
long new_pos = encoder.getPosition();
if (last_encoder_pos != new_pos) {
long change = new_pos - last_encoder_pos;
// Tandai ada aktivitas dan perubahan yang perlu disimpan
last_activity_time = millis();
settings_changed = true;
switch (setting_menu_state) {
case 1: if_frequency += (change * 10); break;
case 2: bfo_lsb_offset += (change * 10); break;
case 3: bfo_usb_offset += (change * 10); break;
case 4: am_rx_offset += (change * 10); break;
case 5: // Menu Kalibrasi
si5351_correction_val += (change * 100);
si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, si5351_correction_val);
si5351.drive_strength(SI5351_CLK0,SI5351_DRIVE_6MA);//you can set this to 2MA, 4MA, 6MA or 8MA
si5351.drive_strength(SI5351_CLK1,SI5351_DRIVE_6MA);//you can set this to 2MA, 4MA, 6MA or 8MA
set_frequency(current_frequency);
break;
case 6: tx_offset_freq += (change * 10); break;
default:
current_frequency += (change * step_values[current_step_index]);
if (current_frequency < 1000000) current_frequency = 1000000;
if (current_frequency > 160000000) current_frequency = 160000000;
break;
}
force_display_update = true;
last_encoder_pos = new_pos;
}
}
void check_buttons() {
// Cek tombol TX
if (digitalRead(PIN_TX_BUTTON) == LOW) {
if (!is_tx) {
last_activity_time = millis();
start_tx();
}
return;
} else {
if (is_tx) stop_tx();
}
// Cek tombol Band
if (digitalRead(PIN_ENCODER_SW_BAND) == LOW) {
if (!band_button_active && setting_menu_state == 0) {
band_button_active = true;
force_display_update = true;
last_activity_time = millis();
settings_changed = true;
save_current_freq_to_band();
current_band = (current_band + 1) % 9; // Band definitions.
load_freq_from_band();
}
} else {
band_button_active = false;
}
// Cek tombol Step
if (digitalRead(PIN_STEP_BUTTON) == LOW) {
if (!step_button_active && setting_menu_state == 0) {
step_button_active = true;
force_display_update = true;
last_activity_time = millis();
settings_changed = true;
current_step_index = (current_step_index + 1) % 6; // STEP Definition
}
} else {
step_button_active = false;
}
// Cek tombol Set
if (digitalRead(PIN_SET_BUTTON) == LOW) {
if (set_button_press_start_time == 0) {
set_button_press_start_time = millis();
}
} else {
if (set_button_press_start_time != 0) {
unsigned long press_duration = millis() - set_button_press_start_time;
last_activity_time = millis(); // Aktivitas terdeteksi saat tombol dilepas
settings_changed = true;
if (press_duration >= long_press_duration) {
if (setting_menu_state > 0) {
setting_menu_state = 0;
save_settings(); // Langsung simpan saat keluar menu
settings_changed = false; // Reset flag karena sudah disimpan
} else {
setting_menu_state = 1;
}
} else {
if (setting_menu_state > 0) {
setting_menu_state = (setting_menu_state % 6) + 1;
} else {
current_mode = (current_mode + 1) % 3;
}
}
force_display_update = true;
set_button_press_start_time = 0;
}
}
set_frequency(current_frequency);
}
void set_frequency(long vfo_freq) {
update_bfo_frequencies();
long vfo_with_offset = vfo_freq;
if (is_tx) { vfo_with_offset += tx_offset_freq; }
uint64_t vfo_output_freq = (uint64_t)vfo_with_offset + if_frequency; //+ if_frequency only VFO frequency
si5351.set_freq(vfo_output_freq * 100, SI5351_CLK0);
si5351.output_enable(SI5351_CLK0, 1); // Enable CLK0
if (current_mode == 2 && is_tx) { // Mode AM saat TX, matikan BFO
si5351.output_enable(SI5351_CLK1, 1); //Enable AM TX CLK1
} else {
si5351.set_freq(bfo_frequency * 100, SI5351_CLK1);
si5351.output_enable(SI5351_CLK1, 1);
}
}
void start_tx() {
is_tx = true;
digitalWrite(PIN_PTT_OUT, HIGH);
force_display_update = true;
set_frequency(current_frequency);
}
void stop_tx() {
is_tx = false;
digitalWrite(PIN_PTT_OUT, LOW);
force_display_update = true;
save_settings();
set_frequency(current_frequency);
}
// ===================================================================
// FUNGSI DISPLAY
// ===================================================================
void draw_setting_screen(const __FlashStringHelper* title, long value) {
u8g2.setFont(u8g2_font_6x10_tf);
char title_buf[17];
strcpy_P(title_buf, (const char*)title);
u8g2.drawStr((128 - u8g2.getStrWidth(title_buf)) / 2, 15, title_buf);
char val_buf[20];
if (strcmp_P(title_buf, (const char*)F("SET IF")) == 0) {
sprintf(val_buf, "%ld", value);
} else {
sprintf(val_buf, "%+ld", value);
}
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawStr((128 - u8g2.getStrWidth(val_buf)) / 2, 52, val_buf);
}
void update_display() {
// Cek apakah ada perubahan yang memaksa update layar penuh
if (force_display_update || current_frequency != last_displayed_freq || is_tx != last_tx_state ||
if_frequency != last_if_frequency || tx_offset_freq != last_tx_offset_freq ||
bfo_lsb_offset != last_bfo_lsb_offset || bfo_usb_offset != last_bfo_usb_offset ||
am_rx_offset != last_am_rx_offset || si5351_correction_val != last_si5351_correction) {
// Biarkan redraw penuh terjadi
} else {
// Cek S-meter/Power meter saja
int current_bar_level;
if (is_tx) {
current_bar_level = map(analogRead(PIN_S_METER_IN), 0, 1023, 0, 91);
} else {
int s_constrained = constrain(analogRead(PIN_S_METER_IN), 0, S_METER_MAX_ADC);
current_bar_level = map(s_constrained, 0, S_METER_MAX_ADC, 0, 91);
}
if (current_bar_level != last_bar_level) {
// Hanya update jika nilai bar berubah
} else {
return; // Tidak ada yang perlu diupdate, keluar dari fungsi
}
}
u8g2.firstPage();
do {
switch (setting_menu_state) {
case 1: draw_setting_screen(F("SET IF"), if_frequency); break;
case 2: draw_setting_screen(F("SET LSB OFFSET"), bfo_lsb_offset); break;
case 3: draw_setting_screen(F("SET USB OFFSET"), bfo_usb_offset); break;
case 4: draw_setting_screen(F("SET AM OFFSET"), am_rx_offset); break;
case 5: draw_setting_screen(F("SET CALIBRATION"), si5351_correction_val); break;
case 6: draw_setting_screen(F("SET TX OFFSET"), tx_offset_freq); break;
default: {
char freq_buf[15];
long mhz = current_frequency / 1000000;
long khz = (current_frequency % 1000000) / 1000;
long hz = (current_frequency % 1000) / 10;
sprintf(freq_buf, "%2ld.%03ld.%02ld", mhz, khz, hz);
u8g2.setFont(u8g2_font_fub20_tn);
u8g2.drawStr(0, 26, freq_buf);
u8g2.setFont(u8g2_font_6x10_tf);
// Garis horizontal pemisah #1
u8g2.drawHLine(0, 30, 128);
// Garis vertikal pemisah #1
u8g2.drawVLine(43, 32, 12);
u8g2.drawVLine(86, 32, 12);
// Teks Band, Mode, Step
u8g2.setCursor(21 - u8g2.getStrWidth(band_names[current_band]) / 2, 42);
u8g2.print(band_names[current_band]);
u8g2.setCursor(64 - u8g2.getStrWidth(mode_names[current_mode]) / 2, 42);
u8g2.print(mode_names[current_mode]);
u8g2.setCursor(107 - u8g2.getStrWidth(step_display_names[current_step_index]) / 2, 42);
u8g2.print(step_display_names[current_step_index]);
// ======================================
// Garis horizontal pemisah #2
u8g2.drawHLine(0, 47, 128);
// Garis vertikal pemisah #2
u8g2.drawStr(32, 59, " 1 3 5 7 9 +30d ");
u8g2.drawVLine(32, 49, 14);
// ======================================
u8g2.drawStr(5, 60, is_tx ? "TX S" : "RX S");
u8g2.drawFrame(34, 50, 93, 11);
int bar_level = 0;
if (is_tx) {
int pwr_value = analogRead(PIN_S_METER_IN);
bar_level = map(pwr_value, 0, 1023, 0, 91);
} else {
int s_value = analogRead(PIN_S_METER_IN);
int s_constrained = constrain(s_value, 0, S_METER_MAX_ADC);
bar_level = map(s_constrained, 0, S_METER_MAX_ADC, 0, 91);
}
if (bar_level > 0) {
u8g2.drawBox(35, 51, bar_level, 9);
}
last_bar_level = bar_level;
break;
}
}
} while (u8g2.nextPage());
force_display_update = false;
last_displayed_freq = current_frequency;
last_tx_state = is_tx;
last_if_frequency = if_frequency;
last_tx_offset_freq = tx_offset_freq;
last_bfo_lsb_offset = bfo_lsb_offset;
last_bfo_usb_offset = bfo_usb_offset;
last_am_rx_offset = am_rx_offset;
last_si5351_correction = si5351_correction_val;
}
// ===================================================================
// FUNGSI-FUNGSI BANTU
// ===================================================================
// Fungsi untuk menyimpan otomatis setelah idle
void check_auto_save() {
// Jika ada perubahan DAN sudah 5 detik tidak ada aktivitas
if (settings_changed && (millis() - last_activity_time > auto_save_delay)) {
save_settings(); // Simpan pengaturan
settings_changed = false; // Reset flag agar tidak menyimpan terus-menerus
}
}
void update_bfo_frequencies() {
if (current_mode == 0) bfo_frequency = if_frequency + bfo_lsb_offset; // LSB
else if (current_mode == 1) bfo_frequency = if_frequency + bfo_usb_offset; // USB
else bfo_frequency = if_frequency + am_rx_offset; // AM
}
void load_settings() {
EepromData data;
EEPROM.get(0, data);
if (data.magic_key != 123456789) {
for (int i = 0; i < 9; i++) band_frequencies[i] = band_defaults[i]; // Band definitions.
current_band = 0;
current_mode = 1;
if_frequency = IF_FREQUENCY_DEFAULT;
tx_offset_freq = TX_OFFSET_FREQ_DEFAULT;
bfo_lsb_offset = BFO_LSB_OFFSET_DEFAULT;
bfo_usb_offset = BFO_USB_OFFSET_DEFAULT;
am_rx_offset = AM_RX_OFFSET_DEFAULT;
si5351_correction_val = SI5351_CORRECTION_DEFAULT;
current_frequency = band_frequencies[current_band];
save_settings();
} else {
for (int i = 0; i < 9; i++) band_frequencies[i] = data.last_freq[i]; // Band definitions.
current_band = data.last_band;
current_mode = data.last_mode;
if_frequency = data.if_frequency;
tx_offset_freq = data.tx_offset_freq;
bfo_lsb_offset = data.bfo_lsb_offset;
bfo_usb_offset = data.bfo_usb_offset;
am_rx_offset = data.am_rx_offset;
si5351_correction_val = data.si5351_correction;
current_frequency = band_frequencies[current_band];
}
update_bfo_frequencies();
}
void save_settings() {
EepromData data;
data.magic_key = 123456789;
band_frequencies[current_band] = current_frequency;
for (int i = 0; i < 9; i++) data.last_freq[i] = band_frequencies[i];
data.last_band = current_band;
data.last_mode = current_mode;
data.if_frequency = if_frequency;
data.tx_offset_freq = tx_offset_freq;
data.bfo_lsb_offset = bfo_lsb_offset;
data.bfo_usb_offset = bfo_usb_offset;
data.am_rx_offset = am_rx_offset;
data.si5351_correction = si5351_correction_val;
EEPROM.put(0, data);
}
void save_current_freq_to_band() {
band_frequencies[current_band] = current_frequency;
}
void load_freq_from_band() {
current_frequency = band_frequencies[current_band];
}
Não vou colocar muitas fotos de cada programa pois são 3 ao todo, vocês já sabem o que cada um faz e como se comportam.
Este esboço abaixo, vocês poderão colocarem no Cobra 148 GTL e outros transceptores comerciais ou feitos em casa. Vale lembrar que uma única tecla a cada toque inicial em USB, e cada toque uma banda e selecionada USB, AM, LSB, fica a critério de montar um simples circuito com o CI 4013, ou outros semelhantes, ou mudar o programa pois eu não conseguir colocar para cada banda uma tecla.
Notem que a linha 313 está com comando bfo_frequency assim. 313 uint64_t vfo_output_freq = (uint64_t)vfo_with_offset + bfo_frequency; // + bfo_frequency AM LSB USB shifted VFO.
Com este comando CLK0 ficará com modos AM, LSB, USB. em TX e RX.
2- Esboço abaixo copie e cole no Arduino IDE.
/*
* DDS VFO Transceiver - 3Band
* =========================================================================
* Fitur :
* - Tampilan Callsign saat startup
* - Band: 80m, 40m, 11m
* - Menu setting : IF (step 10Hz), LSB Offset, USB Offset, AM Offset, Kalibrasi Kristal, TX Offset
* - Gain S-Meter disesuaikan untuk 0.7V = Full Scale
* - Semua setting tersimpan di EEPROM
* - Tampilan UI dengan garis pemisah
* - Auto-save setelah 5 detik idle
* =========================================================================
*/
//---------------------------------------------------------------------------------
// Function
// 1.CLK0 RX VFO 34 MHZ AM,LSB,USB, MODE Cobra 148 GTL OTHER TRANSCEIVERS
// 2.CLK0 TX FREQUENCY VFO - IF = FREQUENCY DISPLAY AM,LSB,USB,OFFSET MODE
// 3.CLK1 BFO RX,TX FI 7800KHZ AM,LSB,USB,OFFSET MODE
// 4.BAND (9) 80M,40M,30M,20M,15M,12M,11M,10M,6M, HF,VHF
// 5.STEP 10Hz,100Hz,1KHz,10KHz,100KHz,1MHz
// 6.MENU CALIBRATION VFO,IF AM,LSB,USB,OFFSET MODE
// 7.S-METER FULL SCALE S 1 3 5 7 9 + 30d
// 8.EEPROM INCLUDED FOR MEMORY
// Sketch modified by Waldir October 2025. Blog: https://projetosetransceptores.blogspot.com/
//////////////////////////////////////////////////////////////////////
// ================== 1. PUSTAKA (LIBRARIES) ==================
#include <EEPROM.h>
#include <Wire.h>
#include <si5351.h>
#include <RotaryEncoder.h>
#include <U8g2lib.h>
// ================== 2. DEFINISI & PENGATURAN INTI ==================
// ----- -----
#define CALLSIGN "JZ13NGF" // Author call prefix, change yours
#define IF_FREQUENCY_DEFAULT 7800000UL // Enter the transceiver IF 7800000,9998500,10000000,10240000,10700000
#define BFO_LSB_OFFSET_DEFAULT -1500L // LSB
#define BFO_USB_OFFSET_DEFAULT 1500L // USB
#define AM_FREQUENCY_DEFAULT 0L // Initialize to zero for Menu correction
#define SI5351_CORRECTION_DEFAULT 18000L // Si5351 crystal frequency correction
#define TX_OFFSET_FREQ_DEFAULT 0L // Adjust IF 7800000L or -7800000L TX VFO frequency display
// Definisi Pin
#define PIN_ENCODER_CLK 2
#define PIN_ENCODER_DT 3
#define PIN_ENCODER_SW_BAND 4
#define PIN_STEP_BUTTON A0
#define PIN_TX_BUTTON A1
#define PIN_SET_BUTTON A2
#define PIN_S_METER_IN A3
#define PIN_PTT_OUT 6
// Pengaturan S-Meter. S-Meter Settings.
#define S_METER_MAX_ADC 143
// Pengaturan OLED. OLED Settings
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// Deklarasi Objek Lainnya. Object Declarations
Si5351 si5351;
RotaryEncoder encoder(PIN_ENCODER_DT, PIN_ENCODER_CLK);
// Variabel & Konstanta Program. Program Variables & Constants
long bfo_frequency;
const char* mode_names[] = {"LSB", "USB", "AM "};
const long step_values[] = {10, 100, 1000, 10000, 100000, 1000000};
const char* step_display_names[] = {"10Hz" , "100Hz", "1KHz", "10KHz", "100KHz", "1MHz"};
byte current_step_index = 3;
// Struktur EEPROM. EEPROM structure
struct EepromData {
uint32_t magic_key;
long last_freq[9];
int last_band;
int last_mode;
long if_frequency;
long tx_offset_freq;
long bfo_lsb_offset;
long bfo_usb_offset;
long am_frequency;
long si5351_correction;
};
// Definisi Band
const char* band_names[] = {"80M","40M","30M","20M","15M","12M","11M","10M","6M"};
const long band_defaults[]={3500000,7000000,10100000,14000000,21000000,24890000,26965000,28000000,50000000 };
long band_frequencies[9];
// ================== 3. VARIABEL GLOBAL ==================
long current_frequency;
int current_band = 0;
int current_mode = 1;
bool is_tx = false;
bool force_display_update = true;
long last_encoder_pos = 0;
// Menu setting sekarang ada 6 item
int setting_menu_state = 0; // 0:Normal, 1:IF, 2:LSB, 3:USB, 4:AM, 5:Cal, 6:TX
// Variabel Pengaturan
long if_frequency;
long tx_offset_freq;
long bfo_lsb_offset;
long bfo_usb_offset;
long am_frequency;
long si5351_correction_val;
// Tombol & Durasi
unsigned long set_button_press_start_time = 0;
const long long_press_duration = 1000;
// Variabel untuk optimasi display
long last_displayed_freq = 0;
bool last_tx_state = false;
int last_bar_level = -1;
long last_if_frequency = 0;
long last_tx_offset_freq = 0;
long last_bfo_lsb_offset = 0;
long last_bfo_usb_offset = 0;
long last_am_frequency = 0;
long last_si5351_correction = 0;
// Variabel untuk state change detection
bool band_button_active = false;
bool step_button_active = false;
// Variabel untuk Auto-Save (setelah idle 5 detik)
unsigned long last_activity_time = 0;
bool settings_changed = false;
const unsigned long auto_save_delay = 5000; // 5 detik dalam milidetik
// ===================================================================
// FUNGSI INTERRUPT UNTUK ENCODER
// ===================================================================
void isrEncoder() {
encoder.tick();
}
// ===================================================================
// FUNGSI SETUP
// ===================================================================
void setup() {
pinMode(PIN_ENCODER_SW_BAND, INPUT_PULLUP);
pinMode(PIN_STEP_BUTTON, INPUT_PULLUP);
pinMode(PIN_TX_BUTTON, INPUT_PULLUP);
pinMode(PIN_SET_BUTTON, INPUT_PULLUP);
pinMode(PIN_PTT_OUT, OUTPUT);
digitalWrite(PIN_PTT_OUT, LOW);
pinMode(PIN_ENCODER_CLK, INPUT_PULLUP);
pinMode(PIN_ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_CLK), isrEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_DT), isrEncoder, CHANGE);
u8g2.begin();
// =================== KODE STARTUP SCREEN ===================
u8g2.firstPage();
do {
// Tampilkan Callsign dengan font besar di tengah
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawStr((128 - u8g2.getStrWidth(CALLSIGN)) / 2, 30, CALLSIGN);
// Tampilkan nama perangkat di bawahnya
u8g2.setFont(u8g2_font_6x10_tf);
const char* subtitle = "DDS VFO 9BAND HF VHF";
u8g2.drawStr((128 - u8g2.getStrWidth(subtitle)) / 2, 50, subtitle);
} while (u8g2.nextPage());
delay(2500); // Tahan tampilan startup selama 2.5 detik
// ================ END OF INITIALIZATION CODE ====================
load_settings();
if (!si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, si5351_correction_val)) {
si5351.drive_strength(SI5351_CLK0,SI5351_DRIVE_6MA);//you can set this to 2MA, 4MA, 6MA or 8MA
si5351.drive_strength(SI5351_CLK1,SI5351_DRIVE_6MA);//you can set this to 2MA, 4MA, 6MA or 8MA
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(10, 35, "Si5351 GAGAL!");
} while (u8g2.nextPage());
while (1);
}
set_frequency(current_frequency);
}
// ===================================================================
// FUNGSI LOOP
// ===================================================================
void loop() {
check_encoder();
check_buttons();
check_auto_save(); //Panggil fungsi auto-save. Call the auto-save function
update_display();
}
// ===================================================================
// FUNGSI-FUNGSI UTAMA
// ===================================================================
void check_encoder() {
long new_pos = encoder.getPosition();
if (last_encoder_pos != new_pos) {
long change = new_pos - last_encoder_pos;
// Tandai ada aktivitas dan perubahan yang perlu disimpan
last_activity_time = millis();
settings_changed = true;
switch (setting_menu_state) {
case 1: if_frequency += (change * 10); break;
case 2: bfo_lsb_offset += (change * 10); break;
case 3: bfo_usb_offset += (change * 10); break;
case 4: am_frequency += (change * 10); break;
case 5: // Menu Calibration
si5351_correction_val += (change * 100);
si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, si5351_correction_val);
si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_6MA); //you can set this to 2MA, 4MA, 6MA or 8MA
si5351.drive_strength(SI5351_CLK1, SI5351_DRIVE_6MA); //you can set this to 2MA, 4MA, 6MA or 8MA
set_frequency(current_frequency);
break;
case 6: tx_offset_freq += (change * 10); break;
default:
current_frequency += (change * step_values[current_step_index]);
if (current_frequency < 1000000) current_frequency = 1000000; // Initial frequency
if (current_frequency > 160000000) current_frequency = 160000000; // Final frequency
break;
}
force_display_update = true;
last_encoder_pos = new_pos;
}
}
void check_buttons() {
// Cek tombol TX
if (digitalRead(PIN_TX_BUTTON) == LOW) {
if (!is_tx) {
last_activity_time = millis();
start_tx();
}
return;
} else {
if (is_tx) stop_tx();
}
// Cek tombol Band. Check the Band button
if (digitalRead(PIN_ENCODER_SW_BAND) == LOW) {
if (!band_button_active && setting_menu_state == 0) {
band_button_active = true;
force_display_update = true;
last_activity_time = millis();
settings_changed = true;
save_current_freq_to_band();
current_band = (current_band + 1) % 9; // Band definitions.
load_freq_from_band();
}
} else {
band_button_active = false;
}
// Cek tombol Step
if (digitalRead(PIN_STEP_BUTTON) == LOW) {
if (!step_button_active && setting_menu_state == 0) {
step_button_active = true;
force_display_update = true;
last_activity_time = millis();
settings_changed = true;
current_step_index = (current_step_index + 1) % 6;// STEP Definition
}
} else {
step_button_active = false;
}
// Cek tombol Set. Check the Set button
if (digitalRead(PIN_SET_BUTTON) == LOW) {
if (set_button_press_start_time == 0) {
set_button_press_start_time = millis();
}
} else {
if (set_button_press_start_time != 0) {
unsigned long press_duration = millis() - set_button_press_start_time;
last_activity_time = millis(); // Aktivitas terdeteksi saat tombol dilepas
settings_changed = true;
if (press_duration >= long_press_duration) {
if (setting_menu_state > 0) {
setting_menu_state = 0;
save_settings(); // Langsung simpan saat keluar menu
settings_changed = false; // Reset flag karena sudah disimpan
} else {
setting_menu_state = 1;
}
} else {
if (setting_menu_state > 0) {
setting_menu_state = (setting_menu_state % 6) + 1;
} else {
current_mode = (current_mode + 1) % 3;
}
}
force_display_update = true;
set_button_press_start_time = 0;
}
}
set_frequency(current_frequency);
}
void set_frequency (long vfo_freq) {
update_bfo_frequencies();
long vfo_with_offset = vfo_freq;
if (is_tx) { vfo_with_offset += tx_offset_freq; }
uint64_t vfo_output_freq = (uint64_t)vfo_with_offset + bfo_frequency; // + bfo_frequency AM LSB USB shifted VFO
si5351.set_freq(vfo_output_freq * 100, SI5351_CLK0);
si5351.output_enable(SI5351_CLK0, 1);
if (current_mode == 2 && is_tx) { // Mode AM saat TX, matikan BFO. AM mode when TX, turn off BFO.
si5351.output_enable(SI5351_CLK1, 1); //Enable AM TX CLK1
} else {
si5351.set_freq(bfo_frequency * 100, SI5351_CLK1);
si5351.output_enable(SI5351_CLK1, 1);
}
}
void start_tx() {
is_tx = true;
digitalWrite(PIN_PTT_OUT, HIGH);
force_display_update = true;
set_frequency(current_frequency);
}
void stop_tx() {
is_tx = false;
digitalWrite(PIN_PTT_OUT, LOW);
force_display_update = true;
save_settings();
set_frequency(current_frequency);
}
// ===================================================================
// FUNGSI DISPLAY
// ===================================================================
void draw_setting_screen(const __FlashStringHelper* title, long value) {
u8g2.setFont(u8g2_font_6x10_tf);
char title_buf[17];
strcpy_P(title_buf, (const char*)title);
u8g2.drawStr((128 - u8g2.getStrWidth(title_buf)) / 2, 15, title_buf);
char val_buf[20];
if (strcmp_P(title_buf, (const char*)F("SET IF")) == 0) {
sprintf(val_buf, "%ld", value);
} else {
sprintf(val_buf, "%+ld", value);
}
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawStr((128 - u8g2.getStrWidth(val_buf)) / 2, 52, val_buf);
}
void update_display() {
// Cek apakah ada perubahan yang memaksa update layar penuh
if (force_display_update || current_frequency != last_displayed_freq || is_tx != last_tx_state ||
if_frequency != last_if_frequency || tx_offset_freq != last_tx_offset_freq ||
bfo_lsb_offset != last_bfo_lsb_offset || bfo_usb_offset != last_bfo_usb_offset ||
am_frequency != last_am_frequency || si5351_correction_val != last_si5351_correction) {
// Biarkan redraw penuh terjadi
} else {
// Cek S-meter/Power meter saja
int current_bar_level;
if (is_tx) {
current_bar_level = map(analogRead(PIN_S_METER_IN), 0, 1023, 0, 91);
} else {
int s_constrained = constrain(analogRead(PIN_S_METER_IN), 0, S_METER_MAX_ADC);
current_bar_level = map(s_constrained, 0, S_METER_MAX_ADC, 0, 91);
}
if (current_bar_level != last_bar_level) {
// Hanya update jika nilai bar berubah
} else {
return; // Tidak ada yang perlu diupdate, keluar dari fungsi
}
}
u8g2.firstPage();
do {
switch (setting_menu_state) {
case 1: draw_setting_screen(F("SET IF"), if_frequency); break;
case 2: draw_setting_screen(F("SET LSB OFFSET"), bfo_lsb_offset); break;
case 3: draw_setting_screen(F("SET USB OFFSET"), bfo_usb_offset); break;
case 4: draw_setting_screen(F("SET AM FREQUENCY"), am_frequency); break;
case 5: draw_setting_screen(F("SET CALIBRATION"), si5351_correction_val); break;
case 6: draw_setting_screen(F("SET TX OFFSET"), tx_offset_freq); break;
default: {
char freq_buf[15];
long mhz = current_frequency / 1000000;
long khz = (current_frequency % 1000000) / 1000;
long hz = (current_frequency % 1000) / 10;
sprintf(freq_buf, "%2ld.%03ld.%02ld", mhz, khz, hz);
u8g2.setFont(u8g2_font_fub20_tn);
u8g2.drawStr(0, 26, freq_buf);
u8g2.setFont(u8g2_font_6x10_tf);
// Garis horizontal pemisah #1
// Linha divisória horizontal #1
u8g2.drawHLine(0, 30, 128);
// Garis vertikal pemisah #1
// Linha divisória vertical #1
u8g2.drawVLine(43, 32, 12);
u8g2.drawVLine(86, 32, 12);
// Teks Band, Mode, Step
// Banda Teks, Modo, Passo
u8g2.setCursor(21 - u8g2.getStrWidth(band_names[current_band]) / 2, 42);
u8g2.print(band_names[current_band]);
u8g2.setCursor(64 - u8g2.getStrWidth(mode_names[current_mode]) / 2, 42);
u8g2.print(mode_names[current_mode]);
u8g2.setCursor(107 - u8g2.getStrWidth(step_display_names[current_step_index]) / 2, 42);
u8g2.print(step_display_names[current_step_index]);
// ========Display dividing lines=========
// Garis horizontal pemisah #2
u8g2.drawHLine(0, 47, 128);
// Garis vertikal pemisah #2
u8g2.drawStr(32, 59, " 1 3 5 7 9 +30d ");
u8g2.drawVLine(32, 49, 14);
// ======================================
u8g2.drawStr(5, 60, is_tx ? "TX S" : "RX S");
u8g2.drawFrame(34, 50, 93, 11);
int bar_level = 0;
if (is_tx) {
int pwr_value = analogRead(PIN_S_METER_IN);
bar_level = map(pwr_value, 0, 1023, 0, 91);
} else {
int s_value = analogRead(PIN_S_METER_IN);
int s_constrained = constrain(s_value, 0, S_METER_MAX_ADC);
bar_level = map(s_constrained, 0, S_METER_MAX_ADC, 0, 91);
}
if (bar_level > 0) {
u8g2.drawBox(35, 51, bar_level, 9);
}
last_bar_level = bar_level;
break;
}
}
} while (u8g2.nextPage());
force_display_update = false;
last_displayed_freq = current_frequency;
last_tx_state = is_tx;
last_if_frequency = if_frequency;
last_tx_offset_freq = tx_offset_freq;
last_bfo_lsb_offset = bfo_lsb_offset;
last_bfo_usb_offset = bfo_usb_offset;
last_am_frequency = am_frequency;
last_si5351_correction = si5351_correction_val;
}
// ===================================================================
// FUNGSI-FUNGSI BANTU FUNÇÕES AUXILIARES
// ===================================================================
// Fungsi untuk menyimpan otomatis setelah idle
void check_auto_save() {
// Jika ada perubahan DAN sudah 5 detik tidak ada aktivitas
if (settings_changed && (millis() - last_activity_time > auto_save_delay)) {
save_settings(); // Simpan pengaturan
settings_changed = false; // Reset flag agar tidak menyimpan terus-menerus
}
}
void update_bfo_frequencies() {
if (current_mode == 0) bfo_frequency = if_frequency + bfo_lsb_offset; // LSB
else if (current_mode == 1) bfo_frequency = if_frequency + bfo_usb_offset; // USB
else bfo_frequency = if_frequency + am_frequency; // AM
}
void load_settings() {
EepromData data;
EEPROM.get(0, data);
if (data.magic_key != 123456789) {
for(int i=0;i<9;i++) band_frequencies[i] = band_defaults[i]; // Band definitions.
current_band = 0;
current_mode = 1;
if_frequency = IF_FREQUENCY_DEFAULT;
tx_offset_freq = TX_OFFSET_FREQ_DEFAULT;
bfo_lsb_offset = BFO_LSB_OFFSET_DEFAULT;
bfo_usb_offset = BFO_USB_OFFSET_DEFAULT;
am_frequency = AM_FREQUENCY_DEFAULT;
si5351_correction_val = SI5351_CORRECTION_DEFAULT;
current_frequency = band_frequencies[current_band];
save_settings();
} else {
for(int i=0;i<9;i++) band_frequencies[i] = data.last_freq[i]; // Band definitions.
current_band = data.last_band;
current_mode = data.last_mode;
if_frequency = data.if_frequency;
tx_offset_freq = data.tx_offset_freq;
bfo_lsb_offset = data.bfo_lsb_offset;
bfo_usb_offset = data.bfo_usb_offset;
am_frequency = data.am_frequency;
si5351_correction_val = data.si5351_correction;
current_frequency = band_frequencies[current_band];
}
update_bfo_frequencies();
}
void save_settings() {
EepromData data;
data.magic_key = 123456789;
band_frequencies[current_band] = current_frequency;
for(int i=0;i<9;i++) data.last_freq[i] = band_frequencies[i]; // Band definitions.
data.last_band = current_band;
data.last_mode = current_mode;
data.if_frequency = if_frequency;
data.tx_offset_freq = tx_offset_freq;
data.am_frequency = am_frequency;
data.si5351_correction = si5351_correction_val;
EEPROM.put(0, data);
}
void save_current_freq_to_band() {
band_frequencies[current_band] = current_frequency;
}
void load_freq_from_band() {
current_frequency = band_frequencies[current_band];
}
Esboço abaixo, vocês poderão colocarem em transceptores comerciais ou feitos em casa. Em CLK0 VFO RX você tem frequência do display mais a soma FI do transceptor, em TX você terão só a frequência do display sem FI. RX e TX com AM SSB CLK0 VFO. Vale lembrar que Modo é uma única tecla a cada toque, inicial em USB, e cada toque uma banda e selecionada USB, AM, LSB.
Notem que a linha 313 está com mesmo comando bfo_frequency assim. 313 uint64_t vfo_output_freq = (uint64_t)vfo_with_offset + bfo_frequency; // + bfo_frequency AM LSB USB shifted VFO.
O que digitei foi a FI na linha 42 #define TX_OFFSET_FREQ_DEFAULT 7800000L
Digitando a FI na linha 42 o TX de CLK0 VFO ficará menos a FI na frequência de RX.
3- Esboço abaixo copie e cole no Arduino IDE.
/*
* DDS VFO Transceiver - 3Band
* =========================================================================
* Fitur :
* - Tampilan Callsign saat startup
* - Band: 80m, 40m, 11m
* - Menu setting : IF (step 10Hz), LSB Offset, USB Offset, AM Offset, Kalibrasi Kristal, TX Offset
* - Gain S-Meter disesuaikan untuk 0.7V = Full Scale
* - Semua setting tersimpan di EEPROM
* - Tampilan UI dengan garis pemisah
* - Auto-save setelah 5 detik idle
* =========================================================================
*/
//---------------------------------------------------------------------------------
// Function
// 1.CLK0 BAND 7 RX VFO 34.765 MHZ AM,LSB,USB, MODE
// 2.CLK0 TX FREQUENCY VFO - IF = FREQUENCY DISPLAY AM,LSB,USB,OFFSET MODE
// 3.CLK1 BFO RX,TX FI 7800KHZ AM,LSB,USB,OFFSET MODE
// 4.BAND 80M,40M,30M,20M,15M,12M,11M,10M,6M, HF,VHF
// 5.STEP 10Hz,100Hz,1KHz,10KHz,100KHz,1MHz
// 6.MENU CALIBRATION VFO,IF AM,LSB,USB,OFFSET MODE
// 7.S-METER FULL SCALE S 1 3 5 7 9 + 30d
// 8.EEPROM INCLUDED FOR MEMORY
// Sketch modified by Waldir October 2025 Blog: https://projetosetransceptores.blogspot.com/
//////////////////////////////////////////////////////////////////////
// ================== 1. PUSTAKA (LIBRARIES) ==================
#include <EEPROM.h>
#include <Wire.h>
#include <si5351.h>
#include <RotaryEncoder.h>
#include <U8g2lib.h>
// ================== 2. DEFINISI & PENGATURAN INTI ==================
// ----- -----
#define CALLSIGN "JZ13NGF" // Author call prefix, change yours
#define IF_FREQUENCY_DEFAULT 7800000UL // Enter the transceiver IF 7800000,9998500,10000000,10240000,10700000
#define BFO_LSB_OFFSET_DEFAULT -1500L // LSB
#define BFO_USB_OFFSET_DEFAULT 1500L // USB
#define AM_FREQUENCY_DEFAULT 0UL // Initialize to zero for Menu correction
#define SI5351_CORRECTION_DEFAULT 18000L // Si5351 crystal frequency correction
#define TX_OFFSET_FREQ_DEFAULT 7800000L // Adjust IF 7800000L or -7800000L TX VFO frequency display
// Definisi Pin
#define PIN_ENCODER_CLK 2
#define PIN_ENCODER_DT 3
#define PIN_ENCODER_SW_BAND 4
#define PIN_STEP_BUTTON A0
#define PIN_TX_BUTTON A1
#define PIN_SET_BUTTON A2
#define PIN_S_METER_IN A3
#define PIN_PTT_OUT 6
// Pengaturan S-Meter. S-Meter Settings.
#define S_METER_MAX_ADC 143
// Pengaturan OLED. OLED Settings
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// Deklarasi Objek Lainnya. Object Declarations
Si5351 si5351;
RotaryEncoder encoder(PIN_ENCODER_DT, PIN_ENCODER_CLK);
// Variabel & Konstanta Program. Program Variables & Constants
long bfo_frequency;
const char* mode_names[] = {"LSB", "USB", "AM "};
const long step_values[] = {10, 100, 1000, 10000, 100000, 1000000};
const char* step_display_names[] = {"10Hz" , "100Hz", "1KHz", "10KHz", "100KHz", "1MHz"};
byte current_step_index = 3;
// Struktur EEPROM. EEPROM structure
struct EepromData {
uint32_t magic_key;
long last_freq[9];
int last_band;
int last_mode;
long if_frequency;
long tx_offset_freq;
long bfo_lsb_offset;
long bfo_usb_offset;
long am_frequency;
long si5351_correction;
};
// Definisi Band
const char* band_names[] = {"80M","40M","30M","20M","15M","12M","11M","10M","6M"};
const long band_defaults[]={3500000,7000000,10100000,14000000,21000000,24890000,26965000,28000000,50000000 };
long band_frequencies[9];
// ================== 3. VARIABEL GLOBAL ==================
long current_frequency;
int current_band = 0;
int current_mode = 1;
bool is_tx = false;
bool force_display_update = true;
long last_encoder_pos = 0;
// Menu setting sekarang ada 6 item
int setting_menu_state = 0; // 0:Normal, 1:IF, 2:LSB, 3:USB, 4:AM, 5:Cal, 6:TX
// Variabel Pengaturan
long if_frequency;
long tx_offset_freq;
long bfo_lsb_offset;
long bfo_usb_offset;
long am_frequency;
long si5351_correction_val;
// Tombol & Durasi
unsigned long set_button_press_start_time = 0;
const long long_press_duration = 1000;
// Variabel untuk optimasi display
long last_displayed_freq = 0;
bool last_tx_state = false;
int last_bar_level = -1;
long last_if_frequency = 0;
long last_tx_offset_freq = 0;
long last_bfo_lsb_offset = 0;
long last_bfo_usb_offset = 0;
long last_am_frequency = 0;
long last_si5351_correction = 0;
// Variabel untuk state change detection
bool band_button_active = false;
bool step_button_active = false;
// Variabel untuk Auto-Save (setelah idle 5 detik)
unsigned long last_activity_time = 0;
bool settings_changed = false;
const unsigned long auto_save_delay = 5000; // 5 detik dalam milidetik
// ===================================================================
// FUNGSI INTERRUPT UNTUK ENCODER
// ===================================================================
void isrEncoder() {
encoder.tick();
}
// ===================================================================
// FUNGSI SETUP
// ===================================================================
void setup() {
pinMode(PIN_ENCODER_SW_BAND, INPUT_PULLUP);
pinMode(PIN_STEP_BUTTON, INPUT_PULLUP);
pinMode(PIN_TX_BUTTON, INPUT_PULLUP);
pinMode(PIN_SET_BUTTON, INPUT_PULLUP);
pinMode(PIN_PTT_OUT, OUTPUT);
digitalWrite(PIN_PTT_OUT, LOW);
pinMode(PIN_ENCODER_CLK, INPUT_PULLUP);
pinMode(PIN_ENCODER_DT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_CLK), isrEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENCODER_DT), isrEncoder, CHANGE);
u8g2.begin();
// =================== KODE STARTUP SCREEN ===================
u8g2.firstPage();
do {
// Tampilkan Callsign dengan font besar di tengah
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawStr((128 - u8g2.getStrWidth(CALLSIGN)) / 2, 30, CALLSIGN);
// Tampilkan nama perangkat di bawahnya
u8g2.setFont(u8g2_font_6x10_tf);
const char* subtitle = "DDS VFO 9BAND HF VHF";
u8g2.drawStr((128 - u8g2.getStrWidth(subtitle)) / 2, 50, subtitle);
} while (u8g2.nextPage());
delay(2500); // Tahan tampilan startup selama 2.5 detik
// ================= END OF INITIALIZATION CODE ====================
load_settings();
if (!si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, si5351_correction_val)) {
si5351.drive_strength(SI5351_CLK0,SI5351_DRIVE_6MA);//you can set this to 2MA, 4MA, 6MA or 8MA
si5351.drive_strength(SI5351_CLK1,SI5351_DRIVE_6MA);//you can set this to 2MA, 4MA, 6MA or 8MA
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(10, 35, "Si5351 GAGAL!");
} while (u8g2.nextPage());
while (1);
}
set_frequency(current_frequency);
}
// ===================================================================
// FUNGSI LOOP
// ===================================================================
void loop() {
check_encoder();
check_buttons();
check_auto_save(); //Panggil fungsi auto-save. Call the auto-save function
update_display();
}
// ===================================================================
// FUNGSI-FUNGSI UTAMA
// ===================================================================
void check_encoder() {
long new_pos = encoder.getPosition();
if (last_encoder_pos != new_pos) {
long change = new_pos - last_encoder_pos;
// Tandai ada aktivitas dan perubahan yang perlu disimpan
last_activity_time = millis();
settings_changed = true;
switch (setting_menu_state) {
case 1: if_frequency += (change * 10); break;
case 2: bfo_lsb_offset += (change * 10); break;
case 3: bfo_usb_offset += (change * 10); break;
case 4: am_frequency += (change * 10); break;
case 5: // Menu Calibration
si5351_correction_val += (change * 100);
si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, si5351_correction_val);
si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_6MA); //you can set this to 2MA, 4MA, 6MA or 8MA
si5351.drive_strength(SI5351_CLK1, SI5351_DRIVE_6MA); //you can set this to 2MA, 4MA, 6MA or 8MA
set_frequency(current_frequency);
break;
case 6: tx_offset_freq += (change * 10); break;
default:
current_frequency += (change * step_values[current_step_index]);
if (current_frequency < 1000000) current_frequency = 1000000; // Initial frequency
if (current_frequency > 160000000) current_frequency = 160000000; // Final frequency
break;
}
force_display_update = true;
last_encoder_pos = new_pos;
}
}
void check_buttons() {
// Cek tombol TX
if (digitalRead(PIN_TX_BUTTON) == LOW) {
if (!is_tx) {
last_activity_time = millis();
start_tx();
}
return;
} else {
if (is_tx) stop_tx();
}
// Cek tombol Band. Check the Band button
if (digitalRead(PIN_ENCODER_SW_BAND) == LOW) {
if (!band_button_active && setting_menu_state == 0) {
band_button_active = true;
force_display_update = true;
last_activity_time = millis();
settings_changed = true;
save_current_freq_to_band();
current_band = (current_band + 1) % 9; // Band definitions.
load_freq_from_band();
}
} else {
band_button_active = false;
}
// Cek tombol Step
if (digitalRead(PIN_STEP_BUTTON) == LOW) {
if (!step_button_active && setting_menu_state == 0) {
step_button_active = true;
force_display_update = true;
last_activity_time = millis();
settings_changed = true;
current_step_index = (current_step_index + 1) % 6;// STEP Definition
}
} else {
step_button_active = false;
}
// Cek tombol Set. Check the Set button
if (digitalRead(PIN_SET_BUTTON) == LOW) {
if (set_button_press_start_time == 0) {
set_button_press_start_time = millis();
}
} else {
if (set_button_press_start_time != 0) {
unsigned long press_duration = millis() - set_button_press_start_time;
last_activity_time = millis(); // Aktivitas terdeteksi saat tombol dilepas
settings_changed = true;
if (press_duration >= long_press_duration) {
if (setting_menu_state > 0) {
setting_menu_state = 0;
save_settings(); // Langsung simpan saat keluar menu
settings_changed = false; // Reset flag karena sudah disimpan
} else {
setting_menu_state = 1;
}
} else {
if (setting_menu_state > 0) {
setting_menu_state = (setting_menu_state % 6) + 1;
} else {
current_mode = (current_mode + 1) % 3;
}
}
force_display_update = true;
set_button_press_start_time = 0;
}
}
set_frequency(current_frequency);
}
void set_frequency (long vfo_freq) {
update_bfo_frequencies();
long vfo_with_offset = vfo_freq;
if (is_tx) { vfo_with_offset -= tx_offset_freq; }
uint64_t vfo_output_freq = (uint64_t)vfo_with_offset + bfo_frequency; // + bfo_frequency AM LSB USB shifted VFO
si5351.set_freq(vfo_output_freq * 100, SI5351_CLK0);
si5351.output_enable(SI5351_CLK0, 1);
if (current_mode == 2 && is_tx) { // Mode AM saat TX, matikan BFO. AM mode when TX, turn off BFO.
si5351.output_enable(SI5351_CLK1, 1); // Enable AM TX CLK1
} else {
si5351.set_freq(bfo_frequency * 100, SI5351_CLK1);
si5351.output_enable(SI5351_CLK1, 1); // Enable CLK1 BFO TX
}
}
void start_tx() {
is_tx = true;
digitalWrite(PIN_PTT_OUT, HIGH);
force_display_update = true;
set_frequency(current_frequency);
}
void stop_tx() {
is_tx = false;
digitalWrite(PIN_PTT_OUT, LOW);
force_display_update = true;
save_settings();
set_frequency(current_frequency);
}
// ===================================================================
// FUNGSI DISPLAY
// ===================================================================
void draw_setting_screen(const __FlashStringHelper* title, long value) {
u8g2.setFont(u8g2_font_6x10_tf);
char title_buf[17];
strcpy_P(title_buf, (const char*)title);
u8g2.drawStr((128 - u8g2.getStrWidth(title_buf)) / 2, 15, title_buf);
char val_buf[20];
if (strcmp_P(title_buf, (const char*)F("SET IF")) == 0) {
sprintf(val_buf, "%ld", value);
} else {
sprintf(val_buf, "%+ld", value);
}
u8g2.setFont(u8g2_font_ncenB14_tr);
u8g2.drawStr((128 - u8g2.getStrWidth(val_buf)) / 2, 52, val_buf);
}
void update_display() {
// Cek apakah ada perubahan yang memaksa update layar penuh
if (force_display_update || current_frequency != last_displayed_freq || is_tx != last_tx_state ||
if_frequency != last_if_frequency || tx_offset_freq != last_tx_offset_freq ||
bfo_lsb_offset != last_bfo_lsb_offset || bfo_usb_offset != last_bfo_usb_offset ||
am_frequency != last_am_frequency || si5351_correction_val != last_si5351_correction) {
// Biarkan redraw penuh terjadi
} else {
// Cek S-meter/Power meter saja
int current_bar_level;
if (is_tx) {
current_bar_level = map(analogRead(PIN_S_METER_IN), 0, 1023, 0, 91);
} else {
int s_constrained = constrain(analogRead(PIN_S_METER_IN), 0, S_METER_MAX_ADC);
current_bar_level = map(s_constrained, 0, S_METER_MAX_ADC, 0, 91);
}
if (current_bar_level != last_bar_level) {
// Hanya update jika nilai bar berubah
} else {
return; // Tidak ada yang perlu diupdate, keluar dari fungsi
}
}
u8g2.firstPage();
do {
switch (setting_menu_state) {
case 1: draw_setting_screen(F("SET IF"), if_frequency); break;
case 2: draw_setting_screen(F("SET LSB OFFSET"), bfo_lsb_offset); break;
case 3: draw_setting_screen(F("SET USB OFFSET"), bfo_usb_offset); break;
case 4: draw_setting_screen(F("SET AM FREQUENCY"), am_frequency); break;
case 5: draw_setting_screen(F("SET CALIBRATION"), si5351_correction_val); break;
case 6: draw_setting_screen(F("SET TX OFFSET"), tx_offset_freq); break;
default: {
char freq_buf[15];
long mhz = current_frequency / 1000000;
long khz = (current_frequency % 1000000) / 1000;
long hz = (current_frequency % 1000) / 10;
sprintf(freq_buf, "%2ld.%03ld.%02ld", mhz, khz, hz);
u8g2.setFont(u8g2_font_fub20_tn);
u8g2.drawStr(0, 26, freq_buf);
u8g2.setFont(u8g2_font_6x10_tf);
// Garis horizontal pemisah #1
// Linha divisória horizontal #1
u8g2.drawHLine(0, 30, 128);
// Garis vertikal pemisah #1
// Linha divisória vertical #1
u8g2.drawVLine(43, 32, 12);
u8g2.drawVLine(86, 32, 12);
// Teks Band, Mode, Step
// Banda Teks, Modo, Passo
u8g2.setCursor(21 - u8g2.getStrWidth(band_names[current_band]) / 2, 42);
u8g2.print(band_names[current_band]);
u8g2.setCursor(64 - u8g2.getStrWidth(mode_names[current_mode]) / 2, 42);
u8g2.print(mode_names[current_mode]);
u8g2.setCursor(107 - u8g2.getStrWidth(step_display_names[current_step_index]) / 2, 42);
u8g2.print(step_display_names[current_step_index]);
// ========Display dividing lines=========
// Garis horizontal pemisah #2
u8g2.drawHLine(0, 47, 128);
// Garis vertikal pemisah #2
u8g2.drawStr(32, 59, " 1 3 5 7 9 +30d ");
u8g2.drawVLine(32, 49, 14);
// ======================================
u8g2.drawStr(5, 60, is_tx ? "TX S" : "RX S");
u8g2.drawFrame(34, 50, 93, 11);
int bar_level = 0;
if (is_tx) {
int pwr_value = analogRead(PIN_S_METER_IN);
bar_level = map(pwr_value, 0, 1023, 0, 91);
} else {
int s_value = analogRead(PIN_S_METER_IN);
int s_constrained = constrain(s_value, 0, S_METER_MAX_ADC);
bar_level = map(s_constrained, 0, S_METER_MAX_ADC, 0, 91);
}
if (bar_level > 0) {
u8g2.drawBox(35, 51, bar_level, 9);
}
last_bar_level = bar_level;
break;
}
}
} while (u8g2.nextPage());
force_display_update = false;
last_displayed_freq = current_frequency;
last_tx_state = is_tx;
last_if_frequency = if_frequency;
last_tx_offset_freq = tx_offset_freq;
last_bfo_lsb_offset = bfo_lsb_offset;
last_bfo_usb_offset = bfo_usb_offset;
last_am_frequency = am_frequency;
last_si5351_correction = si5351_correction_val;
}
// ===================================================================
// FUNGSI-FUNGSI BANTU FUNÇÕES AUXILIARES
// ===================================================================
// Fungsi untuk menyimpan otomatis setelah idle
void check_auto_save() {
// Jika ada perubahan DAN sudah 5 detik tidak ada aktivitas
if (settings_changed && (millis() - last_activity_time > auto_save_delay)) {
save_settings(); // Simpan pengaturan
settings_changed = false; // Reset flag agar tidak menyimpan terus-menerus
}
}
void update_bfo_frequencies() {
if (current_mode == 0) bfo_frequency = if_frequency + bfo_lsb_offset; // LSB
else if (current_mode == 1) bfo_frequency = if_frequency + bfo_usb_offset; // USB
else bfo_frequency = if_frequency + am_frequency; // AM
}
void load_settings() {
EepromData data;
EEPROM.get(0, data);
if (data.magic_key != 123456789) {
for(int i=0;i<9;i++) band_frequencies[i] = band_defaults[i]; // Band definitions.
current_band = 0;
current_mode = 1;
if_frequency = IF_FREQUENCY_DEFAULT;
tx_offset_freq = TX_OFFSET_FREQ_DEFAULT;
bfo_lsb_offset = BFO_LSB_OFFSET_DEFAULT;
bfo_usb_offset = BFO_USB_OFFSET_DEFAULT;
am_frequency = AM_FREQUENCY_DEFAULT;
si5351_correction_val = SI5351_CORRECTION_DEFAULT;
current_frequency = band_frequencies[current_band];
save_settings();
} else {
for(int i=0;i<9;i++) band_frequencies[i] = data.last_freq[i]; // Band definitions.
current_band = data.last_band;
current_mode = data.last_mode;
if_frequency = data.if_frequency;
tx_offset_freq = data.tx_offset_freq;
bfo_lsb_offset = data.bfo_lsb_offset;
bfo_usb_offset = data.bfo_usb_offset;
am_frequency = data.am_frequency;
si5351_correction_val = data.si5351_correction;
current_frequency = band_frequencies[current_band];
}
update_bfo_frequencies();
}
void save_settings() {
EepromData data;
data.magic_key = 123456789;
band_frequencies[current_band] = current_frequency;
for(int i=0;i<9;i++) data.last_freq[i] = band_frequencies[i]; // Band definitions.
data.last_band = current_band;
data.last_mode = current_mode;
data.if_frequency = if_frequency;
data.tx_offset_freq = tx_offset_freq;
data.am_frequency = am_frequency;
data.si5351_correction = si5351_correction_val;
EEPROM.put(0, data);
}
void save_current_freq_to_band() {
band_frequencies[current_band] = current_frequency;
}
void load_freq_from_band() {
current_frequency = band_frequencies[current_band];
}