Mirko Pavleski
Published © GPL3+

Build simple Retro Style VFO (Variable frequency oscillator)

Easy-to-build VFO (Variable Frequency Oscillator) that features a clear, touch-enabled circular display with retro-style virtual scales.

BeginnerFull instructions provided2 hours223
Build simple Retro Style VFO (Variable frequency oscillator)

Things used in this project

Hardware components

Elecrow CrowPanel 1.28inch-HMI ESP32 Rotary Display 240*240
×1
SI5351 Clock Generator module
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Schematics

Schematic

...

Code

Code

C/C++
...
/*
  CrowPanel 1.28" (ESP32-S3 + GC9A01, TFT_eSPI)
  Outer dial: glued labels, reversed order, −30 offset; world-grid ticks; smooth big-step tween.
  Inner dial: world-grid ticks + labels every 20th minor (60°) showing MHz with one decimal,
              computed from the actual frequency at that angle (matches the generator).
    by mircemk  November 2025
*/

#include <Arduino.h>
#include <TFT_eSPI.h>
#include <SPI.h>
#include <Wire.h>
#include <si5351.h>
#include "CST816D.h"
//#include <driver/ledc.h>

#include <Adafruit_NeoPixel.h>

#define LED_PIN 48
#define LED_COUNT 5
#define LED_BRIGHTNESS 0

Adafruit_NeoPixel ring(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
int ledPos = 0;         // current LED index

#define UPPER_DIR (-1)   // +1 = left→right increasing; -1 = reversed

// --- Display power / init ---
#define USE_PANEL_ENABLE_PINS  1
#define PIN_LCD_PWR_EN1   1
#define PIN_LCD_PWR_EN2   2
#define PIN_TFT_BL        46
#define PIN_TFT_RST       14
#define BL_CHANNEL        0
#define BL_FREQ           2000
#define BL_RES_BITS       8
#define FLASH_TIME_MS 200

// --- CrowPanel Touch Pins ---
#define TP_I2C_SDA_PIN 6
#define TP_I2C_SCL_PIN 7
#define TP_RST 13
#define TP_INT 5

// --- Create Touch Object ---
CST816D touch(TP_I2C_SDA_PIN, TP_I2C_SCL_PIN, TP_RST, TP_INT);

TFT_eSPI tft;
static void panelPowerOn(){ if(USE_PANEL_ENABLE_PINS){ pinMode(PIN_LCD_PWR_EN1,OUTPUT); pinMode(PIN_LCD_PWR_EN2,OUTPUT); digitalWrite(PIN_LCD_PWR_EN1,HIGH); digitalWrite(PIN_LCD_PWR_EN2,HIGH); delay(5);} }
static void pulseResetPin(){ pinMode(PIN_TFT_RST,OUTPUT); digitalWrite(PIN_TFT_RST,HIGH); delay(5); digitalWrite(PIN_TFT_RST,LOW); delay(10); digitalWrite(PIN_TFT_RST,HIGH); delay(20); }
static void backlightInit(uint8_t duty){ pinMode(PIN_TFT_BL,OUTPUT); ledcSetup(BL_CHANNEL,BL_FREQ,BL_RES_BITS); ledcAttachPin(PIN_TFT_BL,BL_CHANNEL); ledcWrite(BL_CHANNEL,duty); }

// --- IO pins ---
#define ENC_A 45
#define ENC_B 42
#define ENC_BTN 41
#define SI5351_SDA 38
#define SI5351_SCL 39

uint32_t colors[] = {
  ring.Color(0, 0, 255),     // blue
  ring.Color(0, 255, 255),   // cyan
  ring.Color(255, 0, 255),   // magenta
  ring.Color(255, 255, 0)    // yellow
};


// --- VFO / Si5351 ---
static const uint64_t FREQ_MIN=10000ULL, FREQ_MAX=160000000ULL, FREQ_INIT=10100000ULL;
static const int32_t SI5351_CORR_PPM=0;
uint32_t stepLadder[]={10,100,1000,10000,100000,1000000};
uint8_t  stepIndex=2;    // 1 kHz
uint64_t vfoHz=FREQ_INIT;
Si5351  si5351;

// ---------- Bands ----------
struct BandInfo {
  const char* name;
  uint64_t startFreq;
  const char* wavelength;
};

BandInfo bands[] = {
  {"LW",   148500ULL,   "2010 m"},
  {"MW",   520000ULL,   "577 m"},
  {"SW 1", 1800000ULL,  "160 m"},
  {"SW 2", 3500000ULL,  "80 m"},
  {"SW 3", 5000000ULL,  "60 m"},
  {"SW 4", 7000000ULL,  "40 m"},
  {"SW 5", 10100000ULL, "30 m"},
  {"SW 6", 14000000ULL, "20 m"},
  {"SW 7", 18000000ULL, "17 m"},
  {"SW 8", 21000000ULL, "15 m"},
  {"SW 9", 24800000ULL, "12 m"},
  {"SW10", 28000000ULL, "10 m"},
  {"FM 1", 88000000ULL, "4 m"},
  {"FM 2", 144000000ULL,"2 m"}
};

int currentBand = 4;   // start from SW 1

const int NUM_BANDS = sizeof(bands)/sizeof(bands[0]);


// Compute band boundaries (end freq = next start - 1 Hz)
uint64_t bandEndFreq(int idx) {
  if (idx >= NUM_BANDS - 1) return FREQ_MAX;
  return bands[idx + 1].startFreq - 1ULL;
}

// --- UI / geometry ---
const int TOP_H=140, TOP_Y=0;
int16_t CX=120, CY=120;
TFT_eSprite spriteTop(&tft);

// radii
int16_t R_OUT_A=111, R_OUT_B=96;  // outer dial
int16_t R_IN_A = 70,  R_IN_B =62; // inner dial

// semicircle window (+ your -70° shift)
float ANG0=-120.0f, ANG1=+120.0f;
#define WINDOW_SHIFT_DEG (-70.0f)

// tick grid
const float TICK_MAJOR_STEP=30.0f; // majors each 30° (10 minors)
const float TICK_MINOR_STEP=3.0f;  // minors each 3°
const int   INNER_LABEL_EVERY_MINORS = 20; // label every 20 minors = 60°

// lengths & thickness
const int MINOR_LEN=4, MID_LEN=7, MAJOR_LEN=11;
const int THICK_MINOR=1, THICK_MID=2, THICK_MAJOR=3;

// mapping
#define OUTER_DEG_PER_HZ (0.003f)   // 30° = 10 kHz → 3° = 1 kHz
#define INNER_RATIO      (0.10f)    // inner tape 10× slower (same dir)

// colors
#define COL_OUTER  TFT_CYAN
#define COL_INNER  TFT_GREEN
#define COL_CENTER TFT_RED
#define COL_LABEL  TFT_WHITE
#define FRAME_COL  TFT_DARKGREY

// frame
#define FRAME_R 118
#define FRAME_THICK 3
#define FRAME_POST_LEN 6

// encoder state
int8_t encQuart=0; uint32_t lastBtnMs=0;
volatile bool tweening=false;

// ---------- helpers ----------
static inline float d2r(float d){ return d*PI/180.0f; }
static inline float wrap360(float a){ a=fmodf(a,360.0f); if(a<0) a+=360.0f; return a; }
static inline bool inWindow(float ang,float V0,float V1){ float span=V1-V0; float rel=wrap360(ang-V0); return rel<=span; }
static inline int posmod(int a,int m){ int r=a%m; return (r<0)? r+m : r; }

String formatKHzEU_2dec(uint64_t hz){
  uint64_t khz_hundredths=(hz+5ULL)/10ULL;
  uint32_t frac2=(uint32_t)(khz_hundredths%100ULL);
  uint64_t khz_int=khz_hundredths/100ULL;
  char tmp[32]; snprintf(tmp,sizeof(tmp),"%llu",(unsigned long long)khz_int);
  String sInt(tmp), sSep; int n=sInt.length();
  for(int i=0;i<n;i++){ sSep+=sInt[i]; int left=n-i-1; if(left>0 && (left%3)==0) sSep+='.'; }
  char buf[48]; snprintf(buf,sizeof(buf),"%s,%02u", sSep.c_str(), frac2);
  return String(buf);
}


void drawTickThick(TFT_eSprite* s,float angDeg,int16_t rOuter,int16_t rInner,uint16_t col,int thickness){
  float ang=d2r(angDeg); float nx=-sinf(ang), ny=cosf(ang);
  for(int k=-(thickness/2); k<= (thickness/2); k++){
    float offx=nx*k, offy=ny*k;
    int16_t x1=CX + rOuter*cosf(ang) + offx;
    int16_t y1=CY + rOuter*sinf(ang) + offy;
    int16_t x2=CX + rInner*cosf(ang) + offx;
    int16_t y2=CY + rInner*sinf(ang) + offy;
    s->drawLine(x1,y1,x2,y2,col);
  }
}

void drawTextAtAngle(TFT_eSprite* s,const String& txt,float ang,int16_t r,uint16_t col){
  int16_t x=CX + r*cosf(d2r(ang));
  int16_t y=CY + r*sinf(d2r(ang));
  s->setTextDatum(MC_DATUM);
  s->setTextColor(col,TFT_BLACK);
  s->setTextFont(2);
  s->drawString(txt,x,y);
}

void drawThickArcSprite(int16_t r,float V0,float V1,uint16_t col,int thickness){
  for(int t=-(thickness/2); t<= (thickness/2); t++){
    float step=2.0f;
    for(float a=V0; a<=V1; a+=step){
      int16_t x1=CX+(r+t)*cosf(d2r(a)), y1=CY+(r+t)*sinf(d2r(a));
      float an=(a+step>V1)?V1:a+step;
      int16_t x2=CX+(r+t)*cosf(d2r(an)), y2=CY+(r+t)*sinf(d2r(an));
      spriteTop.drawLine(x1,y1,x2,y2,FRAME_COL);
    }
  }
}

// ---------- TOP (sprite) ----------
void drawTopScalesSprite(uint64_t hz){
  const float V0=ANG0+WINDOW_SHIFT_DEG, V1=ANG1+WINDOW_SHIFT_DEG;

  // world “tapes” (angles)
  const float tapeOut = (float)hz * OUTER_DEG_PER_HZ;              // deg
  const float tapeIn  = (float)hz * OUTER_DEG_PER_HZ * INNER_RATIO;// deg

  spriteTop.fillSprite(TFT_BLACK);

  // ================= OUTER ticks =================
  int nMin=(int)floorf((V0 - tapeOut)/TICK_MINOR_STEP) - 2;
  int nMax=(int)ceilf ((V1 - tapeOut)/TICK_MINOR_STEP) + 2;
  for(int n=nMin; n<=nMax; n++){
    float ang=n*TICK_MINOR_STEP + tapeOut;
    if(!inWindow(ang,V0,V1)) continue;
    bool isMajor = (n % 10 == 0);
    bool mid     = (!isMajor) && (n % 5 == 0);
    if(isMajor) continue; // majors drawn below
    drawTickThick(&spriteTop, ang, R_OUT_A,
                  R_OUT_A - (mid?MID_LEN:MINOR_LEN),
                  COL_OUTER, (mid?THICK_MID:THICK_MINOR));
  }

  int mMin=(int)floorf((V0 - tapeOut)/TICK_MAJOR_STEP) - 1;
  int mMax=(int)ceilf ((V1 - tapeOut)/TICK_MAJOR_STEP) + 1;
  for(int m=mMin; m<=mMax; m++){
    float ang = m*TICK_MAJOR_STEP + tapeOut;
    if(!inWindow(ang,V0,V1)) continue;
    drawTickThick(&spriteTop, ang, R_OUT_A, R_OUT_A - MAJOR_LEN, COL_OUTER, THICK_MAJOR);

    // glued label; reversed; −30
    int tsel = (UPPER_DIR > 0) ? m : -m;
    int tens = posmod(tsel,10);
    tens = posmod(tens - 3, 10);
    char up[4]; snprintf(up, sizeof(up), "%d0", tens);
    drawTextAtAngle(&spriteTop, up, ang, R_OUT_B - 12, COL_LABEL);
  }

  // ================= INNER ticks =================
  int niMin=(int)floorf((V0 - tapeIn)/TICK_MINOR_STEP) - 2;
  int niMax=(int)ceilf ((V1 - tapeIn)/TICK_MINOR_STEP) + 2;
  for(int n=niMin; n<=niMax; n++){
    float ang=n*TICK_MINOR_STEP + tapeIn;
    if(!inWindow(ang,V0,V1)) continue;
    bool isMajor = (n % 10 == 0);
    bool mid     = (!isMajor) && (n % 5 == 0);
    drawTickThick(&spriteTop, ang, R_IN_A,
                  R_IN_A - (isMajor?MAJOR_LEN:(mid?MID_LEN:MINOR_LEN)),
                  COL_INNER, (isMajor?THICK_MAJOR:(mid?THICK_MID:THICK_MINOR)));
  }

  // --------- NEW: INNER labels every 20 minors (60°), matching actual frequency ---------
  // For angles ai = k*60° + tapeIn (k integer), compute frequency at that angle:
  // f_at = hz + (ai - aCenter) / (OUTER_DEG_PER_HZ*INNER_RATIO)
  const float STEP_60 = TICK_MINOR_STEP * INNER_LABEL_EVERY_MINORS; // 60°
  const float aCenter = (V0 + V1) * 0.5f;
  int kMin = (int)floorf((V0 - tapeIn)/STEP_60) - 1;
  int kMax = (int)ceilf ((V1 - tapeIn)/STEP_60) + 1;

  for(int k=kMin; k<=kMax; k++){
    float ai = k*STEP_60 + tapeIn;
    if(!inWindow(ai, V0, V1)) continue;

    // frequency at this angular position (round to 0.1 MHz)
    float deltaDeg = ai - aCenter;
    float deltaHz  = -(deltaDeg) / (OUTER_DEG_PER_HZ * INNER_RATIO);
    int64_t f_at = (int64_t)hz + (int64_t)lroundf(deltaHz) - 100000LL;

    if (f_at < 0) f_at = 0;
    if (f_at > 160000000LL) f_at = 160000000LL;

   int64_t tenthsMHz = (f_at + 50000LL) / 100000LL;
char lo[14];
snprintf(lo, sizeof(lo), "%lld.%01lld",
         (long long)(tenthsMHz / 10),
         (long long)(tenthsMHz % 10));
drawTextAtAngle(&spriteTop, String(lo), ai, R_IN_B - 15, COL_LABEL);
  }

  // ================= Frame + red line =================
  // arc
  for (int t=-(FRAME_THICK/2); t<= (FRAME_THICK/2); t++){
    float step=2.0f;
    for(float a=V0; a<=V1; a+=step){
      int16_t x1=CX+(FRAME_R+t)*cosf(d2r(a)), y1=CY+(FRAME_R+t)*sinf(d2r(a));
      float an=(a+step>V1)?V1:a+step;
      int16_t x2=CX+(FRAME_R+t)*cosf(d2r(an)), y2=CY+(FRAME_R+t)*sinf(d2r(an));
      spriteTop.drawLine(x1,y1,x2,y2,FRAME_COL);
    }
  }
  const int16_t xL=CX+FRAME_R*cosf(d2r(V0)), yL=CY+FRAME_R*sinf(d2r(V0));
  const int16_t xR=CX+FRAME_R*cosf(d2r(V1)), yR=CY+FRAME_R*sinf(d2r(V1));
  int16_t HLINE_Y = (((yL+yR)/2) - 1); if(HLINE_Y>(TOP_H-2)) HLINE_Y=TOP_H-2;
  for(int t=-(FRAME_THICK/2); t<= (FRAME_THICK/2); t++) spriteTop.drawLine(5, HLINE_Y+t, 235, HLINE_Y+t, FRAME_COL);
  spriteTop.drawLine(xL, HLINE_Y, xL, HLINE_Y - FRAME_POST_LEN, FRAME_COL);
  spriteTop.drawLine(xR, HLINE_Y, xR, HLINE_Y - FRAME_POST_LEN, FRAME_COL);

  // red center line
  const int16_t RED_TOP_Y=CY-105, RED_BOT_Y=HLINE_Y;
  for(int t=-1; t<=1; t++) spriteTop.drawLine(CX+t, RED_BOT_Y, CX+t, RED_TOP_Y, COL_CENTER);

  spriteTop.pushSprite(0, TOP_Y);
}

// ---------- bottom readout ----------
String formatKHzEU_2dec(uint64_t); // already defined above

void drawFreqBox(uint64_t hz, uint32_t step) {
  // --- smaller frame, moved up ---
  int bx = 25;   // reduced left margin (was 25)
  int by = 162;  // 5 px below STEP label (was 180)
  int bw = 190;  // narrower box (was 190)
  int bh = 36;
  int br = 6;

  // frame
  tft.drawRoundRect(bx, by, bw, bh, br, TFT_WHITE);
  tft.fillRoundRect(bx + 2, by + 2, bw - 4, bh - 4, br, TFT_BLACK);

  // --- frequency number ---
  tft.setTextDatum(ML_DATUM);
  tft.setTextColor(TFT_CYAN, TFT_BLACK);
  tft.setTextFont(4);

  String freqStr;
  String unitStr;

  char buf[32];

  if (hz < 1000000ULL) {
    // Below 1 MHz → show in kHz, two decimals
    double kHz = hz / 1000.0;
    snprintf(buf, sizeof(buf), "%.2f", kHz);
    freqStr = String(buf);
    unitStr = "KHz";
  } else if (hz < 100000000ULL) {
    // 1–99.999 MHz → show full kHz precision (like 11.880,00 MHz)
    double MHz = hz / 1000000.0;
    uint32_t whole = (uint32_t)MHz;
    uint32_t frac  = (uint32_t)((MHz - whole) * 1000000.0 + 0.5); // Hz remainder

    // Format as ###.###,## (European style)
    snprintf(buf, sizeof(buf), "%lu.%03lu,%02lu",
             (unsigned long)whole,
             (unsigned long)(frac / 1000),
             (unsigned long)((frac / 10) % 100));
    freqStr = String(buf);
    unitStr = "MHz";
  } else {
    // ≥100 MHz → show simplified "M" unit
    double MHz = hz / 1000000.0;
    snprintf(buf, sizeof(buf), "%.2f", MHz);
    freqStr = String(buf);
    unitStr = "MHz";
  }

  int textY = by + 2 + bh / 2;

  // frequency number
  tft.drawString(freqStr, bx + 8, textY);

  // --- unit label ---
  tft.setTextFont(4);
  tft.setTextColor(TFT_CYAN, TFT_BLACK);
  tft.setTextDatum(ML_DATUM);

  // place close to number (shift left ~15 px)
  tft.drawString(unitStr, bx + bw - 60, textY);

  // --- STEP label under gray frame line ---
  int stepY = 150;   // adjust: ~5 px below the gray horizontal line
  int stepX = 120;   // centered

  // clear old area
  tft.fillRect(60, stepY - 10, 120, 22, TFT_BLACK);

  // decide text
  char stepStr[12];
  if      (step == 10)       strcpy(stepStr, "10 Hz");
  else if (step == 100)      strcpy(stepStr, "100 Hz");
  else if (step == 1000)     strcpy(stepStr, "1 KHz");
  else if (step == 10000)    strcpy(stepStr, "10 KHz");
  else if (step == 100000)   strcpy(stepStr, "100 KHz");
  else if (step == 1000000)  strcpy(stepStr, "1 MHz");
  else                       snprintf(stepStr, sizeof(stepStr), "%lu Hz", (unsigned long)step);

  tft.setTextDatum(MC_DATUM);
  tft.setTextFont(2);
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString(String("STEP: ") + stepStr, stepX, stepY);
}

void drawBottomFrameOnce(){ drawFreqBox(vfoHz, stepLadder[stepIndex]); }

// ---------- encoder ----------
int8_t readEncoderTransition(){ static int last=0; int a=digitalRead(ENC_A), b=digitalRead(ENC_B); int val=(a<<1)|b;
  static const int8_t trans[16]={0,-1,+1,0,+1,0,0,-1,-1,0,0,+1,0,+1,-1,0};
  int8_t d=trans[(last<<2)|val]; last=val; return d; }
int8_t readEncoderDetent(){ if(tweening) return 0;
  int8_t t=readEncoderTransition(); if(t){ encQuart+=t; if(encQuart>=4){encQuart=0; return +1;} if(encQuart<=-4){encQuart=0; return -1;} } return 0; }

// ---------- tween (unchanged from your good version) ----------
static inline uint32_t tweenSubstep(uint32_t stepHz){
  if (stepHz >= 1000000)return 100000; // 1 MHz → 10×100 kHz
  if (stepHz >= 100000) return 10000; // 100 kHz → 10×10 kHz
  if (stepHz >= 10000)  return 1000;  // 10 kHz  → 10×1 kHz
  return stepHz;
}

void updateBandFromFreq() {
  // check which band the current frequency belongs to
  for (int i = 0; i < NUM_BANDS; i++) {
    uint64_t start = bands[i].startFreq;
    uint64_t end   = bandEndFreq(i);

    if (vfoHz >= start && vfoHz <= end) {
      if (currentBand != i) {
        bool up = (i > currentBand);          // remember direction
        currentBand = i;

        drawBandInfo(stepLadder[stepIndex]);  // redraw SW x / STEP / m info
        flashButton(up ? 1 : 0);              // flash B+ if up, B– if down
      }
      break;
    }
  }
}



void applyTuningAndRender(int8_t clicks) {
  if (clicks == 0) return;

  uint32_t stepHz = stepLadder[stepIndex];
  int64_t delta   = (int64_t)clicks * (int64_t)stepHz;
  int64_t next    = (int64_t)vfoHz + delta;
  if (next < (int64_t)FREQ_MIN) next = FREQ_MIN;
  if (next > (int64_t)FREQ_MAX) next = FREQ_MAX;

  uint64_t from = vfoHz;
  uint64_t to   = (uint64_t)next;
  uint32_t sub  = tweenSubstep(stepHz);

  if (sub < stepHz) {
    tweening = true;
    int dir = (to > from) ? +1 : -1;
    uint64_t cur = from;
    while (cur != to) {
      uint64_t nextStep = (dir > 0) ? (cur + sub) : (cur >= sub ? cur - sub : 0);
      if ((dir > 0 && nextStep > to) || (dir < 0 && nextStep < to)) nextStep = to;
      drawTopScalesSprite(nextStep);
      cur = nextStep;
      yield();
    }
    vfoHz = to;
  } else {
    vfoHz = to;
  }

  si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0);
  drawTopScalesSprite(vfoHz);
  drawFreqBox(vfoHz, stepHz);
  updateBandFromFreq();      // <-- new call here
  tweening = false;
}



// ---------- Si5351 ----------
bool siInit(){ Wire.begin(SI5351_SDA, SI5351_SCL, 400000);
  if(!si5351.init(SI5351_CRYSTAL_LOAD_8PF,0,SI5351_CORR_PPM)) return false;
  si5351.output_enable(SI5351_CLK0,1); si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); return true; }

void drawBandInfo(uint32_t step) {
  int y = 150;   // same baseline as STEP label
  tft.fillRect(10, y - 10, 220, 22, TFT_BLACK);

  tft.setTextFont(2);
  tft.setTextDatum(MC_DATUM);

  // Left part – band name (red)
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString(bands[currentBand].name, 35, y);

  // Middle part – STEP label (yellow)
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  char stepStr[12];
  if      (step == 10)       strcpy(stepStr, "10 Hz");
  else if (step == 100)      strcpy(stepStr, "100 Hz");
  else if (step == 1000)     strcpy(stepStr, "1 KHz");
  else if (step == 10000)    strcpy(stepStr, "10 KHz");
  else if (step == 100000)   strcpy(stepStr, "100 KHz");
  else if (step == 1000000)  strcpy(stepStr, "1 MHz");
  else                       sprintf(stepStr, "%lu Hz", (unsigned long)step);
  tft.drawString(String("STEP: ") + stepStr, 120, y);

  // Right part – wavelength (red)
  tft.setTextColor(TFT_YELLOW, TFT_BLACK);
  tft.drawString(bands[currentBand].wavelength, 200, y);
}


// ---------- setup / loop ----------
void setup(){
  Serial.begin(115200); delay(100);

ring.begin();
ring.setBrightness(LED_BRIGHTNESS);   // soft brightness
ring.clear();
ring.show();

  
  panelPowerOn(); backlightInit(0); pulseResetPin();
  tft.init(); tft.setRotation(0);tft.setRotation(0);

// --- Turn on backlight gradually ---
for (int d = 0; d <= 200; d += 10) {
  ledcWrite(BL_CHANNEL, d);
  delay(10);
}

// --- Splash / Intro Screen (before UI setup) ---
tft.fillScreen(TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.setTextFont(4);
tft.setTextColor(TFT_YELLOW, TFT_BLACK);
tft.drawString("Retro Style", 120, 90);

tft.setTextColor(TFT_CYAN, TFT_BLACK);
tft.drawString("VFO", 120, 120);

tft.setTextFont(2);
tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
tft.drawString("by mircemk", 120, 150);
delay(2000);
tft.fillScreen(TFT_BLACK);

// --- Now create sprites and draw the main UI ---
spriteTop.setColorDepth(16);
spriteTop.createSprite(240, TOP_H);
spriteTop.setTextDatum(MC_DATUM);

pinMode(ENC_A, INPUT_PULLUP);
pinMode(ENC_B, INPUT_PULLUP);
pinMode(ENC_BTN, INPUT_PULLUP);

tft.fillScreen(TFT_BLACK);
drawTopScalesSprite(vfoHz);
drawBottomFrameOnce();
drawTouchButtons();

if (siInit()) si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0);

// --- Initialize and show the correct band immediately ---
updateBandFromFreq();
drawBandInfo(stepLadder[stepIndex]);

  if(siInit()) si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0);
  // --- Initialize band display based on starting frequency ---
updateBandFromFreq();                        // detect which band 10.100 MHz belongs to
drawBandInfo(stepLadder[stepIndex]);         // draw SW5 30 m info immediately

  // --- Enable main panel power (required for touch rail) ---
pinMode(1, OUTPUT); digitalWrite(1, HIGH);
pinMode(2, OUTPUT); digitalWrite(2, HIGH);
delay(20);

// --- Reset and start the touch controller ---
pinMode(TP_RST, OUTPUT);
digitalWrite(TP_RST, LOW);
delay(10);
digitalWrite(TP_RST, HIGH);
delay(50);

Wire.begin(TP_I2C_SDA_PIN, TP_I2C_SCL_PIN);
touch.begin();

Serial.println("Touch initialized (CrowPanel 1.28)");

}

void drawTouchButtons() {
  // Button geometry
  int btnW = 80, btnH = 36;
  int btnY = 205;                 // bottom area
  int btnLeftX  = 35;             // left button
  int btnRightX = 123;            // right button

  uint16_t btnColor = tft.color565(255, 140, 0);  // orange

  // --- LEFT  (-1) ---
  tft.fillRoundRect(btnLeftX, btnY, btnW, btnH, 6, btnColor);
  tft.drawRoundRect(btnLeftX, btnY, btnW, btnH, 6, TFT_WHITE);
  tft.setTextDatum(MC_DATUM);
  tft.setTextFont(4);
  tft.setTextColor(TFT_WHITE, btnColor);
  tft.drawString("-B", btnLeftX + btnW/2 +10, btnY + btnH/2);

  // --- RIGHT (+1) ---
  tft.fillRoundRect(btnRightX, btnY, btnW, btnH, 6, btnColor);
  tft.drawRoundRect(btnRightX, btnY, btnW, btnH, 6, TFT_WHITE);
  tft.setTextDatum(MC_DATUM);
  tft.setTextFont(4);
  tft.setTextColor(TFT_WHITE, btnColor);
  tft.drawString("+B", btnRightX + btnW/2 - 10, btnY + btnH/2);
}

void flashButton(int btn) {
  // btn: 0 = B– , 1 = B+
  int btnX = (btn == 0) ? 35 : 123;
  int btnY = 205;
  int btnW = 80, btnH = 36;

  // Flash red for 80 ms then return to orange
  uint16_t red    = tft.color565(255, 0, 0);
  uint16_t orange = tft.color565(255, 140, 0);

  // show red
  tft.fillRoundRect(btnX, btnY, btnW, btnH, 6, red);
  tft.drawRoundRect(btnX, btnY, btnW, btnH, 6, TFT_WHITE);
  tft.setTextDatum(MC_DATUM);
  tft.setTextFont(4);
  tft.setTextColor(TFT_WHITE, red);
  tft.drawString((btn == 0) ? "-B" : "+B",
                 btnX + (btn == 0 ? 50 : 30), btnY + btnH / 2);
  delay(FLASH_TIME_MS);

  // back to orange
  tft.fillRoundRect(btnX, btnY, btnW, btnH, 6, orange);
  tft.drawRoundRect(btnX, btnY, btnW, btnH, 6, TFT_WHITE);
  tft.setTextColor(TFT_WHITE, orange);
  tft.drawString((btn == 0) ? "-B" : "+B",
                 btnX + (btn == 0 ? 50 : 30), btnY + btnH / 2);
}


// direction >0 → clockwise (right turn), direction <0 → counterclockwise
void updateLedRing(int direction) {
  // reverse rotation logic so it matches encoder
  if (direction > 0) ledPos = (ledPos - 1 + LED_COUNT) % LED_COUNT;
  else if (direction < 0) ledPos = (ledPos + 1) % LED_COUNT;

  // draw single glowing yellow LED
  ring.clear();
  ring.setPixelColor(ledPos, ring.Color(255, 180, 0));  // warm yellow
  ring.show();
}

void loop() {
  int8_t det = readEncoderDetent();
  if (det) applyTuningAndRender(det);
  updateLedRing(det);

  uint32_t now = millis();
  bool pressed = (digitalRead(ENC_BTN) == LOW);
  if (!tweening && pressed && (now - lastBtnMs) > 250) {
    lastBtnMs = now;
    stepIndex = (stepIndex + 1) % (sizeof(stepLadder) / sizeof(stepLadder[0]));
    drawFreqBox(vfoHz, stepLadder[stepIndex]);
  }

  // --- Touch reading block (runs always) ---
  uint16_t x, y;
  uint8_t gesture;
  static uint32_t lastTouchMs = 0;

  if (millis() - lastTouchMs > 30) {  // poll every 30 ms
    lastTouchMs = millis();
    bool touched = touch.getTouch(&x, &y, &gesture);
    if (touched) {
      Serial.printf("Touch: X=%u  Y=%u  Gesture=0x%02X\n", x, y, gesture);
if (y > 205 && y < 245) {                     // bottom strip only
  if (x >= 25 && x <= 115) {                  // left button
    flashButton(0);                           // fade red→orange
    if (currentBand > 0) currentBand--;
  }
  else if (x >= 125 && x <= 215) {            // right button
    flashButton(1);
    if (currentBand < NUM_BANDS - 1) currentBand++;
  }

  vfoHz = bands[currentBand].startFreq;
  si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0);
  drawTopScalesSprite(vfoHz);
  drawFreqBox(vfoHz, stepLadder[stepIndex]);
  drawBandInfo(stepLadder[stepIndex]);
      }
    }
  }

  delay(5);  // watchdog-friendly pause
}

Libraries

C/C++
..
No preview (download only).

Credits

Mirko Pavleski
201 projects • 1508 followers

Comments