The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

Tag: Arduino

All things Arduino

  • AD9850 DDS Module: OLED Display

    Those little OLED displays might just work:

    Arduino with OLED - simulated DDS
    Arduino with OLED – simulated DDS

    The U8X8 driver produces those double-size bitmap characters; the default 8×8 matrix seem pretty much unreadable on a 0.96 inch OLED at any practical distance from a benchtop instrument. They might be workable on a 1.3 inch white OLED, minus the attractive yellow highlight for the frequency in the top line.

    The OLED uses an SPI interface, although the U8X8 library clobbers my (simpleminded) SPI configuration for the AD9850 DDS and I’ve dummied out the DDS outputs. A soon-to-arrive I²C OLED should resolve that problem; changing the interface from SPI to I²C involves changing the single line of code constructing the driver object, so It Should Just Work.

    The U8X8 driver writes directly to the display, thus eliminating the need for a backing buffer in the Arduino’s painfully limited RAM. I think the library hauls in all possible fonts to support font selection at runtime, even though I need at most two fonts, so it may be worthwhile to hack the unneeded ones from the library (or figure out if I misunderstand the situation and the Flash image includes only the fonts actually used). Each font occupies anywhere from 200 to 2000 bytes, which I’d rather have available for program code. Chopping out unused functions would certainly be less useful.

    The display formatting is a crude hack just to see what the numbers look like:

        int ln = 0;
        u8x8.draw2x2String(0,ln,Buffer);
        ln += 2;
    
        TestFreq.fx_64 = ScanTo.fx_64 - ScanFrom.fx_64;
        PrintFixedPtRounded(Buffer,TestFreq,1);
        u8x8.draw2x2String(0,ln,"W       ");
        u8x8.draw2x2String(2*(8-strlen(Buffer)),ln,Buffer);
        ln += 2;
    
        PrintFixedPtRounded(Buffer,ScanStep,3);
        u8x8.draw2x2String(0,ln,"S       ");
        u8x8.draw2x2String(2*(8-strlen(Buffer)),ln,Buffer);
        ln += 2;
    
        TestFreq.fx_32.high = SCAN_SETTLE;                    // milliseconds
        TestFreq.fx_32.low = 0;
        TestFreq.fx_64 /= KILO;                               // to seconds
        PrintFixedPtRounded(Buffer,TestFreq,3);
        u8x8.draw2x2String(0,ln,"T       ");
        u8x8.draw2x2String(2*(8-strlen(Buffer)),ln,Buffer);
        ln += 2;
    

    Updating the display produces a noticeable and annoying flicker, which isn’t too surprising, so each value should have an “update me” flag to avoid gratuitous writes. Abstracting the display formatting into a table-driven routine might be appropriate, when I need more than one layout, but sheesh.

    I calculate the actual frequency from the 32 bit integer delta phase word written to the DDS, rather than use the achingly precise fixed point value, so a tidy 0.100 Hz frequency step doesn’t produce neat results. Instead, the displayed value will be within ±0.0291 Hz (the frequency resolution) of the desired frequency, which probably makes more sense for the very narrow bandwidths involved in a quartz crystal test gadget.

    Computing the frequency step size makes heavy use of 64 bit integers:

    //  ScanStep.fx_64 = One.fx_64 / 4;                       // 0.25 Hz = 8 or 9 tuning register steps
      ScanStep.fx_64 = One.fx_64 / 10;                    // 0.1 Hz = 3 or 4 tuning register steps
    //  ScanStep.fx_64 = One.fx_64 / 20;                    // 0.05 Hz = 2 or 3 tuning register steps
    //  ScanStep = HzPerCt;                                   // smallest possible frequency step
    

    The fixed point numbers resulting from those divisions will be accurate to nine decimal places; good enough for what I need.

    The sensible way of handling discrete scan width / step size / settling time options is through menus showing the allowed choices, with joystick / joyswitch navigation & selection, rather than keyboard entry. An analog joystick has the distinct advantage of using two analog inputs, not four digital pins, although the U8X8 driver includes a switch-driven menu handler.

    There’s a definite need to log all the values through the serial output for data collection without hand transcription.

    The Arduino source code as a GitHub Gist:

    // OLED display test for 60 kHz crystal tester
    #include <avr/pgmspace.h>
    //#include <SPI.h>
    #include <U8g2lib.h>
    #include <U8x8lib.h>
    // Turn off DDS SPI for display checkout
    #define DOSPI 0
    //———————
    // Pin locations
    // SPI uses hardware support: those pins are predetermined
    #define PIN_HEARTBEAT 9
    #define PIN_DDS_RESET 7
    #define PIN_DDS_LATCH 8
    #define PIN_DISP_SEL 4
    #define PIN_DISP_DC 5
    #define PIN_DISP_RST 6
    #define PIN_SCK 13
    #define PIN_MISO 12
    #define PIN_MOSI 11
    #define PIN_SS 10
    char Buffer[10+1+10+1]; // string buffer for long long conversions
    #define GIGA 1000000000LL
    #define MEGA 1000000LL
    #define KILO 1000LL
    struct ll_fx {
    uint32_t low; // fractional part
    uint32_t high; // integer part
    };
    union ll_u {
    uint64_t fx_64;
    struct ll_fx fx_32;
    };
    union ll_u CtPerHz; // will be 2^32 / 125 MHz
    union ll_u HzPerCt; // will be 125 MHz / 2^32
    union ll_u One; // 1.0 as fixed point
    union ll_u Tenth; // 0.1 as fixed point
    union ll_u TenthHzCt; // 0.1 Hz in counts
    // All nominal values are integers for simplicity
    #define OSC_NOMINAL (125 * MEGA)
    #define OSC_OFFSET_NOMINAL (-344LL)
    union ll_u OscillatorNominal; // nominal oscillator frequency
    union ll_u OscOffset; // … and offset, which will be signed 64-bit value
    union ll_u Oscillator; // true oscillator frequency with offset
    union ll_u CenterFreq; // center of scan width
    #define SCAN_WIDTH 6
    #define SCAN_SETTLE 2000
    union ll_u ScanFrom, ScanTo, ScanFreq, ScanStep; // frequency scan settings
    uint8_t ScanStepCounter;
    union ll_u TestFreq,TestCount; // useful variables
    //U8X8_SH1106_128X64_NONAME_4W_HW_SPI u8x8(PIN_DISP_SEL, PIN_DISP_DC , PIN_DISP_RST);
    U8X8_SH1106_128X64_NONAME_4W_SW_SPI u8x8(PIN_SCK, PIN_MOSI, PIN_DISP_SEL, PIN_DISP_DC , PIN_DISP_RST);
    //U8X8_SH1106_128X64_NONAME_HW_I2C u8x8(U8X8_PIN_NONE);
    #define HEARTBEAT_MS 3000
    unsigned long MillisNow,MillisThen;
    //———–
    // Useful functions
    // Pin twiddling
    void TogglePin(char bitpin) {
    digitalWrite(bitpin,!digitalRead(bitpin)); // toggle the bit based on previous output
    }
    void PulsePin(char bitpin) {
    TogglePin(bitpin);
    TogglePin(bitpin);
    }
    // SPI I/O
    void EnableSPI(void) {
    digitalWrite(PIN_SS,HIGH); // set SPI into Master mode
    SPCR |= 1 << SPE;
    }
    void DisableSPI(void) {
    SPCR &= ~(1 << SPE);
    }
    void WaitSPIF(void) {
    while (! (SPSR & (1 << SPIF))) {
    TogglePin(PIN_HEARTBEAT);
    TogglePin(PIN_HEARTBEAT);
    continue;
    }
    }
    byte SendRecSPI(byte Dbyte) { // send one byte, get another in exchange
    SPDR = Dbyte;
    WaitSPIF();
    return SPDR; // SPIF will be cleared
    }
    // DDS module
    void EnableDDS(void) {
    digitalWrite(PIN_DDS_LATCH,LOW); // ensure proper startup
    digitalWrite(PIN_DDS_RESET,HIGH); // minimum reset pulse 40 ns, not a problem
    digitalWrite(PIN_DDS_RESET,LOW);
    delayMicroseconds(1); // max latency 100 ns, not a problem
    DisableSPI(); // allow manual control of outputs
    digitalWrite(PIN_SCK,LOW); // ensure clean SCK pulse
    PulsePin(PIN_SCK); // … to latch hardwired config bits
    PulsePin(PIN_DDS_LATCH); // load hardwired config bits = begin serial mode
    EnableSPI(); // turn on hardware SPI controls
    SendRecSPI(0x00); // shift in serial config bits
    PulsePin(PIN_DDS_LATCH); // load serial config bits
    }
    // Write delta phase count to DDS
    // This comes from the integer part of a 64-bit scaled value
    void WriteDDS(uint32_t DeltaPhase) {
    SendRecSPI((byte)DeltaPhase); // low-order byte first
    SendRecSPI((byte)(DeltaPhase >> 8));
    SendRecSPI((byte)(DeltaPhase >> 16));
    SendRecSPI((byte)(DeltaPhase >> 24));
    SendRecSPI(0x00); // 5 MSBs = phase = 0, 3 LSBs must be zero
    PulsePin(PIN_DDS_LATCH); // write data to DDS
    }
    //———–
    // Round scaled fixed point to specific number of decimal places: 0 through 8
    // You should display the value with only Decimals characters beyond the point
    // Must calculate rounding value as separate variable to avoid mystery error
    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;
    }
    //———–
    // Multiply two unsigned scaled fixed point numbers without overflowing a 64 bit value
    // The product of the two integer parts mut be < 2^32
    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;
    }
    //———–
    // Long long print-to-buffer helpers
    // Assumes little-Endian layout
    void PrintHexLL(char *pBuffer,union ll_u FixedPt) {
    sprintf(pBuffer,"%08lx %08lx",FixedPt.fx_32.high,FixedPt.fx_32.low);
    }
    // converts all 9 decimal digits of fraction, which should suffice
    void PrintFractionLL(char *pBuffer,union ll_u FixedPt) {
    union ll_u Fraction;
    Fraction.fx_64 = FixedPt.fx_32.low; // copy 32 fraction bits, high order = 0
    Fraction.fx_64 *= GIGA; // times 10^9 for conversion
    Fraction.fx_64 >>= 32; // align integer part in low long
    sprintf(pBuffer,"%09lu",Fraction.fx_32.low); // convert low long to decimal
    }
    void PrintIntegerLL(char *pBuffer,union ll_u FixedPt) {
    sprintf(pBuffer,"%lu",FixedPt.fx_32.high);
    }
    void PrintFixedPt(char *pBuffer,union ll_u FixedPt) {
    PrintIntegerLL(pBuffer,FixedPt); // do the integer part
    pBuffer += strlen(pBuffer); // aim pointer beyond integer
    *pBuffer++ = '.'; // drop in the decimal point, tick pointer
    PrintFractionLL(pBuffer,FixedPt);
    }
    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); // do the fraction
    if (Decimals == 0)
    *pDecPt = 0; // 0 places means discard the decimal point
    else
    *(pDecPt + Decimals + 1) = 0; // truncate string to leave . and Decimals chars
    }
    //———–
    // Calculate useful "constants" from oscillator info
    // Args are integer constants in Hz
    void CalcOscillator(uint32_t Base,uint32_t Offset) {
    union ll_u Temp;
    Oscillator.fx_32.high = Base + Offset; // get true osc frequency from integers
    Oscillator.fx_32.low = 0;
    HzPerCt.fx_32.low = Oscillator.fx_32.high; // divide oscillator by 2^32 with simple shifting
    HzPerCt.fx_32.high = 0;
    CtPerHz.fx_64 = -1; // Compute (2^32 – 1) / oscillator
    CtPerHz.fx_64 /= (uint64_t)Oscillator.fx_32.high; // remove 2^32 scale factor from divisor
    TenthHzCt.fx_64 = MultiplyFixedPt(Tenth,CtPerHz); // 0.1 Hz as delta-phase count
    #if 0
    printf("Inputs: %ld = %ld%+ld\n",Base+Offset,Base,Offset);
    PrintFixedPt(Buffer,Oscillator);
    printf("Osc freq: %s\n",Buffer);
    PrintFixedPt(Buffer,HzPerCt);
    printf("Hz/Ct: %s\n",Buffer);
    PrintFixedPt(Buffer,CtPerHz);
    printf("Ct/Hz: %s\n",Buffer);
    PrintFixedPt(Buffer,TenthHzCt);
    printf("0.1 Hz Ct: %s",Buffer);
    #endif
    }
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    //———–
    void setup ()
    {
    pinMode(PIN_HEARTBEAT,OUTPUT);
    digitalWrite(PIN_HEARTBEAT,HIGH); // show we got here
    Serial.begin (115200);
    fdevopen(&s_putc,0); // set up serial output for printf()
    Serial.println (F("DDS OLED exercise"));
    Serial.println (F("Ed Nisley – KE4ZNU – May 2017\n"));
    // DDS module controls
    pinMode(PIN_DDS_LATCH,OUTPUT);
    digitalWrite(PIN_DDS_LATCH,LOW);
    pinMode(PIN_DDS_RESET,OUTPUT);
    digitalWrite(PIN_DDS_RESET,HIGH);
    // Light up the display
    Serial.println("Initialize OLED");
    u8x8.begin();
    u8x8.setPowerSave(0);
    u8x8.setFont(u8x8_font_pxplusibmcga_f);
    u8x8.draw2x2String(0,0,"OLEDTest");
    u8x8.drawString(0,2,"Ed Nisley");
    u8x8.drawString(0,3," KE4ZNU");
    u8x8.drawString(0,4,"May 2017");
    // configure SPI hardware
    #if DOSPI
    SPCR = B01110001; // Auto SPI: no int, enable, LSB first, master, + edge, leading, f/16
    SPSR = B00000000; // not double data rate
    pinMode(PIN_SS,OUTPUT);
    digitalWrite(PIN_SCK,HIGH);
    pinMode(PIN_SCK,OUTPUT);
    digitalWrite(PIN_SCK,LOW);
    pinMode(PIN_MOSI,OUTPUT);
    digitalWrite(PIN_MOSI,LOW);
    pinMode(PIN_MISO,INPUT_PULLUP);
    #endif
    TogglePin(PIN_HEARTBEAT); // show we got here
    // Calculate useful constants
    One.fx_64 = 1LL << 32; // Set up 1.0, a very useful constant
    Tenth.fx_64 = One.fx_64 / 10; // Likewise, 0.1
    // Set oscillator "constants"
    CalcOscillator(OSC_NOMINAL,OSC_OFFSET_NOMINAL);
    TogglePin(PIN_HEARTBEAT); // show we got here
    // Set the crystal-under-test nominal frequency
    CenterFreq.fx_64 = One.fx_64 * (60 * KILO);
    #if 1
    PrintFixedPtRounded(Buffer,CenterFreq,1);
    printf("Center: %s\n",Buffer);
    #endif
    // Set up scan limits based on center frequency
    ScanFrom.fx_64 = CenterFreq.fx_64 – SCAN_WIDTH * (One.fx_64 >> 1);
    ScanTo.fx_64 = CenterFreq.fx_64 + SCAN_WIDTH * (One.fx_64 >> 1);
    ScanFreq = ScanFrom; // start scan at lower limit
    // ScanStep.fx_64 = One.fx_64 / 4; // 0.25 Hz = 8 or 9 tuning register steps
    ScanStep.fx_64 = One.fx_64 / 10; // 0.1 Hz = 3 or 4 tuning register steps
    // ScanStep.fx_64 = One.fx_64 / 20; // 0.05 Hz = 2 or 3 tuning register steps
    // ScanStep = HzPerCt; // smallest possible frequency step
    #if 1
    Serial.println("\nScan limits");
    PrintFixedPtRounded(Buffer,ScanFrom,1);
    printf(" from: %11s\n",Buffer);
    PrintFixedPtRounded(Buffer,ScanFreq,1);
    printf(" at: %11s\n",Buffer);
    PrintFixedPtRounded(Buffer,ScanTo,1);
    printf(" to: %11s\n",Buffer);
    PrintFixedPtRounded(Buffer,ScanStep,3);
    printf(" step: %s\n",Buffer);
    #endif
    // Wake up and load the DDS
    #if DOSPI
    TestCount.fx_64 = MultiplyFixedPt(ScanFreq,CtPerHz);
    EnableDDS();
    WriteDDS(TestCount.fx_32.high);
    #endif
    delay(2000);
    u8x8.clearDisplay();
    u8x8.setFont(u8x8_font_artossans8_r);
    Serial.println("\nStartup done!");
    MillisThen = millis();
    }
    //———–
    void loop () {
    MillisNow = millis();
    if ((MillisNow – MillisThen) >= SCAN_SETTLE) {
    TogglePin(PIN_HEARTBEAT);
    MillisThen = MillisNow;
    PrintFixedPtRounded(Buffer,ScanFreq,2);
    TestCount.fx_64 = MultiplyFixedPt(ScanFreq,CtPerHz);
    // printf("%12s -> %9ld\n",Buffer,TestCount.fx_32.high);
    #if DOSPI
    WriteDDS(TestCount.fx_32.high);
    #endif
    TestCount.fx_32.low = 0; // truncate to integer
    TestFreq.fx_64 = MultiplyFixedPt(TestCount,HzPerCt); // recompute frequency
    PrintFixedPtRounded(Buffer,TestFreq,2);
    int ln = 0;
    u8x8.draw2x2String(0,ln,Buffer);
    ln += 2;
    TestFreq.fx_64 = ScanTo.fx_64 – ScanFrom.fx_64;
    PrintFixedPtRounded(Buffer,TestFreq,1);
    u8x8.draw2x2String(0,ln,"W ");
    u8x8.draw2x2String(2*(8-strlen(Buffer)),ln,Buffer);
    ln += 2;
    PrintFixedPtRounded(Buffer,ScanStep,3);
    u8x8.draw2x2String(0,ln,"S ");
    u8x8.draw2x2String(2*(8-strlen(Buffer)),ln,Buffer);
    ln += 2;
    TestFreq.fx_32.high = SCAN_SETTLE; // milliseconds
    TestFreq.fx_32.low = 0;
    TestFreq.fx_64 /= KILO; // to seconds
    PrintFixedPtRounded(Buffer,TestFreq,3);
    u8x8.draw2x2String(0,ln,"T ");
    u8x8.draw2x2String(2*(8-strlen(Buffer)),ln,Buffer);
    ln += 2;
    ScanFreq.fx_64 += ScanStep.fx_64;
    if (ScanFreq.fx_64 > (ScanTo.fx_64 + ScanStep.fx_64 / 2)) {
    ScanFreq = ScanFrom;
    }
    }
    }
    view raw DDSOLEDTest.ino hosted with ❤ by GitHub
  • AD9850 DDS Module: Temperature Sensitivity

    While tinkering with the SPI code for the AD9850 DDS module, I wrote down the ambient temperature and the frequency tweak required to zero-beat the 10 MHz output with the GPS-locked oscillator. A quick-n-dirty plot summarizing two days of randomly timed observations ensued:

    AD9850 DDS Module - Frequency vs Temperature
    AD9850 DDS Module – Frequency vs Temperature

    The frequency offset comes from the tweak required to zero-beat the output by adjusting the initial oscillator error: a positive tweak produces a smaller count-per-hertz coefficient and reduces the output frequency. As a result, the thermal coefficient sign is backwards, because increasing temperature raises the oscillator frequency and reduces the necessary tweak. I think so, anyway; you know how these things can go wrong. More automation and reliable data would be a nice touch.

    Foam sheets formed a block around the DDS module, isolating it from stray air currents and reducing the clock oscillator’s sensitivity:

    AD9850 DDS module - foam insulation
    AD9850 DDS module – foam insulation

    I used the ambient temperature, because the thermocouple inside the foam (not shown in the picture) really wasn’t making good contact with the board, the readings didn’t make consistent sense, and, given a (nearly) constant power dissipation, the (average) oscillator temperature inside the foam should track ambient temperature with a constant offset. I think so, anyway.

    The coefficient works out to 0.02 ppm/°C. Of course, the initial frequency offset is something like -400 Hz = 3 ppm, so we’re not dealing with lab-grade instrumentation here.

  • AD9850 DDS Module: Hardware Assisted SPI and Fixed-point Frequency Stepping

    Having conjured fixed-point arithmetic into working, the next step is to squirt data to the AD9850 DDS chip. Given that using the Arduino’s hardware-assisted SPI doesn’t require much in the way of software, the wiring looks like this:

    Nano to DDS schematic
    Nano to DDS schematic

    Not much to it, is there? For reference, it looks a lot like you’d expect:

    AD9850 DDS Module - swapped GND D7 pins
    AD9850 DDS Module – swapped GND D7 pins

    There’s no point in building an asynchronous interface with SPI interrupts and callbacks and all that rot, because squirting one byte at 1 Mb/s (a reasonable speed for hand wiring; the AD9850 can accept bits at 140+ MHz) doesn’t take all that long and it’s easier to have the low-level code stall until the hardware finishes:

    #define PIN_HEARTBEAT    9          // added LED
    
    #define PIN_RESET_DDS    7          // Reset DDS module
    #define PIN_LATCH_DDS    8          // Latch serial data into DDS
    
    #define PIN_SCK        13          // SPI clock (also Arduino LED!)
    #define PIN_MISO      12          // SPI data input
    #define PIN_MOSI      11          // SPI data output
    #define PIN_SS        10          // SPI slave select - MUST BE OUTPUT = HIGH
    
    void EnableSPI(void) {
      digitalWrite(PIN_SS,HIGH);        // set SPI into Master mode
      SPCR |= 1 << SPE;
    }
    
    void DisableSPI(void) {
      SPCR &= ~(1 << SPE);
    }
    
    void WaitSPIF(void) {
      while (! (SPSR & (1 << SPIF))) {
        TogglePin(PIN_HEARTBEAT);
        TogglePin(PIN_HEARTBEAT);
        continue;
      }
    }
    
    byte SendRecSPI(byte Dbyte) {           // send one byte, get another in exchange
      SPDR = Dbyte;
      WaitSPIF();
      return SPDR;                          // SPIF will be cleared
    }
    

    With that in hand, turning on the SPI hardware and waking up the AD9850 looks like this:

    void EnableDDS(void) {
    
      digitalWrite(PIN_LATCH_DDS,LOW);          // ensure proper startup
    
      digitalWrite(PIN_RESET_DDS,HIGH);         // minimum reset pulse 40 ns, not a problem
      digitalWrite(PIN_RESET_DDS,LOW);
      delayMicroseconds(1);                     // max latency 100 ns, not a problem
    
      DisableSPI();                             // allow manual control of outputs
      digitalWrite(PIN_SCK,LOW);                // ensure clean SCK pulse
      PulsePin(PIN_SCK);                        //  ... to latch hardwired config bits
      PulsePin(PIN_LATCH_DDS);                  // load hardwired config bits = begin serial mode
    
      EnableSPI();                              // turn on hardware SPI controls
      SendRecSPI(0x00);                         // shift in serial config bits
      PulsePin(PIN_LATCH_DDS);                  // load serial config bits
    }
    

    Given 32 bits of delta phase data and knowing the DDS output phase angle is always zero, you just drop five bytes into a hole in the floor labeled “SPI” and away they go:

    void WriteDDS(uint32_t DeltaPhase) {
    
      SendRecSPI((byte)DeltaPhase);             // low-order byte first
      SendRecSPI((byte)(DeltaPhase >> 8));
      SendRecSPI((byte)(DeltaPhase >> 16));
      SendRecSPI((byte)(DeltaPhase >> 24));
    
      SendRecSPI(0x00);                         // 5 MSBs = phase = 0, 3 LSBs must be zero
    
      PulsePin(PIN_LATCH_DDS);                  // write data to DDS
    }
    

    In order to have something to watch, the loop() increments the output frequency in steps of 0.1 Hz between 10.0 MHz ± 3 Hz, as set by the obvious global variables:

          PrintFixedPtRounded(Buffer,ScanFreq,1);
    
          TestCount.fx_64 = MultiplyFixedPt(ScanFreq,CtPerHz);
          printf("%12s -> %9ld\n",Buffer,TestCount.fx_32.high);
    
          WriteDDS(TestCount.fx_32.high);
    
          ScanFreq.fx_64 += ScanStep.fx_64;
    
          if (ScanFreq.fx_64 > (ScanTo.fx_64 + ScanStep.fx_64 / 2)) {
            ScanFreq = ScanFrom;
            Serial.println("Scan restart");
          }
    

    Which produces output like this:

    DDS SPI exercise
    Ed Nisley - KE4ZNU - May 2017
    
    Inputs: 124999656 = 125000000-344
    Osc freq: 124999656.000000000
    Hz/Ct: 0.029103750
    Ct/Hz: 34.359832926
    0.1 Hz Ct: 3.435983287
    Test frequency:  10000000.0000
    Delta phase: 343598329
    
    Scan limits
     from:   9999997.0
       at:  10000000.0
       to:  10000003.0
    
    Sleeping for a while ...
    
    Startup done!
    
    Begin scanning
    
      10000000.0 -> 343598329
      10000000.1 -> 343598332
      10000000.2 -> 343598336
      10000000.3 -> 343598339
      10000000.4 -> 343598343
      10000000.5 -> 343598346
      10000000.6 -> 343598349
      10000000.7 -> 343598353
      10000000.8 -> 343598356
      10000000.9 -> 343598360
      10000001.0 -> 343598363
      10000001.1 -> 343598367
      10000001.2 -> 343598370
      10000001.3 -> 343598373
    <<< snippage >>>
    

    The real excitement happens while watching the DDS output crawl across the scope screen in relation to the 10 MHz signal from the Z8301 GPS-locked reference:

    DDS GPS - 10 MHz -48 Hz offset - zero beat
    DDS GPS – 10 MHz -48 Hz offset – zero beat

    The DDS sine in the upper trace is zero-beat against the GPS reference in the lower trace. There’s no hardware interlock, but they’re dead stationary during whatever DDS output step produces exactly 10.0000000 MHz. The temperature coefficient seems to be around 2.4 Hz/°C, so the merest whiff of air changes the frequency by more than 0.1 Hz.

    It’s kinda like watching paint dry or a 3D printer at work, but it’s my paint: I like it a lot!

    The Arduino source code as a GitHub Gist:

    // SPI exercise for 60 kHz crystal tester
    #include <avr/pgmspace.h>
    //———————
    // Pin locations
    // SPI uses hardware support: those pins are predetermined
    #define PIN_HEARTBEAT 9 // added LED
    #define PIN_RESET_DDS 7 // Reset DDS module
    #define PIN_LATCH_DDS 8 // Latch serial data into DDS
    #define PIN_SCK 13 // SPI clock (also Arduino LED!)
    #define PIN_MISO 12 // SPI data input
    #define PIN_MOSI 11 // SPI data output
    #define PIN_SS 10 // SPI slave select – MUST BE OUTPUT = HIGH
    char Buffer[10+1+10+1]; // string buffer for long long conversions
    #define GIGA 1000000000LL
    #define MEGA 1000000LL
    #define KILO 1000LL
    struct ll_fx {
    uint32_t low; // fractional part
    uint32_t high; // integer part
    };
    union ll_u {
    uint64_t fx_64;
    struct ll_fx fx_32;
    };
    union ll_u CtPerHz; // will be 2^32 / 125 MHz
    union ll_u HzPerCt; // will be 125 MHz / 2^32
    union ll_u One; // 1.0 as fixed point
    union ll_u Tenth; // 0.1 as fixed point
    union ll_u TenthHzCt; // 0.1 Hz in counts
    // All nominal values are integers for simplicity
    #define OSC_NOMINAL (125 * MEGA)
    #define OSC_OFFSET_NOMINAL (-344LL)
    union ll_u OscillatorNominal; // nominal oscillator frequency
    union ll_u OscOffset; // … and offset, which will be signed 64-bit value
    union ll_u Oscillator; // true oscillator frequency with offset
    #define SCAN_WIDTH 6
    #define SCAN_SETTLE 2000
    union ll_u ScanFrom, ScanTo, ScanFreq, ScanStep; // frequency scan settings
    union ll_u TestFreq,TestCount; // useful variables
    #define HEARTBEAT_MS 3000
    unsigned long MillisNow,MillisThen;
    //———–
    // Useful functions
    // Pin twiddling
    void TogglePin(char bitpin) {
    digitalWrite(bitpin,!digitalRead(bitpin)); // toggle the bit based on previous output
    }
    void PulsePin(char bitpin) {
    TogglePin(bitpin);
    TogglePin(bitpin);
    }
    // SPI I/O
    void EnableSPI(void) {
    digitalWrite(PIN_SS,HIGH); // set SPI into Master mode
    SPCR |= 1 << SPE;
    }
    void DisableSPI(void) {
    SPCR &= ~(1 << SPE);
    }
    void WaitSPIF(void) {
    while (! (SPSR & (1 << SPIF))) {
    TogglePin(PIN_HEARTBEAT);
    TogglePin(PIN_HEARTBEAT);
    continue;
    }
    }
    byte SendRecSPI(byte Dbyte) { // send one byte, get another in exchange
    SPDR = Dbyte;
    WaitSPIF();
    return SPDR; // SPIF will be cleared
    }
    // DDS module
    void EnableDDS(void) {
    digitalWrite(PIN_LATCH_DDS,LOW); // ensure proper startup
    digitalWrite(PIN_RESET_DDS,HIGH); // minimum reset pulse 40 ns, not a problem
    digitalWrite(PIN_RESET_DDS,LOW);
    delayMicroseconds(1); // max latency 100 ns, not a problem
    DisableSPI(); // allow manual control of outputs
    digitalWrite(PIN_SCK,LOW); // ensure clean SCK pulse
    PulsePin(PIN_SCK); // … to latch hardwired config bits
    PulsePin(PIN_LATCH_DDS); // load hardwired config bits = begin serial mode
    EnableSPI(); // turn on hardware SPI controls
    SendRecSPI(0x00); // shift in serial config bits
    PulsePin(PIN_LATCH_DDS); // load serial config bits
    }
    // Write delta phase count to DDS
    // This comes from the integer part of a 64-bit scaled value
    void WriteDDS(uint32_t DeltaPhase) {
    SendRecSPI((byte)DeltaPhase); // low-order byte first
    SendRecSPI((byte)(DeltaPhase >> 8));
    SendRecSPI((byte)(DeltaPhase >> 16));
    SendRecSPI((byte)(DeltaPhase >> 24));
    SendRecSPI(0x00); // 5 MSBs = phase = 0, 3 LSBs must be zero
    PulsePin(PIN_LATCH_DDS); // write data to DDS
    }
    //———–
    // Round scaled fixed point to specific number of decimal places: 0 through 8
    // You should display the value with only Decimals characters beyond the point
    // Must calculate rounding value as separate variable to avoid mystery error
    uint64_t RoundFixedPt(union ll_u TheNumber,unsigned Decimals) {
    union ll_u Rnd;
    // printf(" round before: %08lx %08lx\n",TheNumber.fx_32.high,TheNumber.fx_32.low);
    Rnd.fx_64 = (One.fx_64 / 2) / (pow(10LL,Decimals));
    // printf(" incr: %08lx %08lx\n",Rnd.fx_32.high,Rnd.fx_32.low);
    TheNumber.fx_64 = TheNumber.fx_64 + Rnd.fx_64;
    // printf(" after: %08lx %08lx\n",TheNumber.fx_32.high,TheNumber.fx_32.low);
    return TheNumber.fx_64;
    }
    //———–
    // Multiply two unsigned scaled fixed point numbers without overflowing a 64 bit value
    // The product of the two integer parts mut be < 2^32
    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;
    }
    //———–
    // Long long print-to-buffer helpers
    // Assumes little-Endian layout
    void PrintHexLL(char *pBuffer,union ll_u FixedPt) {
    sprintf(pBuffer,"%08lx %08lx",FixedPt.fx_32.high,FixedPt.fx_32.low);
    }
    // converts all 9 decimal digits of fraction, which should suffice
    void PrintFractionLL(char *pBuffer,union ll_u FixedPt) {
    union ll_u Fraction;
    Fraction.fx_64 = FixedPt.fx_32.low; // copy 32 fraction bits, high order = 0
    Fraction.fx_64 *= GIGA; // times 10^9 for conversion
    Fraction.fx_64 >>= 32; // align integer part in low long
    sprintf(pBuffer,"%09lu",Fraction.fx_32.low); // convert low long to decimal
    }
    void PrintIntegerLL(char *pBuffer,union ll_u FixedPt) {
    sprintf(pBuffer,"%lu",FixedPt.fx_32.high);
    }
    void PrintFixedPt(char *pBuffer,union ll_u FixedPt) {
    PrintIntegerLL(pBuffer,FixedPt); // do the integer part
    pBuffer += strlen(pBuffer); // aim pointer beyond integer
    *pBuffer++ = '.'; // drop in the decimal point, tick pointer
    PrintFractionLL(pBuffer,FixedPt);
    }
    void PrintFixedPtRounded(char *pBuffer,union ll_u FixedPt,unsigned Decimals) {
    char *pDecPt;
    //char *pBase;
    // pBase = pBuffer;
    FixedPt.fx_64 = RoundFixedPt(FixedPt,Decimals);
    PrintIntegerLL(pBuffer,FixedPt); // do the integer part
    // printf(" Buffer int: [%s]\n",pBase);
    pBuffer += strlen(pBuffer); // aim pointer beyond integer
    pDecPt = pBuffer; // save the point location
    *pBuffer++ = '.'; // drop in the decimal point, tick pointer
    PrintFractionLL(pBuffer,FixedPt);
    // printf(" Buffer all: [%s]\n",pBase);
    if (Decimals == 0)
    *pDecPt = 0; // 0 places means discard the decimal point
    else
    *(pDecPt + Decimals + 1) = 0; // truncate string to leave . and Decimals chars
    // printf(" Buffer end: [%s]\n",pBase);
    }
    //———–
    // Calculate useful "constants" from oscillator info
    // Args are integer constants in Hz
    void CalcOscillator(uint32_t Base,uint32_t Offset) {
    union ll_u Temp;
    Oscillator.fx_32.high = Base + Offset; // get true osc frequency from integers
    Oscillator.fx_32.low = 0;
    HzPerCt.fx_32.low = Oscillator.fx_32.high; // divide oscillator by 2^32 with simple shifting
    HzPerCt.fx_32.high = 0;
    CtPerHz.fx_64 = -1; // Compute (2^32 – 1) / oscillator
    CtPerHz.fx_64 /= (uint64_t)Oscillator.fx_32.high; // remove 2^32 scale factor from divisor
    TenthHzCt.fx_64 = MultiplyFixedPt(Tenth,CtPerHz); // 0.1 Hz as delta-phase count
    if (true) {
    printf("Inputs: %ld = %ld%+ld\n",Base+Offset,Base,Offset);
    PrintFixedPt(Buffer,Oscillator);
    printf("Osc freq: %s\n",Buffer);
    PrintFixedPt(Buffer,HzPerCt);
    printf("Hz/Ct: %s\n",Buffer);
    PrintFixedPt(Buffer,CtPerHz);
    printf("Ct/Hz: %s\n",Buffer);
    PrintFixedPt(Buffer,TenthHzCt);
    printf("0.1 Hz Ct: %s",Buffer);
    }
    }
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    //———–
    void setup ()
    {
    pinMode(PIN_HEARTBEAT,OUTPUT);
    digitalWrite(PIN_HEARTBEAT,HIGH); // show we got here
    Serial.begin (115200);
    fdevopen(&s_putc,0); // set up serial output for printf()
    Serial.println (F("DDS SPI exercise"));
    Serial.println (F("Ed Nisley – KE4ZNU – May 2017\n"));
    // DDS module controls
    pinMode(PIN_LATCH_DDS,OUTPUT);
    digitalWrite(PIN_LATCH_DDS,LOW);
    pinMode(PIN_RESET_DDS,OUTPUT);
    digitalWrite(PIN_RESET_DDS,HIGH);
    // configure SPI hardware
    SPCR = B01110001; // Auto SPI: no int, enable, LSB first, master, + edge, leading, f/16
    SPSR = B00000000; // not double data rate
    pinMode(PIN_SS,OUTPUT);
    digitalWrite(PIN_SCK,HIGH);
    pinMode(PIN_SCK,OUTPUT);
    digitalWrite(PIN_SCK,LOW);
    pinMode(PIN_MOSI,OUTPUT);
    digitalWrite(PIN_MOSI,LOW);
    pinMode(PIN_MISO,INPUT_PULLUP);
    TogglePin(PIN_HEARTBEAT); // show we got here
    // Calculate useful constants
    One.fx_64 = 1LL << 32; // Set up 1.0, a very useful constant
    Tenth.fx_64 = One.fx_64 / 10; // Likewise, 0.1
    // Calculate oscillator "constants"
    CalcOscillator(OSC_NOMINAL,OSC_OFFSET_NOMINAL);
    TogglePin(PIN_HEARTBEAT); // show we got here
    // Set up 10 MHz calibration output
    TestFreq.fx_64 = One.fx_64 * (10 * MEGA);
    PrintFixedPtRounded(Buffer,TestFreq,4);
    printf("\nTest frequency: %s\n",Buffer);
    TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert delta phase counts
    TestCount.fx_64 = RoundFixedPt(TestCount,0); // … to nearest integer
    PrintFixedPt(Buffer,TestCount);
    printf("Delta phase: %lu\n",TestCount.fx_32.high);
    // Set up scan limits
    ScanFreq = TestFreq;
    ScanStep.fx_64 = One.fx_64 / 10; // 0.1 Hz = 3 or 4 tuning register steps
    ScanFrom.fx_64 = ScanFreq.fx_64 – SCAN_WIDTH * (One.fx_64 >> 1); // centered on test freq
    ScanTo.fx_64 = ScanFreq.fx_64 + SCAN_WIDTH * (One.fx_64 >> 1);
    Serial.println("\nScan limits");
    PrintFixedPtRounded(Buffer,ScanFrom,1);
    printf(" from: %11s\n",Buffer);
    PrintFixedPtRounded(Buffer,ScanFreq,1);
    printf(" at: %11s\n",Buffer);
    PrintFixedPtRounded(Buffer,ScanTo,1);
    printf(" to: %11s\n",Buffer);
    // Wake up and load the DDS
    EnableDDS();
    WriteDDS(TestCount.fx_32.high);
    Serial.println("\nSleeping for a while …");
    delay(15 * 1000);
    Serial.println("\nStartup done!");
    Serial.println("\nBegin scanning\n");
    MillisThen = millis();
    }
    //———–
    void loop () {
    MillisNow = millis();
    if ((MillisNow – MillisThen) >= SCAN_SETTLE) {
    TogglePin(PIN_HEARTBEAT);
    MillisThen = MillisNow;
    if (true) {
    PrintFixedPtRounded(Buffer,ScanFreq,1);
    TestCount.fx_64 = MultiplyFixedPt(ScanFreq,CtPerHz);
    printf("%12s -> %9ld\n",Buffer,TestCount.fx_32.high);
    WriteDDS(TestCount.fx_32.high);
    ScanFreq.fx_64 += ScanStep.fx_64;
    if (ScanFreq.fx_64 > (ScanTo.fx_64 + ScanStep.fx_64 / 2)) {
    ScanFreq = ScanFrom;
    Serial.println("Scan restart");
    }
    }
    }
    }
    view raw DDSSPITest.ino hosted with ❤ by GitHub
    DDS SPI exercise
    Ed Nisley – KE4ZNU – May 2017
    Inputs: 124999656 = 125000000-344
    Osc freq: 124999656.000000000
    Hz/Ct: 0.029103750
    Ct/Hz: 34.359832926
    0.1 Hz Ct: 3.435983287
    Test frequency: 10000000.0000
    Delta phase: 343598329
    Scan limits
    from: 9999997.0
    at: 10000000.0
    to: 10000003.0
    Sleeping for a while …
    Startup done!
    Begin scanning
    10000000.0 -> 343598329
    10000000.1 -> 343598332
    10000000.2 -> 343598336
    10000000.3 -> 343598339
    10000000.4 -> 343598343
    10000000.5 -> 343598346
    10000000.6 -> 343598349
    10000000.7 -> 343598353
    10000000.8 -> 343598356
    10000000.9 -> 343598360
    10000001.0 -> 343598363
    10000001.1 -> 343598367
    10000001.2 -> 343598370
    10000001.3 -> 343598373
    view raw DDSSPITest.txt hosted with ❤ by GitHub
  • 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^32 - 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^32 - 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:

    // Fixed point exercise for 60 kHz crystal tester
    #include <avr/pgmspace.h>
    char Buffer[10+1+10+1]; // string buffer for long long conversions
    #define GIGA 1000000000LL
    #define MEGA 1000000LL
    #define KILO 1000LL
    struct ll_fx {
    uint32_t low;
    uint32_t high;
    };
    union ll_u {
    uint64_t fx_64;
    struct ll_fx fx_32;
    };
    union ll_u CtPerHz; // will be 2^32 / 125 MHz
    union ll_u HzPerCt; // will be 125 MHz / 2^32
    union ll_u One; // 1.0 as fixed point
    union ll_u Tenth; // 0.1 as fixed point
    union ll_u TenthHzCt; // 0.1 Hz in counts
    union ll_u Oscillator; // nominal oscillator frequency
    union ll_u OscOffset; // oscillator calibration offset
    union ll_u TestFreq,TestCount; // useful variables
    union ll_u TempFX;
    //———–
    // Round scaled fixed point to specific number of decimal places: 0 through 8
    // You should display the value with only Decimals characters beyond the point
    // Must calculate rounding value as separate variable to avoid mystery error
    uint64_t RoundFixedPt(union ll_u TheNumber,unsigned Decimals) {
    union ll_u Rnd;
    // printf(" round before: %08lx %08lx\n",TheNumber.fx_32.high,TheNumber.fx_32.low);
    Rnd.fx_64 = (One.fx_64 / 2) / (pow(10LL,Decimals));
    // printf(" incr: %08lx %08lx\n",Rnd.fx_32.high,Rnd.fx_32.low);
    TheNumber.fx_64 = TheNumber.fx_64 + Rnd.fx_64;
    // printf(" after: %08lx %08lx\n",TheNumber.fx_32.high,TheNumber.fx_32.low);
    return TheNumber.fx_64;
    }
    //———–
    // Multiply two unsigned scaled fixed point numbers without overflowing a 64 bit value
    // The product of the two integer parts mut be < 2^32
    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;
    }
    //———–
    // Long long print-to-buffer helpers
    // Assumes little-Endian layout
    void PrintHexLL(char *pBuffer,union ll_u FixedPt) {
    sprintf(pBuffer,"%08lx %08lx",FixedPt.fx_32.high,FixedPt.fx_32.low);
    }
    // converts all 9 decimal digits of fraction, which should suffice
    void PrintFractionLL(char *pBuffer,union ll_u FixedPt) {
    union ll_u Fraction;
    Fraction.fx_64 = FixedPt.fx_32.low; // copy 32 fraction bits, high order = 0
    Fraction.fx_64 *= GIGA; // times 10^9 for conversion
    Fraction.fx_64 >>= 32; // align integer part in low long
    sprintf(pBuffer,"%09lu",Fraction.fx_32.low); // convert low long to decimal
    }
    void PrintIntegerLL(char *pBuffer,union ll_u FixedPt) {
    sprintf(pBuffer,"%lu",FixedPt.fx_32.high);
    }
    void PrintFixedPt(char *pBuffer,union ll_u FixedPt) {
    PrintIntegerLL(pBuffer,FixedPt); // do the integer part
    pBuffer += strlen(pBuffer); // aim pointer beyond integer
    *pBuffer++ = '.'; // drop in the decimal point, tick pointer
    PrintFractionLL(pBuffer,FixedPt);
    }
    void PrintFixedPtRounded(char *pBuffer,union ll_u FixedPt,unsigned Decimals) {
    char *pDecPt;
    //char *pBase;
    // pBase = pBuffer;
    FixedPt.fx_64 = RoundFixedPt(FixedPt,Decimals);
    PrintIntegerLL(pBuffer,FixedPt); // do the integer part
    // printf(" Buffer int: [%s]\n",pBase);
    pBuffer += strlen(pBuffer); // aim pointer beyond integer
    pDecPt = pBuffer; // save the point location
    *pBuffer++ = '.'; // drop in the decimal point, tick pointer
    PrintFractionLL(pBuffer,FixedPt);
    // printf(" Buffer all: [%s]\n",pBase);
    if (Decimals == 0)
    *pDecPt = 0; // 0 places means discard the decimal point
    else
    *(pDecPt + Decimals + 1) = 0; // truncate string to leave . and Decimals chars
    // printf(" Buffer end: [%s]\n",pBase);
    }
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    //———–
    void setup ()
    {
    Serial.begin (115200);
    fdevopen(&s_putc,0); // set up serial output for printf()
    Serial.println (F("DDS calculation exercise"));
    Serial.println (F("Ed Nisley – KE4ZNU – May 2017\n"));
    // set up useful constants
    TempFX.fx_64 = -1;
    PrintFixedPt(Buffer,TempFX);
    printf("Max fixed point: %s\n",Buffer);
    One.fx_32.high = 1; // Set up 1.0, a very useful constant
    PrintFixedPt(Buffer,One);
    printf("\n1.0: %s\n",Buffer);
    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);
    CtPerHz.fx_64 = -1; // Set up 2^32 – 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);
    }
    HzPerCt.fx_64 = 125 * MEGA; // 125 MHz / 2^32, without actually shifting!
    PrintFixedPt(Buffer,HzPerCt);
    printf("\nHz/Ct: %s\n",Buffer);
    TenthHzCt.fx_64 = MultiplyFixedPt(Tenth,CtPerHz); // 0.1 Hz as delta-phase count
    PrintFixedPt(Buffer,TenthHzCt);
    printf("\n0.1 Hz as ct: %s\n",Buffer);
    printf("Rounding: \n");
    for (int d = 9; d >= 0; d–) {
    PrintFixedPtRounded(Buffer,TenthHzCt,d);
    printf(" %d: %s\n",d,Buffer);
    }
    // Try out various DDS computations
    TestFreq.fx_64 = One.fx_64 * (60 * KILO); // set 60 kHz
    PrintFixedPt(Buffer,TestFreq);
    printf("\nTest frequency: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestFreq,1);
    printf(" round: %s\n",Buffer);
    TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
    PrintFixedPt(Buffer,TestCount);
    printf("Delta phase ct: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestCount,0);
    printf(" round to int: %s\n",Buffer);
    TestFreq.fx_64 += Tenth.fx_64; // set 60000.1 kHz
    PrintFixedPt(Buffer,TestFreq);
    printf("\nTest frequency: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestFreq,1);
    printf(" round: %s\n",Buffer);
    TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
    PrintFixedPt(Buffer,TestCount);
    printf("Delta phase ct: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestCount,0);
    printf(" round to int: %s\n",Buffer);
    TestFreq.fx_64 -= Tenth.fx_64 * 2; // set 59999.9 kHz
    PrintFixedPt(Buffer,TestFreq);
    printf("\nTest frequency: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestFreq,1);
    printf(" round: %s\n",Buffer);
    TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
    PrintFixedPt(Buffer,TestCount);
    printf("Delta phase ct: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestCount,0);
    printf(" round to int: %s\n",Buffer);
    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);
    TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
    PrintFixedPt(Buffer,TestCount);
    printf("Delta phase ct: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestCount,0);
    printf(" round to int: %s\n",Buffer);
    TempFX.fx_64 = RoundFixedPt(TestCount,0); // compute frequency from integer count
    TestFreq.fx_64 = MultiplyFixedPt(TempFX,HzPerCt);
    PrintFixedPt(Buffer,TestFreq);
    printf("Int ct -> freq: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestFreq,1);
    printf(" round: %s\n",Buffer);
    TestFreq.fx_64 = One.fx_64 * (10 * MEGA); // set 10 MHz
    PrintFixedPt(Buffer,TestFreq);
    printf("\nTest frequency: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestFreq,1);
    printf(" round: %s\n",Buffer);
    TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
    PrintFixedPt(Buffer,TestCount);
    printf("Delta phase ct: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestCount,0);
    printf(" round to int: %s\n",Buffer);
    TempFX.fx_64 = RoundFixedPt(TestCount,0); // compute frequency from integer count
    TestFreq.fx_64 = MultiplyFixedPt(TempFX,HzPerCt);
    PrintFixedPt(Buffer,TestFreq);
    printf("Int ct -> freq: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestFreq,1);
    printf(" round: %s\n",Buffer);
    TestFreq.fx_64 = One.fx_64 * (10 * MEGA); // set 10 MHz + 0.1 Hz
    TestFreq.fx_64 += Tenth.fx_64;
    PrintFixedPt(Buffer,TestFreq);
    printf("\nTest frequency: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestFreq,1);
    printf(" round: %s\n",Buffer);
    TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
    PrintFixedPt(Buffer,TestCount);
    printf("Delta phase ct: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestCount,0);
    printf(" round to int: %s\n",Buffer);
    TempFX.fx_64 = RoundFixedPt(TestCount,0); // compute frequency from integer count
    TestFreq.fx_64 = MultiplyFixedPt(TempFX,HzPerCt);
    PrintFixedPt(Buffer,TestFreq);
    printf("Int ct -> freq: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestFreq,1);
    printf(" round: %s\n",Buffer);
    TestFreq.fx_64 = One.fx_64 * (10 * MEGA); // set 10 MHz – 0.1 Hz
    TestFreq.fx_64 -= Tenth.fx_64;
    PrintFixedPt(Buffer,TestFreq);
    printf("\nTest frequency: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestFreq,1);
    printf(" round: %s\n",Buffer);
    TestCount.fx_64 = MultiplyFixedPt(TestFreq,CtPerHz); // convert to counts
    PrintFixedPt(Buffer,TestCount);
    printf("Delta phase ct: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestCount,0);
    printf(" round to int: %s\n",Buffer);
    TempFX.fx_64 = RoundFixedPt(TestCount,0); // compute frequency from integer count
    TestFreq.fx_64 = MultiplyFixedPt(TempFX,HzPerCt);
    PrintFixedPt(Buffer,TestFreq);
    printf("Int ct -> freq: %s\n",Buffer);
    PrintFixedPtRounded(Buffer,TestFreq,1);
    printf(" round: %s\n",Buffer);
    Oscillator.fx_64 = One.fx_64 * (125 * MEGA);
    Serial.println("Oscillator tune: CtPerHz");
    PrintFixedPtRounded(Buffer,Oscillator,2);
    printf(" Oscillator: %s\n",Buffer);
    for (int i=-10; i<=10; i++) {
    OscOffset.fx_64 = i * One.fx_64;
    CtPerHz.fx_64 = 1LL << 63;
    CtPerHz.fx_64 /= (Oscillator.fx_64 + OscOffset.fx_64) >> 33;
    PrintFixedPt(Buffer,CtPerHz);
    printf(" %+3d -> %s\n",i,Buffer);
    }
    }
    //———–
    void loop () {
    }
    view raw DDSCalcTest.ino hosted with ❤ by GitHub
    DDS calculation exercise
    Ed Nisley – KE4ZNU – May 2017
    Max fixed point: 4294967295.999999999
    1.0: 1.000000000
    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)
    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
    Hz/Ct: 0.029103830
    0.1 Hz as ct: 3.435973831
    Rounding:
    9: 3.435973832
    8: 3.43597383
    7: 3.4359738
    6: 3.435974
    5: 3.43597
    4: 3.4360
    3: 3.436
    2: 3.44
    1: 3.4
    0: 3
    Test frequency: 60000.000000000
    round: 60000.0
    Delta phase ct: 2061584.302070550
    round to int: 2061584
    Test frequency: 60000.099999999
    round: 60000.1
    Delta phase ct: 2061587.738044382
    round to int: 2061588
    Test frequency: 59999.900000000
    round: 59999.9
    Delta phase ct: 2061580.866096718
    round to int: 2061581
    Test frequency: 59999.899999999
    round: 59999.9
    Delta phase ct: 2061580.866096710
    round to int: 2061581
    Int ct -> freq: 59999.914551639
    round: 59999.9
    Test frequency: 10000000.000000000
    round: 10000000.0
    Delta phase ct: 343597383.678425103
    round to int: 343597384
    Int ct -> freq: 10000000.014506079
    round: 10000000.0
    Test frequency: 10000000.099999999
    round: 10000000.1
    Delta phase ct: 343597387.114398935
    round to int: 343597387
    Int ct -> freq: 10000000.114506079
    round: 10000000.1
    Test frequency: 9999999.900000000
    round: 9999999.9
    Delta phase ct: 343597380.242451271
    round to int: 343597380
    Int ct -> freq: 9999999.914506079
    round: 9999999.9
    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
    view raw DDSCalcTest.txt hosted with ❤ by GitHub
  • 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:

    Long long integer exercise
    Ed Nisley – KE4ZNU – May 2017
    Long long size = 8 bytes
    .. value = [12345678 9abcdef0]
    divided result = [01234567 89abcdef]
    2^32 = [00000001 00000000]
    125M = [00000000 07735940]
    2^32 / 125M = [00000000 00000022]
    Scaled fixed point tests
    2^63 = [80000000 00000000]
    2^63 / 125/2 M = [00000022 5c17d04d]
    Integer: 34
    Fraction: 359738367
    Decimal: 34.359738367
    Hz Per Ct: 0.029103830
    60000: 60000.000000000
    60 kHz as count: 2061584.302070550
    0.1 Hz as count: 3.435973836
    60000.1 Hz as count: 2061587.738044387
    60000.1 Hz from count: 60000.078519806
    60000.2 Hz as count: 2061591.174018223
    60000.2 Hz from count: 60000.194935128
    60000.2 Hz rnd count: 60000.224038958
    Union size: 8
    TestFreq: [0000ea60 395a9e00]
    Union ll: [0000ea60 395a9e00]
    From union: 60000.224038958
    Buffer length: 15
    Trunc dec: 60000.224
    view raw LongLong.txt hosted with ❤ by GitHub
    // Long long integer exercise for 60 kHz crystal tester
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    char Buffer[10+1+10]; // string buffer for long long conversions
    #define ONEGIG 1000000000LL
    uint64_t CtPerHz; // will be 2^32 / 125 MHz
    uint64_t HzPerCt; // will be 125 MHz / 2^32
    uint64_t TenthHzCt; // 0.1 Hz in counts
    struct ll_s {
    uint32_t low;
    uint32_t high;
    };
    union ll_u {
    uint64_t ll_64;
    struct ll_s ll_32;
    };
    //———–
    // Long long print-to-buffer helpers
    // Assumes little-Endian layout
    void PrintHexLL(char *pBuffer,uint64_t *pLL) {
    sprintf(pBuffer,"%08lx %08lx",*((uint32_t *)pLL+1),(uint32_t)*pLL);
    }
    // converts 9 decimal digits
    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);
    }
    //———–
    void setup ()
    {
    Serial.begin (115200);
    fdevopen(&s_putc,0); // set up serial output for printf()
    Serial.println ("Long long integer exercise");
    Serial.println ("Ed Nisley – KE4ZNU – May 2017");
    unsigned long long LongLong;
    uint64_t LongLong2;
    LongLong = 0x123456789abcdef0LL;
    printf("Long long size = %d bytes\n",sizeof(LongLong));
    printf(" .. value = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)(LongLong & 0x00000000ffffffffLL));
    LongLong /= 16;
    printf(" divided result = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)LongLong);
    LongLong = 1LL << 32;
    printf(" 2^32 = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)LongLong);
    LongLong2 = 125000000LL;
    printf(" 125M = [%08lx %08lx]\n",(long)(LongLong2 >> 32),(long)LongLong2);
    LongLong /= LongLong2;
    printf("2^32 / 125M = [%08lx %08lx]\n",(long)(LongLong >> 32),(long)LongLong);
    Serial.println("Scaled fixed point tests");
    uint64_t TestFreq,TestCount;
    CtPerHz = 1LL << 63; // start with 2^31 to avoid overflow
    PrintHexLL(Buffer,&CtPerHz);
    printf("2^63 = [%s]\n",Buffer);
    CtPerHz /= 125000000LL / 2; // divided by 2 to to match 2^31
    PrintHexLL(Buffer,&CtPerHz);
    printf("2^63 / 125/2 M = [%s]\n",Buffer);
    PrintIntegerLL(Buffer,&CtPerHz);
    printf("Integer: %s\n",Buffer);
    PrintFractionLL(Buffer,&CtPerHz);
    printf("Fraction: %s\n",Buffer);
    PrintDecimalLL(Buffer,&CtPerHz);
    printf("Decimal: %s\n",Buffer);
    HzPerCt = 125000000LL; // 125 MHz / 2^32, directly to fraction part
    PrintDecimalLL(Buffer,&HzPerCt);
    printf("Hz Per Ct: %s\n",Buffer);
    TestFreq = 60000LL << 32;
    PrintDecimalLL(Buffer,&TestFreq);
    printf("60000: %s\n",Buffer);
    TestCount = 60000LL * CtPerHz;
    PrintDecimalLL(Buffer,&TestCount);
    printf("60 kHz as count: %s\n",Buffer);
    TenthHzCt = CtPerHz / 10; // 0.1 Hz as counts
    PrintDecimalLL(Buffer,&TenthHzCt);
    printf("0.1 Hz as count: %s\n",Buffer);
    TestCount += TenthHzCt;
    PrintDecimalLL(Buffer,&TestCount);
    printf("60000.1 Hz as count: %s\n",Buffer);
    TestFreq = (TestCount >> 32) * HzPerCt;
    PrintDecimalLL(Buffer,&TestFreq);
    printf("60000.1 Hz from count: %s\n",Buffer);
    TestCount = (60000LL * CtPerHz) + (2 * TenthHzCt);
    PrintDecimalLL(Buffer,&TestCount);
    printf("60000.2 Hz as count: %s\n",Buffer);
    TestFreq = (TestCount >> 32) * HzPerCt;
    PrintDecimalLL(Buffer,&TestFreq);
    printf("60000.2 Hz from count: %s\n",Buffer);
    TestFreq = ((TestCount + (TenthHzCt / 2)) >> 32) * HzPerCt;
    PrintDecimalLL(Buffer,&TestFreq);
    printf("60000.2 Hz rnd count: %s\n",Buffer);
    union ll_u LongLongUnion, TempLLU;
    printf("Union size: %d\n",sizeof(LongLongUnion));
    LongLongUnion.ll_64 = TestFreq;
    PrintHexLL(Buffer,&TestFreq);
    printf("TestFreq: [%s]\n",Buffer);
    PrintHexLL(Buffer,&LongLongUnion.ll_64);
    printf("Union ll: [%s]\n",Buffer);
    TempLLU.ll_64 = LongLongUnion.ll_32.low;
    TempLLU.ll_64 *= ONEGIG; // times 10^9 for conversion
    TempLLU.ll_64 >>= 32; // align integer part in low long
    sprintf(Buffer,"%lu.%09lu",LongLongUnion.ll_32.high,TempLLU.ll_32.low);
    printf("From union: %s\n",Buffer);
    sprintf(Buffer,"%lu.%09lu",LongLongUnion.ll_32.high,TempLLU.ll_32.low);
    printf("Buffer length: %d\n",strlen(Buffer));
    Buffer[strlen(Buffer) – 9 + 3] = 0;
    printf(" Trunc dec: %s\n",Buffer);
    }
    //———–
    void loop () {
    }
  • 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:

    float TwoTo32, TwoTo24;
    float CtPerHz, HzPerCt;
    double Double;
    float Oscillator,Frequency;
    unsigned long int DeltaPhase;
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    //– Calculate delta phase from output & oscillator
    uint32_t CalculateDP(float Freq, float Osc) {
    uint32_t DP;
    Serial.print("Freq: ");
    Serial.print(Freq,3);
    Serial.print(" Osc: ");
    Serial.print(Osc,3);
    DP = Freq * TwoTo32 / Osc;
    printf(" -> DP: %lu = 0x%08lx\n",DP,DP);
    return DP;
    }
    //– Calculate frequency from delta phase & oscillator
    float CalculateFreq(uint32_t DP, float Osc) {
    float Freq;
    Freq = DP * Osc / TwoTo32;
    printf("DP: %lu = 0x%08lx ",DP,DP);
    Serial.print(" Osc: ");
    Serial.print(Osc,3);
    Serial.print(" -> Freq: ");
    Serial.println(Freq,3);
    return Freq;
    }
    //——————
    void setup() {
    Serial.begin(115200);
    fdevopen(&s_putc,0); // set up serial output for printf()
    Serial.println (F("DDS Numeric Values"));
    Serial.println (F("Ed Nisley – KE4ZNU – May 2017\n"));
    printf("Size of float: %u\n",sizeof(TwoTo32));
    printf(" double: %u\n",sizeof(Double));
    TwoTo24 = pow(2.0,24);
    Serial.print("2^24: ");
    Serial.println(TwoTo24,3);
    printf("printf: %8.8f\n",TwoTo24);
    TwoTo32 = pow(2,32);
    Serial.print("2^32: ");
    Serial.print(TwoTo32,3);
    Serial.print(" or ");
    Serial.println(TwoTo32,3);
    TwoTo32 = 4294967296.0;
    Serial.print("2^32: ");
    Serial.println(TwoTo32,0);
    Serial.print("2^32 / 256: ");
    Serial.println(TwoTo32 / 256.0,3);
    Oscillator = 125e6;
    Serial.print("Oscillator: ");
    Serial.println(Oscillator,3);
    Frequency = 10e6;
    Serial.print("Frequency: ");
    Serial.println(Frequency,3);
    HzPerCt = Oscillator / TwoTo32;
    Serial.print("HzPerCt: ");
    Serial.println(HzPerCt,9);
    CtPerHz = TwoTo32 / Oscillator;
    Serial.print("CtPerHz: ");
    Serial.println(CtPerHz,9);
    Frequency = 10e6 + 0.0;
    DeltaPhase = Frequency * CtPerHz;
    Serial.print(Frequency,3);
    printf(": Delta Phase: %lu = %08lx\n",DeltaPhase,DeltaPhase);
    Frequency = 10e6 + 0.5;
    DeltaPhase = Frequency * CtPerHz;
    Serial.print(Frequency,3);
    printf(": Delta Phase: %lu = %08lx\n",DeltaPhase,DeltaPhase);
    Frequency = 10e6 + 0.8;
    DeltaPhase = Frequency * CtPerHz;
    Serial.print(Frequency,3);
    printf(": Delta Phase: %lu = %08lx\n",DeltaPhase,DeltaPhase);
    Frequency = 10e6 + 1.0;
    DeltaPhase = Frequency * CtPerHz;
    Serial.print(Frequency,3);
    printf(": Delta Phase: %lu = %08lx\n",DeltaPhase,DeltaPhase);
    Serial.println("Oscillator steps: HzPerCt");
    Serial.print(" Oscillator: ");
    Serial.println(Oscillator,2);
    for (int i=-25; i<=25; i++) {
    HzPerCt = (Oscillator + i) / TwoTo32;
    printf(" %+3d -> ",i);
    Serial.println(HzPerCt,11);
    }
    Serial.println("Oscillator tune: CtPerHz ");
    Serial.print(" Oscillator: ");
    Serial.println(Oscillator,2);
    for (int i=-25; i<=25; i++) {
    CtPerHz = TwoTo32 / (Oscillator + i);
    printf(" %+3d -> ",i);
    Serial.println(CtPerHz,11);
    }
    // printf("CtPerHz int: %lu = %08lx\n",long(CtPerHz),long(CtPerHz));
    }
    //——————
    void loop() {}
    view raw FloatTest.ino hosted with ❤ by GitHub
    DDS Numeric Values
    Ed Nisley – KE4ZNU – May 2017
    Size of float: 4
    double: 4
    2^24: 16777216.000
    printf: ?
    2^32: ovf or ovf
    2^32: ovf
    2^32 / 256: 16777216.000
    Oscillator: 125000000.000
    Frequency: 10000000.000
    HzPerCt: 0.029103830
    CtPerHz: 34.359737396
    10000000.000: Delta Phase: 343597376 = 147ae140
    10000000.000: Delta Phase: 343597376 = 147ae140
    10000001.000: Delta Phase: 343597408 = 147ae160
    10000001.000: Delta Phase: 343597408 = 147ae160
    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
    +1 -> 0.02910383033
    +2 -> 0.02910383033
    +3 -> 0.02910383033
    +4 -> 0.02910383033
    +5 -> 0.02910383224
    +6 -> 0.02910383224
    +7 -> 0.02910383224
    +8 -> 0.02910383224
    +9 -> 0.02910383224
    +10 -> 0.02910383224
    +11 -> 0.02910383224
    +12 -> 0.02910383224
    +13 -> 0.02910383224
    +14 -> 0.02910383224
    +15 -> 0.02910383224
    +16 -> 0.02910383224
    +17 -> 0.02910383224
    +18 -> 0.02910383224
    +19 -> 0.02910383224
    +20 -> 0.02910383224
    +21 -> 0.02910383701
    +22 -> 0.02910383701
    +23 -> 0.02910383701
    +24 -> 0.02910383701
    +25 -> 0.02910383701
    Oscillator tune: CtPerHz
    Oscillator: 125000000.00
    -25 -> 34.35974502563
    -24 -> 34.35974502563
    -23 -> 34.35974502563
    -22 -> 34.35974502563
    -21 -> 34.35974502563
    -20 -> 34.35974121093
    -19 -> 34.35974121093
    -18 -> 34.35974121093
    -17 -> 34.35974121093
    -16 -> 34.35974121093
    -15 -> 34.35974121093
    -14 -> 34.35974121093
    -13 -> 34.35974121093
    -12 -> 34.35974121093
    -11 -> 34.35974121093
    -10 -> 34.35974121093
    -9 -> 34.35974121093
    -8 -> 34.35974121093
    -7 -> 34.35974121093
    -6 -> 34.35974121093
    -5 -> 34.35974121093
    -4 -> 34.35973739624
    -3 -> 34.35973739624
    -2 -> 34.35973739624
    -1 -> 34.35973739624
    +0 -> 34.35973739624
    +1 -> 34.35973739624
    +2 -> 34.35973739624
    +3 -> 34.35973739624
    +4 -> 34.35973739624
    +5 -> 34.35973739624
    +6 -> 34.35973739624
    +7 -> 34.35973739624
    +8 -> 34.35973739624
    +9 -> 34.35973739624
    +10 -> 34.35973739624
    +11 -> 34.35973739624
    +12 -> 34.35973358154
    +13 -> 34.35973358154
    +14 -> 34.35973358154
    +15 -> 34.35973358154
    +16 -> 34.35973358154
    +17 -> 34.35973358154
    +18 -> 34.35973358154
    +19 -> 34.35973358154
    +20 -> 34.35973358154
    +21 -> 34.35973358154
    +22 -> 34.35973358154
    +23 -> 34.35973358154
    +24 -> 34.35973358154
    +25 -> 34.35973358154
    view raw FloatTest.txt hosted with ❤ by GitHub
  • 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