Pinguino based DDS VFO

DDS Signal Generator
The DDS VFO used as a bench signal generator

DDS image
100MHz OCXO, DDS and 33MHz LPF

pic board image
PIC Board with Maplin N27AZ LCD module

This is the support page for the Pinguino based DDS VFO project in RadCom magazine for March 2010. Details of the PIC PCB can be found here: USB PIC Board and in RadCom for November 2009. This project uses the PIC18F4550 with a 20MHz clock crystal and an Analog Devices AD9851 DDS with a refclock frequency of 100MHz.

The PIC18F4550 is currently running V2.12 of the Pinguino bootloader.

The PCB for the surface-mount DDS IC was first described in RadCom for October 2009. This is a double sided board, but only one side needs to be etched. The bottom layer is used as a ground plane.

DDS Layout
DDS PCB layout

The DDS PCB layout was made using PCB software on a Linux PC. the Layout is available in PNG (600dpi), Postscript and native PCB format here:

DDS schematic diagram
DDS schematic

DDS image

The PCB was made using the toner transfer method as described in RadCom for July 2006. As you can see in the photo, the board is not absolutely flawless, but it is quite adequate for this job.

The first stage in this project is getting the PIC to drive a HD44780 compatible 2x16 LCD display module. The display is initialised in 2 line mode using an 8 bit interface to the PIC. The display data lines are connected directly to PORTB 0-7 of the PIC. The display RS (register select) line is driven by PORTE1 (Pinguino #19) and the ENA (enable) line is driven by PORTE0 (Pinguino #18). As the display R/W (read/write) line is not used in this application, it is connected to ground. The schematic below shows the connections for the LCD module.

All of my previous PIC + LCD projects have used a 4 bit interface between the PIC and LCD module. Since this project uses a 40 pin PIC with an abundance of I/O pins, I have decided that I can afford the luxury of an 8 bit interface.

LCD connections
Pinguino + LCD
Right click & View Image

Before we can use the LCD module, it must be initialised in 8 bit mode using the following command sequence:
0x30, 0x30, 0x30, 0x3C, 0x0C, 0x01, 0x06

Modern LCD modules require a delay of more than 30ms after power-up before the first command or data is sent. Some older modules require a longer delay of 100ms. Sending a command to the display is a fairly simple matter. The RS line is pulled low to select the command rather than the data register. The command, in this case: 0x30 (00110000 binary) is placed on PORTB and an enable pulse is sent to the display by pulling the ENA pin first high and then low. To ensure that the enable pulse is longer than the minimum specification of 500ns, I used a 1 microsecond delay between the rising and falling edges of the pulse.

The first command is:

delay(100); //Delay for 100ms

This satisifies the requirement for a delay of greater than 30ms.

The next job is to pull the RS line low for command instead of data:

digitalWrite(RS,LOW); //RS command mode

Putting the command on PORTB is just a simple assignment:


Then generate the enable pulse:

digitalWrite(ENA,HIGH); //Enable line high
delayMicroseconds(1); // Delay
digitalWrite(ENA,LOW); //Enable line low
delay(1); // Delay 1ms

The 1ms delay after the falling edge of the enable pulse satisfies the LCD module requirement for a delay of greater than 39 microseconds before the next operation begins.

Sending data to the display is just as easy as sending a command. The following code will show "x" on the display.

digitalWrite(RS,HIGH); //RS data mode


digitalWrite(ENA,HIGH); //Enable line high
delayMicroseconds(1); // Delay
digitalWrite(ENA,LOW); //Enable line low
delay(1); // Delay 1ms

The code above has been expanded into a few user-defined functiones called:
command(); display(); display_string(); clear(); home(); line2();
These functions make the job of writing to the display trivially easy. The core part of the classic 'Hello World' program can be as simple as:

//Pinguino main loop
void loop()
lcdinit(); // Initialise LCD
display_string("Hello World!"); // Message
while(1){} // Wait forever

The source code for this program is here: hello.pde

LCD Photo
Winstar WH1602B LCD module

Next up: Connecting the PIC to the AD9851DDS.
The DDS has a simple serial interface which uses just three wires and the ground connection. The 40 bit frequency and control word is sent to the DDS one bit at a time via the SERIAL LOAD data pin (25). Each bit is clocked into the DDS by a serial clock pulse to pin 7 (W_CLK) of the DDS IC. The first 32 bits are the frequency word, the remaing remaing 8 bits control the output phase, 6x REFCLOCK multiplier and Power-Down functions. See the the AD9851 data sheet for full details. When all 40 bits have been clocked into the DDS, an update pulse is applied to the FQ_UD pin of the DDS (pin 8). Connections to the Pinguino are shown below.

DDS connections
DDS connections

The code for clocking the frequency word into the DDS is quite simple. The key part of the dds(freq); function is:

DDS code image

The for loop is executed 32 times, one time for each bit of the 32 bit frequency word. The conditional test for the if statement (freq&1) does a bitwise AND of the 32 bit binary frequency word and 1 binary. This effecively masks off the upper 31 bits, leaving only the least significant bit (LSB). If this bit is 1, the condition is TRUE and the serial data line is pulled high, if it is 0, the condition is FALSE and the serial data line is pulled low. The next two lines generate the W_CLK pulse. The next line: freq=freq >> 1; does a bitwise right shift by one place for the next iteration of the loop... My simple demo program sets the DDS output to 5.000,000MHz if the REFCLOCK is 100MHz. The required value for freq is 5000000/(100000000/2^32) = 214748365 decimal or 0xCCCCCCD Hex. The source code is here.

Keypad connection
Keypad connection details

A 3 column by 4 row numeric keypad is used for direct frequency entry. Suitable keypads are available from PIC-Projects on eBay. The Maplin JM09K appears to be identical. The keypad connection details are shown above.

The core part of the get_key(); function is shown below:

keypad code

The first line: PORTD=PORTD|0x07; sets all three of the column outputs high by performing a binary OR of the current value of the PORTD output latch and 7 (00000111). When no key is pressed, all of the row inputs will be zero. The keys are scanned by the while((0xF0&PORTD)==0) statement. The binary AND of PORTD and 0xF0) (11110000) masks off the lower four bits of PORTD so that only the upper four bits (PORTD 4,5,6,7) are read. When there is no key pressed, the result will always be zero, so the while loop will repeat forever. This while loop is nested within a for loop which must execute 50 times with a 1ms delay during each iteration. This is a very effective way of debouncing the keypad switches.

A non-zero result during the keypad scan will show that a key has been pressed. If the key switch is constantly on for 50 iterations of the debounce loop, we can be pretty sure that this is a genuine keypress and not just random noise. At this stage we know that a key has been pressed, but we have no way of knowing which one. The next couple of lines of code pull COL_2 and COL_3 low, leaving only COL_1 high. We can now read the rows again:
key_state=PORTD&0xF0; // Get upper nibble of PORTD (Row_1,2,3,4)

Because we are loking at the upper nibble of PORTD, the values we are looking for are: 16 (00010000), 32 (00100000), 64 (01000000) and 128 (10000000). A zero result means that the keypress is not on this column and it will be detected when we scan the other two columns a few microseconds later. Any result other than the values above indicate that more than one key is pressed and the result will be ignored.

A switch statement is used to determine which key is pressed. A value equal to the ASCII code for 1,4,7,* (the keys in COL_1) is assigned to the variable key_char.

This Demo Code combines the keypad scanning code with the LCD module code that we looked at earlier.

Rotary Encoder
Rotary encoder details

Finally, we get to the rotary encoder. I used a cheap mechanical type of rotary encoder for VFO control. This is the most common type with a built-in push switch. The switch is used to change the frequency tuning steps when the knob is pushed rather than turned. These encoders have a click detent at the A=0/B=0 position. This makes them particularly easy to read using a simple partially-decoded scheme. Due to the action of the click detents, there is little to be gained by using a fully decoded method of reading the encoder. I will try a proper fully decoded scheme when I get around to fitting an optical encoder. I have one in the junk-box with 50CPR which should give 200 steps per turn with a fully decoded solution.

Once again, we will only consider the core part of the code:
encoder code

The encoder detents ensure that both A and B are at 0 when the encoder is 'parked'. A rising edge at ENC_A shows that the encoder has turned. The direction can be determined by checking the state of ENC_B. The if else statements are used to determine the course of action to be followed. In this case, increasing or decreasing the VFO dial frequency by an amount equal to the current value of tuning step.

Source code for LF/MF receiver local oscillator with 100MHz REFCLOCK: LF-MF.pde
This is for the RadCom LF/MF receiver project with a ~12MHz LPF at the DDS output. As this receiver only covers 0-4MHz, there is no provision for a 10s of MHz digit during frequency entry and display. This version has an IF offset of 10.699950MHz to match my IF filter. This is easily modified to work with other IF frequencies. A spare I/O pin can be used to sense the position of the USB/LSB switch so that the correct offset can be applied for the mode in use. I haven't done this yet, but it is on my to-do list.

Source code for an LF-33MHz signal generator with 100MHz REFCLOCK: siggen.pde
Similar to the version above. The IF offset has been removed and a 10s of MHz digit has been added.

I didn't bother to work out the correct connections for the encoder. If you find that your VFO tunes backwards, just swap the A and B channel wires at the encoder.

Some of the delays for the LCD functions have been cut to the bone to keep up with the rotary encoder. It works fine with my N27AZ and Winstar 1602 2x16 modules. You might need to tweak the timings for some other display modules.

DDS schematic
Output circuit for LF/MF RX local-osc

Revised output circuit and LPF for signal generator
Revised output circuit and LPF for the 0-33MHz signal generator