Build simple Retro Style VFO (Variable frequency oscillator) with Crowoanel 1.28 inch Round Display

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

Today I received a shipment with a Small round LCD display from Elecrow. The device is packed in two boxes so that it is fully protected from damage during transportation. Inside there is a display, a USB cable for power and communication, as well as an additional cable for connecting an external module.
This is CrowPanel 1.28inch-HMI ESP32 Rotary Display 240*240 which has some really impressive features:
- high-performance ESP32-S3 chip
- WiFi and low-power Bluetooth
- capacitive touch screen with knob
- 5 WS2812 RGB Leds
- and the Rotary encoder in the form of a circular ring

It supports Arduino IDE, Lua RTOS, Home Assistant/PlatformIO/Micro Python, and LVGL library. When the device is turned on for the first time, a demo application is installed that presents several basic features on the display. Thanks to the high resolution, the image on the display is clear and beautifully visible from different angles.
This is my first time encountering this display, so I decided to use it to create a device that would demonstrate all of its features. Otherwise, as a passionate Shortwave listener, I was impressed by the original idea of ​​T Uebo from TJlab for making a VFO with virtual retro circular scales.

The circular shape of this display is ideal for making such a device, so I decided to make a similar but also usefu l and functional device based on the previously mentioned project. The fact that this display contains a built-in rotary encoder, buttons and a touch screen makes it even more usable for this purpose. Also very important is the fact that we only need to solder a few wires to make a fully functional VFO so that even a beginner in this field can easily make it.

So to make the device, we generally need only two components
- CrowPanel 1.28inch-HMI ESP32 Rotary Display 240*240
- and SI5351 Clock Generator module

Of course, if we want the device to be independent and universal, we need to install a 3.7V lithium battery, a switch and a suitable connector. The device should be installed in a small suitable box. It's useful to also include a small battery charger module in the box (I didn't have one at the time) that costs less than a dollar.

Let's see what the display looks like with all the information and functions. Immediately after switching on, we get a lot of information on the display. First of all, there is a window with two virtual scales that rotate in a ratio of 1 to 10. Both scales are marked in tenths. In the middle of the scale there is a vertical red line that indicates the exact frequency of the scales.

In the lower part of the display, is presented the generated frequency with large blue numbers. Immediately above this information, on the sides in yellow letters, is the Band and Wavelength, and in the middle is shown the step with which the scales move when turning therotary encoder. At the bottom are two buttons B+ and B- which are touch-sensitive and are used to change the band.
Now let's see how this device works in real conditions. The initial band and frequency are entered in the code and can be changed as desired. The default step of the scales movement is 1Khz. The entire screen represents a large button that I specifically used to change the step value. This value can be changed from 10Hz to 1MHz. The speed of rotation of the scale changes proportionally to the step. At every moment, the scale shows the exact generated frequency. At the bottom of the display are two buttons with which we can change the band. When touching the button, it briefly changes color to red to let us know that it is activated. At the same time, the frequency, band and scale change. I determined the width of each band to be maximum so that when one band ends, it immediately continues to the next. I did this so that we can continuously scan all frequencies. The transition from one band to another is signaled by a short flash of red on the corresponding button. Let me emphasize that the touch function only responds to the lower part of the screen where the buttons are drawn.

So as you can see, we have all the functions of this small display module. The rotary encoder responds very easily and precisely, and it is also robustly made in such a way that the specially processed inner edge of the ring (dial) activates two microswitches, which guarantees long-term operation. The image on the screen is extremely clear and readable from every angle, and the touch is of the capacitive type and responds immediately when the finger approaches the buttons.
As for the code, I made it in a way that allows you to change many parameters very easily.

It is important to emphasize that in order to compile the code without errors, you need to use ESP32 Core V 2.0.14 along with the provided libraries. I should say that this is the first version and I hope to make many more improvements in terms of visual and functional aspects in the future.
Next, let's trace the shape of the output signal on an oscilloscope. It is clear that it depends exclusively on the Si5351 module and to some extent on the library that drives it. The frequency range of the generated signal is impressive and can range from a few tens of kilohertz to over 160 Megahertz.

And finally a short conclusion. This is easy-to-build VFO (Variable Frequency Oscillator) that features a clear, touch-enabled circular display with retro-style virtual scales. The combination of the CrowPanel ESP32 display and the Si5351 module allows for a wide frequency range and precise control with minimal wiring.

CODE
/*
  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
}

icon Libraies All.zip 5.88MB Download(0)
License
All Rights
Reserved
licensBg
0