Looking a little like a VFO (4)
Ok, it looks like a LOT like a VFO now...
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.)
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.
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.
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 DDS } void 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). */