sábado, 31 de outubro de 2026

DDS VFO RX HF scanner Cobra 148 GTL.

Pessoal, muito obrigado por esperar assista ao novo vídeo da conclusão e termino do projeto, não é fácil e sei que muitos entendem mas não posso deixar de contribuir com nossos seguidores da eletrônica digital. Ai está todo conteúdo 100% do projeto com fotos, esquema de ligações ao Arduino com proteção sobretensão, filtros de desacoplamentos, ajustes com os componentes com ( * ) eles poderão sofrerem aumento ou diminuição de capacidades 'valores'. O esquema abaixo refere-se ao Cobra 148 GTL mas poderá servir perfeitamente a outros transceptores ignorando os nomes e números do esquema do Cobra 148 GTL em questão. 

Olá pessoal que acompanha o blog, canal no YouTube. Este mês com muita dificuldades mas não deixei de brincar com mais algumas atualizações implementações no nosso DDS VFO para o Cobra 148 GTL. Aguardem terminando ajustes finais sketch. Como vocês já sabem, o sketch poderá ser modificado para outros tipos de rádios e montagens com FI de 21.4 MHz,10 MHz, 10.245 MHz, 9 MHz, 7.8 MHz, entre outras. Á maioria dos ajustes são feitos em "#define" começo e corpo do sketch, Menu e sub Menu. Sobre Implementações este projeto. Juntei alguns sketches e com muitas pesquisas conseguir: Modo VFO de 100 kHz a 225 MHz banda corrida, STEP piscando 5 vezes escolhido com toque rápido encoder de 1 Hz a 1 MHz. Receptor HF saída CLK2 ajustável pelo Menu no modo VFO de 100 kHz a 26 MHz. Modo Canal com 309, copia do DVFO atualizado por mim, com STEP toque rápido encoder pisca 1 e 10 canais, 2 duas telas: Tela 1 com S/Meter em RX em TX mede RF SWR, mostra frequência total Canais, deslocamento Clarificador 'CL' ajuste Hz e centro posição ajustável pelo Menu. Tela mostra modo 'AM, LSB, USB'. RX RB se o beep tiver acionado 'BIP ON' ou TX se beep estiver desligado 'BIP OFF'. Scanner modo Canal 'SC' na tela com tempo de parada disparo, limite aferido pelo S/Meter, ajustável pelo Menu principal 1. Modo Canal tela 2: Frequência total com Hz. Centro da tela Canais -150 a 150 com telecomandos, caracteres 'números' grandes. Clarificador 'CL' modo, em 'RX' fica 'SC' se scanner estiver acionado 'SCAN ON,' ou TX fica 'RB' vide tela 1.Tela. Pressionando encoder demorado gira para selecionar Menu principal 1 - CANAL /VFO, define com toque rápido entre Canal e VFO. TELA 1/2, toque rápido define tela 1 ou 2, SCAN ON/OFF, toque rápido liga ou desliga scanner, BIP ON/OFF, toque rápido liga ou desliga beep, RX HF ON/OFF, toque rápido liga ou desliga RX HF modo Canal, AJUSTES, toque rápido leva para a outra tela de submenu, temos então: Toque rápido encoder. OFS RX LSB, OFS RX USB, OFS RX AM, OFS TX LSB, OFS TX USB, OFS TX AM, XTAL 5351, CLARIF, RECEPT HF, TEMPO RB, LIM. SCAN, TEMPO SCAN. Toque rápido encoder seleciona ou muda. Para sair dos Menus pressiona demorado. Todos ajustes com memória total.
Assistam ao vídeo sobre este projeto DDS VFO Receptor HF, Scanner, VOICE LOCK, Clarificador ajustável centro meio dia ou zona morta.
 
Depois de muito tempo vamos ver como ficou nosso DDS VFO mistura de três sketches.
Modo Canal tela 1, acima S/Meter em RX e neste TX com SWR e RF power, novidade.
Ainda em ajustes da tela 2 Canal, e procurando perfeição, vamos prosseguir.
Modo VFO. procurando perfeição e ajustes para os caracteres.
Vejam que em TX VFO também medidor de SWR e RF.
Ainda não definidos os caracteres de ajustes.
Menu  principal 1 ajustar para sob menu 2, ainda em análise de perfeição.
Submenu de Ajuste. tela 2 definido e será assim.
Definição de 1 MHz para Mixer do receptor HF, esta frequência sairá em CLK2 ao ligar ON Recept HF OFF.
Agora sim ON e CLK2 sairá 1 MHz para entrar G2 no BF 980 RX Mixer HF e FI de 27 MHz rádio.
No canal 1: 26.965000 MHz a tela mostrará. 25.965000 MHz.
Primeiro Menu 1 e selecionado AJUSTES para entrar no sob menu
Ainda Menu 1 principal, selecionado: Modo VFO Canal.
Tela Sobmenu 2. Selecionado XTAL SI5351.
Descendo o Sobmenu 2 temos ainda.
Ainda no Sobmenu 2. Temos ainda os OFF SETs TX e RX, 
A tela de Canal 2 ficará assim.
CL: 0000Hz   AM  SC com Scanner ON.
No modo VFO ficará assim em RX.
 
No modo VFO e Receptor HF já com frequência em CLK2 de 23.500.000 MHz, tela mostra a frequência de recepção correta.
Ajustado para 23.500.000 MHz frequência de CLK2 para Mixer G2 . Receptor HF.
Agora CLK2 ajustado para 10.000.00 MHz, em REceptor HF ON modo VFO.
 
A tela mostra a frequência exata de recepção HF com clarificador Modo AM RX.
OFF SET TX ajustado para +24 MHz 100% ok.
Este foi um dos motivos de minha demora a publicar este projeto. 
Em - só estava indo até -9.999 MHz, igual a FI de SSB.
 
Agora ajustado para +22 MHz.
Agora sim em - menos, frequencia de -22 MHz.
Esquema 'diagrama' ligações Arduino e periféricos.
Pessoal agora sim acabei com os demorados testes e está 100% bom, não sou programador e meus conhecimentos são poucos, mas com muita persistência, dedicação, e muita força de vontade, eu conseguir deixar o sketch 100% como queria, estava com uma instabilidade de frequência quando passava para os 225 MHz e retornava para os 100 kHz a frequência ficava instável. Este sketch não é uma cópia do 10 kHz 225 MHz do Júlio Cesar, pois lá também enfrentei tal comportamento com a implementação da BAND +  que não tinha. 
Selecione todo sketch, copie e cole. Abra Arduino IDE vá em "Arquivo" depois "Novo", ou ícone ao lado da seta para "Carregar ou Compilar, abra um novo delete todas ás linhas e cole este sketch, renomeei com final .ino se preferir e salve em uma pasta. adicione todas ás bibliotecas. Lembre-se que muitas bibliotecas para o mesmo arquivo pode causar conflitos, observe a porta USB entre outras preferencias poderá provocar erros na compilação. 
Sketch abaixo. 

 /*
  ================================================================================================
  DDS VFO Cobra 148 GTL: Canais (309) VOICE LOCK + VFO + Receptor HF + RF SWR TX S/Meter RX
  Arduino Nano. Si5351. OLED SSD1306 128x64. ENCODER. 
  Sketch: Implementações e modificações por Waldir Cardoso.
  Blog: https://projetosetransceptores.blogspot.com/ Bahia, Brasil.
  ================================================================================================
  PINOS ARDUINO NANO
    D2 / D3  encoder          D4  botão do encoder
    D6 USB   D7 LSB   D8 AM   (chave de modo, ao GND)
    D10 relé RB/PTT (liga o TX)         D12 saída de áudio do Roger Beep
    A1  relé do Receptor HF             A2  entrada PTT (ao GND = TX)
    A3  S-Meter                         A6  clarificador (potenciômetro)
    A4 / A5  I2C (display + Si5351)

  SAÍDAS DO Si5351
    CLK0 = frequência do rádio + BFO      CLK1 = BFO (FI 7800 kHz +/- offset)
    CLK2 = oscilador do Receptor HF       Ajustável no Menu (AJUSTES RECEPT HF)

  USO DO ENCODER
    Girar ........ sintoniza (Canal ou VFO) / navega no menu
    Toque curto .. VFO: troca o passo (o dígito pisca 5 vezes)
                   Canal: 5 pisca o último caractere; novo toque pisca os do meio...
                   Menu: confirma / muda o dígito no editor
    Segurar ...... abre o MENU / volta um nível / sai do editor  
  
   VOICE LOCK (Clarificador) 
   Girar ..........sintonia fina (VFO Canal) centro saindo 10 ou 20 Hz finais 8 kHz ajustável
   Ajustes.........Menu (AJUSTES CLARIF) Gire CL: ajuste centro para onde quiser   
                         AJUSTE a cada toque sublinhado de 1 Hz a 20 kHz
*/

//-----Bibliotecas------------
#include <EEPROM.h>
#include <Wire.h>
#include <Rotary.h>
#include <PU2REO_Si5351Lite.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

//--------- pinos----------
#define PIN_BTN    4
#define PIN_USB    6
#define PIN_LSB    7
#define PIN_AM     8
#define PIN_RELAY  10
#define PIN_BEEP   12
#define PIN_PTT    A2
#define PIN_RXRL   A1
#define PIN_SM     A3
#define PIN_CLAR   A6

//-------Ajustes fixos-----------
#define IF_BASE       7800000L   // FI (BFO) em Hz (AM Cobra 148 GTL(
//#define IF_BASE       455000L    // FI (BFO) em Hz: 455 kHz (walkie-talkie AM)
#define MAX_CH        309        // canais da tabela
#define VFO_MIN       100000L    // 100 kHz limite mínimo frequência VFO
#define VFO_MAX       225000000L // 225 MHz limite máximo frequência VFO
#define MAGIC         0xA8       // marca da EEPROM
#define CLAR_RANGE    8000       // clarificador: +/- 8 kHz ajustável Menu
#define S_GAIN        300        // Ajuste com referência rádio sensibilidade do RF SWR TX, S-Meter em RX (300 a 600)
#define BEEP_FREQ_HZ  1800       // frequência do roger beep ajustável Menu
#define BEEP_MS       150        // duração do roger beep ajustável Menu
#define BEEP_ADJ_US   4          // ajuste fino do digitalWrite ajustável Menu
#define SCAN_STEP_MS  150        // scanner: tempo em cada canal ajustável Menu
#define SCAN_HOLD_S   10         // scanner: espera padrão em segundos (Menu: TEMPO SCAN)

//---------- Objetos (display, Si5351, encoder) ----------
Adafruit_SSD1306 display(128, 64, &Wire, -1);
Si5351 si5351;
//----------- Rotação do encoder-----------
Rotary r = Rotary(3, 2);   // Direita sobe frequencia canal
//Rotary r = Rotary(2, 3); // Direita desce frequencia canal
//----------- Configuração salva na EEPROM-------------
struct Cfg {
  byte    magic;
  byte    vfoMode;   // 0 = Canal, 1 = VFO
  byte    tela;      // tela do Canal: 0 = normal (S-Meter), 1 = canal grande
  byte    beep;      // roger beep ligado
  byte    rly;       // atraso do relé PTT em décimos de segundo (0..99)
  byte    hf;        // Receptor HF ligado
  byte    stp;       // passo do VFO (0..6 = 1Hz..1MHz)
  int16_t ch;        // índice do canal (0..308)
  int16_t ofs[6];    // offsets (Hz): 0-2 RX LSB,USB,AM  3-5 TX LSB,USB,AM
  int32_t xtal;      // correção do cristal do Si5351 (ppb)
  uint32_t vfo;      // frequência do VFO (Hz)
  uint32_t hfclk;    // CLK2 do Receptor HF (Hz)
  uint16_t rbms;     // Duração do roger beep em ms (Menu: Tempo RB)
  byte    scan;      // scanner do modo Canal ligado
  byte    scanLim;   // limite do scanner: S1 a S9
  byte    scanHold;  // espera do scanner depois que o sinal some (s)
  int32_t ofsAm[2];  // offset AM em Hz (até 30 MHz): 0 = RX, 1 = TX
  byte    ofsOk;     // marca: ofsAm já migrado
  uint16_t clRange;  // alcance do clarificador em Hz (1 a 20000)
  uint16_t clCenter; // centro (zona morta) do clarificador: leitura do A6 (10 a 1013)
};
Cfg cfg;
#define HF_ON (cfg.hf && cfg.vfoMode)        // Receptor HF só vale no modo VFO

//---------- Tabelas e variáveis de estado-----------
const uint32_t RDX[7]    = {1, 10, 100, 1000, 10000, 100000, 1000000}; const byte IDX_LO[8]     = {9, 8, 7, 5, 4, 3, 1, 0};   // posição dos dígitos em "26.965.000"
const byte IDX_HI[8]     = {9, 8, 6, 5, 4, 2, 1, 0};   // posição dos dígitos em "143.965.00"
const char* const MT[3]  = {"LSB", "USB", "AM"};
volatile int8_t encDelta = 0;
int8_t scanDir = 1;             // sentido do scan: +1 sobe, -1 desce
long  clarifier = 0;
byte  mode = 2;                 // 0 LSB, 1 USB, 2 AM
bool  tx = false, relayOn = false;
unsigned long txT0 = 0, blinkUntil = 0, tDraw = 0, tSave = 0, pkT = 0;
bool  dirty = false;
long  loHz = 0; bool loOn = false;
int   sm = 0, peak = 0;
byte  chSel = 0; bool chTouched = false;       // caracteres do canal que o encoder muda
byte  menuLvl = 0, menuSel = 0, menuTop = 0;
long  edVal = 0; byte edId = 0, edDig = 0, edN = 0;

// -------- Tabela de canais compactada-----------
// Frequência (Hz) do canal de índice i (0..308): 25465 kHz + 10 kHz por índice,
// com um pulo (não existe 27445) e duas trocas de 3 canais (27255/27235/27245 e 27705/27685/27695)
  unsigned long chanFreq(int i) {
  int k = 25465 + 10 * i;
  if (i >= 198) k += 10;                                       // Pulo: não existe 27445
  if (i == 177 || i == 221) k += 20;                           // Primeiro canal da troca
  if (i == 178 || i == 179 || i == 222 || i == 223) k -= 10;   // Os outros dois da troca
  return (unsigned long)k * 1000UL;
}
//----------- Números Canais------------
// Número do canal de índice idx (0..308): -150..-1, depois 1..150 com os TC 701 a 709
  int chanNumber(int idx) {
  if (idx < 150) return idx - 150;
  int k = idx - 149;                                           // Posição entre os positivos
  for (int n = 1; n <= 150; n++) {
  if (--k == 0) return n;
  if (n % 4 == 3 && (n <= 19 || (n >= 47 && n <= 59))) {     // TC depois de 3,7,11,15,19,47,51,55,59
  if (--k == 0) return 700 + (n < 47 ? (n + 1) / 4 : (n - 23) / 4);
  }
 }
  return 1;
}

//------- Textos do menu (na flash)----------
const char a0[] PROGMEM = "OFS RX LSB";
const char a1[] PROGMEM = "OFS RX USB";
const char a2[] PROGMEM = "OFS RX AM";
const char a3[] PROGMEM = "OFS TX LSB";
const char a4[] PROGMEM = "OFS TX USB";
const char a5[] PROGMEM = "OFS TX AM";
const char a6[] PROGMEM = "XTAL 5351";
const char a7[] PROGMEM = "CLARIF";
const char a8[] PROGMEM = "RECEPT HF";
const char a9[] PROGMEM = "TEMPO RB";
const char a10[] PROGMEM = "LIM. SCAN";
const char a11[] PROGMEM = "TEMPO SCAN";
const char* const AJ[12] PROGMEM = {a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11};
void printAj(byte i) { display.print((const __FlashStringHelper*)pgm_read_word(&AJ[i])); }

//----------- EEPROM-----------
void saveSoon() { dirty = true; tSave = millis(); }
void loadCfg() {
  EEPROM.get(0, cfg);
  if (cfg.magic != MAGIC || cfg.ch < 0 || cfg.ch >= MAX_CH || cfg.stp > 6 ||
      cfg.xtal < 0 || cfg.xtal > 50000 || cfg.vfo < VFO_MIN || cfg.vfo > VFO_MAX ||
      cfg.hfclk < 100000UL || cfg.hfclk > 26000000UL || cfg.rly > 99) {
    cfg.magic = MAGIC; cfg.vfoMode = 0; cfg.tela = 0; cfg.beep = 1;
    cfg.rly = 0; cfg.hf = 0; cfg.stp = 4; cfg.ch = 150;          // canal 1 = 26.965
    cfg.ofs[0] = -1500; cfg.ofs[1] = 1500; cfg.ofs[2] = 0;       // RX LSB, USB, AM
    cfg.ofs[3] = -1500; cfg.ofs[4] = 1500; cfg.ofs[5] = 0;       // TX LSB, USB, AM
    cfg.xtal = 18500; cfg.vfo = 26965000UL; cfg.hfclk = 10000000UL; cfg.rbms = BEEP_MS;
    EEPROM.put(0, cfg);
  }
  if (cfg.rbms < 20 || cfg.rbms > 999) { cfg.rbms = BEEP_MS; EEPROM.put(0, cfg); }
  if (cfg.scan > 1 || cfg.scanLim < 1 || cfg.scanLim > 9) { cfg.scan = 0; cfg.scanLim = 4; EEPROM.put(0, cfg); }
  if (cfg.scanHold < 1 || cfg.scanHold > 99) { cfg.scanHold = SCAN_HOLD_S; EEPROM.put(0, cfg); }
  if (cfg.ofsOk != MAGIC) { cfg.ofsAm[0] = cfg.ofs[2]; cfg.ofsAm[1] = cfg.ofs[5]; cfg.ofsOk = MAGIC; EEPROM.put(0, cfg); }
  if (cfg.clRange < 1 || cfg.clRange > 20000 || cfg.clCenter < 10 || cfg.clCenter > 1013) { cfg.clRange = 8000; cfg.clCenter = 512; EEPROM.put(0, cfg); }
}

// --------- Interrupção do encoder----------
ISR(PCINT2_vect) {
  unsigned char res = r.process();
  if (res == DIR_CW) encDelta++;
  else if (res == DIR_CCW) encDelta--;
}

int8_t takeEnc() {
  noInterrupts(); int8_t d = encDelta; encDelta = 0; interrupts();
  return d;
}

//------- Botão: 1 = toque curto, 2 = segurou-------
byte btnEvent() {
  static bool down = false, longDone = false;
  static unsigned long t0 = 0;
  bool p = !digitalRead(PIN_BTN);
  if (p && !down) { down = true; longDone = false; t0 = millis(); }
  else if (p && down && !longDone && millis() - t0 > 700) { longDone = true; return 2; }
  else if (!p && down) { down = false; if (!longDone && millis() - t0 > 30) return 1; }
  return 0;
}

// ----- Clarificador (sem EEPROM, +/-8 kHz, zona morta com 10 ou 20 Hz)---------
void readClarifier() {
  analogRead(PIN_CLAR);                                   // leitura de descarte
  int clarRead = analogRead(PIN_CLAR);
  long c = cfg.clCenter, R = cfg.clRange;                 // centro e alcance salvos na EEPROM
  long raw = clarRead >= c ? (clarRead - c) * R / (1023 - c) : (clarRead - c) * R / c;
  static long lastSmoothed = 0, out = 0;
  long smooth = (raw * 2 + lastSmoothed) / 3;             // filtro
  lastSmoothed = smooth;
  int STEP = R < 100 ? 1 : 10;                            // passos de 10 Hz (1 Hz se o alcance for pequeno)
  long q = (smooth >= 0) ? (smooth + STEP / 2) / STEP * STEP : (smooth - STEP / 2) / STEP * STEP;
  
//Zona morta ou meio dia saindo com 10 ou 20 Hz abaixo escolha uma e desloque os dois //
  if (R >= 100 && q > -20 && q < 20) q = 0;               // zona morta central de 20 Hz (a atual)
  //if (R >= 100 && q > -10 && q < 10) q = 0;               // zona morta central de 10 Hz: só 0 Hz fica parado              
  if (abs(q - out) >= STEP) out = q;                      // histerese
  clarifier = out;
}
//----------------S/Meter--------------
// ---------------------------------------------------------------- S-Meter (leitura + suavização + peak hold)
//31/10/2026. Atualuizada S/Meter igual ao DVFO.
void readSmeter() {
  static long smf = 0;                                    // S-Meter com 4 bits a mais: suaviza sem perder o sinal fraco
  static unsigned long tLido = 0;
  if (millis() - tLido < 50) return;                      // uma leitura a cada 50 ms, como o loop do DVFO antigo
  tLido = millis();
  analogRead(PIN_SM); delayMicroseconds(50);              // descarte do ADC
  int v = analogRead(PIN_SM);
  long px = tx ? (long)v * 1600 / 1023                    // TX: escala cheia (RF / SWR)
               : (long)v * 1600 / S_GAIN;                 // RX: igual ao DVFO antigo (S_GAIN = leitura que enche a barra)
  px = constrain(px, 0, 1600);
  smf += (px - smf) / 3;                                  // igual ao antigo: (2*anterior + novo) / 3
  sm = (smf + 8) / 16;
  if (sm >= peak) { peak = sm; pkT = millis(); }
  else { peak = (peak > 1) ? peak - 2 : 0; if (peak < sm) peak = sm; }   // cai 2 px a cada leitura, como o antigo
}

// ------------- frequências---------------
long radioFreq() {                       // frequência do rádio (PX) com clarificador
  long f = (cfg.vfoMode ? (long)cfg.vfo : (long)chanFreq(cfg.ch)) + clarifier;
  return f < 0 ? 0 : f;
}
void calcLo() {                          // oscilador externo: Receptor HF (CLK2)
  loOn = HF_ON; loHz = HF_ON ? (long)cfg.hfclk : 0;
}

long dispFreq() {                        // frequência mostrada na tela
  long f = radioFreq();
  if (loOn) f = labs(f - loHz);
  return f;
}
long bfoHz() { return IF_BASE + (mode == 2 ? cfg.ofsAm[tx] : cfg.ofs[mode + (tx ? 3 : 0)]); }

//------------ Si5351----------
void applySi() {                                                // atualiza os 3 clocks do Si5351
  static uint32_t o0 = 0, o1 = 0, o2 = 0;                       // últimos valores enviados (evita reenviar igual)
  static bool wasHi = false, en0 = true;                        // wasHi = CLK0 estava acima de 100 MHz; en0 = estado da saída CLK0
  long b = bfoHz();                                             // FI atual (IF_BASE + offset)
  long s0 = radioFreq() + b;                                    // CLK0 = frequência + FI (FI pode ser negativa)
  uint32_t f0 = s0 < 8000 ? 8000UL : (uint32_t)s0;              // O Si5351 não gera menos que 8 kHz
  uint32_t f1 = labs(b) < 8000 ? 8000UL : (uint32_t)labs(b);    // CLK1 sempre positivo (valor absoluto da FI)
  uint32_t f2 = loOn ? loHz : 0;                                // CLK2 (Receptor HF), 0 = desligado
  bool hi = f0 > 100000000UL;                                   // CLK0 acima de 100 MHz agora?
  if (wasHi && !hi) {                                           // CLK0 voltou abaixo de 100 MHz (ex.: 225 MHz -> 100 kHz)
  si5351.set_pll(SI5351_PLL_FIXED, SI5351_PLLA);                // PLL A volta para o valor fixo de 700 MHz                               
  o0 = 0;                                                       // força reenviar o CLK0
  }
 
 //07/11/2026. Atualizada para estabilizar 100 kHz e 225 MHz no retorno
  bool w0 = !(tx && HF_ON);                                     // CLK0 desliga em TX no Receptor HF (sem a chave TX HF)                          
  if (w0 != en0) { si5351.output_enable(SI5351_CLK0, w0); en0 = w0; }   // liga/desliga a saída CLK0 só quando muda  
  //07/11/2026. Atualizada para estabilizar 100 kHz e 225 MHz no retorno
  if (w0 && f0 != o0) { bool rst = (o0 == 0 && wasHi); o0 = f0; si5351.set_freq(f0 * 100ULL, SI5351_CLK0);
  if (rst) si5351.pll_reset(SI5351_PLLA); }   // envia o CLK0; se acabou de voltar de >100 MHz, reinicia a PLL A DEPOIS do envio
  if (f1 != o1)       { o1 = f1; si5351.set_freq(f1 * 100ULL, SI5351_CLK1); }   // envia CLK1 (FI)
  if (f2 != o2) {                                               // CLK2 mudou
    /* ... mantenha aqui o seu trecho atual do CLK2 (set_freq / set_clock_pwr) sem alterar ... */
    o2 = f2;                                                    // guarda o último valor do CLK2
  }
  wasHi = hi;                                                   // lembra se estava acima de 100 MHz
}

// --------- PTT, relé e roger beep----------
bool beepOk() { return !cfg.vfoMode; }
void rogerBeep() {
  long n = 2L * BEEP_FREQ_HZ * cfg.rbms / 1000;
  for (long i = 0; i < n; i++) {
  digitalWrite(PIN_BEEP, i & 1);
  delayMicroseconds(500000UL / BEEP_FREQ_HZ - BEEP_ADJ_US);
  }
  digitalWrite(PIN_BEEP, LOW);
}

//---------PTT------------
void handlePtt() {
  static bool st = false;                                // PTT filtrado (anti-repique 40 ms)
  static unsigned long tb = 0;
  bool raw = !digitalRead(PIN_PTT);
  if (raw != st && millis() - tb > 40) { st = raw; tb = millis(); }
  bool p = st;
  if (p && !tx) { tx = true; txT0 = millis(); }
  if (tx && p && !relayOn && !HF_ON) {
  digitalWrite(PIN_RELAY, HIGH); relayOn = true;         // aciona o relé depois do atraso
  }
  if (tx && !p) {                                        // soltou o PTT
    if (relayOn && cfg.beep && beepOk()) rogerBeep();    // o relé segura o TX durante o beep
    digitalWrite(PIN_RELAY, LOW); relayOn = false; tx = false;
  }
}

// -------- Formatação---------
void printPad(long v, byte w) {          // número com zeros à esquerda
  long p = 1;
  for (byte i = 1; i < w; i++) p *= 10;
  while (p > 1 && v < p) { display.print('0'); p /= 10; }
  display.print(v);
}
void fmtFreq(char *s, long f) {
  if (f > 999999999L) f = 999999999L;
  bool hi = f >= 100000000L;
  long n = hi ? f / 10 : f;
  for (int8_t i = 9; i >= 0; i--) {
  if (hi ? (i == 3 || i == 7) : (i == 2 || i == 6)) s[i] = '.';
  else { s[i] = '0' + n % 10; n /= 10; }
  }
  s[10] = 0;
  if (s[0] == '0') s[0] = ' ';
}
//---------Texto Canal----------
byte chanStr(char *s) {                  // Texto do canal: -150..150, TC1..TC9
  int n = chanNumber(cfg.ch); byte l = 0;
  if (n > 700) { s[l++] = 'T'; s[l++] = 'C'; n -= 700; }
  else if (n < 0) { s[l++] = '-'; n = -n; }
  if (n >= 100) s[l++] = '0' + n / 100;
  if (n >= 10)  s[l++] = '0' + (n / 10) % 10;
  s[l++] = '0' + n % 10; s[l] = 0;
  return l;
}
//---------Pisca pisca Canal----------
bool blinkOff() {                        // 5 Piscadas depois de um toque no encoder
  long rem = (long)(blinkUntil - millis());
  return rem > 0 && ((rem / 250) & 1);
}
//-----------Scanner-----------------
void doScan() {
  static unsigned long t = 0;
  if (!cfg.scan || cfg.vfoMode || menuLvl) return;
  if (tx) { t = millis() + cfg.scanHold * 1000UL; return; }                           // PTT: para e espera o TEMPO SCAN
  if (sm >= 6 * cfg.scanLim + 2) { t = millis() + cfg.scanHold * 1000UL; saveSoon(); return; }   // sinal: para e espera
  if ((long)(millis() - t) < 0) return;
  t = millis() + SCAN_STEP_MS;
  int n = (cfg.ch + scanDir) % MAX_CH;
  if (n < 0) n += MAX_CH;
  cfg.ch = n;
}

//--------Display------------
//29/10/2026. sempre escreve CL:
void printCl(bool full) {
  display.print(F("CL:"));
  display.print(clarifier < 0 ? '-' : (clarifier > 0 ? '+' : ' '));
  printPad(labs(clarifier), 4);
  if (full) display.print(F("Hz"));
}
void printSt() { display.print(tx ? (cfg.beep && beepOk() ? F("RB") : F("TX")) : (cfg.scan && !cfg.vfoMode ? F("SC") : F("RX"))); }
void drawFooter(byte y, bool hf) {         // rodapé: CL + (RX HF) + modo + RX/TX juntos
    display.setTextSize(1);                // letra pequena
    if (hf) {                              // VFO com Receptor HF: CL:+0000Hz RX HF AM RX
    display.setCursor(0, y);   printCl(true);            // CL começa em x=0
    display.setCursor(64, y);  display.print(F("RX HF")); // texto RX HF em x=64
    display.setCursor(96, y);  display.print(MT[mode]);  // AM / LSB / USB em x=96
    display.setCursor(116, y);                           // RX/TX no canto direito, x=116
  } else {                                 // normal: CL:+0000Hz  AM  RX
    display.setCursor(4, y);   printCl(true);            // CL começa no início (x=4) da tela
    display.setCursor(82, y);  display.print(MT[mode]);  // AM / LSB / USB em x=82
    display.setCursor(106, y);                           // RX/TX em x=106
  }
  printSt();                                // escreve RX, TX, RB ou SC na posição escolhida acima
}

void printBfo() {                        // ex.: 7800.0kHz
  long b = bfoHz();
  display.print(b / 1000); display.print('.'); display.print((b % 1000) / 100); display.print(F("kHz"));
}

//-------S/Meter---------
//------- Desenha escala caracteres números no alto da tela ----------
void drawSmeter() {                      // S-Meter no alto da tela
  display.setTextSize(1);
  if (!tx) {
    display.setCursor(0, 0); display.print(F("SIG.1.3.5.7.9...+30dB"));
  } else {                               // régua SWR + "RF" na barra
    display.setCursor(15, 0); display.print('1');
    display.setCursor(24, 0); display.print(F("SWR"));
    display.setCursor(45, 0); display.print('2');
    display.setCursor(68, 0); display.print('3');
    display.setCursor(92, 0); display.print(F("CAL!"));
    display.setCursor(0, 4);  display.print(F("RF"));
  }
  display.fillRect(16, 9, sm, 3, WHITE);
  for (byte x = 20; x < 116; x += 5) display.drawFastVLine(x, 9, 3, BLACK);   // serrilha
  display.drawFastVLine(16 + peak, 8, 5, WHITE);                              // pico
}

void drawFreqLine(byte y, bool blinkStep) {
  char s[11];
  long f = dispFreq();
  fmtFreq(s, f);
  if (blinkStep && blinkOff()) {         // STEP escondido: só o dígito pisca
    bool hi = f >= 100000000L;
    byte p = cfg.stp;
    if (hi && p) p--;
    s[hi ? IDX_HI[p] : IDX_LO[p]] = ' ';
  }
// Posição VFO frequência horizontal
//O 4 é a posição horizontal (x). Aumente para empurrar a frequência para a direita, diminua para a esquerda.
  display.setTextSize(2); display.setCursor(4, y); display.print(s); 
}

void drawMain() {
  display.clearDisplay();
  display.setTextColor(WHITE);
  if (cfg.vfoMode) {                                     // ---- tela VFO
    drawSmeter();                                        // S-Meter no alto da tela
    drawFreqLine(18, true);  // Posição vertical VFO frequência em y=18, STEP pisca o dígito
    display.setTextSize(1);                              // letra pequena para a linha abaixo
    if (cfg.hf) { display.setCursor(0, 41); display.print(F("RECEPTOR HF")); display.setCursor(73, 41); printBfo(); }   // HF: nome em x=0 e 7800.0kHz em x=72, y=41
    else { display.setCursor(4, 41); display.print(F("CLK1:")); printBfo(); }   // sem HF: CLK1:7800.0kHz em x=4, y=41
    drawFooter(54, cfg.hf);                              // rodapé em y=54; true = modo Receptor HF
  } else {
    char cs[6];
    byte l = chanStr(cs);
    if (blinkOff()) {                                    // caracteres do canal que o encoder muda
      if (chSel == 0) cs[l - 1] = ' ';
      else for (byte i = (cs[0] == '-'); i + 1 < l; i++) cs[i] = ' ';
    }
    if (cfg.tela == 0) {                                 // ---- Canal: tela normal com S-Meter
      drawSmeter();
      drawFreqLine(18, false);
      byte x0 = (98 - l * 12) / 2;                       // x onde começa "CH"+número, para ficar no centro (l = nº de caracteres)
      display.setTextSize(2); display.setCursor(x0, 37); display.print(F("CH"));   // "CH" grande em y=37
      display.setCursor(x0 + 30, 37); display.print(cs); // número do canal logo depois (30 px à direita)
      drawFooter(56, false);                             // rodapé em y=56, sem Receptor HF no Canal
    } else {                                             // ---- Canal: número grande
      drawFreqLine(0, false);
      display.setTextSize(4); display.setCursor((128 - l * 24) / 2, 20); display.print(cs);
      drawFooter(56, false);
    }
  }
  display.display();
}
//--------Tela menu---------
// ---------------------------------------------------------------- MENU
byte menuCount() { return menuLvl == 1 ? 6 : 12; }
void printItem(byte i) {
  if (menuLvl == 1) {
    switch (i) {
      case 0: display.print(F("CANAL<>VFO")); break;
      case 1: display.print(F("TELA ")); display.print(cfg.tela + 1); break;
      case 2: display.print(F("SCAN ")); display.print(cfg.scan ? F("ON") : F("OFF")); break;
      case 3: display.print(F("BIP ")); display.print(cfg.beep ? F("ON") : F("OFF")); break;
      case 4: display.print(F("RX HF ")); display.print(cfg.hf ? F("ON") : F("OFF")); break;
      default: display.print(F("AJUSTES"));
    }
  } else printAj(i);
}

void drawMenu() {
  display.clearDisplay();
  display.setTextSize(2);
  byte n = menuCount();
  
  if (menuSel < menuTop) menuTop = menuSel;               // mantém o item escolhido visível
  if (menuSel > menuTop + 3) menuTop = menuSel - 3;
  
  for (byte rw = 0; rw < 4 && menuTop + rw < n; rw++) {
    byte i = menuTop + rw;
    bool s = (i == menuSel);
    if (s) display.fillRect(0, rw * 16, 128, 16, WHITE);
    display.setTextColor(s ? BLACK : WHITE);
    display.setCursor(2, rw * 16 + 1);
    printItem(i);
  }
  display.setTextColor(WHITE);
  display.display();
}

// ---- Editor de valores (offsets, XTAL, relé, Receptor HF)-----
void edLoad() {
  switch (edId) {
    case 6:  edVal = cfg.xtal;  edN = 5; break;
    case 7:  edVal = cfg.clRange; edN = 5; break;
    case 8:  edVal = cfg.hfclk; edN = 8; break;
    case 9:  edVal = cfg.rbms;  edN = 3; break;
    case 10: edVal = cfg.scanLim; edN = 1; break;  // Scanner
    case 11: edVal = cfg.scanHold; edN = 2; break;
    case 2:  edVal = cfg.ofsAm[0]; edN = 8; break;
    case 5:  edVal = cfg.ofsAm[1]; edN = 8; break;
    default: edVal = cfg.ofs[edId]; edN = 4;
  }
}
// ---- MENU > AJUSTES: EDITOR DE VALORES
// edStore: limita o valor digitado (mínimo/máximo de cada item) e grava na configuração.
// Os limites de cada ajuste ficam aqui: offsets LSB/USB (+-9999 Hz), offsets AM RX e TX (+-30 MHz),
// XTAL, CLARIF., RECEPT HF, TEMPO RB, LIM. SCAN e TEMPO SCAN.
void edStore() {                    // grava na hora: o ajuste já vale enquanto edita
  long mn = -9999, mx = 9999;       // padrão (LSB e USB): de -9999 a +9999 Hz 
//02/11/26.Atualizada com AM FI para 22MHz TX RX.
// Se quiser ajuste FI AM RX TX de 100kHz a 30MHz é só trocar em { mn = -24000000L; mx = 24000000L; }
  if (edId == 2 || edId == 5) { mn = -30000000L; mx = 30000000L; }   // offset AM RX e TX: de -22 MHz a +22 MHz
  //if (edId == 2 || edId == 5) mx = 24000000L;  // offset AM: até 24 MHz  
  if (edId == 6) { mn = 0; mx = 50000; }
 
 //----- CLARIF-------
 else if (edId == 7) { mn = 1; mx = 20000; }
  else if (edId == 8) { mn = 100000L; mx = 26000000L; }
  else if (edId == 9) { mn = 20; mx = 999; }
  else if (edId == 10) { mn = 1; mx = 9; }
  else if (edId == 11) { mn = 1; mx = 99; }
  edVal = constrain(edVal, mn, mx);
  switch (edId) {
  case 6:  cfg.xtal = edVal; si5351.set_correction(cfg.xtal, SI5351_PLL_INPUT_XO); break;
  case 7:  cfg.clRange = edVal; break;
  case 8:  cfg.hfclk = edVal; break;
  case 9:  cfg.rbms = edVal; break;
  case 10: cfg.scanLim = edVal; break;
  case 11: cfg.scanHold = edVal; break;
  case 2:  cfg.ofsAm[0] = edVal; break;
  case 5:  cfg.ofsAm[1] = edVal; break;
  default: cfg.ofs[edId] = edVal;
  }
  saveSoon();
}
//------Girar o encoder muda o dígito selecionado.------
void edRotate(int8_t d) {
  if (edId == 8 && edDig == 8) { cfg.hf ^= 1; saveSoon(); return; }   // campo ON/OFF do Receptor HF
  long p = 1;
  for (byte i = 0; i < edDig; i++) p *= 10;
  edVal += d * p;
  edStore();
}
//---------Desenha a tela do editor.
void drawEditor() {
  display.clearDisplay();
  display.setTextSize(1); display.setCursor(0, 0); printAj(edId);
  byte y = 18, ix = 0;
  display.setTextSize(2); display.setCursor(0, y);
  if (edId <= 5) {                       // offset em Hz
  display.print(edVal < 0 ? '-' : '+');
  if (edN == 8) {                      // AM: 8 dígitos (até 24 MHz)
  printPad(labs(edVal), 8); display.setTextSize(1); display.print(F("Hz")); display.setTextSize(2);
  ix = 8 - edDig;
  } else {
  printPad(labs(edVal), 4); display.print(F("Hz"));
  ix = 1 + (3 - edDig);
 } 
  } else if (edId == 6) {                // XTAL
    printPad(edVal, 5); display.setTextSize(1); display.print(F(" ppb")); display.setTextSize(2);
    ix = 4 - edDig;
    } else if (edId == 7) {                // alcance do clarificador (Hz)
    printPad(edVal, 5); display.print(F("Hz"));
    display.setTextSize(1); display.setCursor(0, 40); printCl(true); display.setTextSize(2);
    ix = 4 - edDig;
    } else if (edId == 9) {                // tempo do beep (ms)
    printPad(edVal, 3); display.print(F("ms"));
    ix = 2 - edDig;
    } else if (edId == 10) {               // limite do scanner + S-Meter ao vivo
    display.print('S'); display.print(edVal);
    display.fillRect(0, 40, sm, 6, WHITE);
    display.drawFastVLine(6 * edVal + 2, 36, 14, WHITE);
    ix = 1;
    } else if (edId == 11) {               // espera do scanner (segundos)
    printPad(edVal, 2); display.print('s');
    ix = 1 - edDig;
    } else {                               // Receptor HF: CLK2 e ON/OFF
    char s[11]; fmtFreq(s, edVal);
    y = 14; display.setCursor(0, y); display.print(s);
    ix = (edDig < 8) ? IDX_LO[edDig] : 0;
    display.setCursor(0, 36); display.print(F("HF:")); display.print(cfg.hf ? F("ON") : F("OFF"));
    if (edDig == 8) display.drawFastHLine(36, 53, cfg.hf ? 24 : 36, WHITE);
  }
  if (!(edId == 8 && edDig == 8)) display.drawFastHLine(ix * 12, y + 17, 10, WHITE);   // sublinha o dígito
  display.setTextSize(1); display.setCursor(0, 57); display.print(F("toque:digito seg:ok"));
  display.display();
}

//---------Clarificador menu---------
void menuUi(byte e, int8_t d) {
  if (menuLvl == 3) {                                    // editor
    if (d) edRotate(d);
    if (e == 1) { edDig++; if (edDig >= edN + (edId == 8 ? 1 : 0)) edDig = 0; }
    if (e == 2) {
      if (edId == 7) {                                   // sair do CLARIF.: a posição do potenciômetro vira o centro
        long sum = 0; analogRead(PIN_CLAR);
        for (byte i = 0; i < 8; i++) sum += analogRead(PIN_CLAR);
        cfg.clCenter = constrain(sum / 8, 10, 1013);
      }
      menuLvl = 2; saveSoon();
    }
    return;
  }
  byte n = menuCount();
  if (d) {
   //------Rotação do encoder Menus-------
    int s = ((int)menuSel - d) % (int)n;   // Giro à direita desce na lista do Menu (sentido invertido).
    //int s = ((int)menuSel + d) % (int)n;  // Giro à direita sobe na lista do Menu (sentido invertido).
    if (s < 0) s += n;
    menuSel = s;
    if (menuSel < menuTop) menuTop = menuSel;
    if (menuSel > menuTop + 3) menuTop = menuSel - 3;
  }
  if (e == 2) {                                          // volta um nível
    if (menuLvl == 2) { menuLvl = 1; menuSel = 5; menuTop = 0; }
    else { menuLvl = 0; saveSoon(); }
  } else if (e == 1) {
    if (menuLvl == 1) {
      switch (menuSel) {
        case 0: cfg.vfoMode ^= 1; menuLvl = 0; break;    // Canal <-> VFO
        case 1: cfg.tela ^= 1; menuLvl = 0; break;       // tela 1 / tela 2 do Canal
        case 2: cfg.scan ^= 1; break;                    // Scanner ON/OFF
        case 3: cfg.beep ^= 1; break;                    // Bip RB/TX
        case 4: cfg.hf ^= 1; break;                      // Receptor HF ON/OFF
        default: menuLvl = 2; menuSel = 0; menuTop = 0;
      }
      saveSoon();
    }
    else { edId = menuSel; edLoad(); edDig = 0; menuLvl = 3; }          // abre o editor
  }
}

// --------Uso normal (fora do menu)---------
void mainUi(byte e, int8_t d) {
  if (e == 2) { menuLvl = 1; menuSel = 0; menuTop = 0; return; }
  if (cfg.vfoMode) {
    if (e == 1) { cfg.stp = (cfg.stp + 1) % 7; blinkUntil = millis() + 2500; saveSoon(); }
    if (d) {
      long st = RDX[cfg.stp];
      if (st == 1 && dispFreq() >= 100000000L) st = 10;  // acima de 100 MHz a tela só mostra 10 Hz

 //---- VFO dando volta inteira encoder----
      long n = (long)cfg.vfo + d * st;                   // dá a volta: 100 kHz <-> 225 MHz
      if (n < VFO_MIN) n = VFO_MAX; else if (n > VFO_MAX) n = VFO_MIN;
      cfg.vfo = n;     
      saveSoon();
    }
  } else {
    if (e == 1) {                                        // último caractere -> meio -> último ...
      if (!chTouched) { chTouched = true; chSel = 0; } else chSel ^= 1;
      blinkUntil = millis() + 2500;
    }
    if (d) {
      int n = (cfg.ch + d * (chSel ? 10 : 1)) % MAX_CH;
      if (n < 0) n += MAX_CH;
      cfg.ch = n; saveSoon();
      if (cfg.scan) scanDir = d > 0 ? 1 : -1;
    }
  }
}

// ------- SETUP----------
void setup() {
  pinMode(2, INPUT_PULLUP); 
  pinMode(3, INPUT_PULLUP);
  pinMode(PIN_BTN, INPUT_PULLUP);
  pinMode(PIN_USB, INPUT_PULLUP); 
  pinMode(PIN_LSB, INPUT_PULLUP); 
  pinMode(PIN_AM, INPUT_PULLUP);
  pinMode(PIN_PTT, INPUT_PULLUP);
  pinMode(PIN_RELAY, OUTPUT); 
  pinMode(PIN_BEEP, OUTPUT); 
  pinMode(PIN_RXRL, OUTPUT);
  digitalWrite(PIN_RELAY, LOW); 
  digitalWrite(PIN_BEEP, LOW); 
  digitalWrite(PIN_RXRL, LOW);

  Wire.begin();
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextColor(WHITE);
  loadCfg();

  display.clearDisplay();                                // tela inicial
  display.setTextSize(2);
  display.setCursor(9, 1);  display.print(F("Cobra 148"));
  display.setCursor(47, 25); display.print(F("GTL"));
  display.display();
  delay(2000);

  si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, cfg.xtal);
  si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_6MA);
  si5351.drive_strength(SI5351_CLK1, SI5351_DRIVE_4MA);  // aumente se precisar 
  si5351.drive_strength(SI5351_CLK2, SI5351_DRIVE_6MA);
  si5351.pll_assignment[SI5351_CLK1] = SI5351_PLLB;
  si5351.pll_assignment[SI5351_CLK2] = SI5351_PLLB;
  si5351.set_ms_source(SI5351_CLK1, SI5351_PLLB);
  si5351.set_ms_source(SI5351_CLK2, SI5351_PLLB);
  
  PCICR |= (1 << PCIE2);                                 // interrupção do encoder (D2 e D3)
  PCMSK2 |= (1 << PCINT18) | (1 << PCINT19);
  sei();
}

//--------LOOP---------
void loop() {
  if (!digitalRead(PIN_AM)) mode = 2;                    // chave de modo
  else if (!digitalRead(PIN_USB)) mode = 1;
  else if (!digitalRead(PIN_LSB)) mode = 0;
  calcLo();

  handlePtt();
  readClarifier();
  readSmeter();

  byte e = btnEvent();
  int8_t d = takeEnc();
  if (menuLvl) menuUi(e, d); else mainUi(e, d);
  doScan();
  applySi();
  digitalWrite(PIN_RXRL, HF_ON && !tx); 
  if (millis() - tDraw > 60) {
    tDraw = millis();
    if (menuLvl == 3) drawEditor();
    else if (menuLvl) drawMenu();
    else drawMain();
  }
  if (dirty && millis() - tSave > 3000) { EEPROM.put(0, cfg); dirty = false; }   // salva 3 s depois
}

terça-feira, 29 de setembro de 2026

DVFO 100kHz 225MHz VFO 309 Canais Cobra 148 GTL 2026.

Olá pessoal que acompanha nosso blog e canal no YouTube. Este mês trago aqui em vídeo, mais algumas modificações e implementações deste famoso projeto, DVFO 1.5 autor Sr. Paulo Roberto PU2REO. Postado no GitHub de 24 de Dezembro 2025. E seu perfil oficial GitHub. Sr. Paulo Roberto tem feito contribuições ao Radioamadorismo brasileiro e porque não mundial, com seus projetos e bibliotecas mais enxutas e leves para nossos DDS VFOs Arduíno. Meus agradecimentos por postar este maravilhoso projeto livre DVFO digital, especialmente para nosso lendário Cobra 148 GTL. Desde Fevereiro 2025 em pesquisas, salvei tudo. Tenho tentado fazer alterações atualizações cortando algumas funções ou rotinas no sketch DVFO. Não é fácil, pois queria deixar o programa bootloader no meu Atmega328P para outras gravações, este mês partir para cortes e atualizações.
Sobre atualização do sketch DVFO. Primeiro tive que cortar muitas instruções ou linhas de comandos como costumo falar para ver o quanto eu poderia ficar com o projeto original, mas gravando normal pela porta USB sem mexer no bootloader. Não deu muito certo e eu poderia perder muito tempo e ficaria desagradável, então partir para retirar ás bibliotecas que são mais pesadas e substitui-las, também não deu muito certo para ter espaço e continuar original. Então resolvi fazer o seguinte unir o que tenho experiência e estava na prática, decidi modificar ás bibliotecas:  #include <ButtonV2.h>  e  #include <RotaryEncoder.h> . Deixando ás mesmas bibliotecas do sketch do Júlio Cesar, com exceção da biblioteca do próprio autor para si5351,  e não foi só ás bibliotecas, fiz uma mistura dos dois sketches unindo o já conhecido pelo desconhecido para mim, o resultado foi muito melhor que esperava, com 309 canais em torno 27 MHz PX CB, é tudo que precisamos para ter os 40 canais mundialmente conhecidas (UIT) padrão (FCC), e muito mais poderemos modular acima até os 80 canais. Aproveitei que temos canais e VFO banda corrida, coloquei ás mesmas funções nas saídas CLK0 e CLK1 em: void tunegen() { . E outras atualizações.
Nosso DVFO atualizado ficou modo Canal: Tela com 309 canais como original x1 e x10 telecomandos. Roger Beep com som do lendário Cobra 148 GTL DX ou SuperStar 360 FM clássicos dos anos 80 e 90. Números frequências mostrando ponto e os zeros de Hz. CLK1 FI AM LSB USB. Clarificador 8 kHz para esquerda e direita aprovado com filtro e zona morta meio dia saindo em 20 Hz. S/Meter dupla escala em RX de 0 a +30dB, em TX mede RF, SWR com escala de medições 1, 2, 3, CAL!, dos clássicos transceptores CB PX. 
Nosso DVFO atualizado ficou modo VFO: Banda corrida de 100 kHz a 225 MHz com todos os Hz. STEP: 8 definições 1 Hz a 2 MHz. CLK1 FI mostra display a frequência de saída da FI. Roger Beep mesmo do modo Canal. S/Meter mesma escala de medições modo Canal. 
Nosso DVFO atualizado ficou MENU: Menu 1/5. Disp Tipe - Pressionando Encoder rápido entra modo EDITAR, girando Encoder seleciona Canal ou VFO, pressione rápido e volta NAVEGAR ou segure firme e saia modo MENU. Menu 2/5. Roger Beep - Pressione Encoder rápido e gire selecionando Liga ou Desliga, pressione rápido e volta a navegar MENU ou segure e saia. Menu 3/5. IF - Pressione rápido e ajuste IF no STEP selecionado ou segure um pouco Encoder até ver o STEP mudar cima tela, selecione de 1 Hz a 1 MHz. Aperte rápido e volte a NAVEGAR ou segure Encoder e saia MENU. Menu 4/5. Of.LSB - Pressione rápido Encoder para ajustar Offset LSB USB em STEP 10 Hz.Vejam algumas fotos deste mês com este sketch sendo montado cuidadosamente e exaustivamente. 
Assistam ao vídeo deste DVFO mais enxuto atualizado para Cobra 148 GTL.
 
Sem comentar em cada uma foto, vejam até o fim.
Vejam em TX a escala de RF e SWR já ajustada com os mesmos parâmetros.
Ficou assim no final.
Modo VFO RX.
Modo VFO TX e escala RF SWR.
Modo Canal RX.
Modo Canal TX.
Estas são algumas das fotos que tirei, claro que não começou assim e nem terminou assim, só no vídeo vocês verão como ele ficou do jeito que achei melhor para mim e todos os nossos seguidores que acompanha nosso blog. Quero dizer mais uma vez que o sketch poderá ser modificado em tudo, clarificador, ajuste do si5351, frequências VFO de início e final, tempo e som beep, FI para outros transceptores ou montagens AM SSB, nomes caracteres na tela, assim como outras modificações poderão ser feitas fora do MENU diretamente no sketch em: #define, variáveis globais, void loop. O sketch está todo comentado em português Brasil para iniciantes e aprendizado das linhas de comandos ou instrução.
Esquema elétrico atualizado DVFO 2026.
No esquema acima o autor PU2REO já deixou tudo fácil para adaptação implementação do VFO digital no seu Cobra 148 GTL, pinos do VCO UHIC 007 aproveitem L19 e L20. Outro circuito de acionamento de TX e RX com relé poderá ser com um reed swítch ou chave magnética substituindo o relé, caso tenha estalos ao soltar o PTT ou falta de espaço.
Selecione todo sketch, copie e cole. Abra Arduino IDE vá em "Arquivo" depois "Novo", ou ícone ao lado da seta para "Carregar ou Compilar, abra um novo delete todas ás linhas e cole este sketch, renomeei com final .ino se preferir e salve em uma pasta. adicione todas ás bibliotecas. Lembre-se que muitas bibliotecas para o mesmo arquivo pode causar conflitos, observe o processador e porta USB entre outras preferencias do seu Arduino isso também provocara erros na compilação.
Sketch abaixo. Cuidado ao copiar vem junto algumas propagandas isso também dá erro na compilação ou gravação. Verifiquem ás propagandas elas são também copiadas e o Arduíno IDE poderá vê-la como uma linha de comando e dá erro na compilação, por isso bom verificar qual o nome da propaganda que está em letras azuis, anote o nome e procure no seu sketch salvo e delete. Vocês podem fazer o seguinte, com o Arduíno IDE sketch aberto digite Ctrl F, vai abrir uma janela pequena em cima tem Localizar, digite o nome da propaganda click novamente em baixo a esquerda Localizar. Então vai dá onde tem a propaganda pelo nome que você digitou. Apague ou delete o nome e faça novas buscas de localização, até não achar mais o nome que você digitou.
Atualização 12/10/2026. Pessoal o dígito -60 estava erroneamente digitado -80, passou despercebido entre muitos números, por eu copiar a tabela e colar levei este erro para nosso sketch. já atualizei, o erro vem do sketch original DVFO.  
// ====================================================================================
// Sketch atualizado do projeto DVFO, Sr. Paulo Roberto (PU2REO) licenciado GPL-3.0.
// (https://github.com/PU2REO/PU2REO_DVFO). Para COBRA 148 GTL, outros transceptores. 
// AM LSB USB, MENU 5/5, 309 CANAIS, VFO 100 kHz 225 MHz, CLARIFICADOR, ROGER BEEP.
// S/Meter dupla escala RX SIG. TX RF SWR. Por Waldir Cardoso, bibliotecas já usadas
// nos outros sketches Cobra 148 GTL, mantendo os pinos originais do esquema do PU2REO,
// sketch e esquema postado blog codigo livre. Meus agradecimentos ao PU2REO. 
// Blog:
// ============================================================================
// Pressione curto 500ms encoder entra modo MENU NAVEGAR. 1/5, 2/5, 3/5, 4/5, 5/5.
// Dentro do MENU NAVEGAR gire ou pressione médio encoder e entra em EDITAR.
// MENU 1/5 - Pressione encoder EDIT. gire e escolha modo Canal ou VFO na tela display.
// MENU 2/5 - Pressione encoder EDIT. gire e ligar desligar beep do Cobra 148 GTL DX.
// MENU 3/5 - Pressione encoder EDIT. gire ajustar IF 400kHz a 22MHz, STEP segure longo  
// MENU 4/5 - Pressione encoder EDIT. gire ajustar offset LSB cada 10 Hz. 
// MENU 5/5 - Pressione encoder EDIT. gire ajustar offset USB cada 10 Hz.
// Pressione encoder 1500ms longo para saír modo MENU.
// ============================================================================
// CLK0 - Canal 1- AM 34.765.00 KHz, LSB 34.763.5 KHz, USB 34.766.5, saída si5351.
// CLK1 - AM IF 7800 KHz, LSB 7798.5 KHz, USB 7801.5, saída mostra display VFO.
// S/Meter atualizado SIG. 0, +30dB RX, com RF SWR 1, 2, 3 CAL! em TX. CB PX. 
// STEP VFO 1Hz, 10Hz, 100Hz, 1kHz, 10kHz, 100KHz, 1MHz, 2MHz.
// VFO 100kHz a 225MHz, com todos os zeros '000' dos Hz. 
// Modo Canal com x1 e x10, avanço de 1 ou 10 canais. 
// Clarificador zona morta meio dia saindo 20Hz, atualizado do mesmo sketch anterior.
// Menoria total VFO Canal nos modos MENU.
// ============================================================================
// 1. BIBLIOTECAS sketches Júlio Casar (mesmas dos outros sketches Cobra148GTL)
// ============================================================================
#include <EEPROM.h>
#include <Wire.h>
#include <Rotary.h>
#include "PU2REO_Si5351Lite.h"
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// --- Endereços da EEPROM ---
#define EEPROM_FREQ         0   // Frequência do VFO livre (41 bytes)
#define EEPROM_CHANNEL      10  // Índice do canal atual (2 bytes)
#define EEPROM_MENUTYPE     20  // Tipo de tela: 0=VFO, 1=Canal (1 byte)
#define EEPROM_ROGERBEEP    30  // Roger Beep ligado/desligado (1 byte)
#define EEPROM_INTFREQ      40  // Frequência IF (4 bytes)
#define EEPROM_LSBOFFSET    50  // Offset de LSB (4 bytes)
#define EEPROM_USBOFFSET    60  // Offset de USB (4 bytes)

// --- Valores fixos que ainda não estão no Menu (por espaço) ---
// Podem ser definidos para atualização de outros equipamentos.
#define SI5351_CAL         18500    // Minha calibração do Si5351 mesmo valor dos outros sketches 
#define S_METER_GAIN        600     // De 900 a 300. Ganho do S-Meter — ajuste este número após medir com multímetro

// --- Roger Beep ---
#define ROGERBEEP_FREQ     1800     // Hz- Frequencia som beep dos lemdários Cobra 148GTL-DX SuperStar 360FM é 1200-2000Hz
#define ROGERBEEP_MS       120      // Duração do beep, em milissegundos

// --- Faixa de sintonia do VFO livre ---
#define VFO_MIN_FREQ       100000UL    // Minima Frequencia modo VFO
#define VFO_MAX_FREQ       225000000UL // Maxima frequencia modo VFO

#define MAX_CHANNEL_INDEX  309      // Total de canais na tabela (valor original do PU2REO —
                                    // com 150 os canais "normais" acima de 26.965MHz até canal 80.

// Pinos — ORIGINAIS do esquema do PU2REO (fiação física já montada assim) 
#define ENC_KEY        A1     // Botão/chave do próprio Encoder
#define ENC_PIN2       A2     // Encoder — sinal de rotação 2
#define ENC_PIN1       A3     // Encoder — sinal de rotação 1
#define AM_PIN         5      // Tecla de pressão para modo AM
#define USB_PIN        6      // Tecla de pressão para modo USB
// Não existe pino dedicado para LSB — se nem AM nem USB estiverem pressionados, já é LSB
#define PTT_PIN        7      // Entrada do botão físico do PTT (LOW = pressionado)
#define TX_ENABLE_PIN  9      // Saída que liga o transmissor de verdade (segura ligado até o beep terminar)
#define ROGERBEEP_PIN  10     // Saída digital do tom do Roger Beep Cobra 148GTL DX SuperStar 360FM
#define SMETER_PIN     A6     // Entrada analógica do S-Meter
#define CLARIFIER_PIN  A7     // Entrada analógica do potenciômetro do Clarificador voice lock

// ============================================================================
// 2. VARIÁVEIS GLOBAIS
// ============================================================================
Rotary r = Rotary(ENC_PIN2, ENC_PIN1);
Adafruit_SSD1306 display(128, 64, &Wire, -1);
Si5351 si5351(0x60);

int smoothedSMeter = 0;
int peakHold = 0;
unsigned long peakTimer = 0;
unsigned long lastSave = 0;

unsigned long freq = 2700000UL;     // Frequência do VFO livre (Hz)
volatile bool wrapOccurred = false;

byte mode = 0;                      // 0=AM, 1=LSB, 2=USB
unsigned long bfo = 0;              // BFO final (IF + offset), recalculado a cada mudança de modo
long intFreq = 7800000UL;           // Frequência IF — Ajustável pelo Menu de 400 kHz a 22 MHz
long lsbOffset = -1500;             // Offset de LSB (negativo) — ajustável pelo Menu
long usbOffset = 1500;              // Offset de USB (positivo) — ajustável pelo Menu

long clarifier = 0;                 // Offset do Clarificador em Hz (-8000 a +8000) Saindo meio com 20 Hz

bool menuType = false;              // false = VFO livre; true = Canal
int channelIndex = 0;

bool menuMode = false;
bool menuEditMode = false;
byte menuOption = 0;
const byte totalMenuOptions = 5;    // Tipo de Tela MENU, Roger Beep, IF, Offset LSB, Offset USB
int encoderData = 0;
bool rogerBeepOn = true;

// STEP Passos de sintonia do VFO: 1Hz até 2MHz
const unsigned long stepTable[] PROGMEM = {1, 10, 100, 1000, 10000, 100000, 1000000, 2000000};
byte stepIndex = 4; // Começa em 10kHz

// STEP próprio do IF (Menu) — de 1Hz até 1MHz, independente do STEP do VFO
const unsigned long ifStepTable[] PROGMEM = {1, 10, 100, 1000, 10000, 100000, 1000000};
byte ifStepIndex = 3; // Começa em 1kHz

// Multiplicador do modo Canal — só x1 ou x10,
// senão o giro do encoder "pula" longe demais na tabela de 309 canais
const unsigned long chanCoefTable[] PROGMEM = {1, 10};
byte chanCoefIndex = 0; // Começa em x1

// Controle do PTT / Roger Beep (sem máquina de estados — só duas variáveis)
bool pttOld = false;
bool beepPending = false;
unsigned long beepStartTime = 0;

// ============================================================================
// 3. TABELA DE CANAIS (herdada do DVFO original). Números >= 700 são canais
// especiais "TC" (Tele-comando/RCRS): mostra "TC" + (valor - 700).
// ============================================================================
const int16_t channelNumbers[] PROGMEM = {
  -150,-149,-148,-147,-146,-145,-144,-143,-142,-141,
  -140,-139,-138,-137,-136,-135,-134,-133,-132,-131,
  -130,-129,-128,-127,-126,-125,-124,-123,-122,-121,
  -120,-119,-118,-117,-116,-115,-114,-113,-112,-111,
  -110,-109,-108,-107,-106,-105,-104,-103,-102,-101,
  -100, -99, -98, -97, -96, -95, -94, -93, -92, -91,
   -90, -89, -88, -87, -86, -85, -84, -83, -82, -81,
   -80, -79, -78, -77, -76, -75, -74, -73, -72, -71,
   -70, -69, -68, -67, -66, -65, -64, -63, -62, -61,
   -60, -59, -58, -57, -56, -55, -54, -53, -52, -51,
   -50, -49, -48, -47, -46, -45, -44, -43, -42, -41,
   -40, -39, -38, -37, -36, -35, -34, -33, -32, -31,
   -30, -29, -28, -27, -26, -25, -24, -23, -22, -21,
   -20, -19, -18, -17, -16, -15, -14, -13, -12, -11,
   -10,  -9,  -8,  -7,  -6,  -5,  -4,  -3,  -2,  -1,
     1,   2,   3, 701,   4,   5,   6,   7, 702,  8,   9,  10,
    11, 703,  12,  13,  14,  15, 704,  16,  17,  18,  19, 705, 20,
    21,  22,  23,  24,  25,  26,  27,  28,  29,  30,
    31,  32,  33,  34,  35,  36,  37,  38,  39,  40,
    41,  42,  43,  44,  45,  46,  47, 706,  48,  49,  50,
    51, 707,  52,  53,  54,  55, 708,  56,  57,  58,  59, 709, 60,
    61,  62,  63,  64,  65,  66,  67,  68,  69,  70,
    71,  72,  73,  74,  75,  76,  77,  78,  79,  80,
    81,  82,  83,  84,  85,  86,  87,  88,  89,  90,
    91,  92,  93,  94,  95,  96,  97,  98,  99, 100,
   101, 102, 103, 104, 105, 106, 107, 108, 109, 110,
   111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
   121, 122, 123, 124, 125, 126, 127, 128, 129, 130,
   131, 132, 133, 134, 135, 136, 137, 138, 139, 140,
   141, 142, 143, 144, 145, 146, 147, 148, 149, 150
};

// Frequência real (em kHz) de cada canal, na mesma ordem da tabela acima
const int16_t channelFreqs[] PROGMEM = {
  25465, 25475, 25485, 25495, 25505, 25515, 25525, 25535, 25545, 25555,
  25565, 25575, 25585, 25595, 25605, 25615, 25625, 25635, 25645, 25655,
  25665, 25675, 25685, 25695, 25705, 25715, 25725, 25735, 25745, 25755,
  25765, 25775, 25785, 25795, 25805, 25815, 25825, 25835, 25845, 25855,
  25865, 25875, 25885, 25895, 25905, 25915, 25925, 25935, 25945, 25955,
  25965, 25975, 25985, 25995, 26005, 26015, 26025, 26035, 26045, 26055,
  26065, 26075, 26085, 26095, 26105, 26115, 26125, 26135, 26145, 26155,
  26165, 26175, 26185, 26195, 26205, 26215, 26225, 26235, 26245, 26255,
  26265, 26275, 26285, 26295, 26305, 26315, 26325, 26335, 26345, 26355,
  26365, 26375, 26385, 26395, 26405, 26415, 26425, 26435, 26445, 26455,
  26465, 26475, 26485, 26495, 26505, 26515, 26525, 26535, 26545, 26555,
  26565, 26575, 26585, 26595, 26605, 26615, 26625, 26635, 26645, 26655,
  26665, 26675, 26685, 26695, 26705, 26715, 26725, 26735, 26745, 26755,
  26765, 26775, 26785, 26795, 26805, 26815, 26825, 26835, 26845, 26855,
  26865, 26875, 26885, 26895, 26905, 26915, 26925, 26935, 26945, 26955,
  26965, 26975, 26985, 26995, 27005, 27015, 27025, 27035, 27045, 27055, 27065, 27075,
  27085, 27095, 27105, 27115, 27125, 27135, 27145, 27155, 27165, 27175, 27185, 27195, 27205,
  27215, 27225, 27255, 27235, 27245, 27265, 27275, 27285, 27295, 27305,
  27315, 27325, 27335, 27345, 27355, 27365, 27375, 27385, 27395, 27405,
  27415, 27425, 27435, 27455, 27465, 27475, 27485, 27495, 27505, 27515, 27525,
  27535, 27545, 27555, 27565, 27575, 27585, 27595, 27605, 27615, 27625, 27635, 27645, 27655,
  27665, 27675, 27705, 27685, 27695, 27715, 27725, 27735, 27745, 27755,
  27765, 27775, 27785, 27795, 27805, 27815, 27825, 27835, 27845, 27855,
  27865, 27875, 27885, 27895, 27905, 27915, 27925, 27935, 27945, 27955,
  27965, 27975, 27985, 27995, 28005, 28015, 28025, 28035, 28045, 28055,
  28065, 28075, 28085, 28095, 28105, 28115, 28125, 28135, 28145, 28155,
  28165, 28175, 28185, 28195, 28205, 28215, 28225, 28235, 28245, 28255,
  28265, 28275, 28285, 28295, 28305, 28315, 28325, 28335, 28345, 28355,
  28365, 28375, 28385, 28395, 28405, 28415, 28425, 28435, 28445, 28455,
  28465, 28475, 28485, 28495, 28505, 28515, 28525, 28535, 28545, 28555
};

// ============================================================================
// 4. INTERRUPÇÃO DO ENCODER — pinos A2/A3 ficam no grupo PCINT1 (não PCINT2,
// que é só para pinos digitais D0-D7)
// ============================================================================
ISR(PCINT1_vect) {
  char result = r.process();
  if (result == DIR_CW) {
    if (menuMode) encoderData = 1;
    else set_frequency(1);
  }
  else if (result == DIR_CCW) {
    if (menuMode) encoderData = -1;
    else set_frequency(-1);
  }
}

// ============================================================================
// 5. FUNÇÃO: Ajusta a frequência (modo VFO) ou o canal (modo Canal).
// Cada modo tem sua própria tabela de passo — o VFO usa stepTable (1Hz-2MHz),
// o Canal usa chanCoefTable (só x1 ou x10, bem mais restrito)
// ============================================================================
void set_frequency(short dir) {
  if (!menuType) {
    unsigned long step = pgm_read_dword(&stepTable[stepIndex]);
    if (dir == 1) {
      freq += step;
      if (freq > VFO_MAX_FREQ) { freq = VFO_MIN_FREQ; wrapOccurred = true; }
    } else {
      if (freq <= VFO_MIN_FREQ + step) { freq = VFO_MAX_FREQ; wrapOccurred = true; }
      else freq -= step;
    }
  } else {
    int chanStep = pgm_read_dword(&chanCoefTable[chanCoefIndex]);
    channelIndex += dir * chanStep;
    if (channelIndex >= MAX_CHANNEL_INDEX) channelIndex = 0;
    if (channelIndex < 0) channelIndex = MAX_CHANNEL_INDEX - 1;
  }
}

// ============================================================================
// 6. FUNÇÃO: Recalcula o BFO (IF + offset de SSB) de acordo com o modo atual
// ============================================================================
void updateBFO() {
  if (mode == 0) bfo = intFreq;             // AM: sem deslocamento
  if (mode == 1) bfo = intFreq + lsbOffset; // LSB (já vem negativo, ex: -1500)
  if (mode == 2) bfo = intFreq + usbOffset; // USB (já vem positivo, ex: +1500)
}

// ============================================================================
// 7. FUNÇÃO: Lê os botões físicos de pressão AM/USB. Se nenhum estiver
// pressionado, já cai automaticamente em LSB (não existe pino dedicado)
// ============================================================================
void readMode() {
  byte oldMode = mode;
  if (digitalRead(AM_PIN)  == LOW) mode = 0;
  else if (digitalRead(USB_PIN) == LOW) mode = 2;
  else mode = 1;      // Nenhum dos dois pressionado = LSB
  if (mode != oldMode) updateBFO();
}

// ============================================================================
// 8. FUNÇÃO: Frequência de sintonia atual (VFO livre ou do canal), em Hz
// ============================================================================
unsigned long getTuneFreq() {
  if (!menuType) return freq;
  return (unsigned long)pgm_read_word(&channelFreqs[channelIndex]) * 1000UL;
}

// ============================================================================
// 9. FUNÇÃO: Gera a frequência final Si5351 (CLK0) e (CLK1)
// ============================================================================
  void tunegen() {
  long vfoFreq = (long)(getTuneFreq() + bfo) + clarifier;
  si5351.set_freq((uint64_t)vfoFreq * 100ULL, SI5351_CLK0);
  si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA);
  si5351.output_enable(SI5351_CLK0, 1);

  // NOVO: gera o BFO (IF + offset de AM/LSB/USB) também no CLK1 —
  si5351.set_freq((uint64_t)bfo * 100ULL, SI5351_CLK1);
  si5351.drive_strength(SI5351_CLK1, SI5351_DRIVE_6MA);
  si5351.output_enable(SI5351_CLK1, 1);
}

// ============================================================================
// 10. FUNÇÕES DE FORMATAÇÃO DA TELA
// ============================================================================

// Imprime a frequência com pontos separadores e os Hz finais (2 dígitos,
// resolução de 10Hz) em tamanho MENOR que o resto — ex: "26.965." grande, "00" pequeno
// Atualizada 11/09/2026 frequencia com 3 zeros final display sempre
  void printFreqMixed(unsigned long hz, uint8_t bigSize, int endX, int y) {
  unsigned int m = hz / 1000000;
  unsigned int k = (hz % 1000000) / 1000;
  unsigned int h = (hz % 1000);

  // Calcula quantos caracteres o número grande vai ter, pra saber onde começar
  // de forma que ele sempre TERMINE no mesmo lugar (endX), não importa o tamanho
  byte numChars;
  if (m >= 100) numChars = 7;      // ex: "225.965"
  else if (m >= 10) numChars = 6;  // ex: "25.965"
  else if (m >= 1) numChars = 5;   // ex: "5.965"
  else numChars = 3;               // ex: "965" (abaixo de 1MHz, sem MHz na frente)

  int charWidth = 6 * bigSize; // largura de 1 caractere nesse tamanho de fonte
  int startX = endX - (numChars * charWidth);
  if (startX < 0) startX = 0;

  display.setTextSize(bigSize);
  display.setCursor(startX, y);
  if (m > 0) {
  display.print(m);
  display.print(F("."));
  if (k < 100) display.print(F("0"));
  if (k < 10) display.print(F("0"));
  }
  display.print(k);

  // Os 3 dígitos pequenos do final sempre começam no mesmo X (endX), fixo
  display.setTextSize(1);
  display.setCursor(endX + 2, y); // +2 pixels de espaço antes dos 3 zeros pequenos
  if (h < 100) display.print(F("0"));
  if (h < 10) display.print(F("0"));
  display.print(h);
 }

// Imprime um valor em Hz já formatado com a unidade certa (Hz/kHz/MHz) —
// usado no rodapé do modo VFO para mostrar o STEP atual
void printStepUnit(unsigned long hz) {
  if (hz >= 1000000UL) { display.print(hz / 1000000UL); display.print(F("MHz")); }
  else if (hz >= 1000UL) { display.print(hz / 1000UL); display.print(F("kHz")); }
  else { display.print(hz); display.print(F("Hz")); }
}

// ============================================================================
// 11. FUNÇÃO: Salva/Carrega configurações na EEPROM
// ============================================================================
void saveMemory() {
  EEPROM.put(EEPROM_FREQ, freq);
  EEPROM.put(EEPROM_CHANNEL, channelIndex);
  EEPROM.put(EEPROM_MENUTYPE, (byte)menuType);
  EEPROM.put(EEPROM_ROGERBEEP, (byte)rogerBeepOn);
  EEPROM.put(EEPROM_INTFREQ, intFreq);
  EEPROM.put(EEPROM_LSBOFFSET, lsbOffset);
  EEPROM.put(EEPROM_USBOFFSET, usbOffset);
 // EEPROM.put(EEPROM_SIDEBAND, sidebandOffset);
}

void loadMemory() {
  EEPROM.get(EEPROM_FREQ, freq);
  EEPROM.get(EEPROM_CHANNEL, channelIndex);

  byte savedMenuType;
  EEPROM.get(EEPROM_MENUTYPE, savedMenuType);
  menuType = (savedMenuType == 1);

  byte savedRogerBeep;
  EEPROM.get(EEPROM_ROGERBEEP, savedRogerBeep);
  rogerBeepOn = (savedRogerBeep != 0);

  EEPROM.get(EEPROM_INTFREQ, intFreq);
  EEPROM.get(EEPROM_LSBOFFSET, lsbOffset);
  EEPROM.get(EEPROM_USBOFFSET, usbOffset);
  
  // --- Validações de segurança, contra EEPROM "virgem" ou valores corrompidos ---
  if (freq < VFO_MIN_FREQ || freq > VFO_MAX_FREQ) freq = 2700000UL;
  if (channelIndex < 0 || channelIndex >= MAX_CHANNEL_INDEX) channelIndex = 0;
  if (intFreq < 400000UL || intFreq > 22000000UL) intFreq = 7800000UL; // Valida frequências 400kHz a 22MHz
  //if (intFreq < 1000000UL || intFreq > 15000000UL) intFreq = 7800000UL;
  if (lsbOffset > 0 || lsbOffset < -5000) lsbOffset = -1500;
  if (usbOffset < 0 || usbOffset > 5000) usbOffset = 1500;
}

// ============================================================================
// 12. SETUP
// ============================================================================
void setup() {
  Wire.begin();
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.display();

  // --- Tela de boas-vindas ---
  display.setTextSize(2);
  display.setCursor(1, 4);
  display.print(F("Cobra 148"));
  display.setTextSize(1);
  display.print(F("GTL"));
  display.setTextSize(2);
  display.setCursor(38, 27);
  display.print(F("DVFO"));
  display.setCursor(9, 48);
  display.print(F("Clarifier"));
  display.display();
  delay(2000);

  pinMode(ENC_KEY, INPUT_PULLUP);
  pinMode(ENC_PIN1, INPUT_PULLUP);
  pinMode(ENC_PIN2, INPUT_PULLUP);
  pinMode(AM_PIN, INPUT_PULLUP);
  pinMode(USB_PIN, INPUT_PULLUP);
  pinMode(PTT_PIN, INPUT_PULLUP);
  pinMode(TX_ENABLE_PIN, OUTPUT);
  pinMode(ROGERBEEP_PIN, OUTPUT);
  digitalWrite(TX_ENABLE_PIN, LOW);

  loadMemory();

  si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0);
  si5351.set_correction(SI5351_CAL, SI5351_PLL_INPUT_XO);
  si5351.output_enable(SI5351_CLK0, 1);
  si5351.output_enable(SI5351_CLK1, 1);
  
  // Encoder nos pinos A2/A3 (grupo PCINT1, diferente dos outros sketches que usam D2/D3)
  PCICR |= (1 << PCIE1);
  PCMSK1 |= (1 << PCINT10) | (1 << PCINT11);
  sei();

  updateBFO();
  tunegen();
}

// ============================================================================
// 13. LOOP PRINCIPAL
// ============================================================================
void loop() {
  static unsigned long btnTimer = 0;
  static bool btnState = false;
  static bool mediumFired = false; // Já disparou a ação do toque médio nesse aperto?
  static bool longFired = false;   // Já disparou a ação do toque longo nesse aperto?

  // --- Encoder: 3 durações de toque ---
  // Curto  (<500ms):    NAV/EDIT no Menu, ou STEP/multiplicador fora do Menu
  // Médio  (500-1500ms): só troca o STEP do IF, e só dentro do item da IF (menuOption == 2)
  // Longo  (>1500ms):   entra/sai do Menu, em qualquer item
  if (digitalRead(ENC_KEY) == LOW) {
    if (!btnState) { // Instante em que o botão foi pressionado 
      btnState = true;
      btnTimer = millis();
      mediumFired = false;
      longFired = false;
    }

    unsigned long held = millis() - btnTimer;

    // Toque médio: dispara uma vez só, ao passar de 500ms, e só se estiver editando o IF
    if (!mediumFired && held > 500 && held <= 1500) {
      if (menuMode && menuEditMode && menuOption == 2) {
        ifStepIndex = (ifStepIndex + 1) % 7; // Troca o STEP do IF
      }
      mediumFired = true; // Marca como disparado mesmo fora do item do IF, pra não repetir à toa
    }

    // Toque longo: dispara uma vez só, ao passar de 1500ms — sempre entra/sai do Menu
    if (!longFired && held > 1500) {
      menuMode = !menuMode;
      menuEditMode = false;
      encoderData = 0;
      display.clearDisplay();
      delay(500);
      longFired = true;
    }

  } else {
    if (btnState) {
      // Só conta como "toque curto" se nem o médio nem o longo chegaram a disparar
      if (!mediumFired && !longFired) {
        if (menuMode) {
          menuEditMode = !menuEditMode;
        } else if (!menuType) {
          stepIndex = (stepIndex + 1) % 8;       // VFO: percorre a tabela de passos
        } else {
          chanCoefIndex = (chanCoefIndex + 1) % 2; // Canal: alterna x1/x10
        }
      }
      btnState = false;
    }
  }

  if (menuMode) {
    handleMenu();
    return;
  }

  readMode();

  // =============================================================================
  // LEITURA DO CLARIFICADOR — mesma versão testada e aprovada dos outros sketches
  // (substitui o "Voice Lock" original PU2REO, que não tinha filtro nem zona morta
  // =============================================================================
  analogRead(CLARIFIER_PIN);
  int clarRead = analogRead(CLARIFIER_PIN);
  long rawClarifier = map(clarRead, 0, 1023, -8000, 8000); // 8 kHz para esquerda e direita

  static long lastSmoothed = 0;
  long smoothedClarifier = (rawClarifier * 2 + lastSmoothed) / 3;
  lastSmoothed = smoothedClarifier;

  const int CLAR_STEP = 10;
  long quantized;
  if (smoothedClarifier >= 0) quantized = (smoothedClarifier + CLAR_STEP / 2) / CLAR_STEP * CLAR_STEP;
  else quantized = (smoothedClarifier - CLAR_STEP / 2) / CLAR_STEP * CLAR_STEP;

  const int CLAR_DEADZONE = 20;  // Zona morta meio dia saindo em 20Hz
  if (quantized > -CLAR_DEADZONE && quantized < CLAR_DEADZONE) quantized = 0;

  static long clarifierOutput = 0;
  if (abs(quantized - clarifierOutput) >= CLAR_STEP) clarifierOutput = quantized;
  if (clarifier != clarifierOutput) clarifier = clarifierOutput;

  // ============================================================
  // PTT e Roger Beep versão tipo 148GTL DX SuperStar 360FM  
  // ============================================================
  bool pttNow = (digitalRead(PTT_PIN) == LOW);
  if (pttNow) {
    digitalWrite(TX_ENABLE_PIN, HIGH); // Mantém o transmissor ligado enquanto o PTT estiver pressionado
    beepPending = false;
  } else if (pttOld && !pttNow) {
    
    // Soltar o PTT: dispara o beep (se estiver ligado) modo MENU 2/5
    if (rogerBeepOn) {
      tone(ROGERBEEP_PIN, ROGERBEEP_FREQ, ROGERBEEP_MS);
      beepPending = true;
      beepStartTime = millis();
    } else {
      digitalWrite(TX_ENABLE_PIN, LOW); // Sem beep: já solta o transmissor na hora
    }
  }
  if (beepPending && (millis() - beepStartTime >= (unsigned long)ROGERBEEP_MS + 20)) {
    digitalWrite(TX_ENABLE_PIN, LOW); // Beep terminou: agora sim solta o transmissor de verdade
    beepPending = false;
  }
  pttOld = pttNow;

  // Atualiza o Si5351 só se algo mudou 
  static unsigned long tuneFreqOld = 0;
  static long clarifierOld = 0;
  static unsigned long bfoOld = 0;
  unsigned long currentTuneFreq = getTuneFreq();
  if (tuneFreqOld != currentTuneFreq || clarifierOld != clarifier || bfoOld != bfo) {
    tunegen();
    if (wrapOccurred) { si5351.pll_reset(SI5351_PLLA); wrapOccurred = false; }
    if (currentTuneFreq >= 100000000UL || tuneFreqOld >= 100000000UL) si5351.pll_reset(SI5351_PLLA);
    tuneFreqOld = currentTuneFreq;
    clarifierOld = clarifier;
    bfoOld = bfo;
  }

  //--- Leitura do S-Meter com leitura de descarte ---
  analogRead(SMETER_PIN);
  delayMicroseconds(50);
  int sMeterVal = analogRead(SMETER_PIN);
  display.clearDisplay();
  drawScreen(sMeterVal, pttNow || beepPending); // CHAMA a função; o corpo dela está logo abaixo
  display.display();
  if (millis() - lastSave > 5000) {
    saveMemory();
    lastSave = millis();
  }
  delay(50);
}

// ========================================================================================
// 14. FUNÇÃO: S/Meter - Desenha a tela — layout Atualizada TX SWR RF semelhante do PU2REO
// ========================================================================================
void drawScreen(int sMeterVal, bool isTx) {
  // S-Meter escala diferente para TX e RX  
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(0, 0);
 // display.print(F("SIG.1.3.5.7.9...+30dB")); // Lendário Cobra 148GTL e outros equipamentos PX CB

 // TX ficar com escala SWR e RF
   if (isTx) {
  // Régua de SWR durante a transmissão — números posicionados
  // nos mesmos lugares onde ficavam o "5", "9" e "+30dB" de RX    
  display.setCursor(15, 0);   // Posição caractere numero 1 
  display.print(F("1"));       
  display.setCursor(24, 0);    // Posição nome caracteres SWR entre 1 e 2
  display.print(F("SWR"));  
  display.setCursor(45, 0);    // 2 abaixo pouco onde era o "5"
  display.print(F("2"));       // Alinhado onde era o "5"
  display.setCursor(68, 0);    // 3 Abaixo onde era o S "9"
  display.print(F("3"));       // Alinhado onde era o "9"  
  display.setCursor(0, 8);     // Começo da escala com RF em TX
  display.print(F("RF")); 
  display.setCursor(92, 0);     // Posição CAL! ajustar barras para medir SWR
  display.print(F("CAL!"));     // Alinhado ponteiro no meio 0d "+30dB"  
  } else {
  display.print(F("SIG.1.3.5.7.9...+30dB")); // Escala Cobra normal de sinal, em RX
  }
  
  // Escala diferente para TX e RX — durante a transmissão o sinal no pino é
  // bem mais forte (mede potência de saída), então usa a faixa cheia do ADC;
  // na recepção usa a faixa mais sensível de sempre (S_METER_GAIN)
  int rawBarLen;
  if (isTx) {
    rawBarLen = map(sMeterVal, 0, 1023, 0, 100); // TX: faixa cheia (medidor de potência/RF de saída)
  } else {
    int sConstrained = constrain(sMeterVal, 0, S_METER_GAIN);
    rawBarLen = map(sConstrained, 0, S_METER_GAIN, 0, 100); // RX: faixa sensível de sempre
  }

  // Suavização (média móvel) — evita "tremedeira" na barra
  smoothedSMeter = ((smoothedSMeter * 2) + rawBarLen) / 3;

  // Peak Hold — segura visualmente o valor mais alto por um instante, depois cai devagar
  if (smoothedSMeter > peakHold) {
    peakHold = smoothedSMeter;
    peakTimer = millis();
  }
  if (millis() - peakTimer > 15) {
    if (peakHold > 0) peakHold -= 2; // Queda gradual do pico
  }
  peakHold = constrain(peakHold, 0, 100);

  if (smoothedSMeter > 0) display.writeFillRect(16, 9, smoothedSMeter, 3, WHITE); // Barra principal
  for (int i = 1; i < 16; i++) display.fillRect(18 + (i * 5), 9, 1, 3, BLACK);    // "Serrilha" a barra em segmentos
  display.drawFastVLine(peakHold + 16, 8, 5, WHITE); // Linha do pico, marca o valor mais alto recente
  display.writeFastHLine(0, 16, 128, WHITE);
  
  long displayFreq = (long)getTuneFreq() + clarifier;
  if (!menuType) {
   
  //==========================================================
  // MODO VFO — Caracteres números frequência mesmo tamanho e formato do modo Canal
  //==========================================================
  display.setTextSize(1);
  display.setCursor(1, 20); // Posição caracteres VFO 
  display.print(F("VFO"));
  // Frequência do VFO — todos os dígitos no mesmo tamanho, igual ao modo Canal, com ponto antes dos Hz
  display.setTextSize(2);
  display.setCursor(1, 32); // Colada em x=0, igual ao Canal
 {
  unsigned long m = displayFreq / 1000000;
  unsigned long k = (displayFreq % 1000000) / 1000;
  unsigned long h = displayFreq % 1000;
  if (m > 0) { // MHz só aparece se for >=1MHz (senão fica "0." feio em frequências baixas)
  display.print(m);
  display.print(F("."));
  if (k < 100) display.print(F("0"));
  if (k < 10) display.print(F("0"));
 }
  display.print(k);
  //23/09/26 Para sumir ponto nos kHz
  if (m < 100) display.print(F("."));
  //display.print(F("."));
  if (h < 100) display.print(F("0"));
  if (h < 10) display.print(F("0"));
  display.print(h);
 }
  int vfoFreqEndX = display.getCursorX(); // Onde a frequência terminou, pra colocar a unidade logo depois
  //15/09/26. MHz em cima lado direito
  display.setTextSize(1);
  display.setCursor(109, 20); // Posição fixa, mesma linha do modo/RB (y=20), canto direito
  if (displayFreq >= 1000000UL) display.print(F("MHz")); else display.print(F("kHz"));
  
    //display.writeFastHLine(0, 47, 128, WHITE); //Linha separa coluna 2 e 3
    display.writeFastHLine(0, 50, 128, WHITE); //Linha separa coluna 2 e 3
    display.setTextSize(1);
    // IF (CLK1) — canto esquerdo do rodapé, mostra o BFO atual em kHz
    // (AM sem casa decimal, LSB/USB com uma casa quando necessário: 7798.5, 7801.5)
    display.setCursor(0, 55);
    display.print(F("IF:"));
    display.print(bfo / 1000); // Parte inteira, em kHz
    if (bfo % 1000 != 0) {     // Só mostra a casa decimal se ela existir (LSB/USB)
    display.print(F("."));
    display.print((bfo % 1000) / 100);
   }

    // TS (Step de sintonia) — canto direito do rodapé, abreviado de "STEP" pra "TS"
    display.setCursor(73, 55); // Posição STEP TS no rodapé
    display.print(F("TS:"));
    printStepUnit(pgm_read_dword(&stepTable[stepIndex])); // Mesmo valor de antes, só mudou o rótulo e a posição

    //display.setCursor(50, 20);
    display.setCursor(45, 20); // Modos + RB/TX juntos de novo, na mesma linha
    if (mode == 0) display.print(F("AM "));
    if (mode == 1) display.print(F("LSB"));
    if (mode == 2) display.print(F("USB"));
    if (isTx) display.print(F("   TX")); //Posições três '3' espaços de RB/TX em modo VFO
    else if (rogerBeepOn) display.print(F("   RB")); //Posições três '3' espaços de RB/TX em modo VFO  
    } else {
    
    // ==========================================================
    // MODO CANAL
    // ==========================================================
    int chNum = pgm_read_word(&channelNumbers[channelIndex]);

    // Posição do nome Canal pequeno, no canto, igual ao "VFO" do outro modo
    display.setTextSize(1);
    display.setCursor(0, 21); //Posição do nome Canal display
    display.print(F("Canal"));

   // Calcula quantos caracteres o número vai ter, pra ele sempre TERMINAR
   // no mesmo lugar (não sobrepor o LSB/x1), crescendo pra ESQUERDA quando for maior
   int dispValue = (chNum >= 700) ? (chNum - 700) : chNum;
   bool isTC = (chNum >= 700);
   bool isNeg = (dispValue < 0);
   int absVal = abs(dispValue);
   byte numDigits = (absVal >= 100) ? 3 : (absVal >= 10) ? 2 : 1;
   byte totalChars = numDigits + (isNeg ? 1 : 0) + (isTC ? 2 : 0); // +1 pro sinal "-", +2 pro "TC"

   int endX = 95; // Posição do número canal direita ou esquerda horizontalmente
   int startX = endX - (totalChars * 18); // 18 = largura de 1 caractere no tamanho 3
   if (startX < 0) startX = 0;

   display.setTextSize(3);
   //display.setCursor(startX, 21);
   display.setCursor(startX, 22); // Faz o número canal subir ou descer verticalmente.
   if (isTC) { display.print(F("TC")); display.print(dispValue); }
   else display.print(chNum);
  
  // Modo (AM/LSB/USB) — canto direito, em cima (pequeno)
   display.setTextSize(1);
   //display.setCursor(107, 21);
   display.setCursor(109, 20);
   if (mode == 0) display.print(F("AM"));
   if (mode == 1) display.print(F("LSB"));
   if (mode == 2) display.print(F("USB"));

   // Multiplicador (x1/x10) — embaixo do "Canal", canto esquerdo
   display.setTextSize(1);
   display.setCursor(0, 37);
   display.print(F("x"));
   display.print(pgm_read_dword(&chanCoefTable[chanCoefIndex]));

   //14/09/26. Atualizada para RB/TX ficar no lado esquerdo ao lado x10
   int multEndX = display.getCursorX(); // Onde "x1"/"x10" terminou
   display.setCursor(multEndX + 4, 37); // Escreve RB/TX logo depois, com 6px de espaço
   if (isTx) display.print(F("TX"));
   else if (rogerBeepOn) display.print(F("RB"));
   
   //14/09/26 Bom 
   //display.writeFastHLine(0, 47, 128, WHITE);
   display.writeFastHLine(0, 47, 105, WHITE);  // Linha divisória canal frequência
   //display.setCursor(0, 50);

   
   // Para todos os dígitos do mesmo tamanho 14/09/26
   display.setTextSize(2);
   //display.setCursor(0, 50);
   display.setCursor(3, 50); // Início do display, encostado na esquerda
  {
  unsigned long m = displayFreq / 1000000;
  unsigned long k = (displayFreq % 1000000) / 1000;
  unsigned long h = displayFreq % 1000;
  display.print(m);
  display.print(F(".")); // Ponto de volta, separando os Hz — fica "26.965.000"
  if (k < 100) display.print(F("0"));
  if (k < 10) display.print(F("0"));
  display.print(k);
  display.print(F(".")); 
  if (h < 100) display.print(F("0")); // Hz colados direto, sem ponto, mas ainda com zeros à esquerda
  if (h < 10) display.print(F("0"));
  display.print(h);
}
 
   // "MHz" fixo, sempre terminando pertinho da borda direita (x=126)
  display.setTextSize(1);
  display.setCursor(109, 40); // 108 + 18px de "MHz" = termina em 126
  //display.setCursor(110, 39); // 108 + 18px de "MHz" = termina em 126
  display.print(F("MHz"));
   }
 }     

// ============================================================================
// 15. FUNÇÃO: Menu de Serviço — 4 opções: Tipo de Tela, Roger Beep, Freq. IF,
// Offset de SSB (LSB/USB)
// ============================================================================

void handleMenu() {
  int encDir = encoderData;
  encoderData = 0;

  if (encDir != 0) {
    if (!menuEditMode) {
      menuOption = (menuOption + encDir + totalMenuOptions) % totalMenuOptions;
    } else {
      switch (menuOption) {
        case 0: menuType = !menuType; break;              // Tipo de Tela
        case 1: rogerBeepOn = !rogerBeepOn; break;        // Roger Beep
       
       // 15/09/26.  Agora com STEP FI MENU
        case 2:
        intFreq += encDir * pgm_read_dword(&ifStepTable[ifStepIndex]); // Agora usa o STEP próprio do IF
        if (intFreq < 400000UL) intFreq = 400000UL;
        if (intFreq > 22000000UL) intFreq = 22000000UL;
        updateBFO();
        break;
       
        //Atualizada para 400kHz a 22MHz acima 14/09/26
        //case 2: intFreq += encDir * pgm_read_dword(&stepTable[stepIndex]); updateBFO(); break; // Usa o mesmo STEP do VFO        
        case 3:                                             // Offset de LSB
          lsbOffset += encDir * 10;
          if (lsbOffset > 0) lsbOffset = 0; // LSB sempre <= 0
          updateBFO();
          break;
        case 4:                                             // Offset de USB
          usbOffset += encDir * 10;
          if (usbOffset < 0) usbOffset = 0; // USB sempre >= 0
          updateBFO();
          break;
      }
    }
  }

  display.clearDisplay();
  display.setTextColor(WHITE);

  // --- Título grande, igual ao layout original do PU2REO ---
  display.setTextSize(2);
  display.setCursor(0, 1);
  switch (menuOption) {
    case 0: display.print(F(" Disp Tipe")); break;
    case 1: display.print(F(" RogerBeep")); break;
    case 2: display.print(F("IF")); break;
    case 3: display.print(F(" Of.LSB")); break;
    case 4: display.print(F(" Of.USB")); break;
  }

  // --- Valor grande, no meio da tela ---
  display.setTextSize(2);
  display.setCursor(10, 26);  //Nome Canal posição display
  switch (menuOption) {
    case 0: display.print(menuType ? F("CANAL") : F("VFO")); break;    
    case 1: display.print(rogerBeepOn ? F("LIGA") : F("DESLIGA")); break;
        
    case 2:
    display.print(intFreq); // Numeros frequencia IF Sem sufixo "Hz" — estourava a linha
    display.setTextSize(1);  // Tamanho fonte
    display.setCursor(27, 13); // Posição Press/Step/Sair
    display.print(F("Press/Step/Sair"));
    display.setCursor(50, 1); // Mostra o STEP depois de nome IF cima display
    //display.setCursor(6, 45) // Posição Step abaixo 7800
    display.print(F("Step: "));
    printStepUnit(pgm_read_dword(&ifStepTable[ifStepIndex]));
    break;
        
    case 3: display.print(lsbOffset); display.print(F(" Hz")); break;
    case 4: display.print(usbOffset); display.print(F(" Hz")); break;
  }

  // --- Rodapé: "x1" (decorativo, igual ao original) + posição atual no Menu ---
  display.setTextSize(1);  
  //Atualizada 23/09/26
  display.setCursor(6, 45); //Posição'Gire/Press' Gire/Press/Sair meio entre valor 7800 FI e NAVEGAR
  display.print(menuEditMode ? F("Gire/Press/NAV/Sair") : F("Gire/Press/Sair"));
  display.setCursor(6, 57); // Posição "EDITAR NAVEGAR"
  display.print(menuEditMode ? F("EDITAR") : F("NAVEGAR"));
  display.setCursor(72, 57); // Posição "MENU: " + números
  display.print(F("MENU:"));  
  //display.setCursor(100, 57); //Posição números adicionado linha acima com nome "MENU"
  display.print(menuOption + 1);
  display.print(F("/"));      // Mostra números do "MENU"
  display.print(totalMenuOptions);
  display.display();
  delay(80);
}