sexta-feira, 31 de outubro de 2025

DDS VFO AM SSB HF VHF 9 Bandas Oled JZ13NGF.

Olá pessoal. Postei em Fevereiro 2025, que comecei a fazer minhas experiências com Arduíno DDS VFO, sempre no interesse de incentivar a todos os iniciantes que querem montar um VFO digital em suas montagens ou seus transceptores AM, FM, SSB, mesmo o Cobra 148 GTL. Já fiz várias experiências modificações em sketch e algumas deram certo. 
Não sei nada, estou aprendendo com vocês. Mas o que sei disponibilizo para todos em vídeo e blog. Hoje publico algumas simples modificações feita em um esboço ou sketch, do amigo JZ13NGF canal YouTube SPR Elektro da Indonésia. Meus agradecimentos ao Sr. JZ13NGF por publicar e disponibilizar os arquivos, sketch e bibliotecas. Eu recebi o link do vídeo DDS HF 3 Band Oled display, enviado por um amigo no final do mês Outubro 2025. Já tinha escolhido um outro sketch esboço para modificar e publicar com vídeo, mas este do JZ13NGF me deixou bem entusiasmado para fazer ás modificações e deixa-lo como eu queria para o Cobra 148 GTL, pois tem S/meter, Menu, e 3 bandas HF. Então primeiro eu gravo o original para ver quais os recursos que tem, e para ver no Frequencímetro como ele se comporta para nossos projetos RTX AM, FM, SSB. Em CLK0 VFO não tem variação de AM, LSB, USB, no Menu em SET TX OFFSET, ou no programa podemos digitar um valor que queremos que o VFO transmita acima da frequência, Ex: O CLK0 VFO digitando a FI de 7800, ou no programa 7800000UL, no display e 26.965 Mhz, fica na frequência de 34.765 Mhz, canal 1 CB, isso ocorre em RX sem variação de AM, LSB, USB, em TX, temos uma única variação em AM, LSB, USB nesta frequência de 34.765 Mhz em Hz ou Khz, podemos definir Menu SET TX OFFSET ou no programa +1500 ou -1500, assim em TX em todas ás bandas terás mais ou menos 1500 acima ou abaixo. Assim não serviria para o Cobra 148 GTL mas em outros transceptores comercial ou nossos feito em casa. Não esqueçam que o modulo si5351 não tem potencia de RF suficiente para certos circuitos osciladores, por isso melhor adicionar um amplificador buffer RF, nas saídas principalmente VFO CLK0, e em CLK1 também é bom adicionar um buffer RF. 

Ás modificações que fiz neste esboço ou sketch do original JZ13NGF.
1- 9 bandas iniciais de 3.5 Mhz a 50 Mhz HF VHF, para comunicações no Brasil
2-  STEP de 1Hz, 10Hz, 100Hz, 1Khz, 10Khz, 100Khz, 1Mhz. 
3- TX, RX em AM CLK1 BFO SSB FI.
4- Inicializa STEP em 10 Khz.
5- S/Meter com escala completa numéricas S 1, 3, 5, 7, 9,  +  30d dentro do espectro. 
6- Retirei a memoria EEPROM para facilitar modificações na gravação do Arduino.
7- Rotary Encoder incremental típico.
8-Modo Menu tecla MODE pressionada por 5 segundos.

CLK1 BFO, tem ás variações AM, LSB, USB, digitando no programa #define BFO_LSB_OFFSET_DEFAULT -1500L // LSB #define BFO_USB_OFFSET_DEFAULT 1500L // USB. #define IF_FREQUENCY_DEFAULT 7800000UL . ou em Menu SET IF. para pequenas correções. Assim servirá para o CARRIER do Cobra 148 GTL. Outra complicação para o nosso Cobra 148 GTL é que uma única tecla chave SET MODE A2 é usada para selecionar AM, LSB, USB, e pressionada por alguns segundos entramos no modo Menu. Então partir para ás modificações, notei logo que ele não estava aceitando ás FI, BAND, STEP e quando gravava o novo esboço, a FI e demais configurações retornavam ao original do autor que foi gravado primeiro, ou ficava desencontradas. Não dava para eu fazer nada se eu não desabilitasse a EEPROM.h, e assim continuei ás modificações. Se desejarem habilitar a EEPROM retire ás barras duplas // das linhas, 27 //#include <EEPROM.h> linha 483 //EEPROM.get(0, data); linha 522 //EEPROM.put(0, data); Vocês deverão fazer isso antes de gravar os programas, esboços, se não ficar bom e ficar com ás 9 bandas desencontradas, desabilite novamente ás linhas acimas da EEPROM, e grave novamente sem a EEPROM. Mande-me um comentário se deu certo e ficou com memoria nas 9 bandas corretas.
Abaixo esquema de ligações do Arduino Nano e componentes.
Ás fotos abaixo refere-se ao segundo esboço com FI do Cobra 148 GTL 7.800 Khz
Vejam o display 128 x 64 SSD1306 vai até 160Mhz nesta banda de 6 metros.
Mas ficou instável depois de 99 Mhz, creio tenha sido a entrada do Frequencímetro.
Vejam a foto do display e Frequencímetro em CLK0 AM 50 Mhz 6 metros com FI de 7.800 Khz.
Agora fica melhor CLK0 AM 50 Mhz, FI 7.800 Khz.
Agora abaixo está em CLK0 LSB com FI de 7.800 Khz.
Vejam no display modo LSB, CLK0 foto acima no Frequencímetro.
Agora modo USB no display CLK0 e foto abaixo no Frequencímetro.
Vejam está em USB CLK0.
Vejam agora 11 metros canal 1 PX CB CLK0 AM.
Vejam frequência CLK0 AM.
Frequência CLK0 LSB 11 metros canal 1 PX CB.
Frequência CLK0 USB 11 metros canal 1 PX CB.
Frequência CLK1 BFO AM.
Frequência USB CLK1 BFO.
Frequência LSB CLK1 BFO.
 
Este esboço abaixo vocês terão em CLK0, frequência do display mais FI fixa de 7.800Khz, ou seja não haverá variação de frequência mesmo nos modos, AM, LSB, USB, terá sempre a mesma frequência do AM RX fixa. Mesmo em MENU AM SET, não mudará de frequência em CLK0, isso por causa da linha 313 está com comando if_frequency assim. 313 uint64_t vfo_output_freq = (uint64_t)vfo_with_offset + if_frequency; //+ if_frequency only VFO frequency. Mas em MENU SET TX OFFSET, terá uma variação em TX da qual for digitada no MENU. Ás definições no esboço já estão para (0) zero. #define AM_RX_OFFSET_DEFAULT 0L e #define TX_OFFSET_FREQ_DEFAULT 0L, onde vocês poderão alterar o que quiserem se desejarem. Em CLK1 vocês terão os modos AM 7.800Khz, LSB 7.798.5Khz, USB 7.801.5Khz. Servindo assim para qualquer transceptor SSB como Naja, Minhoca, Ararinha, Transceptor SSB 40M, Lagunero, Curió, entre muitos transceptores AM, SSB publicado aqui no blog ou na WEB.
1- 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 NOT INCLUDED DOES NOT SAVE 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[3];
  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 NOT INCLUDED DOES NOT SAVE 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[3]; 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 NOT INCLUDED DOES NOT SAVE 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[3]; 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]; }