Different DDS devices have different widths to their tuning words and D/A converters. The above mentioned parts in the AD985x series have 10 bit DACs, while the AD995x series have 14 bit converters. Both series have 32 bit tuning words. The purity of the output is better with more precise DACs, and the resolution of tuning (frequency steps) is determined by the width of the tuning word. With a 32 bit tuning word these parts can tune in fractional hertz resolutions.
The tuning word is determined by multiplying the desired frequency by a tuning constant. This constant (for a 32 bit tuning word) is equal to (2^32) / (DDS clock frequency). So for a typical 180 MHZ clock rate for the popular AD9851 device, the tuning constant is 23.86092942. Unfortunately, this leaves us with the ugly problem of performing floating point math in our micro controller to determine the value of the tuning word in real time (for a VFO application, anyway).
We can however play a few tricks to allow us to make use of simpler integer math functions to replace the floating point ones. Since the tuning constant is going to be a small number for the typically used clock frequencies and word widths (in the range of 10-100 for clocks of 50 - 400 mhz and 32 bit tuning words), the whole part of the constant fits into less than 8 bits. We can multiply this floating point number by a suitable power of two and throw away any small fractional remainder, and then multiply our resulting larger constant by the desired frequency. We then right shift the result by the same power of two before applying the answer as our frequency word.
This will require 64 bit math of course, but building a 64 bit multiply routine on an 8 bit processor isn't quite as bad as programming a floating point multiply followed by an integer conversion. The generation of our constant can be done on a calculator, and we then enter this value directly into our program, where we let our compiler do the heavy lifting. The AVR GCC "C" library has the required 64 bit multiply code in it, so all we need do (in "C") is to define the necessary long long integer variables and simply perform the multiplication.
So how many bits do we left and right shift? It turns out that by multiplying the floating point constant with an integer part value of less than 2^8 in size to fit into a 32 bit word, we end up with a binary floating point format looking like this: IIIIIIII.FFFFFFFFFFFFFFFFFFFFFFFF , that is a 'real' or 'integer' portion of 8 bits and a fractional portion of 24 bits. IE: we will multiply the floating point constant by 2^24, or a left shift of 24 bits.
As an example, consider a tuning word of 32 bits and a clock frequency of 180MHZ. Our tuning constant is 23.86092942 as stated before. To build our binary floating point format constant follow the following formula: Konst = ((2^32) / RefClockFrequency) * (2^24).
So (4294967296 / 180000000) * (1677216) = 400319966.8 We throw away the 0.8 at the end, and enter the result into our code. In hex, our constant is 0x17dc65de.
Now for a few tricks in programming. We'll use a union to make handling things a bit easier.
First we call init_dds_calibration to initialize the tuning constant. In this code we either read the value from eeprom, or set the initial eeprom value. Later on we can fine tune the constant to a value suited for the ACTUAL value of our clock, since the frequency value stamped on our crystal isn't going to be exact. We will use Adjust_DDS_Konst() to perform this tweaking, allowing the user to zero beat the DDS against a frequency standard by turning the tuning knob.
CalculateDDS() is used to perform the math. Note that instead of right shifting by 24 bits at the end, we have left shifted by eight and return the UPPER part of the 64 bit result. It's a bit faster that way, and the use of the union gives us the flexibility to do this.
The functions for actually writing to the DDS chip, and accepting user input are not included here.
//Konst is a 32 bit number, but is stored in a 64 bit variable
//because it will be multiplied by a 64 bit number
union K
{
uint64_t K64;
uint32_t K32[2];
}K;
//
// read the stored calibration konstant from eeprom. If the
// eeprom has not been programmed with a stored konstant, use
// the default value based on an EXACT clock frequency of 180.0000000 mhz.
void init_dds_calibration(void)
{
K.K32[1]= 0; //zero upper part
K.K32[0]= (uint32_t)eeprom_read_dword(EE_DDS_KONST);
//see if we have a valid konstant in the eeprom and use it if so
if(K.K32[0] == 0xffffffff){
//eeprom is unprogrammed at this location, so use the default
K.K32[0] = Konst;
}
}
// adjust the dds constant by a small amount.
unsigned long Adjust_DDS_Konst(int adj)
{
if(adj > 0)
K.K64 += (unsigned long long)abs(adj);
else if(adj < 0)
K.K64 -= (unsigned long long)abs(adj);
return K.K32[0];
}
// dds constant for 180mhz clock and a 32 bit accumulator.
// Integer math is performed and the fractional portion
// of the result is shifted away.
unsigned long CalculateDDS(unsigned long freq)
{
union dds{
uint64_t Product; //64 bits
uint32_t Part[2]; //32 bits * 2
uint16_t i[4]; //16 bits * 4
}dds;
//store the 32 bit frequency number as the low half of a 64 bit number
dds.Part[0] = freq;
dds.Part[1] = 0;
//multpliy 32 bit frequency by 32 bit constant to get 64 bit number
dds.Product *= K.K64;
//shift left to remove fractional part of result
dds.Product = dds.Product << 8; //shift left one byte
//result is the integer part of the result. We have
//performed a floating point multiply using integer math.
return dds.Part[1]; //return upper long int as result
}