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.
Devices & Components
1
Elecrow CrowPanel 1.28inch-HMI ESP32 Rotary Display 240*240
1
SI5351 CLOCK GEN MODULE
Hardware & Tools
1
Soldering Iron Kit
Software & Tools
Arduino IDE
Project description
Code
Code
cpp
...
1/* 2 CrowPanel 1.28" (ESP32-S3 + GC9A01, TFT_eSPI) 3 Outer dial: glued labels, reversed order, −30 offset; world-grid ticks; smooth big-step tween. 4 Inner dial: world-grid ticks + labels every 20th minor (60°) showing MHz with one decimal, 5 computed from the actual frequency at that angle (matches the generator). 6 by mircemk November 2025 7*/ 8 9#include <Arduino.h> 10#include <TFT_eSPI.h> 11#include <SPI.h> 12#include <Wire.h> 13#include <si5351.h> 14#include "CST816D.h" 15//#include <driver/ledc.h> 16 17#include <Adafruit_NeoPixel.h> 18 19#define LED_PIN 48 20#define LED_COUNT 5 21#define LED_BRIGHTNESS 0 22 23Adafruit_NeoPixel ring(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); 24int ledPos = 0; // current LED index 25 26#define UPPER_DIR (-1) // +1 = left→right increasing; -1 = reversed 27 28// --- Display power / init --- 29#define USE_PANEL_ENABLE_PINS 1 30#define PIN_LCD_PWR_EN1 1 31#define PIN_LCD_PWR_EN2 2 32#define PIN_TFT_BL 46 33#define PIN_TFT_RST 14 34#define BL_CHANNEL 0 35#define BL_FREQ 2000 36#define BL_RES_BITS 8 37#define FLASH_TIME_MS 200 38 39// --- CrowPanel Touch Pins --- 40#define TP_I2C_SDA_PIN 6 41#define TP_I2C_SCL_PIN 7 42#define TP_RST 13 43#define TP_INT 5 44 45// --- Create Touch Object --- 46CST816D touch(TP_I2C_SDA_PIN, TP_I2C_SCL_PIN, TP_RST, TP_INT); 47 48TFT_eSPI tft; 49static 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);} } 50static 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); } 51static 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); } 52 53// --- IO pins --- 54#define ENC_A 45 55#define ENC_B 42 56#define ENC_BTN 41 57#define SI5351_SDA 38 58#define SI5351_SCL 39 59 60uint32_t colors[] = { 61 ring.Color(0, 0, 255), // blue 62 ring.Color(0, 255, 255), // cyan 63 ring.Color(255, 0, 255), // magenta 64 ring.Color(255, 255, 0) // yellow 65}; 66 67 68// --- VFO / Si5351 --- 69static const uint64_t FREQ_MIN=10000ULL, FREQ_MAX=160000000ULL, FREQ_INIT=10100000ULL; 70static const int32_t SI5351_CORR_PPM=0; 71uint32_t stepLadder[]={10,100,1000,10000,100000,1000000}; 72uint8_t stepIndex=2; // 1 kHz 73uint64_t vfoHz=FREQ_INIT; 74Si5351 si5351; 75 76// ---------- Bands ---------- 77struct BandInfo { 78 const char* name; 79 uint64_t startFreq; 80 const char* wavelength; 81}; 82 83BandInfo bands[] = { 84 {"LW", 148500ULL, "2010 m"}, 85 {"MW", 520000ULL, "577 m"}, 86 {"SW 1", 1800000ULL, "160 m"}, 87 {"SW 2", 3500000ULL, "80 m"}, 88 {"SW 3", 5000000ULL, "60 m"}, 89 {"SW 4", 7000000ULL, "40 m"}, 90 {"SW 5", 10100000ULL, "30 m"}, 91 {"SW 6", 14000000ULL, "20 m"}, 92 {"SW 7", 18000000ULL, "17 m"}, 93 {"SW 8", 21000000ULL, "15 m"}, 94 {"SW 9", 24800000ULL, "12 m"}, 95 {"SW10", 28000000ULL, "10 m"}, 96 {"FM 1", 88000000ULL, "4 m"}, 97 {"FM 2", 144000000ULL,"2 m"} 98}; 99 100int currentBand = 4; // start from SW 1 101 102const int NUM_BANDS = sizeof(bands)/sizeof(bands[0]); 103 104 105// Compute band boundaries (end freq = next start - 1 Hz) 106uint64_t bandEndFreq(int idx) { 107 if (idx >= NUM_BANDS - 1) return FREQ_MAX; 108 return bands[idx + 1].startFreq - 1ULL; 109} 110 111// --- UI / geometry --- 112const int TOP_H=140, TOP_Y=0; 113int16_t CX=120, CY=120; 114TFT_eSprite spriteTop(&tft); 115 116// radii 117int16_t R_OUT_A=111, R_OUT_B=96; // outer dial 118int16_t R_IN_A = 70, R_IN_B =62; // inner dial 119 120// semicircle window (+ your -70° shift) 121float ANG0=-120.0f, ANG1=+120.0f; 122#define WINDOW_SHIFT_DEG (-70.0f) 123 124// tick grid 125const float TICK_MAJOR_STEP=30.0f; // majors each 30° (10 minors) 126const float TICK_MINOR_STEP=3.0f; // minors each 3° 127const int INNER_LABEL_EVERY_MINORS = 20; // label every 20 minors = 60° 128 129// lengths & thickness 130const int MINOR_LEN=4, MID_LEN=7, MAJOR_LEN=11; 131const int THICK_MINOR=1, THICK_MID=2, THICK_MAJOR=3; 132 133// mapping 134#define OUTER_DEG_PER_HZ (0.003f) // 30° = 10 kHz → 3° = 1 kHz 135#define INNER_RATIO (0.10f) // inner tape 10× slower (same dir) 136 137// colors 138#define COL_OUTER TFT_CYAN 139#define COL_INNER TFT_GREEN 140#define COL_CENTER TFT_RED 141#define COL_LABEL TFT_WHITE 142#define FRAME_COL TFT_DARKGREY 143 144// frame 145#define FRAME_R 118 146#define FRAME_THICK 3 147#define FRAME_POST_LEN 6 148 149// encoder state 150int8_t encQuart=0; uint32_t lastBtnMs=0; 151volatile bool tweening=false; 152 153// ---------- helpers ---------- 154static inline float d2r(float d){ return d*PI/180.0f; } 155static inline float wrap360(float a){ a=fmodf(a,360.0f); if(a<0) a+=360.0f; return a; } 156static inline bool inWindow(float ang,float V0,float V1){ float span=V1-V0; float rel=wrap360(ang-V0); return rel<=span; } 157static inline int posmod(int a,int m){ int r=a%m; return (r<0)? r+m : r; } 158 159String formatKHzEU_2dec(uint64_t hz){ 160 uint64_t khz_hundredths=(hz+5ULL)/10ULL; 161 uint32_t frac2=(uint32_t)(khz_hundredths%100ULL); 162 uint64_t khz_int=khz_hundredths/100ULL; 163 char tmp[32]; snprintf(tmp,sizeof(tmp),"%llu",(unsigned long long)khz_int); 164 String sInt(tmp), sSep; int n=sInt.length(); 165 for(int i=0;i<n;i++){ sSep+=sInt[i]; int left=n-i-1; if(left>0 && (left%3)==0) sSep+='.'; } 166 char buf[48]; snprintf(buf,sizeof(buf),"%s,%02u", sSep.c_str(), frac2); 167 return String(buf); 168} 169 170 171void drawTickThick(TFT_eSprite* s,float angDeg,int16_t rOuter,int16_t rInner,uint16_t col,int thickness){ 172 float ang=d2r(angDeg); float nx=-sinf(ang), ny=cosf(ang); 173 for(int k=-(thickness/2); k<= (thickness/2); k++){ 174 float offx=nx*k, offy=ny*k; 175 int16_t x1=CX + rOuter*cosf(ang) + offx; 176 int16_t y1=CY + rOuter*sinf(ang) + offy; 177 int16_t x2=CX + rInner*cosf(ang) + offx; 178 int16_t y2=CY + rInner*sinf(ang) + offy; 179 s->drawLine(x1,y1,x2,y2,col); 180 } 181} 182 183void drawTextAtAngle(TFT_eSprite* s,const String& txt,float ang,int16_t r,uint16_t col){ 184 int16_t x=CX + r*cosf(d2r(ang)); 185 int16_t y=CY + r*sinf(d2r(ang)); 186 s->setTextDatum(MC_DATUM); 187 s->setTextColor(col,TFT_BLACK); 188 s->setTextFont(2); 189 s->drawString(txt,x,y); 190} 191 192void drawThickArcSprite(int16_t r,float V0,float V1,uint16_t col,int thickness){ 193 for(int t=-(thickness/2); t<= (thickness/2); t++){ 194 float step=2.0f; 195 for(float a=V0; a<=V1; a+=step){ 196 int16_t x1=CX+(r+t)*cosf(d2r(a)), y1=CY+(r+t)*sinf(d2r(a)); 197 float an=(a+step>V1)?V1:a+step; 198 int16_t x2=CX+(r+t)*cosf(d2r(an)), y2=CY+(r+t)*sinf(d2r(an)); 199 spriteTop.drawLine(x1,y1,x2,y2,FRAME_COL); 200 } 201 } 202} 203 204// ---------- TOP (sprite) ---------- 205void drawTopScalesSprite(uint64_t hz){ 206 const float V0=ANG0+WINDOW_SHIFT_DEG, V1=ANG1+WINDOW_SHIFT_DEG; 207 208 // world “tapes” (angles) 209 const float tapeOut = (float)hz * OUTER_DEG_PER_HZ; // deg 210 const float tapeIn = (float)hz * OUTER_DEG_PER_HZ * INNER_RATIO;// deg 211 212 spriteTop.fillSprite(TFT_BLACK); 213 214 // ================= OUTER ticks ================= 215 int nMin=(int)floorf((V0 - tapeOut)/TICK_MINOR_STEP) - 2; 216 int nMax=(int)ceilf ((V1 - tapeOut)/TICK_MINOR_STEP) + 2; 217 for(int n=nMin; n<=nMax; n++){ 218 float ang=n*TICK_MINOR_STEP + tapeOut; 219 if(!inWindow(ang,V0,V1)) continue; 220 bool isMajor = (n % 10 == 0); 221 bool mid = (!isMajor) && (n % 5 == 0); 222 if(isMajor) continue; // majors drawn below 223 drawTickThick(&spriteTop, ang, R_OUT_A, 224 R_OUT_A - (mid?MID_LEN:MINOR_LEN), 225 COL_OUTER, (mid?THICK_MID:THICK_MINOR)); 226 } 227 228 int mMin=(int)floorf((V0 - tapeOut)/TICK_MAJOR_STEP) - 1; 229 int mMax=(int)ceilf ((V1 - tapeOut)/TICK_MAJOR_STEP) + 1; 230 for(int m=mMin; m<=mMax; m++){ 231 float ang = m*TICK_MAJOR_STEP + tapeOut; 232 if(!inWindow(ang,V0,V1)) continue; 233 drawTickThick(&spriteTop, ang, R_OUT_A, R_OUT_A - MAJOR_LEN, COL_OUTER, THICK_MAJOR); 234 235 // glued label; reversed; −30 236 int tsel = (UPPER_DIR > 0) ? m : -m; 237 int tens = posmod(tsel,10); 238 tens = posmod(tens - 3, 10); 239 char up[4]; snprintf(up, sizeof(up), "%d0", tens); 240 drawTextAtAngle(&spriteTop, up, ang, R_OUT_B - 12, COL_LABEL); 241 } 242 243 // ================= INNER ticks ================= 244 int niMin=(int)floorf((V0 - tapeIn)/TICK_MINOR_STEP) - 2; 245 int niMax=(int)ceilf ((V1 - tapeIn)/TICK_MINOR_STEP) + 2; 246 for(int n=niMin; n<=niMax; n++){ 247 float ang=n*TICK_MINOR_STEP + tapeIn; 248 if(!inWindow(ang,V0,V1)) continue; 249 bool isMajor = (n % 10 == 0); 250 bool mid = (!isMajor) && (n % 5 == 0); 251 drawTickThick(&spriteTop, ang, R_IN_A, 252 R_IN_A - (isMajor?MAJOR_LEN:(mid?MID_LEN:MINOR_LEN)), 253 COL_INNER, (isMajor?THICK_MAJOR:(mid?THICK_MID:THICK_MINOR))); 254 } 255 256 // --------- NEW: INNER labels every 20 minors (60°), matching actual frequency --------- 257 // For angles ai = k*60° + tapeIn (k integer), compute frequency at that angle: 258 // f_at = hz + (ai - aCenter) / (OUTER_DEG_PER_HZ*INNER_RATIO) 259 const float STEP_60 = TICK_MINOR_STEP * INNER_LABEL_EVERY_MINORS; // 60° 260 const float aCenter = (V0 + V1) * 0.5f; 261 int kMin = (int)floorf((V0 - tapeIn)/STEP_60) - 1; 262 int kMax = (int)ceilf ((V1 - tapeIn)/STEP_60) + 1; 263 264 for(int k=kMin; k<=kMax; k++){ 265 float ai = k*STEP_60 + tapeIn; 266 if(!inWindow(ai, V0, V1)) continue; 267 268 // frequency at this angular position (round to 0.1 MHz) 269 float deltaDeg = ai - aCenter; 270 float deltaHz = -(deltaDeg) / (OUTER_DEG_PER_HZ * INNER_RATIO); 271 int64_t f_at = (int64_t)hz + (int64_t)lroundf(deltaHz) - 100000LL; 272 273 if (f_at < 0) f_at = 0; 274 if (f_at > 160000000LL) f_at = 160000000LL; 275 276 int64_t tenthsMHz = (f_at + 50000LL) / 100000LL; 277char lo[14]; 278snprintf(lo, sizeof(lo), "%lld.%01lld", 279 (long long)(tenthsMHz / 10), 280 (long long)(tenthsMHz % 10)); 281drawTextAtAngle(&spriteTop, String(lo), ai, R_IN_B - 15, COL_LABEL); 282 } 283 284 // ================= Frame + red line ================= 285 // arc 286 for (int t=-(FRAME_THICK/2); t<= (FRAME_THICK/2); t++){ 287 float step=2.0f; 288 for(float a=V0; a<=V1; a+=step){ 289 int16_t x1=CX+(FRAME_R+t)*cosf(d2r(a)), y1=CY+(FRAME_R+t)*sinf(d2r(a)); 290 float an=(a+step>V1)?V1:a+step; 291 int16_t x2=CX+(FRAME_R+t)*cosf(d2r(an)), y2=CY+(FRAME_R+t)*sinf(d2r(an)); 292 spriteTop.drawLine(x1,y1,x2,y2,FRAME_COL); 293 } 294 } 295 const int16_t xL=CX+FRAME_R*cosf(d2r(V0)), yL=CY+FRAME_R*sinf(d2r(V0)); 296 const int16_t xR=CX+FRAME_R*cosf(d2r(V1)), yR=CY+FRAME_R*sinf(d2r(V1)); 297 int16_t HLINE_Y = (((yL+yR)/2) - 1); if(HLINE_Y>(TOP_H-2)) HLINE_Y=TOP_H-2; 298 for(int t=-(FRAME_THICK/2); t<= (FRAME_THICK/2); t++) spriteTop.drawLine(5, HLINE_Y+t, 235, HLINE_Y+t, FRAME_COL); 299 spriteTop.drawLine(xL, HLINE_Y, xL, HLINE_Y - FRAME_POST_LEN, FRAME_COL); 300 spriteTop.drawLine(xR, HLINE_Y, xR, HLINE_Y - FRAME_POST_LEN, FRAME_COL); 301 302 // red center line 303 const int16_t RED_TOP_Y=CY-105, RED_BOT_Y=HLINE_Y; 304 for(int t=-1; t<=1; t++) spriteTop.drawLine(CX+t, RED_BOT_Y, CX+t, RED_TOP_Y, COL_CENTER); 305 306 spriteTop.pushSprite(0, TOP_Y); 307} 308 309// ---------- bottom readout ---------- 310String formatKHzEU_2dec(uint64_t); // already defined above 311 312void drawFreqBox(uint64_t hz, uint32_t step) { 313 // --- smaller frame, moved up --- 314 int bx = 25; // reduced left margin (was 25) 315 int by = 162; // 5 px below STEP label (was 180) 316 int bw = 190; // narrower box (was 190) 317 int bh = 36; 318 int br = 6; 319 320 // frame 321 tft.drawRoundRect(bx, by, bw, bh, br, TFT_WHITE); 322 tft.fillRoundRect(bx + 2, by + 2, bw - 4, bh - 4, br, TFT_BLACK); 323 324 // --- frequency number --- 325 tft.setTextDatum(ML_DATUM); 326 tft.setTextColor(TFT_CYAN, TFT_BLACK); 327 tft.setTextFont(4); 328 329 String freqStr; 330 String unitStr; 331 332 char buf[32]; 333 334 if (hz < 1000000ULL) { 335 // Below 1 MHz → show in kHz, two decimals 336 double kHz = hz / 1000.0; 337 snprintf(buf, sizeof(buf), "%.2f", kHz); 338 freqStr = String(buf); 339 unitStr = "KHz"; 340 } else if (hz < 100000000ULL) { 341 // 1–99.999 MHz → show full kHz precision (like 11.880,00 MHz) 342 double MHz = hz / 1000000.0; 343 uint32_t whole = (uint32_t)MHz; 344 uint32_t frac = (uint32_t)((MHz - whole) * 1000000.0 + 0.5); // Hz remainder 345 346 // Format as ###.###,## (European style) 347 snprintf(buf, sizeof(buf), "%lu.%03lu,%02lu", 348 (unsigned long)whole, 349 (unsigned long)(frac / 1000), 350 (unsigned long)((frac / 10) % 100)); 351 freqStr = String(buf); 352 unitStr = "MHz"; 353 } else { 354 // ≥100 MHz → show simplified "M" unit 355 double MHz = hz / 1000000.0; 356 snprintf(buf, sizeof(buf), "%.2f", MHz); 357 freqStr = String(buf); 358 unitStr = "MHz"; 359 } 360 361 int textY = by + 2 + bh / 2; 362 363 // frequency number 364 tft.drawString(freqStr, bx + 8, textY); 365 366 // --- unit label --- 367 tft.setTextFont(4); 368 tft.setTextColor(TFT_CYAN, TFT_BLACK); 369 tft.setTextDatum(ML_DATUM); 370 371 // place close to number (shift left ~15 px) 372 tft.drawString(unitStr, bx + bw - 60, textY); 373 374 // --- STEP label under gray frame line --- 375 int stepY = 150; // adjust: ~5 px below the gray horizontal line 376 int stepX = 120; // centered 377 378 // clear old area 379 tft.fillRect(60, stepY - 10, 120, 22, TFT_BLACK); 380 381 // decide text 382 char stepStr[12]; 383 if (step == 10) strcpy(stepStr, "10 Hz"); 384 else if (step == 100) strcpy(stepStr, "100 Hz"); 385 else if (step == 1000) strcpy(stepStr, "1 KHz"); 386 else if (step == 10000) strcpy(stepStr, "10 KHz"); 387 else if (step == 100000) strcpy(stepStr, "100 KHz"); 388 else if (step == 1000000) strcpy(stepStr, "1 MHz"); 389 else snprintf(stepStr, sizeof(stepStr), "%lu Hz", (unsigned long)step); 390 391 tft.setTextDatum(MC_DATUM); 392 tft.setTextFont(2); 393 tft.setTextColor(TFT_YELLOW, TFT_BLACK); 394 tft.drawString(String("STEP: ") + stepStr, stepX, stepY); 395} 396 397void drawBottomFrameOnce(){ drawFreqBox(vfoHz, stepLadder[stepIndex]); } 398 399// ---------- encoder ---------- 400int8_t readEncoderTransition(){ static int last=0; int a=digitalRead(ENC_A), b=digitalRead(ENC_B); int val=(a<<1)|b; 401 static const int8_t trans[16]={0,-1,+1,0,+1,0,0,-1,-1,0,0,+1,0,+1,-1,0}; 402 int8_t d=trans[(last<<2)|val]; last=val; return d; } 403int8_t readEncoderDetent(){ if(tweening) return 0; 404 int8_t t=readEncoderTransition(); if(t){ encQuart+=t; if(encQuart>=4){encQuart=0; return +1;} if(encQuart<=-4){encQuart=0; return -1;} } return 0; } 405 406// ---------- tween (unchanged from your good version) ---------- 407static inline uint32_t tweenSubstep(uint32_t stepHz){ 408 if (stepHz >= 1000000)return 100000; // 1 MHz → 10×100 kHz 409 if (stepHz >= 100000) return 10000; // 100 kHz → 10×10 kHz 410 if (stepHz >= 10000) return 1000; // 10 kHz → 10×1 kHz 411 return stepHz; 412} 413 414void updateBandFromFreq() { 415 // check which band the current frequency belongs to 416 for (int i = 0; i < NUM_BANDS; i++) { 417 uint64_t start = bands[i].startFreq; 418 uint64_t end = bandEndFreq(i); 419 420 if (vfoHz >= start && vfoHz <= end) { 421 if (currentBand != i) { 422 bool up = (i > currentBand); // remember direction 423 currentBand = i; 424 425 drawBandInfo(stepLadder[stepIndex]); // redraw SW x / STEP / m info 426 flashButton(up ? 1 : 0); // flash B+ if up, B– if down 427 } 428 break; 429 } 430 } 431} 432 433 434 435void applyTuningAndRender(int8_t clicks) { 436 if (clicks == 0) return; 437 438 uint32_t stepHz = stepLadder[stepIndex]; 439 int64_t delta = (int64_t)clicks * (int64_t)stepHz; 440 int64_t next = (int64_t)vfoHz + delta; 441 if (next < (int64_t)FREQ_MIN) next = FREQ_MIN; 442 if (next > (int64_t)FREQ_MAX) next = FREQ_MAX; 443 444 uint64_t from = vfoHz; 445 uint64_t to = (uint64_t)next; 446 uint32_t sub = tweenSubstep(stepHz); 447 448 if (sub < stepHz) { 449 tweening = true; 450 int dir = (to > from) ? +1 : -1; 451 uint64_t cur = from; 452 while (cur != to) { 453 uint64_t nextStep = (dir > 0) ? (cur + sub) : (cur >= sub ? cur - sub : 0); 454 if ((dir > 0 && nextStep > to) || (dir < 0 && nextStep < to)) nextStep = to; 455 drawTopScalesSprite(nextStep); 456 cur = nextStep; 457 yield(); 458 } 459 vfoHz = to; 460 } else { 461 vfoHz = to; 462 } 463 464 si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0); 465 drawTopScalesSprite(vfoHz); 466 drawFreqBox(vfoHz, stepHz); 467 updateBandFromFreq(); // <-- new call here 468 tweening = false; 469} 470 471 472 473// ---------- Si5351 ---------- 474bool siInit(){ Wire.begin(SI5351_SDA, SI5351_SCL, 400000); 475 if(!si5351.init(SI5351_CRYSTAL_LOAD_8PF,0,SI5351_CORR_PPM)) return false; 476 si5351.output_enable(SI5351_CLK0,1); si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA); return true; } 477 478void drawBandInfo(uint32_t step) { 479 int y = 150; // same baseline as STEP label 480 tft.fillRect(10, y - 10, 220, 22, TFT_BLACK); 481 482 tft.setTextFont(2); 483 tft.setTextDatum(MC_DATUM); 484 485 // Left part – band name (red) 486 tft.setTextColor(TFT_YELLOW, TFT_BLACK); 487 tft.drawString(bands[currentBand].name, 35, y); 488 489 // Middle part – STEP label (yellow) 490 tft.setTextColor(TFT_YELLOW, TFT_BLACK); 491 char stepStr[12]; 492 if (step == 10) strcpy(stepStr, "10 Hz"); 493 else if (step == 100) strcpy(stepStr, "100 Hz"); 494 else if (step == 1000) strcpy(stepStr, "1 KHz"); 495 else if (step == 10000) strcpy(stepStr, "10 KHz"); 496 else if (step == 100000) strcpy(stepStr, "100 KHz"); 497 else if (step == 1000000) strcpy(stepStr, "1 MHz"); 498 else sprintf(stepStr, "%lu Hz", (unsigned long)step); 499 tft.drawString(String("STEP: ") + stepStr, 120, y); 500 501 // Right part – wavelength (red) 502 tft.setTextColor(TFT_YELLOW, TFT_BLACK); 503 tft.drawString(bands[currentBand].wavelength, 200, y); 504} 505 506 507// ---------- setup / loop ---------- 508void setup(){ 509 Serial.begin(115200); delay(100); 510 511ring.begin(); 512ring.setBrightness(LED_BRIGHTNESS); // soft brightness 513ring.clear(); 514ring.show(); 515 516 517 panelPowerOn(); backlightInit(0); pulseResetPin(); 518 tft.init(); tft.setRotation(0);tft.setRotation(0); 519 520// --- Turn on backlight gradually --- 521for (int d = 0; d <= 200; d += 10) { 522 ledcWrite(BL_CHANNEL, d); 523 delay(10); 524} 525 526// --- Splash / Intro Screen (before UI setup) --- 527tft.fillScreen(TFT_BLACK); 528tft.setTextDatum(MC_DATUM); 529tft.setTextFont(4); 530tft.setTextColor(TFT_YELLOW, TFT_BLACK); 531tft.drawString("Retro Style", 120, 90); 532 533tft.setTextColor(TFT_CYAN, TFT_BLACK); 534tft.drawString("VFO", 120, 120); 535 536tft.setTextFont(2); 537tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK); 538tft.drawString("by mircemk", 120, 150); 539delay(2000); 540tft.fillScreen(TFT_BLACK); 541 542// --- Now create sprites and draw the main UI --- 543spriteTop.setColorDepth(16); 544spriteTop.createSprite(240, TOP_H); 545spriteTop.setTextDatum(MC_DATUM); 546 547pinMode(ENC_A, INPUT_PULLUP); 548pinMode(ENC_B, INPUT_PULLUP); 549pinMode(ENC_BTN, INPUT_PULLUP); 550 551tft.fillScreen(TFT_BLACK); 552drawTopScalesSprite(vfoHz); 553drawBottomFrameOnce(); 554drawTouchButtons(); 555 556if (siInit()) si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0); 557 558// --- Initialize and show the correct band immediately --- 559updateBandFromFreq(); 560drawBandInfo(stepLadder[stepIndex]); 561 562 if(siInit()) si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0); 563 // --- Initialize band display based on starting frequency --- 564updateBandFromFreq(); // detect which band 10.100 MHz belongs to 565drawBandInfo(stepLadder[stepIndex]); // draw SW5 30 m info immediately 566 567 // --- Enable main panel power (required for touch rail) --- 568pinMode(1, OUTPUT); digitalWrite(1, HIGH); 569pinMode(2, OUTPUT); digitalWrite(2, HIGH); 570delay(20); 571 572// --- Reset and start the touch controller --- 573pinMode(TP_RST, OUTPUT); 574digitalWrite(TP_RST, LOW); 575delay(10); 576digitalWrite(TP_RST, HIGH); 577delay(50); 578 579Wire.begin(TP_I2C_SDA_PIN, TP_I2C_SCL_PIN); 580touch.begin(); 581 582Serial.println("Touch initialized (CrowPanel 1.28)"); 583 584} 585 586void drawTouchButtons() { 587 // Button geometry 588 int btnW = 80, btnH = 36; 589 int btnY = 205; // bottom area 590 int btnLeftX = 35; // left button 591 int btnRightX = 123; // right button 592 593 uint16_t btnColor = tft.color565(255, 140, 0); // orange 594 595 // --- LEFT (-1) --- 596 tft.fillRoundRect(btnLeftX, btnY, btnW, btnH, 6, btnColor); 597 tft.drawRoundRect(btnLeftX, btnY, btnW, btnH, 6, TFT_WHITE); 598 tft.setTextDatum(MC_DATUM); 599 tft.setTextFont(4); 600 tft.setTextColor(TFT_WHITE, btnColor); 601 tft.drawString("-B", btnLeftX + btnW/2 +10, btnY + btnH/2); 602 603 // --- RIGHT (+1) --- 604 tft.fillRoundRect(btnRightX, btnY, btnW, btnH, 6, btnColor); 605 tft.drawRoundRect(btnRightX, btnY, btnW, btnH, 6, TFT_WHITE); 606 tft.setTextDatum(MC_DATUM); 607 tft.setTextFont(4); 608 tft.setTextColor(TFT_WHITE, btnColor); 609 tft.drawString("+B", btnRightX + btnW/2 - 10, btnY + btnH/2); 610} 611 612void flashButton(int btn) { 613 // btn: 0 = B– , 1 = B+ 614 int btnX = (btn == 0) ? 35 : 123; 615 int btnY = 205; 616 int btnW = 80, btnH = 36; 617 618 // Flash red for 80 ms then return to orange 619 uint16_t red = tft.color565(255, 0, 0); 620 uint16_t orange = tft.color565(255, 140, 0); 621 622 // show red 623 tft.fillRoundRect(btnX, btnY, btnW, btnH, 6, red); 624 tft.drawRoundRect(btnX, btnY, btnW, btnH, 6, TFT_WHITE); 625 tft.setTextDatum(MC_DATUM); 626 tft.setTextFont(4); 627 tft.setTextColor(TFT_WHITE, red); 628 tft.drawString((btn == 0) ? "-B" : "+B", 629 btnX + (btn == 0 ? 50 : 30), btnY + btnH / 2); 630 delay(FLASH_TIME_MS); 631 632 // back to orange 633 tft.fillRoundRect(btnX, btnY, btnW, btnH, 6, orange); 634 tft.drawRoundRect(btnX, btnY, btnW, btnH, 6, TFT_WHITE); 635 tft.setTextColor(TFT_WHITE, orange); 636 tft.drawString((btn == 0) ? "-B" : "+B", 637 btnX + (btn == 0 ? 50 : 30), btnY + btnH / 2); 638} 639 640 641// direction >0 → clockwise (right turn), direction <0 → counterclockwise 642void updateLedRing(int direction) { 643 // reverse rotation logic so it matches encoder 644 if (direction > 0) ledPos = (ledPos - 1 + LED_COUNT) % LED_COUNT; 645 else if (direction < 0) ledPos = (ledPos + 1) % LED_COUNT; 646 647 // draw single glowing yellow LED 648 ring.clear(); 649 ring.setPixelColor(ledPos, ring.Color(255, 180, 0)); // warm yellow 650 ring.show(); 651} 652 653void loop() { 654 int8_t det = readEncoderDetent(); 655 if (det) applyTuningAndRender(det); 656 updateLedRing(det); 657 658 uint32_t now = millis(); 659 bool pressed = (digitalRead(ENC_BTN) == LOW); 660 if (!tweening && pressed && (now - lastBtnMs) > 250) { 661 lastBtnMs = now; 662 stepIndex = (stepIndex + 1) % (sizeof(stepLadder) / sizeof(stepLadder[0])); 663 drawFreqBox(vfoHz, stepLadder[stepIndex]); 664 } 665 666 // --- Touch reading block (runs always) --- 667 uint16_t x, y; 668 uint8_t gesture; 669 static uint32_t lastTouchMs = 0; 670 671 if (millis() - lastTouchMs > 30) { // poll every 30 ms 672 lastTouchMs = millis(); 673 bool touched = touch.getTouch(&x, &y, &gesture); 674 if (touched) { 675 Serial.printf("Touch: X=%u Y=%u Gesture=0x%02X\n", x, y, gesture); 676if (y > 205 && y < 245) { // bottom strip only 677 if (x >= 25 && x <= 115) { // left button 678 flashButton(0); // fade red→orange 679 if (currentBand > 0) currentBand--; 680 } 681 else if (x >= 125 && x <= 215) { // right button 682 flashButton(1); 683 if (currentBand < NUM_BANDS - 1) currentBand++; 684 } 685 686 vfoHz = bands[currentBand].startFreq; 687 si5351.set_freq(vfoHz * 100ULL, SI5351_CLK0); 688 drawTopScalesSprite(vfoHz); 689 drawFreqBox(vfoHz, stepLadder[stepIndex]); 690 drawBandInfo(stepLadder[stepIndex]); 691 } 692 } 693 } 694 695 delay(5); // watchdog-friendly pause 696}
Downloadable files
Libraries
...
Libraies All.zip
Documentation
Schematic
...
Schematic.jpg

Comments
Only logged in users can leave comments