2020-09-28 - Uploaded new program files (see bottom of page) to fix bugs that affected operation with the AD9850 (the AD9851 worked correctly).
2020-02-11 - Added option to use a faster microcontroller with
additional software features.
A link to all of the program files is provided at the bottom of this page.
The software for this project has undergone several significant changes. I’ll refer to them as versions 1, 2 and 3. And maybe more versions in the future. The details will be explained below, but a brief summary of differences will be given here.
Version 1 (PIC16F630) used a lookup table of ten entries of 5 bytes each to simplify the calculation of the Frequency Tuning Word (FTW).
Version 2 (PIC16F630) eliminated the lookup table, replacing it with a single stored constant value and a routine to calculate the FTW. This freed up sufficient program memory to add some minor additional features.
Version 3 (PIC16F1703) using a newer microcontroller, pin compatible with the earlier one, is significantly faster, and has more program memory. This has allowed for the addition of a number of features, such as:
The following discussion of the program has been edited to reflect the final code, but also includes information about the early design, and how it developed into the final program code. The discussion deals mostly with the code required to calculate the AD9850/51 Frequency Tuning Word (FTW) parameter. This is the data that must be sent to the DDS chip to set its output frequency. It’s easy enough to calculate the FTW value on a calculator or spreadsheet where we have high precision math functions available. However, on typical microcontrollers we are limited to single byte add, subtract, and left/right shift operations. That can make it a bit daunting to take an input 8 digit decimal frequency value and convert it to a 32 bit FTW. Consequently, a number of hobby projects that I’ve seen restrict the number of available output frequencies, either in frequency range or increment. Yet, with some analysis it is possible to simplify the problem and come up with a short and simple algorithm that will generate the FTW to a precision of 1 Hz and cover the full frequency range of the DDS chip. At the risk of being longwinded, in the following paragraphs, I go into a lot of detail explaining how the calculation is done.
To control the DDS chip, we must send it an 8-bit control word and a 32-bit frequency tuning word (FTW). These are sent as a single serial stream of 40 bits. The control word consists of:
For this project, the control word is always set for zero phase, 6× multiplier enabled (ignored on the AD9850), sleep mode off, and factory test mode off. Thus, the control word is always 00000001. So, the only thing that varies is the frequency tuning word. The frequency can be set to a precision of about 0.03 Hz for the AD9850, and about 0.04 Hz for the AD9851 (based on reference frequencies of 125 and 180 MHz respectively). For this project, I decided on a minimum increment of 1 Hz, across the entire frequency range, in order to make this device as flexible as possible.
The DDS output frequency is given by the formula:
FOUT = FTW × FREF / 232
where FOUT and FREF are both in Hz.
Therefore, for a given output frequency, we can calculate the frequency tuning word by rearranging the above formula to get:
FTW = FOUT × 232 / FREF
Since 232/FREF will have a significant fractional part, the arithmetic must be done in floating point in order to get a precise 32-bit value for FTW. This is easy enough to do on a spreadsheet which easily handles 15 decimal digit precision, but may be beyond the precision of some calculators, unless they can handle at least ten decimal digits. Even the constant 232 requires 33 bits to represent it, putting it beyond the capabilities of long integers.
Handling this properly in normal floating point math, would require 64 bit double precision floating point, or 64 bit integers, which is not available in many microcontroller C compilers. This requires a decision: either write some extended precision math routines, or make do with the loss of precision and live with the fact that the DDS frequency output won’t be exact as set. One of the applications that I had planned for this project is an accurate frequency reference for testing various experimental frequency counter circuits that I’m working on. Therefore, the loss of precision is not acceptable. The limitations of PIC compilers isn’t a big issue, because most of my development work has been done in assembly language, and I’m comfortable working at that level. An added advantage is more efficient use of memory in these inexpensive microcontrollers.
To handle floating point in an integer only CPU at the assembly language level, the usual procedure is to multiply the numbers by factors large enough that the fractional part becomes integer. The factor is chosen to be a power of two, 2n, where n is large enough to eliminate the fractional part, or at least to reduce the fractional part to an insignificant level. Enough bytes must be used in subsequent calculations to prevent overflow. After the final operation, the resulting number is divided by 2k, where k is a value which reverses all of the earlier 2n multiplications. By always using powers of two, these multiplication and division operations are simply reduced to left and right bit shifts.
Following is a more detailed discussion of how the tuning word is calculated.
From the AD9850/51 datasheet the output frequency is:
FOUT = W × FREF / 232
where W is the 32 bit frequency tuning word (FTW), and FREF is the frequency of the reference oscillator. If we define the constant k as the value 232/FREF, then:
W = k × FOUT
Eight decimal digits allow us to represent frequencies from 0 to 99,999,999 Hz with 1 Hz resolution. We can represent FOUT as the sum of its eight decimal digits (D0..D7) multiplied by the appropriate powers of 10. Hence:
FOUT = D7×107 + D6×106 + D5×105 + D4×104 + D3×103 + D2×102 + D1×101 + D0×100
D0 is the least significant digit and D7 is the most significant.
Now the tuning word will be:
W = k×(D7×107 + D6×106 + D5×105 + D4×104 + D3×103 + D2×102 + D1×101 + D0×100)
We can rearrange this formula as follows:
W = k×D7×107 + k× D6×106 + k× D5×105 + k× D4×104 + k× D3×103 + k× D2×102 + k× D1×101 + k× D0×100
Since D0..D7 are decimal digits, they can each have only one of ten possible values: 0,1,2,3,4,5,6,7,8,9, and so we can create a ten entry lookup table for the corresponding values of k×DN.
Having done that, the only remaining operations are multiplication by powers of 10. Again rearranging the formula, we get:
W = ((((((k×D7×10 + k×D6)×10 + k×D5)×10 + k×D4)×10 + k×D3)×10 + k×D2)×10 + k×D1)×10 + k×D0
With this rearrangement, we have turned the original short calculation into a longer one. However, the operations that are involved are much simpler and easier to implement. It is only necessary to use the lookup table to get the value k×DN, add it to the accumulated total, and multiply by ten (which is easily done: To multiply by 10, we require 3 left shifts and one addition). This process is repeated in a loop until all digits have been processed.
For this application, it is sufficient to implement all numeric constants as 40-bit numbers (5 bytes each), and use 64 bits (8 bytes) for all intermediate calculations, then rounding the final value to 32 bits. This prevents both overflow and roundoff error problems, and gives the most precise FTW value possible. I say “most precise” rather than “exact” because it isn’t possible to represent all frequencies exactly with the 32-bit FTW value. So, we have to generate the FTW value that gets us as close as possible to the desired frequency. The algorithm given here will do that. It will generate an FTW that is correct to the least significant bit. The error will never exceed 0.04 Hz for a 180 MHz reference, or 0.03 Hz for a 125 MHz reference.
While proofreading an early draft of this webpage, it occurred to me that I had conveniently glossed over the messy details of doing 64 bit math on an 8 bit microprocessor. The PIC processor that I used for this project has instructions that can directly add and subtract 8 bit numbers. It has no multiply or divide instructions, and it won’t directly operate on numbers bigger than 8 bits. This is fairly typical of simple microcontrollers. Fortunately, it is fairly straightforward to use chained 8 bit add/subtract instructions to do extended precision math, even including multiply and divide operations. Anyone doing very much programming for these low level devices tends to accumulate a library of useful math subroutines. That is the case here. I have copied and adapted math subroutines from various application notes, user groups, and have also created some of my own over the years. So, when I say something like, “All you have to do is divide this 64 bit number by this other 32 bit number,” what I’m really saying is that I went though all the grief of doing this a long time ago, but I saved the code, and now I have a handy subroutine that takes care of it. With a bit of organization, the multi-byte values can be loaded into a set of working registers, and the appropriate subroutine can be called to perform the task. Performing complicated math becomes relatively straightforward. All of the extended precision math routines that are needed for this project are included in the source code file.
Prior to digressing into the discussion of 64 bit math, it was said that much of the work could be simplified by using a lookup table for the ten values of k×DN . The PIC16F630 is a low end member of the Microchip midrange PIC processor family, but it can efficiently implement lookup tables. Using this feature, we need only precompute ten 40-bit lookup values. The lookup values are:
0 × 2n × 232/FREF
1 × 2n × 232/FREF
2 × 2n × 232/FREF
3 × 2n × 232/FREF
4 × 2n × 232/FREF
5 × 2n × 232/FREF
6 × 2n × 232/FREF
7 × 2n × 232/FREF
8 × 2n × 232/FREF
9 × 2n × 232/FREF
Notice that an additional factor of 2n has been included. The value of n is chosen to be the largest possible integer such that the largest calculated table value (9×2n×232/FREF) can just fit in five bytes (240 = 1099511627776). The reason for this is that we want the maximum precision possible. If the values in our lookup table have a lot of leading zeroes, then we don’t really have the maximum precision possible. For a reference frequency of 125 MHz, n=63; and for a reference frequency of 180 MHz, n=64. Because of this additional factor of 2n, it is necessary to divide the final calculation result by 2n in order to get the correct value for the FTW. This is a trivial operation, because division by powers of 2 are simply right shift operations.
For reference frequencies of 125 and 180 MHz, this will give the following values:
By using these pre-calculated values we conveniently avoid dealing with both floating point math and messy 8 digit decimal to binary conversion when generating the FTW.
It should be noted however, that all of the values in the lookup table are just simple multiples of the first entry. That is, Value[n] for any index n is just Value multiplied by n. Since the largest multiplier is 9, an alternative approach is to store only value, and then generate the other values as needed. Multiplication by the small values of 9 or less is simple and quick, and uses less total memory than the 5 byte × 10 entry lookup tables. Nevertheless, the lookup table method was used for version 1 of the code. It was a logical choice, because it reduced the necessary number of calculations at a time when there was much debugging to be done. Anything that would simplify the calculations was seen as an advantage.
With the revamp of the code to versions 2 and later, the direct calculation of the values is now done. The amount of code to be changed was quite small: the lookup code being replaced with a simple repeated addition of the FTW base value which is now stored in EEROM. This not only saved quite a bit of program memory previously used by the lookup tables, but also allowed a significant number of working registers to be freed up. That, in turn, allowed for the possibility of some additional program features not previously possible.
Following is an example calculation of the FTW.
Let’s say our reference frequency is 180 MHz, and we wish to set the DDS output frequency to 3925 Hz.
We start with the most significant digit ‘3’ of the desired frequency 3925; we look up the corresponding value from the table (or directly calculate it by multiplying the base value 102481911521 by 3):
We multiply this value by 10 to get:
3074457345620 (This is our accumulated sum.)
Taking the next digit ‘9’ of the desired frequency 3925, we look up or calculate its corresponding value:
We add this to the accumulated sum 3074457345620 to get:
We again multiply the accumulated sum by 10:
Taking the next digit ‘2’ of the desired frequency 3925 we look up or calculate its corresponding value:
We add this to the accumulated sum 39967945493050 to get:
We again multiply the accumulated sum by 10:
Taking the last digit ‘5’ of the desired frequency 3925 we look up or calculate its corresponding value:
We add this to the accumulated sum 401729093160910 to get:
Finally, we divide the accumulated sum 402241502718513 by 232 to get:
(Note, that in binary arithmetic, dividing by 232 is the same as discarding the least significant 4 bytes of the number. So, in the microprocessor, we don’t need to carry out this division at all. If our reference frequency were 125 MHz, we would divide by 231 instead of 232, which is accomplished by doing one left shift operation before discarding the last 4 bytes.)
The final value, 93654, is the FTW value we were looking for, and as demonstrated, we were able to calculate with nothing more than table lookups, left shifts and integer additions. Certainly, we could have chosen a higher example frequency with more digits than the 4 of 3925, but it would have involved repeating exactly the same sequence of steps a few more times, not really proving anything.
Of course, we should verify that this algorithm gives the same value that we would get with the original formula:
FTW = FOUT × 232/FREF
Plugging in the numbers:
FTW = 3925 × 232/180000000 = 3925 × 4294967296/180000000
which agrees exactly with the value calculated by our algorithm which uses only simple arithmetic operations.
The general algorithm is as follows:
Initialize the accumulated sum to zero; For each digit (going left to right) of specified output frequency: Multiply accumulated sum by 10; Get the table value corresponding to the next digit; Add table value to accumulated sum; End Loop; Divide accumulated sum by 2^(32-k) (where k is the number of left shifts described above);
As can be seen, the algorithm is simpler than the preceding description would imply, and it will work with an input string of up to 8 decimal digits, which is sufficient to cover the full frequency range of the DDS chip from 0 Hz to its maximum frequency, with a resolution of 1 Hz.
In the source code for the original PIC program, I used assembler directives for conditional assembly to handle the choice of reference frequencies. Thus, by changing a couple of lines in the source code, it would assemble into either the 125 MHz version or the 180 MHz version. This was because there wasn’t enough program memory to include lookup tables for both reference frequencies. And so, it was necessary to program the PIC differently for each different reference frequency. With the revised code, it is necessary to store only the base value of the FTW for each reference oscillator frequency. These are now stored in EEROM and a single version of the program will work with either one. The reference oscillator frequency can be changed at any time by holding down either the ‘0’ or ‘1’ key as the circuit powers up. ‘0’ sets the reference oscillator frequency to 125 MHz, and ‘1’ set it to 180 MHz.
The foregoing discussion is applicable to both the original 16F630, and the newer 16F1703 programs.
There is currently enough memory available in the 16F630 to allow for other reference frequency selections, but I’m not aware of any devices that use frequencies other than 125 and 180 MHz. On the other hand, the 16F1703 has a lot more memory, and this has allowed for an additional feature in version 3 that is significant. It has a configuration mode that allows the user to enter any reference oscillator frequency to a resolution of 1 Hz. So, custom applications that use non standard reference oscillators can be used without having to rewrite the program. This also allows fine tuning of the standard oscillators.
When discussing the circuit in Part 1 of this article, I stated that I’d opted for keyboard entry because I felt it was more practical for a general purpose signal generator, which was the primary goal of this project. However, things have a habit of evolving, and after some recent email correspondence about adapting the program to use an encoder, I decided to look at this option. One of the items discussed was the need to have two PICs in order to get sufficiently responsive performance (like the immediacy of turning a variable capacitor). This seemed unnecessary to me, but apparently it was a common practice, at one time. A bit of research revealed where this notion came from. One of the first PIC controlled DDS construction projects was described in an article by Curtis Preuss, WB2V, in the July 1997 issue of QEX. It used a PIC16C54 controller which is very slow by today’s standards. One of the limitations noted by users of the circuit was the lack of responsive tuning. This was addressed in a later project by George Heron, N2APB, by using two PIC controllers: one to drive the DDS chip, and one to drive the LCD display. This apparently worked very well, and as a result, seems to have become a generally accepted convention. However, N2APB’s project dates back to 2002, and it used PIC16F84’s which are only a marginal improvement over the PIC16C54. In the 18 years that have elapsed since the Dual-PIC project, massive improvements have been made in microcontroller performance. So, there is no longer any need to use two PICs. Yet, having a very fast microcontroller is no excuse for lazy programming. In the following paragraphs, I describe how to prioritize the reading of the encoder, and the updating of the DDS chip and the LCD display, to maximize responsiveness.
Fortunately, changing from a 16F630 to a 16F1703, freed up two I/O pins that were previously required for the external 20 MHz crystal. No changes to the circuit board design were made. The encoder phase A and B inputs were connected to the solder pads previously used by the crystal. The 22 pF load capacitors were changed to 0.01µF as part of a debounce circuit. Debounce is not strictly necessary with fully decoded quadrature inputs, but it does reduce the possibility of a bit of up/down jitter. I used an inexpensive panel mount 12 PPR (pulse/revolution) mechanical encoder (Bourns PEC12R series). While this may seem to be very coarse resolution, a 12 PPR encoder when fully decoded gives 48 transitions per revolution, which equate to 48 frequency increments or decrements. I wouldn’t recommend going any higher than a 24 PPR encoder which would give 96 frequency increments or decrements per revolution. The encoder should not be the detent type, when fully decoding the phase transitions, as frequency changes will occur between the detents. The encoder that I used is the detent type, but I opened it up and flattened the detent bump on the loading spring, turning it into a smooth turning type. The Bourns PEC12R series is available in non detent styles and with 24 PPR resolution. The encoder wiring followed the manufacturer’s suggested method, which is shown below.
As previously mentioned the two 0.01µF capacitors were mounted on the circuit board in place of the 22pF crystal load capacitors. The four 10k resistors were mounted on the encoder assembly. Terminal A and Terminal B connect to PIC inputs RA4 and RA5 (pins 2 and 3).
With the circuit details taken care of, the challenge now, is to program the microcontroller to give a fast responding natural feel to the encoder. It should behave as much as possible like a traditional variable capacitor tuning control: negligible delay between turning the encoder and updated DDS frequency output, and negligible delay in LCD display update.
The trick to getting this speed is to pre-calculate the binary increment value, so that incrementing the encoder input results in a single binary add to (or subtract from) the current FTW, which is then sent to the DDS chip. The fast DDS frequency increment code had already been written for the sweep modes and was easily adapted to encoder operation. It is very fast, even with the DDS chip communications in serial mode.
We must also update the frequency on the LCD display. Again, we pre-calculate an increment value, this time in BCD, and we add (or subtract) the this value to (or from) the displayed BCD value whenever the encoder input changes. This operation, because it is in BCD, is more involved than the update to the DDS chip.
To avoid any program lag caused by slow display update, the encoder routines are prioritized. Detecting encoder movement is the highest priority. The reading of the encoder input is done in an interrupt routine, so that the new encoder position is updated instantly. An 8 bit register is used to track encoder increments and decrements. When the program starts, this register is zeroed. With each encoder increment, 1 is added to the register. With each encoder decrement, 1 is subtracted from the register. The resulting value is a signed 8 bit number (−128..+127) representing the number of pending frequency increments or decrements.
The remaining encoder operations are handled by the encoder service routine in the main program loop. The service routine reads the value in the encoder register, adds that value to two other registers: a frequency update register, and a display update register. The original encoder register is then cleared.
The frequency update register is used by the DDS frequency update routine. In most cases the register will have a value of −1, 0 or +1. However, if the operator is spinning the encoder knob very quickly, it’s possible that the DDS frequency update routine won’t process the encoder transitions quickly enough, and so one or two additional counts may accumulate in this register. However, the DDS frequency update routine will do all of the pending frequency increments to the FTW before transmitting it to the DDS chip. This DDS frequency update routine is second highest priority, and when it has sent the FTW to the DDS chip, it immediately rechecks the encoder count register to see if it has changed during the preceding FTW calculation. If it has changed, the program loops back to the start of the encoder service routine to update the frequency update register and the display update register, and then repeat the FTW update.
When all pending DDS frequency updates have been handled, then control passes to the display update routine which has the lowest priority. At this point, if the operator has been turning the encoder knob quickly, several counts may have accumulated in the display update register. The display update routine, performs all of the pending BCD increments or decrements to the display value before sending the final result to the LCD display.
Doing the DDS frequency update and display update independently of each other is generally considered bad programming practice, because there are numerous situations where things can get out of sync, so that the displayed frequency is not the same as the operating frequency. Unfortunately, for maximum performance, there is no easy alternative. However, special measures are taken to minimize the risk. When reading the primary encoder register, adding the count to the secondary registers and then clearing the primary encoder register, this is done with interrupts disabled, so that a change of encoder state will not cause any divergence in the independent update operations. The interrupts are disabled for only four instructions, or 125 nanoseconds, and any single encoder transition that occurs during that time will still be processed when the interrupts are re-enabled.
An additional action is taken to ensure that the operating frequency and display do not get out of sync. A timer is used to sense inactivity in encoder movement. If there is no encoder activity for approximately a half second, a re-sync subroutine is called to take the displayed frequency value and calculate its corresponding FTW value, which is then used to update the DDS thus assuring that all values are in agreement. Since this routine only executes when the encoder movement has stopped, its operation is invisible to the user. In fact, to demonstrate that it is indeed working, the leftmost character on line 2 of the display, changes to ‘^’ while the encoder is being turned, and then when the re-sync occurs the symbol is changed to ‘*’. It should be mentioned that prior to adding the re-sync routine, no situation had ever been encountered where the operating frequency and display got out of sync, even with extremely rapid encoder operation. However, the re-sync function provides an extra level of reliability.
The encoder increment rate is set by the user when the unit is set to mode 1. The increment value is entered on the second line of the display using the keyboard. When the unit is switched to mode 2 (encoder mode), this is the value used for the encoder increment. In addition, pressing the +INCR button will increase the increment value by a factor of 10, and the −INCR button will decrease the increment value by a factor of 10.
With the encoder routines programmed as described, the operation is very smooth and responsive. Even with the display update routine having the lowest priority, display update appears to be virtually instantaneous. In fact, the physical limitations of the liquid crystal cause the changing digits to blur together with very rapid rotation of the encoder.
It’s interesting to compare this encoder implementation to a less than ideal one: in this case, the Tecsun PL-380 portable shortwave receiver. This is a great little receiver for the price, but the rotary encoder used for the tuning can most accurately be described as horrid. It is a 24 PPR detent type encoder (similar to the one used on this DDS project), but not fully decoded, and as the contacts age and get dirty, it can skip phases, resulting in the frequency changing opposite to the direction of rotation. It also has a two speed increment rate which has been carefully designed to transition at exactly the wrong moment. Fortunately, they also provided a numeric keypad for direct frequency entry. As I said, it’s a great little radio for the price, but the encoder implementation is a disaster.
The PL-380 experience is part of the reason why I initially stayed away from implementing an encoder for this project. It’s also why, after I did implement an encoder, I didn’t attempt to provide an automatic variable increment. However, there are some very good algorithms for variable rate encoders that are available as part of open source SDR (Software Defined Radio) projects. Their logic could be applied to this project. However, I’ll leave that task for others.
Following is a description of the operating modes of the controller.
1 - Direct Entry and Up/Down Increment Mode (all versions)
This was originally intended to be two separate modes, but it turned out that there was no reason to separate them.
the +⁄− key toggles between the upper and lower lines of the LCD display. The active line is indicated by a right arrow at the left side of the display. On the upper line, the operating frequency can be entered directly. On the lower line, the value entered is the amount that the frequency will be incremented or decremented is the Up/Down Increment buttons are pressed.
In this mode, the FTW for the starting frequency is calculated as described previously. A second calculation using the exact same method is carried out for the increment frequency, producing a 32 bit binary frequency increment value. Whenever the +INCR button is pressed, this frequency increment value is added to the current FTW and the then sent to the DDS. When the −INCR button is pressed, this frequency increment value is subtracted from the current FTW and the then sent to the DDS.
2 - Encoder Mode (version 3 only)
In this mode, the frequency is controlled by the encoder. The rate of change is set using the +INCR and −INCR buttons, with the increment value shown on the lower line of the LCD display.
(Mode 2 in software versions 1 and 2, disables the keyboard and only allows operation of the +INCR and −INCR buttons to change the frequency.)
3 & 4 - Sweep Modes (all versions)
When operating in sweep mode, the centre frequency is that displayed on the upper line of the LCD display, entered while in mode 1 before switching to mode 3 or 4. The sweep offset is value shown on the bottom line of the LCD display (entered while in mode 1). Mode 3 is 128 steps, and Mode 4 is 255 steps.
the FTW for the centre frequency is calculated as described above. Then a second calculation, again using the same method, is carried out for the offset frequency producing a 32 bit binary frequency offset. This value is subtracted from the centre frequency FTW to get the lower starting frequency FTW. The binary frequency offset value is now shifted right 7 or 8 bits (effectively dividing the value by 128 or 256) depending on whether we are in 128 or 255 increment sweep mode. The resulting value is the amount that the FTW must be incremented for each sweep step. The increment is a simple binary value. So, it’s just a matter of adding this increment to the FTW each time through the sweep loop.
In sweep mode, a ramp value is also generated. It is a binary number that starts at one, and is incremented by one each time through the loop, then sent to the D/A shift register at the same time that the new FTW is sent to the DDS chip.
A benefit of the recent revamp of the program code is that it freed up enough memory to allow the display of frequency markers on the oscilloscope sweep display. While iterating through the sweep loop, whenever a marker frequency is reached, all that is necessary is to have a short delay. The delay causes the scope trace to increase in brightness at the current position. After the delay, everything continues on as before, until the next marker frequency. As the program currently exists, there are three fixed markers: one a the centre frequency, one at the 25% frequency position and one at the 75% frequency position. With the current program code, any frequency could be easily highlighted, but the programming required to allow the user to enter custom frequencies is still beyond the hardware limitations of this circuit. So the three fixed values were chosen as reasonable values.
Setup functions allow the user to change the reference oscillator frequency. They are executed by pressing one of the keyboard keys before applying power to the circuit, and then releasing the key once power is applied. This feature was not available in Version 1, because changing reference frequencies would have required a complete recalculation of all of the lookup table entries. With version 2, it was only necessary to store a six byte number for each reference frequency. So version 2 has the capability of choosing between the two standard reference frequencies of 125 MHz and 180 MHz (and others with minor program changes). With version 3, the significant increase in program memory has made it possible for the user to enter any reference frequency value to a resolution of 1Hz.
The various setup functions are as follows:
The “0” key sets AD9850 mode and frequency reference value 125 MHz.
The “1” key sets AD9851 mode and frequency reference value 180 MHz.
The “2” key (version 3 only) displays the currently configured reference frequency. Pressing any key exits, and returns to normal operation.
The “3” key (version 3 only) allows entering any arbitrary reference frequency. The LCD displays a prompt asking the user to enter the new reference frequency, in Hz, as a 9 digit number. If the reference frequency is less than 100 MHz, then leading zeros should be entered so that there are exactly 9 digits. After the 9th digit is entered, the new reference value is calculated and displayed. To accept and save, press any key. Otherwise, cycle the power to cancel and return to the original value. If an entry error is made before 9 digits have been entered, pressing the clear key, will clear the entry completely, allowing the entry to be started again.
The program is implemented as a collection of subroutines which perform:
And finally there is a small main program which initializes the I/O, and then enters a main loop which reads the mode selector switch and then jumps to the appropriate handler routine for the selected mode.
One of the
nice things about the AD9850/51 DDS chips is that even if your
reference oscillator has no frequency trim adjustment, you can still
do the trimming by adjusting the FTW reference value. This can be
accomplished in the 16F1703 version, because you can go into setup
mode and enter any FTW reference value. For example, suppose we are
using an AD9851 with nominal 180 MHz oscillator. We compare it to a
known standard frequency, and discover that it is slightly off. For
example, zero beating it with WWV on 5 MHz, we actually have to enter
a frequency value of 5.000059 MHz in order to get a zero beat. The
ratio of the actual frequency to the entered frequency is
5/5.000059 = 0.99998820.
This means that the reference oscillator is running slightly low at a frequency of
180×0.99998820 = 179.997876 MHz.
If we now go into setup mode, by holding down the “3” key while powering up, we can enter 179997876 as the true reference frequency. Rechecking against the WWV 5 MHz standard, we see that the DDS output is now correct to 1 Hz, and with 5.000,000 MHz entered in the DDS we get a zero beat. Of course, the oscillators provided with the DDS modules will drift with time and temperature. But at least we have a means to make corrections when required. I noticed that the reference frequency on my DDS module changes by about 0.5 parts/million (100 Hz with 180 MHz reference frequency) in the first few minutes after the DDS is powered up, and then stabilizes to a negligible drift afterwards, as long as the ambient temperature does not change. This frequency value is very repeatable on my module, but other modules may have different reference oscillators with different drift characteristics.
Some previous versions of the program contained a number of bugs affecting its use mostly with the AD9850 DDS, but not the AD9851. These were nearly all related to an incorrect control word being sent to the AD9850 DDS controller. The last of these bugs appear to have been eliminated as of the revision posted on 2020-09-28.
With the latest (2021-08-13) update, an additional zip archive is now included for the PIC16F1703, for use with keyboards having arbitrary key arrangements. The hex file included with this version is configured to work with a standard telephone keypad arrangement. For other arrangements, the source code file must be modified, changing the keyboard lookup table to suit the desired key arrangement. Please refer to the information document included in the zip archive for further information.
A zip archive of the source code and hex file for the program is available for download here:
PIC16F1703 (custom keyboard) version