Looking a little like a VFO (4)

Ok, it looks like a LOT like a VFO now...

Arduino Nano + I2C 20x4 LCD + rotary encoder + AD9851 

On Friday I spent about four hours tweaking the code. I had a couple of bugs that I wanted to shake out before moving the next phase of the project.

Saturday was spent hitting one of the local radio club breakfasts, then the monthly club meeting, followed by running errands.  I spent some time Saturday evening looking at some old code that I had from Peter VK2TPM and Jeff KO7M.  I was trying to decide how I wanted to handle reading out the serial bits to the DDS. (I could understand Peter's code pretty well.  Jeff's code is more efficent but harder for a noob like me to understand.) 

Sunday morning was spent running errands (groceries). By the time I got home was feeling pretty guilty about my lack of progress (Sat & Sun morning) so I decided to get back to it.  It took me about an hour to move some code fragments from my 2012 project into my new project. I also changed many of the variables and object names to be consistent with the the current project. 

Surprisingly it worked pretty much right away when I fired it up. After the initial test I moved around one block of code to minimize how often I update the DDS. (If the dial doesn't move, the DDS does not need to be updated which saves a LOT of clock cycles for other future things.)

AD9851 with a 7MHz signal on the spectrum analyzer. (5MHz horizontal steps)

AD9851 with a 7MHz signal on the spectrum analyzer. (5MHz horizontal steps)

The next step was to put it on the spectrum analyzer, oscope, frequency counter, and oscope to look at the signal.  The picture of the spectrum analyzer shows a 7.040MHz signal on the left with 5MHz divisions out to 45MHz on the right side of the screen. The harmonics are 40-50dB (or more) down from the fundamental frequency.

AD9851 on the oscope at 7.040MHz

AD9851 on the oscope at 7.040MHz

The picture on the oscope was pretty boring. It looked pretty good overall. One open question might be some better bypass filtering related to ground. But since this is on a breadboard with flying leads I am pretty happy with it as a prototype.

My counter(s) do not have a 10MHz reference so I take their exact accuracy with a grain of salt. Typically I don't need 1-10Hz level accuracy so they are close enough. In this case I do need an accurate reference if I want to adjust the true timing of the 30MHz clock on the DDS board.  It is not overly critical unless you want to operate modes like WSPR or QRSS which need to be accurate to within a couple of Hz. My initial testing was indicating that I was +/- about 60Hz with my non-referenced gear.

RF measured power levels

My next experiment was to measure the power of the DDS over a range of frequencies. It looks like it does not have much gain from 1.8 - 5MHz. From 6 - 26MHz it is running 4 - 6dBm or 2.5 - 4mW of power. From 26 - 30MHz it is running about 2dBm or 2mW. That is quite a bit of power from the DDS and is a nice starting point for real RF projects.

I am pretty pleased with the project. I figure that I have about 12 hours or so into the project. Most of that is simply because I don't know the language and I am having to look up the syntax as I go. This is going to be starting point for a HF rig. Clearly this is just one building block out of several that I will need in the future but this is the building block that I have been contemplating for a while. 

The code below is not exciting or even highly optimized at this point but it is fully functional. Since I have leveraged the internet for a lot of my learning I thought that I should share my code in case someone else needs help getting starting. (Thanks to folks like Jeff,  Eldon, Jason, and Peter that have helped me in various ways with my projects.) The sketch below is about 8k and should provide a fully functioning signal generator between 1.8 - 30MHz.

73 de NG0R

/* 
   
   This sketch will setup an Arduino (Nano in my example)
   with the an I2C 20x4 LCD and a AD9851 DDS module (from ebay)
   and a rotary encoder.
   
   The encoder can be spun from 1.8MHz to 30MHz. 
   When you push the encoder push button it will allow to
   change the decade position of the number being changed/tuned.
   
   This code is heavily commented so that you can try to 
   understand what is going on.
   
   This tries to avoid the use of the delay command since that is
   a blocking statement. Use of the millis function and variable
   checking as a timer are used instead if/when needed.
   
   The base sketch takes about 8k of memory.
   
   This could be easily used a the builing block for a HF radio.
    
   -------------------
   
   DDS AD9850/AD9850 info from:
   Mike Bowthorpe
   http://www.ladyada.net/rant/2007/02/cotw-ltc6903/ 
   http://www.geocities.com/leon_heller/dds.html
   Function for sending the byte word by Peter Marks http://marxy.org

       
   
   ---Encoder info---
   https://forum.sparkfun.com/viewtopic.php?p=65052

   http://hifiduino.files.wordpress.com/2010/10/rotarynodebounce.jpg

   read a rotary encoder with interrupts
   Encoder hooked up with common to GROUND,
   Encoder Pin A to pin 2, 
   Encoder Pin B to pin 4 (or pin 3 see below)
   
   uses Arduino pullups on A & B channel outputs
   turning on the pullups saves having to hook up resistors 
   to the A & B channel outputs
   
   
   ---I2C LCD INFO---
   http://arduino-info.wikispaces.com/LCD-Blue-I2C
   YWROBOT
   LCD header pins/cable color code
   VCC = Red         -- 5v
   GND = Black       -- Gnd   
   SDA = Yellow      -- A4
   SCL = Green/White -- A5

*/ 

//---Libraries
#include <Wire.h>                                       // I2C library
#include <LiquidCrystal_I2C.h>                          // I2C LCD library
LiquidCrystal_I2C lcd(0x27,20,4);                       // set the LCD address to 0x27 for a 20 chars and 4 line display

//--Define constants
#define encoder0PinA  2                        // Setup the encoder pins
#define encoder0PinB  4                        // Setup the encoder pins
#define encoderbtn 6                           // encoder pushbutton
#define DDS_CLOCK 180000000                    // 30MHz x 6 (onboard rock)


//---define variables
volatile long encoder0Pos = 10000000;          // setup a value to count with
volatile long oldencoder0Pos = encoder0Pos ;   // used to compare
int varVal;                                    // variable for reading the pin status
int varHz = 1;                                 // 1=Hz, 2=KHz, 3=10KHz, 4=100KHz, 5=MHz tuning step mode
int long varMult = 1;                          // used to plug in as the multiplier
char varBuf1[8];                               // used to convert an int to a string array for the freq display
char varBuf2[10];                              // used to format the freq display with commas      
unsigned long varCurrentMillis;                // current time via the Millis function
long varPreviousMillis = 0;                    // used as a timer to avoid bounce
long varInterval = 500;                        // interval used with a timer (milliseconds)
byte ddsLOAD = 8;                              // AD9851 LOAD   Arduino D8
byte ddsCLOCK = 9;                             // AD9851 CLOCK  Arduino D9
byte ddsDATA = 10;                             // AD9851 FQ_UD  Ardunio D10
long varTuning_word;                           // Used to hold the word for the DDS 



void setup() { 
//---Setup encoder  
  pinMode(encoderbtn, INPUT);
  pinMode(encoder0PinA, INPUT); 
  digitalWrite(encoder0PinA, HIGH);       // turn on pullup resistor
  pinMode(encoder0PinB, INPUT); 
  digitalWrite(encoder0PinB, HIGH);       // turn on pullup resistor
  attachInterrupt(0, doEncoder, CHANGE);  // encoder pin on interrupt 0 - pin 2
//---Initial screen setup   
  lcd.init();                             // initialize the lcd 
  lcd.backlight();                        // turn on the lcd backlight
  lcd.clear();                            // good practice to make sure that display is clear
  lcd.setCursor(0,0);                     // column,row
  lcd.print("Freq: "); 
  sprintf(varBuf1,"%8lu", encoder0Pos);   // convert initial freq for display
  sprintf(varBuf2,"%1c%1c,%1c%1c%1c,%1c%1c%1c", varBuf1[0], varBuf1[1], varBuf1[2], varBuf1[3], varBuf1[4], varBuf1[5], varBuf1[6], varBuf1[7], varBuf1[8]);
  lcd.print(varBuf2);
  lcd.setCursor(18,0);                    // column,row
  lcd.print("Hz");                        // prints out step; should variablize
//---setup pins for AD9851
  pinMode (ddsDATA, OUTPUT);              // sets pin 10 as OUPUT
  pinMode (ddsCLOCK, OUTPUT);             // sets pin 9 as OUTPUT
  pinMode (ddsLOAD, OUTPUT);              // sets pin 8 as OUTPUT
  sendFrequency(encoder0Pos);             // send the initial freq to the DDSvoid loop(){
  encoderStatus();                        // check for encoder button press 
  lcdStatus();                            // function for lcd updates
  if (encoder0Pos != oldencoder0Pos)      // only update the DDS if the freq has changed 
    sendFrequency(encoder0Pos);           // function to update the DDS
}



void sendFrequency(long encoder0Pos){                        // function to update the DDS
  varTuning_word = (encoder0Pos * pow(2, 32)) / DDS_CLOCK;   // set value for the DDS
  digitalWrite (ddsLOAD, LOW);                               // take load pin low
  //--start loop--
  for(int i = 0; i < 32; i++)                                // loop through the bits
  {
    if ((varTuning_word & 1) == 1)                           // test for binary 1
      outOne();                                              // function to send 1 serial
    else
      outZero();                                             // function to send 0 serial
    varTuning_word = varTuning_word >> 1;                    
  }
  //--end loop--
  byte_out(0x09);                                            // send the end command
  digitalWrite (ddsLOAD, HIGH);                              // take the load pin high
}




void byte_out(unsigned char byte)         // spin through a byte (8 bits)
{                                         // send it a bit a time
  int i;

  for (i = 0; i < 8; i++)
  {
    if ((byte & 1) == 1)
      outOne();
    else
      outZero();
    byte = byte >> 1;
  }
}




void outOne(){                            // send 1 to the DDS         
  digitalWrite(ddsCLOCK, LOW);            // set the ddsCLOCK pin low
  digitalWrite(ddsDATA, HIGH);            // set the ddsDATA pin high
  digitalWrite(ddsCLOCK, HIGH);           // set the ddsCLOCK pin HIGH
  digitalWrite(ddsDATA, LOW);             // set the ddsDATA ping low
}



void outZero(){                           // send 0 to the DDS
  digitalWrite(ddsCLOCK, LOW);            // set the ddsCLOCK pin low
  digitalWrite(ddsDATA, LOW);             // set the ddsDATA pin low
  digitalWrite(ddsCLOCK, HIGH);           // set the ddsCLOCK pin high
}



void encoderStatus (){                    // this is checking for the button press and setting the MHZ, KHz, Hz cursor
  varCurrentMillis = millis();            // set variable = time
  if(varCurrentMillis - varPreviousMillis > varInterval) {
     varPreviousMillis = varCurrentMillis;   
     varVal = digitalRead(encoderbtn);    // read input value and store it in val
     if (varVal == LOW) {                 // check if the button is pressed
        if (varHz == 6) {                 // 1 Hz, 2 100Hz, 3 KHz, 4 10KHz, 5 100KHz, 6 MHz 
        varHz = 0;                        // reset from MHz to Hz  
        }     
      varHz = varHz + 1;                  // move one position
     }
  }
}





void lcdStatus (){
    sprintf(varBuf1,"%8lu", encoder0Pos);        
    sprintf(varBuf2,"%1c%1c,%1c%1c%1c,%1c%1c%1c", varBuf1[0], varBuf1[1], varBuf1[2], varBuf1[3], varBuf1[4], varBuf1[5], varBuf1[6], varBuf1[7], varBuf1[8]);
    if (encoder0Pos != oldencoder0Pos){
      lcd.setCursor(6,0);                   // column,row
      lcd.print(varBuf2);                   // prints out the freq
    }
 
   switch (varHz) {
    case 1:                                 // if 1 move the cursor to Hz
      lcd.setCursor(18,0);                  // column,row
      lcd.print("H1");                      // prints out step
      varMult = 1;        
      break;
    case 2:                                 // if 2 move the cursor to 100 Hz
      lcd.setCursor(18,0);                  // column,row
      lcd.print("H2");                      // prints out step
      //varMult = 1000/2;                   // original encoder with detents
      varMult = 100;                        // AA0ZZ encoder, no detents
      break;
    case 3:                                 // if 2 move the cursor to KHz
      lcd.setCursor(18,0);                  // column,row
      lcd.print("K1");                      // prints out step
      //varMult = 1000/2;                   // original encoder with detents
      varMult = 1000;                       // AA0ZZ encoder, no detents
      break;
     case 4:                                // if 3 move the cursor to 10KHz
      lcd.setCursor(18,0);                  // column,row
      lcd.print("K2");                      // prints out step
      //varMult = 10000/2;                  // original encoder with detents
      varMult = 10000;                      // AA0ZZ encoder, no detents
      break;  
    case 5:                                 // if 4 move the cursor to 100KHz
      lcd.setCursor(18,0);                  // column,row
      lcd.print("K3");                      // prints out step
      //varMult = 100000/2;                 // original encoder with detents
      varMult = 100000;                     // AA0ZZ encoder, no detents
      break;  
    case 6:                                 // if 5 move the cursor to MHz
      lcd.setCursor(18,0);                  // column,row
      lcd.print("MH");                      // prints out step
      //varMult = 500000;                   // original encoder with detents 
      varMult = 1000000;                    // AA0ZZ encoder, no detents
      break;
    }
    if (encoder0Pos <= 1800000) {            // lower bounds limit
        encoder0Pos  = 1800000;
    }
    if (encoder0Pos >= 30000000) {           // upper bounds limit
        encoder0Pos  = 30000000;
    }
}





void doEncoder(){
 oldencoder0Pos = encoder0Pos;                // reset the variables so that we can compare them next time 
  if (digitalRead(encoder0PinA) == HIGH) {    // found a low-to-high on channel A
    if (digitalRead(encoder0PinB) == LOW) {   // check channel B to see which way
                                              // encoder is turning
      encoder0Pos = encoder0Pos - varMult;    // CCW
    } 
    else {
      encoder0Pos = encoder0Pos + varMult;    // CW
    }
  }
  else                                        // found a high-to-low on channel A
  { 
    if (digitalRead(encoder0PinB) == LOW) {   // check channel B to see which way
                                              // encoder is turning  
      encoder0Pos = encoder0Pos + varMult;    // CW
    } 
    else {
      encoder0Pos = encoder0Pos - varMult;    // CCW
    }
  }
 }

/*  to read the other two transitions - just use another attachInterrupt()
in the setup and duplicate the doEncoder function into say, 
doEncoderA and doEncoderB. 
You also need to move the other encoder wire over to pin 3 (interrupt 1). 
*/