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.
Ainda Menu 1 principal, selecionado: Modo VFO Canal.
Tela Sobmenu 2. Selecionado XTAL SI5351.
Descendo o Sobmenu 2 temos ainda.
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
}

