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.
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.
// ====================================================================================
// 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,
-80, -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);
}

