Advertisements

Posts Tagged SDR

Arduino vs. Significant Figures: Useful 64-bit Fixed Point

Devoting eight bytes to every fixed point number may be excessive, but having nine significant figures apiece for the integer and fraction parts pushes the frequency calculations well beyond the limits of the DDS hardware, without involving any floating point library routines. This chunk of code performs a few more calculations using the format laid out earlier and explores a few idioms that may come in handy later.

Rounding the numbers to a specific number of decimal places gets rid of the repeating-digit problem that turns 0.10 into 0.099999:

uint64_t RoundFixedPt(union ll_u TheNumber,unsigned Decimals) {
union ll_u Rnd;

  Rnd.fx_64 = (One.fx_64 / 2) / (pow(10LL,Decimals));
  TheNumber.fx_64 = TheNumber.fx_64 + Rnd.fx_64;
  return TheNumber.fx_64;
}

That pretty well trashes the digits beyond the rounded place, so you shouldn’t display any more of them:

void PrintFixedPtRounded(char *pBuffer,union ll_u FixedPt,unsigned Decimals) {
char *pDecPt;

  FixedPt.fx_64 = RoundFixedPt(FixedPt,Decimals);

  PrintIntegerLL(pBuffer,FixedPt);  // do the integer part

  pBuffer += strlen(pBuffer);       // aim pointer beyond integer

  pDecPt = pBuffer;                 // save the point location
  *pBuffer++ = '.';                 // drop in the decimal point, tick pointer

  PrintFractionLL(pBuffer,FixedPt);

  if (Decimals == 0)
    *pDecPt = 0;                    // 0 places means discard the decimal point
  else
    *(pDecPt + Decimals + 1) = 0;   // truncate string to leave . and Decimals chars
}

Which definitely makes the numbers look prettier:

  Tenth.fx_64 = One.fx_64 / 10;             // Likewise, 0.1
  PrintFixedPt(Buffer,Tenth);
  printf("\n0.1: %s\n",Buffer);
  PrintFixedPtRounded(Buffer,Tenth,9);                    // show rounded value
  printf("0.1 to 9 dec: %s\n",Buffer);

  TestFreq.fx_64 = RoundFixedPt(Tenth,3);                 // show full string after rounding
  PrintFixedPt(Buffer,TestFreq);
  printf("0.1 to 3 dec: %s (full string)\n",Buffer);

  PrintFixedPtRounded(Buffer,Tenth,3);                    // show truncated string with rounded value
  printf("0.1 to 3 dec: %s (truncated string)\n",Buffer);

0.1: 0.099999999
0.1 to 9 dec: 0.100000000
0.1 to 3 dec: 0.100499999 (full string)
0.1 to 3 dec: 0.100 (truncated string)

  CtPerHz.fx_64 = -1;                       // Set up 2^31 - 1, which is close enough
  CtPerHz.fx_64 /= 125 * MEGA;              // divide by nominal oscillator
  PrintFixedPt(Buffer,CtPerHz);
  printf("\nCt/Hz = %s\n",Buffer);

  printf("Rounding: \n");
  for (int d = 9; d >= 0; d--) {
    PrintFixedPtRounded(Buffer,CtPerHz,d);
    printf("     %d: %s\n",d,Buffer);
  }

Ct/Hz = 34.359738367
Rounding:
     9: 34.359738368
     8: 34.35973837
     7: 34.3597384
     6: 34.359738
     5: 34.35974
     4: 34.3597
     3: 34.360
     2: 34.36
     1: 34.4
     0: 34

Multiplying two scaled 64-bit fixed-point numbers should produce a 128-bit result. For all the values we (well, I) care about, the product will fit into a 64-bit result, because the integer parts will always multiply out to less than 232 and we don’t care about more than 32 bits of fraction. This function multiplies two fixed point numbers of the form a.b × c.d by adding up the partial products thusly: ac + bd + ad + bc. The product of the integers ac won’t overflow 32 bits, the cross products ad and bc will always be slightly less than their integer factors, and the fractional product bd will always be less than 1.0.

Soooo, just multiply ’em out as 64-bit integers, shift the products around to align the appropriate parts, and add up the pieces:


uint64_t MultiplyFixedPt(union ll_u Mcand, union ll_u Mplier) {
union ll_u Result;

  Result.fx_64  = ((uint64_t)Mcand.fx_32.high * (uint64_t)Mplier.fx_32.high) << 32; // integer parts (clear fract) 
  Result.fx_64 += ((uint64_t)Mcand.fx_32.low * (uint64_t)Mplier.fx_32.low) >> 32;   // fraction parts (always < 1)
  Result.fx_64 += (uint64_t)Mcand.fx_32.high * (uint64_t)Mplier.fx_32.low;          // cross products
  Result.fx_64 += (uint64_t)Mcand.fx_32.low * (uint64_t)Mplier.fx_32.high;

  return Result.fx_64;
}

This may be a useful way to set magic numbers with a few decimal places, although it does require keeping the decimal point in mind:

  TestFreq.fx_64 = (599999LL * One.fx_64) / 10;           // set 59999.9 kHz differently
  PrintFixedPt(Buffer,TestFreq);
  printf("\nTest frequency: %s\n",Buffer);
  PrintFixedPtRounded(Buffer,TestFreq,1);
  printf("         round: %s\n",Buffer);

Test frequency: 59999.899999999
         round: 59999.9

Contrary to what I thought, computing the CtPerHz coefficient doesn’t require pre-dividing both 232 and the oscillator by 2, thus preventing the former from overflowing a 32 bit integer. All you do is knock the numerator down by one little itty bitty count you’ll never notice:

  CtPerHz.fx_64 = -1;                       // Set up 2^31 - 1, which is close enough
  CtPerHz.fx_64 /= 125 * MEGA;              // divide by nominal oscillator
  PrintFixedPt(Buffer,CtPerHz);
  printf("\nCt/Hz = %s\n",Buffer);

Ct/Hz = 34.359738367

That’s also the largest possible fixed-point number, because unsigned:

  TempFX.fx_64 = -1;
  PrintFixedPt(Buffer,TempFX);
  printf("Max fixed point: %s\n",Buffer);

Max fixed point: 4294967295.999999999

With nine.nine significant figures in the mix, tweaking the 125 MHz oscillator to within 2 Hz will work:

Oscillator tune: CtPerHz
 Oscillator: 125000000.00
 -10 -> 34.359741116
  -9 -> 34.359741116
  -8 -> 34.359740566
  -7 -> 34.359740566
  -6 -> 34.359740017
  -5 -> 34.359740017
  -4 -> 34.359739467
  -3 -> 34.359739467
  -2 -> 34.359738917
  -1 -> 34.359738917
  +0 -> 34.359738367
  +1 -> 34.359738367
  +2 -> 34.359737818
  +3 -> 34.359737818
  +4 -> 34.359737268
  +5 -> 34.359737268
  +6 -> 34.359736718
  +7 -> 34.359736718
  +8 -> 34.359736168
  +9 -> 34.359736168
 +10 -> 34.359735619

So, all in all, this looks good. The vast number of strings in the test program bulk it up beyond reason, but in actual practice I think the code will be smaller than the equivalent floating point version, with more significant figures. Speed isn’t an issue either way, because the delays waiting for the crystal tester to settle down at each frequency step should be larger than any possible computation.

The results were all verified with my trusty HP 50g and HP-15C calculators, both of which wipe the floor with any other way of handling mixed binary / hex / decimal arithmetic. If you do bit-wise calculations, even on an irregular basis, get yourself a SwissMicro DM16L; you can thank me later.

The Arduino source code as a GitHub Gist:

Advertisements

, ,

6 Comments

Arduino vs. Significant Figures: Preliminary 64-bit Fixed Point Exercise

Although it’s not advertised, the Arduino / AVR compiler mostly does the right thing with long long = uint64_t variables: add & subtract work fine, but multiplication & division discard anything that doesn’t fit into 64 bits. Fitting a 32 bit integer and a 32 bit fraction into such a thing should eliminate (most) problems with significant figures.

The general idea is to set up a struct giving access to the two 32 bit halves for direct manipulation, then overlay / union them with a single 64 bit integer for arithmetic purposes:

struct ll_s {
  uint32_t low;
  uint32_t high;
};

union ll_u {
  uint64_t ll_64;
  struct ll_s ll_32;
};

Of course, the integer part still falls one bit shy of holding 2³². At the cost of one bit’s worth of resolution, you can still compute 2³² / 125×10⁶ by pre-dividing each quantity by 2:

2^63 = [80000000 00000000]
2^63 / 125/2 M = [00000022 5c17d04d]

The low-order digit should be 0xe, not 0xd, but I think that’s survivable.

Unfortunately, printf doesn’t handle 64 bit quantities, necessitating some awkward conversion routines. “Printing” to a string seems the least awful method, as I’ll eventually squirt the strings to a display, not send them to the serial port:

void PrintFractionLL(char *pBuffer,uint64_t *pLL) {
  uint64_t Fraction;

  Fraction = (uint32_t)*pLL;                      // copy 32 fraction bits, high order = 0
  Fraction *= ONEGIG;                             // times 10^9 for conversion
  Fraction >>= 32;                                // align integer part in low long
  sprintf(pBuffer,"%09lu",(uint32_t)Fraction);    // convert low long to decimal
}

void PrintIntegerLL(char *pBuffer,uint64_t *pLL) {
  sprintf(pBuffer,"%lu",*((uint32_t *)pLL+1));
}

void PrintDecimalLL(char *pBuffer,uint64_t *pLL) {
  PrintIntegerLL(pBuffer,pLL);
  pBuffer += strlen(pBuffer);       // pointer to end of integer part
  *pBuffer++ = '.';                 // drop in the decimal point, tick pointer
  PrintFractionLL(pBuffer,pLL);
}

The result seems nearly indistinguishable from the Right Answer:

Integer:  34
Fraction: 359738367
Decimal: 34.359738367

This whole mess has a bunch of rough edges, but it looks promising. The code coalesced while fiddling around, so the union notation didn’t get much love at first.

The Arduino source code as a GitHub Gist:

,

3 Comments

Arduino vs Significant Figures: Floating Point Calculations

Herewith, to nail down the reasons why you can’t (or, perhaps, shouldn’t) use Arduino float variables, a small collection of DDS-oid calculations.

Remember that float and double variable are both IEEE 754 single-precision floating point numbers:

Size of float: 4
       double: 4

The Arduino floating-point formatter gags on some values, although they calculate correctly:

2^24: 16777216.000
printf:        ?
2^32: ovf or ovf
2^32: ovf
2^32 / 256: 16777216.000

Don’t add values differing by more than seven orders of magnitude and suspect any results beyond the first half-dozen significant figures:

Oscillator steps: HzPerCt
 Oscillator: 125000000.00
 -25 -> 0.02910382461
 -24 -> 0.02910382461
 -23 -> 0.02910382461
 -22 -> 0.02910382461
 -21 -> 0.02910382461
 -20 -> 0.02910382747
 -19 -> 0.02910382747
 -18 -> 0.02910382747
 -17 -> 0.02910382747
 -16 -> 0.02910382747
 -15 -> 0.02910382747
 -14 -> 0.02910382747
 -13 -> 0.02910382747
 -12 -> 0.02910382747
 -11 -> 0.02910382747
 -10 -> 0.02910382747
  -9 -> 0.02910382747
  -8 -> 0.02910382747
  -7 -> 0.02910382747
  -6 -> 0.02910382747
  -5 -> 0.02910382747
  -4 -> 0.02910383033
  -3 -> 0.02910383033
  -2 -> 0.02910383033
  -1 -> 0.02910383033
  +0 -> 0.02910383033

The Arduino source code as a GitHub Gist:

,

3 Comments

DDS Musings: Arithmetic with 32-bit Fixed Point Numbers

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:

AD9850 DDS Fixed-point Number Doodles - 1

AD9850 DDS Fixed-point Number Doodles – 1

AD9850 DDS Fixed-point Number Doodles - 2

AD9850 DDS Fixed-point Number Doodles – 2

AD9850 DDS Fixed-point Number Doodles - 3

AD9850 DDS Fixed-point Number Doodles – 3

AD9850 DDS Fixed-point Number Doodles - 4

AD9850 DDS Fixed-point Number Doodles – 4

AD9850 DDS Fixed-point Number Doodles - 5

AD9850 DDS Fixed-point Number Doodles – 5

AD9850 DDS Fixed-point Number Doodles - 6

AD9850 DDS Fixed-point Number Doodles – 6

AD9850 DDS Fixed-point Number Doodles - 7

AD9850 DDS Fixed-point Number Doodles – 7

AD9850 DDS Fixed-point Number Doodles - 8

AD9850 DDS Fixed-point Number Doodles – 8

,

12 Comments

Generic AD8950 DDS Modules: Beware Swapped D7 and GND Pins!

Compare this picture:

AD9850 DDS Module - swapped GND D7 pins

AD9850 DDS Module – swapped GND D7 pins

… with any of the doc for the generic AD8950/51 DDS modules you’ll find out on the Interwebs. This snippet from the seller’s schematic will suffice:

AD9850 module schematic - cropped

AD9850 module schematic – cropped

Here’s a closer look at the 2×7 header in the upper left corner:

 

AD9850 module schematic - J5 detail

AD9850 module schematic – J5 detail

Don’t blame me for the blur, the schematic is a JPG.

Compared it with the board in hand:

AD9850 DDS Module - swapped GND D7 pins - detail

AD9850 DDS Module – swapped GND D7 pins – detail

Yup, the D7 and GND pins are reversed.

Some careful probing showed the silkscreen is correct: the pins are, in fact, correctly labeled.

Should you be laying out a PCB in the expectation of using any DDS module from the lowest-price supplier, remember this high truth: Hell hath no fury like that of an unjustified assumption.

Fortunately, I’m hand-wiring the circuit and caught it prior to the smoke test.

, ,

4 Comments

AD8310 Log Amp Module: Video Bandwidth Rolloff

The part I didn’t understand turned out to be the bandwidth of the final output stage = “video bandwidth”, which defaults to 25 MHz. After fixing the input circuitry, a 25 MHz VBW let the output track a 60 kHz input signal just fine:

AD8310 - modified - 60 kHz 1Vpp

AD8310 – modified – 60 kHz 1Vpp

Adding a 56 nF cap across the C6 terminals (just above the AD8310) lowered the VBW to about 1 kHz:

AD8310 Log Amp module - VBW rolloff cap

AD8310 Log Amp module – VBW rolloff cap

Which flattened that sucker right out:

AD8310 - 1 kHz VBW cap - 60 kHz 1.394 V

AD8310 – 1 kHz VBW cap – 60 kHz 1.394 V

The ripple for an absurdly high amplitude 32 kHz signal amounts to 36 mV:

AD8310 - 1 kHz VBW cap - 32 kHz - VBW ripple

AD8310 – 1 kHz VBW cap – 32 kHz – VBW ripple

Firing the tracking generator into the input with a frequency sweep from 100 kHz to 250 MHz shows the low end looks much better:

AD8310 - 1 kHz VBW cap - 100 kHz 250 MHz - 0 dB atten

AD8310 – 1 kHz VBW cap – 100 kHz 250 MHz – 0 dB atten

There’s a slight droop across the sweep that might amount to 50 mV = 2 dB, which I’m inclined to not worry about in this context.

Applying the attenuators once again produces a scale factor of 23.5 mV/dB across 30 dB of RF, but this time the 60 kHz output makes sense, too.

Using the typical output curve from AN-691, that 2.0 V output corresponds to -13 dBm, which sounds about right for the tracking generator (which might really be -10 dBm).

I must calibrate the log amp output to find the actual intercept point (nominally -95 dBm, but could range from -86 to -102) at 60 kHz. The intercept is the extrapolated RF input producing 0 V out, which then acts as an offset for the voltage-to-dBm calculation; you start by finding the slope of the voltage vs. dBm line at some convenient power levels, then solve for dBm with V=0.

So a cheap AD8310 Log Amp module from eBay can work in the LF band, after you rearrange the input circuitry and tweak the chip’s filters. At least now I have a better understanding of what’s going on …

,

2 Comments

AD8310 Log Amp Module: Corrected Input Circuit

After puzzling over the AD8310 Log Amp module’s peculiar frequency response, I hacked up the front end circuitry to match the data sheet’s recommended layout:

AD8310 Log Amp module - revised

AD8310 Log Amp module – revised

Given the intended LF crystal-measurement application, a hulking 51 Ω metal film resistor sprawled across the ground plane will work just fine. All three ceramic caps measure a bit under 1 µF; I intended to solder the input caps atop the existing 10 nF caps, but that didn’t work out well at all.

I should harvest the InLo SMA connector to prevent anyone from mistaking it for an actual input.

With that in place, the log amp output makes more sense:

AD8310 - modified - 100 kHz 150 MHz - 0 dB atten

AD8310 – modified – 100 kHz 150 MHz – 0 dB atten

That trace tops out at 150 MHz, not the previous 500 MHz, but now the response is flat all the way out. The log amp generates plenty of hash when the tracking generator isn’t producing a valid signal.

The 60 kHz response looks different:

AD8310 - modified - 60 kHz 1Vpp

AD8310 – modified – 60 kHz 1Vpp

So it’s really the log amp response to the absolute value of the sine wave (or, more accurately, to the sine wave re-zeroed around Vcc/2), with minimum output at the input’s zero crossings. At 500 mV/div, the log amp says the input varies by 42 dB = 1000 mV/(24 mV/dB), which might actually be about right for a zero-crossing (or zero-approaching absolute value of a) signal; logarithms don’t deal well with zeros.

The AD8310 datasheet  and AN-691 suggest the 2.5 V output corresponds to +10 dBm = 12.5 Vrms input, which flat-out isn’t the case. However, the actual 500 mVpeak = 350 mVrms input is 2.5 mW = +4 dBm, so maybe it’s within spitting distance of being right.

AN-691 recommends 10 µF input caps for “low frequency” use, showing results down to 20 Hz; 1 µF seems to get the circuit close enough to the goal for use near 60 kHz.

It also recommends a cap on the BFIN pin (pin 6) to reduce the output stage bandwidth = “video bandwidth” and improve the overall accuracy, which remains to be done. The datasheet suggests rolling VBW off at 1/10 the minimum input frequency, which would be around 3 kHz for use with 32.768 kHz crystals. The equation, with reference to the internal 3 kΩ bias resistor:

CFILT = 1/(2π 3 kΩ VBW) – 2.1 pF = 18 nF

For a bit more margin, 1 kHz would require 56-ish nF.

The PCB has a convenient pair of pads labeled C6 for that capacitor. This may require protracted rummaging in the SMD capacitor stash.

Rolling off the VBW should reduce the hash on the 100 kHz end of the frequency sweep and filter the 60 kHz response down to pretty much a DC level.

Applying the 10 dB and 20 dB SMA attenuators to the input from the tracking generator and recording the log amp output voltage produces this useful table:

AD8310 Log Amp - mods and log response

AD8310 Log Amp – mods and log response

With the terminating resistor on the correct side of the input caps, the log amp seems to be working the way it should, with an output varying a bit under the nominal 24-ish mV/dB over a 30 dB range.

We need caps! Lots of caps!

A quick search with the obvious keywords suggests nobody else has noticed how these modules work over a reasonable bandwidth. Maybe I’m the first person to use them in the LF band?

, ,

7 Comments