Mirko Pavleski
Published © GPL3+

DIY Arduino VFO with AD9850 & TM1638 Pure Sine Wave Signal

He AD9850-based VFO provides a clean, sine-wave alternative to square-wave oscillators, ensuring your HF receiver remains free from interfer

BeginnerFull instructions provided3 hours139
DIY Arduino VFO with AD9850 & TM1638 Pure Sine Wave Signal

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
×1
TM1638 Module
×1
Rotary Encoder with Push-Button
Rotary Encoder with Push-Button
×1
AD9850 DDS 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 VFO

..

Schematic Amp.

..

Code

Code

C/C++
...
// by mircemk Mar.2026

#include <TM1638lite.h>

TM1638lite module(4, 3, 2); 

#define W_CLK 8
#define FQ_UD 9
#define DATA 10
#define RESET 11
#define ENC_A A0
#define ENC_B A1
#define ENC_SW A2 // Копчето на енкодерот

unsigned long freq = 7000000; 
unsigned long steps[] = {10, 100, 1000, 10000, 100000, 1000000};
int stepIndex = 2; 
const char* modes[] = {"A       ", "USB     ", "LSB     "};
int modeIndex = 0;
long offset = 0; 
bool offsetActive = false;

// Brightness контроли
int brightnessPercent = 50; // Почетна на 50%
bool brightnessMode = false;

unsigned long tempDisplayTimer = 0;
bool showingTemp = false;

const byte segmentMap[] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F };

void setup() {
  pinMode(W_CLK, OUTPUT); pinMode(FQ_UD, OUTPUT);
  pinMode(DATA, OUTPUT);  pinMode(RESET, OUTPUT);
  pinMode(ENC_A, INPUT_PULLUP); 
  pinMode(ENC_B, INPUT_PULLUP);
  pinMode(ENC_SW, INPUT_PULLUP);
  
  ad9850_reset();
  updateDDS(freq + offset);
  setBrightnessLevel(brightnessPercent);
  updateMainDisplay();
}

void loop() {
  // Читање на копчето на енкодерот (A3)
  if (digitalRead(ENC_SW) == LOW) {
    brightnessMode = !brightnessMode;
    if (brightnessMode) showTempMsg("BRIGHT  ");
    else updateMainDisplay();
    delay(300); // Debounce
  }

  uint8_t buttons = module.readButtons();

  // Додека сме во Brightness Mode, другите тастери се деактивирани
  if (!brightnessMode) {
    if (buttons == 1) { 
      stepIndex = (stepIndex + 1) % 6;
      char buffer[9];
      sprintf(buffer, "S%7ld", steps[stepIndex]); 
      showTempMsg(buffer);
      while(module.readButtons() == 1); 
    }
    if (buttons == 2) {
      modeIndex = (modeIndex + 1) % 3;
      showTempMsg(modes[modeIndex]);
      while(module.readButtons() == 2);
    }
    if (buttons == 4)   setBand(1800000, 2); 
    if (buttons == 8)   setBand(3500000, 3); 
    if (buttons == 16)  setBand(7000000, 4); 
    if (buttons == 32)  setBand(14000000, 5); 
    if (buttons == 64)  setBand(21000000, 6); 

    if (buttons == 128) {
      offsetActive = !offsetActive;
      offset = offsetActive ? 455000 : 0;
      showTempMsg(offsetActive ? "_455_   " : "OFF     ");
      module.setLED(7, offsetActive ? 1 : 0); 
      updateDDS(freq + offset);
      while(module.readButtons() == 128);
    }
  }

  // Енкодер логика (за Фреквенција или за Brightness)
  static int lastA = HIGH;
  int currentA = digitalRead(ENC_A);
  if (currentA != lastA && currentA == LOW) {
    if (brightnessMode) {
      // Менување на осветленост (чекор по 10%)
      if (digitalRead(ENC_B) == LOW) brightnessPercent -= 10;
      else brightnessPercent += 10;
      brightnessPercent = constrain(brightnessPercent, 0, 100);
      
      setBrightnessLevel(brightnessPercent);
      char brBuf[9];
      sprintf(brBuf, "BR  %3d ", brightnessPercent);
      module.displayText(brBuf);
    } else {
      // Стандардна фреквенција
      if (digitalRead(ENC_B) == LOW) freq -= steps[stepIndex];
      else freq += steps[stepIndex];
      freq = constrain(freq, 0, 49999999);
      updateDDS(freq + offset);
      if (!showingTemp) updateMainDisplay();
    }
  }
  lastA = currentA;

  if (showingTemp && (millis() > tempDisplayTimer) && !brightnessMode) {
    showingTemp = false;
    updateMainDisplay();
  }
}

void showTempMsg(const char* msg) {
  module.displayText(msg);
  tempDisplayTimer = millis() + 1000;
  showingTemp = true;
}

void setBrightnessLevel(int percent) {
  // TM1638 користи 0x88 (минимум) до 0x8F (максимум) за осветленост
  byte level = map(percent, 0, 100, 0, 7);
  digitalWrite(4, LOW); // STB
  shiftOut(2, 3, LSBFIRST, 0x88 + level); 
  digitalWrite(4, HIGH); // STB
}

void updateMainDisplay() {
  unsigned long tempFreq = freq;
  for (int i = 7; i >= 0; i--) {
    byte segs = 0;
    if (tempFreq > 0 || i == 7) { 
      segs = segmentMap[tempFreq % 10];
      tempFreq /= 10;
    } else {
      segs = 0x00; 
    }
    if (i == 1 || i == 4) segs |= 0x80; 
    sendRawData(i, segs);
  }
}

void sendRawData(byte position, byte data) {
  digitalWrite(4, LOW);
  shiftOut(2, 3, LSBFIRST, 0xC0 + (position * 2));
  shiftOut(2, 3, LSBFIRST, data);
  digitalWrite(4, HIGH);
}

void setBand(unsigned long f, int ledIndex) {
  freq = f;
  for (int i = 0; i < 7; i++) module.setLED(i, 0); 
  module.setLED(ledIndex, 1); 
  updateDDS(freq + offset);
  updateMainDisplay();
}

void ad9850_reset() {
  digitalWrite(W_CLK, LOW); digitalWrite(FQ_UD, LOW);
  digitalWrite(RESET, LOW); digitalWrite(RESET, HIGH); digitalWrite(RESET, LOW);
}

void updateDDS(unsigned long f) {
  unsigned long tuning_word = f * 4294967296ULL / 125000000;
  for (int i = 0; i < 32; i++) {
    digitalWrite(DATA, tuning_word & 0x01);
    digitalWrite(W_CLK, HIGH); digitalWrite(W_CLK, LOW);
    tuning_word >>= 1;
  }
  for (int i = 0; i < 8; i++) {
    digitalWrite(DATA, LOW);
    digitalWrite(W_CLK, HIGH); digitalWrite(W_CLK, LOW);
  }
  digitalWrite(FQ_UD, HIGH); digitalWrite(FQ_UD, LOW);
}

Credits

Mirko Pavleski
218 projects • 1578 followers

Comments