Spoiler alert: having spent a while trying to fit the DDS calculations into fixed-point numbers stuffed into a single 32 bit `unsigned long`

value, it’s just a whole bunch of nope.

The basic problem, as alluded to earlier, comes from calculations on numbers near 32768.0 and 60000.0 Hz, which require at least 6 significant digits. Indeed, 0.1 Hz at 60 kHz works out to 1.7 ppm, so anything around 0.05 Hz requires seven digits.

The motivation for fixed-point arithmetic, as alluded to earlier, comes from the amount of program memory and RAM blotted up by the BigNumber arbitrary precision arithmetic library, which seems like a much bigger hammer than necessary for this problem.

So, we begin.

Because the basic tuning increment works out to 0.0291 Hz, you can’t adjust the output frequency in nice, clean 0.01 Hz clicks. That doesn’t matter, as long as you know the actual frequency with some accuracy.

Setting up the DDS requires calculations involving numbers near 125.000000 MHz and 2³², both of which sport nine or ten significant figures, depending on how fussy you are about calibrating the actual oscillator frequency and how you go about doing it. Based on a sample of one AD8950 DDS board, the 125 MHz oscillator runs 300 to 400 Hz below its nominal 125 MHz: about 3 ppm low, with a -2.3 Hz/°C tempco responding to a breath. It’s obviously not stable enough for precise calibration, but even 1 ppm = 125 Hz chunks seem awkwardly large.

Many of the doodles below explore various ways to fit integer values up to 125 MHz and fractions down to 0.0291 Hz/count into fixed point numbers with 24 integer bits + 8 fraction bits, perhaps squeezed a few bits either way. Fairly obviously, at least in retrospect, it can’t possibly work: 125×10⁶ requires 28 bits. Worse, 8 fraction bits yield steps of 0.0039, so you start with crappy resolution.

The DDS tuning word is about 2×10⁶ for outputs around 60 kHz, barely covered by 21 bits. You really need at least seven significant figures = 0.1 ppm for those computations, which means the `125 MHz / 2³²`

ratio must carry seven significant figures, which means eight decimal places: 0.02910383 and not a digit less.

En passant, it’s disturbing how many Arduino DDS libraries declare all their variables as `double`

and move on as if the quantities were thereby encoded in 64 bit floating point numbers. Were that the case, I’d agree `125e6 / pow(2.0,32)`

actually meant something, but it ain’t so.

The original non-linear doodles, which, despite containing some values useful in later computations, probably aren’t worth your scrutiny: