In the datasheet for the AD9850 they give us the formula to calculate the output frequency for any given tuning value:
fOut = Tuning Value * RefClk / 2^32
If we know the desired output frequency and want to calculate the tuning value we need to rearange the formula:
Tuning Value = fOut * 2^32 / RefClk
Since the values 2^32 and RefClk (125Mhz) are both known that part of the equation can be precalculated, ie 2^32/125000000=34.359738368:
Tuning Value = fOut * 34.359738368
So, what's the problem?
Well, the obvious problem is that because 2^32/125000000 doesn't result in an integer number we can't simply use the formula as it is. Rounding it down to 34 would give as an error in output frequency of more than 1% which probably isn't acceptable. Besides, how do we store the frequency and tuning value when they are such big numbers?
Before we start looking at how to write the PBP code for this lets do the calculation on paper for a random target frequency of 12.5Mhz:
Tuning Value = 12500000 * 2^32 / 125000000 = 429496729.6 or in our short form 12500000 * 34.359738368 = 429496729.6 same results - great. Obviously we're not going to be able to get that .6 into the AD9850 so lets round that up and call the "ideal" tuning word for 12.5MHz 429496730.
The easiest way to tackle this is most likely to use an 18F series PIC and switch on the support for LONGS (32bit variables) in the compiler. This allows us to simply declare a variable called Frequency and assign a nice big number like 125000000 (12.5MHz) to it. But how do we handle that non integer multiplication?
One solution for this application is the ** operator which is great for multiplying with a non integer value. It performs the multiplication of the two values that we give it (which when used on LONGS is stored internally as a 48bit result) and then it discards the two lowest signficant bytes, returning bytes 2 to 5 of the internal 48bit result. This gives the same result as multiplying the two values and then divide the result by 65536. Alternatively we can see it as multiplying our value by units of 1/65536.
OK, so we need to come with a value to use with the ** operator which is as close to a multiplication by 34.359738368 as possible. The easiest way is to simply multiply our "target value" by 65536: 34.359738368 * 65536 = 2251799,813685248. Again we're going to have to have to round that to the nearest integer value and settle for 2251800. If this doesn't make sense then think of it as multiplying by 2251800 "units" where one "unit" is 1/65536: 1/65536 * 2251800 = 34.3597412109375
Tuning Value = Frequency ** 2251800
Worth mentioning here is that we must have the support for LONGS switched on within the compiler in order to use such big numbers with the ** operator. Without support for LONGS switched on the maximum value for the ** operator would be 65535.
Now lets see how this works out on paper. First the value 12500000 gets multiplied by 2251800 and then the two lowest siginificant bytes are discarded. Which, again, is effectively the same as dividing the result by 65536. So what do we get?
12500000 * 2251800 / 65536 = 429496765 which is an error of only 35 LSBs or +0.00000815% compared to our ideal value. That's around 1Hz off target at 12.5Mhz - not bad.
So, all it takes really is the following:
TuningValue VAR LONG Frequency VAR LONG TuningValue = Frequency ** 2251800
Tuning Value for frequency: 50000000Hz: 1717987060.......13862 cycles.
Tuning Value for frequency: 25000000Hz: 858993530.......10160 cycles.
Tuning Value for frequency: 10000000Hz: 343597412.......10156 cycles.
Tuning Value for frequency: 1000000Hz: 34359741.......8516 cycles.
Tuning Value for frequency: 100000Hz: 3435974.......7816 cycles.
Tuning Value for frequency: 10000Hz: 343597.......6988 cycles.
Tuning Value for frequency: 1000Hz: 34359.......6152 cycles.
Let's double check the 10kHz as well. Calculated tuning value: 343597, ideal tuning value = 10000 * 2^32 / 125000000 = 343597.
OK, that turned out to be quite easy but what if we can't use an 18F chip or cant use LONGS for whatever reason? Then it gets a bit more complicated.
First of all we have no easy way to store our target frequency because the largest number we can store in a single variable is 65535. What I've opted to do is to use two WORD sized variables called kHz and Hz. So if we want to set the frequeuncy to 12.5MHz all we need to do is kHz=12500, Hz=0 (12500kHz = 12.5MHz). Not as straight forward as with LONGS but still pretty managable. But what about the calculations?
Fortunately PBP wizard Darrel Taylor found a set of assembly math routines that he tweaked into a format which allows them to, quite easily, be used inside a PBP program. These are called N-Bit_Math and allows us to declare variables of basically any size and perform math operations on the values assigned to them. With the help of these routines I've managed to get the calculated tuning value to within +/-1LSB of the "ideal" value. The obvious drawbacks are that it takes longer to execute, uses more RAM and occupies more space in FLASH.
The first thing we need to do is to specify the number of bytes in our working variables and include the math routines in our program. Then we declare the variables that we're going to need for doing the calculations:
' The actual tuning value is 32bits but we need 40bits for the calculations. PRECISION CON 5 SYSTEM ' 40 bits working variable used for calculations. INCLUDE "N-bit_Math.pbp" ' Include the math routines. ' Create some variables for the math routines to use. MathVar1 VAR BYTE[PRECISION] ' Used by N-Bit math routines MathVar2 VAR BYTE[PRECISION] ' Used by N-Bit math routines MathVar3 VAR BYTE[PRECISION] ' Used by N-Bit math routines Compensator VAR WORD kHz VAR WORD ' 0-65535 kHz Hz VAR WORD ' 0-999 Hz
Now we can start working on the actual calculations. The first thing to do is to create a single "large" variable out of our kHz and Hz variables:
CalculateTuningValue: ' Multiply the kHz variable by 1000 to get from kHz to Hz @ MOVE?WP _kHz, _MathVar1 ; Move WORD to P_VAR @ MOVE?CP 1000, _MathVar2 ; Move 1k to P_VAR @ MATH_MUL _MathVar1, _MathVar2, _MathVar3 ; Multiply, store result in MathVar3 ' Add the Hz variable to the previously calculated result @ MOVE?WP _Hz, _MathVar2 ; Move WORD to MathVar2 @ MATH_ADD _MathVar3, _MathVar2, _MathVar1 ; Add MathVar3 to MathVar2, result in MathVar1
' Multiply the previous result by 2199. 2199 is pretty close 2^32 / 125000000 * 64. ' Actually it's as close as 99,99894% @ MOVE?CP 2199, _MathVar2 ; Load constant to MathVar2 @ MATH_MUL _MathVar1, _MathVar2, _MathVar3 ; Multimply, result in MathVar3 ' Add 32 to the current result. This is done to perform rounding of the value ' to the closest integer when later dividing by 64 instead of simply truncating ' the value as would be the case otherwise. @ MOVE?CP 32, _MathVar2 ; Load constant to MathVar2 @ MATH_ADD _MathVar3, _MathVar2, _MathVar1 ; Add to MathVar3, result in MathVar1 ' Divide the previous result by 64 to end up with the tuning value. @ MOVE?CP 64, _MathVar2 ; Load MathVar2 with the value 64 @ MATH_DIV _MathVar1, _MathVar2, _MathVar3 ; Divide, result now in MathVar3
' By adding the ~36% of the value of the kHz variable to the calculated tuning value ' we can get a little closer to the exact value. Compensator = kHz ** 23815 ' ~36% @ MOVE?WP _Compensator, _MathVar2 ; Move value to P_Var @ MATH_ADD _MathVar3, _MathVar2, _MathVar1 ; Add it to previous result. RETURN
TuningValue VAR MathVar1
The above subroutine will calculate the tuning word to within 1LSB of the "ideal" value, here are some example numbers:
Frequency Ideal Calculated Accuracy % 50MHz 1717986918 1717986919 100,0000001 37MHz 1271310320 1271310320 100,0000000 25Mhz 858993459 858993459 100,0000000 10MHz 343597384 343597383 99,9999997 1MHz 34359738 34359738 100,0000000 125kHz 4294967 4294967 100,0000000 10kHz 343597 343597 100,0000000 1kHz 34360 34359 99,9970896 100Hz 3436 3436 100,0000000
OK, but how do we get the value from the PIC into the AD9850. Well, the AD9850 can be "loaded" in two ways, parallel using a 8bit bus or serial using a clock, a single data line and a strobe line. Using the serial interface fits PBP's SHIFTOUT command just perfectly so that's what I used when testing the routines.
The AD9850 requires 40 bits to shifted in, first the 32 bit frequency tuning value which we've just calculated then a control word (or Byte rather) consisting of 8 bits which are (kind of vaguely) described in the datasheet.
One note though: When the AD9850 powers up it defaults to parallel mode. In order to change it serial you have to send a "serial load enable sequence" which is described in the datasheet. An alternative aproach is to tie pin 2 to GND and pins 3 & 4 to Vcc. This forces the AD9850 into serial load mode when it powers up. (Please note that pin numbers are the pins on the actual chip, if you've bought a finished module you need to "translate" that into the proper pin number on your specific module.)
If using PBPL and LONGS to calculate the tuning value we can send it to the AD9850 with something like:
Control VAR BYTE ' Control byte to send to AD9850 D_Pin VAR PortB.0 ' Data pin Clk_Pin VAR PortB.1 ' Clock pin UD_F VAR PortB.2 ' Load pin Control = 0 SHIFTOUT D_Pin, Clk_Pin, 0, [TuningValue.BYTE0, TuningValue.BYTE1, TuningValue.BYTE2, TuningValue.BYTE3, Control] UD_F = 1 ' Pulse the Update Frequency pin of the AD9850. PauseUS 10 UD_F = 0
SHIFTOUT D_Pin, Clk_Pin, 0, [TuningValue[0], TuningValue[1], TuningValue[2], TuningValue[3], Control]
Finally, here's a scope-shot of the AD9850 generating a 10MHz signal:
/Henrik.
EDIT: Here's a forum thread for questions and discussions around the article.
Re: K42 and Timer Interrupts
Thanks for the explanation.
Ioannis - 28th April 2025, 19:28I misinterpreted these paragraphs. My understanding was to have ASYNC cleared and use Fosc/4.
Ioannis