DDS

DDS is an acronym standing for "direct digital synthesis." It's sometimes called a "numerically controller oscillator," but DDS seems to be the more popular term.

I devoted most of a chapter in my book to using a 16F877 PIC to build an audio DDS generator, and I don't propose to duplicate that effort here. In brief, a DDS generates its output by computing the value of the desired sine wave (or other waveform, as DDS is not limited to sinusoidal waveforms) at specific time intervals and then sending the value to a digital-to-analog converter, where the numerical data is converted to a corresponding voltage level.

The upper trace in the oscilloscope capture shows a typical DDS output. Note that the desired sine wave is approximated by discrete voltage steps. The PIC calculates the appropriate voltage level as a byte-range number (0...255) and sends it to the DAC where it is converted to a voltage. In this case, the output is a 730 Hz sine wave.

The lower trace is the DAC's output after it is run through a low pass filter, which cleans up most of the digital step artifacts seen in the raw DAC's output.

To show you that the DDS is not restricted to sine wave outputs, the lower trace in the following oscilloscope capture is a DDS-generated sawtooth or ramp waveform. The upper trace is a synchronization waveform.

DAC - Digital to Analog Converter

Let's look at how a DAC works. We'll start with a simple 4-bit R-RL ladder type DAC. The two oscilloscope plots above were taken using this circuit, breadboarded up with five discrete resistors.

The four switches, DB0...DB3 switch between ground and +5V. Of course, our circuit will not use real SPDT switches, but rather the PIC's output pins will switch between logic high (+5V, more or less) and logic low (0.2V or so). How does this simple circuit work? The easiest way to understand how it works is to regard the four resistors as current sources. Into a low impedance load (such as Rload, the 47 ohm resistor), when DB3 is switched to the +5V source, it will force 5 mA (5V/1000 ohms) through Rload. In this case, the output voltage is 235 mV, calculated as 0.005 A * 47 ohms = 0.235V

Likewise, when DB2 is switched to +5V, Rload has 2.5 mA through it, DB1 controls a 1.25 mA source and DB0 controls a 0.63 mA source.

With the switches set as shown in the diagram, the total current through Rload is 2.5 mA (from DB2, through R2) and 1.25 mA (from DB1, through R3), totaling 3.75 mA, for an output voltage of 176 mV.

What about the shunting effect of R1 and R4, you may be thinking. These clearly drain current away from Rload. And, how can you just sum the currents?

The answer is yes, these are all real effects. However, we've chosen R1...R4 and Rload carefully. A 1K resistor shunting 47 ohms is negligible, at least at the resolution we expect from a crude 4-bit DAC. Likewise, a couple hundred millivolts across Rload won't make our assumption that R1...R4 are current sources that bad. Hence, we can simply sum the currents and be "close enough" for our purposes.

Don't believe me? Dig out your calculator and work through Ohm's law and send me an E-mail with the exact answer. It'll be within 10% of our simplistic analysis.

Oh yes, why is this called an R-2R ladder? Looking at the resistor values and diagram should answer this question for you. (I used real values at 3.9K and 8.2K; in theory these should be 4.0K and 8.0K).

And, why make the ratios 2:1? It has something to do with the binary system doesn't it? Yes, indeed it does. If we look at a 4-bit binary number (usually called a nibble) defined by DB3...DB0, we see that the DC output of our R-2R ladder DAC automatically tracks the binary value of the nibble. The binary value to voltage multiplier is the current generated by the least significant bit (R4, or 0.625 mA) times Rload, or 29.4 mV.

Let's see if this works. The nibble data value represented by these switches is 0110 binary, or in decimal 6. Hence, the output voltage should be 6 * 29.4 mV = 176 mV, exactly what we calculated before.

Nice when it all fits together, isn't it?

Simple Swordfish DDS Code--Program DDS-1

OK, time for some code. I'll first list the complete program and then explain what is behind it. The program assumes we have two of the simple 4-bit resistive DAC circuits connected to the PIC's Port B.

The following schematic fragment shows the resistive DAC connection.

Our first code generates two 4-bit sine waves, one on Chan_A_Out and one on Chan_B_Out, at different frequencies.

{
****************************************************************
* Name    : Toner.BAS                                          *
* Author  : Jack R. Smith                                      *
* Notice  : Copyright (c) 2006 Clifton Laboratories            *
*         : All Rights Reserved                                *
* Date    : 7/22/2006                                          *
* Version : 1.0                                                *
* Notes   :                                                    *
*         :                                                    *
****************************************************************
}

'Program to output two simultaneous DDS-generated sine waves
'into 4-bit DACs on Port B. Channel A is lower nibble
'Channel B is upper nibble.

Device = 18F4620
Clock = 40 'remember to enable HSPLL in configuration
Config
   OSC = HSPLL, //turn 4x PLL ON
   LVP = OFF,
   DEBUG = OFF,
   PWRT = ON,
   BOREN = ON,
   WDT = OFF,
   PBADEN = OFF, 'for 18F4620 - makes port B digital
   XINST=OFF

Const
   SinTable(16) As Byte = (8,10,13,14,15,14,13,10,8,5,2,1,0,1,2,5)
Dim
   aStep, bStep As Word,
   PhaseAccumA As Word, 'use these as a 4.12 (15 bit total)
   PhaseAccumB As Word, 'phase accumulators
   Temp As Byte

// program start...
TRISB = $00 'make output

'adjust these values when loop code is finished as the
'output frequency depends on Repeat / Until loop timing
aStep = 935
bStep = 267 'generates 780 Hz with current timing
PhaseAccumA = 0
PhaseAccumB = 0

Repeat
   High(PORTC.0)
   PhaseAccumA = PhaseAccumA + i
   PhaseAccumB = PhaseAccumB + j
   Temp = SinTable(PhaseAccumA >> 12)
   Temp = Temp + 16 * (SinTable(PhaseAccumB >> 12))
   PORTB = Temp
   Low(PORTC.0)
Until False

Program DDS-1 Analysis

The first part of a Swordfish program identifies the PIC to be used and the clock speed in MHz. This is done using Swordfish's keywords "Device" to identify the PIC and "Clock" to define the clock speed.

Device = 18F4620
Clock = 40 'remember to enable HSPLL in configuration

I used an 18F4620 for the DDS experiments, because I happened to have one set up in a plug-type breadboard, but an 18F4620 is gross over-kill to demonstrate how a DDS works.

DDS is a demanding application for a PIC, and the faster the clock, the better off you are, particularly when programming in a high level language, such as Swordfish. If you look at Chapter 16 of my book, Programming the PIC Microcontroller with MBasic, you will find a chapter on Digital-to-Analog conversion and several DDS programs written in MBasic, for the 16F877 PICs. In order to wring acceptable performance, I had to code time-sensitive segments in assembler. We'll see later that Swordfish produces such efficient code that we do not need assembler routines to achieve respectable performance from the 18F devices.

The next part of our program follows Swordfish's keyword config, which establishes the PIC's "configuration" which sets certain options that must be set during programming. This process is also known as setting the configuration "fuses."

Swordfish will supply a set of default configuration fuses, but it's a good idea to establish any necessary configuration settings in your code, rather than depend on defaults.

OSC = HSPLL //turn 4x PLL ON

Microchip added a built-in clock multipler phase locked loop to the 18F series chips. Thus, to obtain a 40 MHz clock rate, we have two options:

  • Use a 40 MHz crystal or resonator or external clock; or
  • Use a 10 MHz crystal, or resonator or clock source and enable the built-in 4x PLL multiplier

It's much easier to use a 10 MHz resonator and enable the PLL, via the OSC=HSPLL statement.

LVP = OFF

LVP is the "low voltage programming" command. I won't delve into the details of low voltage programming versus high voltage programming, but our program requires LVP to be disabled. LVP is a feature added by Microchip that turns out to be more of a hindrance than a benefit. Microchip ships its devices with LVP enabled, so it's necessary to explicitly turn it off the first time it is programmed.

DEBUG = OFF

The 18F-series, along with some late 16F PICs has an internal option that allows easy extraction of internal settings and variables when DEBUG is enabled. However, the compiler and the test board must also support DEBUG, so we disable this feature.

PWRT = ON

This line enables the "power-up" timer (PWRT). The power-up timer provides a 65 ms (nominal) delay from the time the PIC detects that it has been powered up until program code begins execution. This delay allows the device's power supply to stabilize and you will normally wish to set the PWRT to ON.

BOREN = ON

BOREN is Microchip's mnemonic for the "brownout reset" function. A "brown-out" is detected by the PIC when the supply voltage drops below safe operating levels, but not necessarily down to zero. Enabling BOREN, as in our test program, is the safest option, as a momentary power interruption may cause unknown side effects. A clean reset on brown-out (re-starting the code as if the circuit had its power removed) will help ensure proper code operation should a power abnormality occur.

WDT = OFF

WDT is the mnemonic for the "watch-dog timer." The watch-dog timer causes the PIC to reset, at a specific time interval. The reset interval has several optional durations settable by the programmer.

Why would you want your PIC code to reset? The answer is normally you don't want it to reset, so if you wish to use the WDT, your code will periodically disarm the WDT and thus prevent it from resetting the PIC. But, if your code has a problem and enters an endless loop, or gets hung up waiting for an external input, it won't be able to disarm the WDT and hence the WDT will force a reset, thereby extricating your program from an unexpected freeze.

If you do not need this level of supervision, keep the watch-dog timer disabled, as in the example. Otherwise, you will have to add code to periodically disarm the WDT.

PBADEN = OFF 'for 18F4620 - makes port B digital

Reading the 18F4620 data sheet quickly shows that each pin has several potential uses. In the 18F4620's case, for example, the Port B pins may either be used for normal digital logic input and output, or they may serve as additional analog inputs to the PIC's analog-to-digital converter.

The default configuration is for the Port B pins RB0...RB4 to be analog inputs on power-on reset. Since we use these pins for digital output, we must over-ride the default configuration and force these pins to be digital. (RB5...RB7 are assigned to digital mode at power up.)

XINST = OFF 	

The last configuration control we set is to disable the "extended instruction set." The extended instruction set allows the 18F-series chips to execute assembler instructions that were not available in older devices. Unfortunately, some of these extended instruction set operations have proven to be buggy, and to avoid trouble we'll disable them.

If I were writing a book on programming the 18F Microcontroller with Swordfish Basic, I would have covered the configuration settings in a separate chapter, and we could therefore jump directly into the DDS-related code. Since I'm not writing that book, I've tried to make this page more stand-alone and unfortunately this means we have to slog through some things related to the nuts and bolts of how a PIC works before we get to the interesting code.

One more thing. I've mentioned that Swordfish will automatically set many configuration bits for you. Where do you find out what configuration bits Swordfish sets?

Assuming you have a normal Swordfish installation, look in the directory ..\Swordfish\Includes\ for the file with the same name as the DEVICE you set at the outset of the program, with the extension .BAS. In this case, the file will be 18F4620.BAS. (If you were using an 18F2580, the file would be 18F2580.BAS.)

If you open this file with Swordfish's IDE, or with a text editor such as Notepad, you will find a list of all the possible configuration fuses that you can access via Swordfish. Here's the first few lines of the public configuration fuses for the 18F4620:

// configuration fuses...
public config
   OSC(OSC) = [LP, XT, HS, RC, EC, ECIO6, HSPLL, RCIO6, INTIO67, INTIO7],
   FCMEN(FCMEN) = [OFF, ON],
   IESO(IESO) = [OFF, ON]...

Then, towards the bottom of the file you will find the configuration fuse settings Swordfish will use, by default:

// default fuses...

config
   OSC = HS,
   FCMEN = OFF,
   IESO = OFF,
   PWRT = ON,
   BOREN = ON,
   WDT = OFF,
   WDTPS = 128,
   PBADEN = OFF,
   STVREN = ON,
   LVP = OFF,
   XINST = OFF,
   DEBUG = OFF

If you do not explicitly define a setting for each of these configuration fuses, Swordfish will apply these default values.

By in large, the default values are good choices. In our case, the default oscillator parameter would be wrong, but the rest of the default settings match our manual overrides.

So, then, why did we go on a long detour to look at configuration fuses that do nothing but duplicate existing settings? There are two reasons:

  • Default configuration fuses are a relative recent addition to Swordfish, and the earlier versions I used had no Swordfish-defined defaults. Either you manually entered the desired configuration fuse setting or you lived with whatever Microchip established as the power-on configuration.
  • Although Swordfish does a very good job at defining default configuration fuses, your program may need something different. Hence, it's a good habit to set the configuration fuses the way you want them to be, and not depend upon the default. And, it's possible that a later release file might change the default settings, without you being aware of it.

You might be tempted to modify the 18F4620.BAS file, for example, to make the default oscillator mode HSPLL. That's not a good idea for several reasons. One is that a Swordfish update might replace your modified 18F4620.BAS file without you being aware of it. Another is that you might wish to provide a copy of your program source code to a friend or colleague for testing or other purpose. If your code depends upon a specially modified 18F4620.BAS file, you must also include it, and that file must be copied into the other system, displacing the standard file. All in all, it not a good idea to modify the files in the INCLUDE directory unless you have an excellent reason.

Next time, we'll look at a bit of real code...