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.

Category: Software

General-purpose computers doing something specific

  • 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
  • Bathroom Door Retainer

    The weather got warm enough to open the windows before pollen season started, which led to the front bathroom door slamming closed in the middle of the night when a gusty rainstorm blew through town. After far too many years, I decided this was an annoyance up with which I need no longer put.

    A few minutes with OpenSCAD and Slic3r produces the shape:

    Bathroom Door Retainer - Slic3r
    Bathroom Door Retainer – Slic3r

    It’s basically an extrusion of a 2D shape with a rectangular recess for the door chewed out.

    An hour later, it’s in full effect:

    Bathroom Door Retainer - installed
    Bathroom Door Retainer – installed

    The model now sports a little ball to secure the retainer against the towel bar:

    Bathroom Door Retainer - bump
    Bathroom Door Retainer – bump

    Maybe someday I’ll reprint it.

    That was easy …

    The cast-iron pig sometimes standing guard as a doorstop in the relatively narrow doorway poses a bit of a foot hazard, so he moves into a closet during the off season. He can now remain there, snug and comfy, until a need for ballast arises.

    The OpenSCAD source code as a GitHub Gist:

    // Bathroom Door Retainer
    // Ed Nisley KE4ZNU – May 2017
    Layout = "Show"; // Show Build
    //——-
    //- Extrusion parameters must match reality!
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    //——-
    // Dimensions
    TowelBarSide = 20.5; // towel bar across flat side
    TowelBarAngle = 45; // rotation of top flat from horizontal
    DoorOffset = 16.0; // from towel bar to door
    DoorThick = 36.5;
    WallThick = 4.0; // minimum wall thickness
    RetainerDepth = 10.0; // thickness of retaining notch
    NumSides = 6*4;
    CornerRad = WallThick;
    BarClipOD = TowelBarSide*sqrt(2) + 2*WallThick;
    BarClipRad = BarClipOD/2;
    OAH = RetainerDepth + WallThick;
    module LatchPlan() {
    union() {
    linear_extrude(height=OAH,convexity=4)
    difference() {
    union() {
    circle(d=BarClipOD,$fn=NumSides);
    hull()
    for (i=[0,1], j=[0,1])
    translate([i*(BarClipRad + DoorOffset + DoorThick + WallThick – CornerRad),j*(BarClipRad – CornerRad)])
    circle(r=CornerRad,$fn=4*4);
    }
    rotate(TowelBarAngle) // towel bar shape
    square(size=TowelBarSide,center=true);
    translate([0,-TowelBarSide/sqrt(2)]) // make access slot
    rotate(-TowelBarAngle)
    square(size=[2*TowelBarSide,TowelBarSide],center=false);
    }
    translate([0,-TowelBarSide/sqrt(2),OAH/2])
    rotate([90,0,45])
    sphere(r=TowelBarSide/25,$fn=4*3);
    }
    }
    module Latch() {
    difference() {
    LatchPlan();
    translate([BarClipRad + DoorOffset,-BarClipRad/2,-Protrusion])
    cube([DoorThick,BarClipOD,RetainerDepth + Protrusion],center=false);
    }
    }
    //——-
    // Build it!
    if (Layout == "Show") {
    Latch();
    }
    if (Layout == "Build") {
    translate([0,0,OAH])
    rotate([180,0,0])
    Latch();
    }
  • 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
  • Copying Video Files From Action Cameras to a NAS Drive

    For unknown reasons, a recent VLC update caused it to ignore uppercase file extensions: MP4 and AVI files no longer appear in its directory listings, while mp4 and avi files do. The least-awful solution involved renaming the files after copying them:

    find /mnt/video -name \*AVI -print0 | xargs -0 rename -v -f 's/AVI/avi/'
    find /mnt/video -name \*MP4 -print0 | xargs -0 rename -v -f 's/MP4/mp4/'
    find /mnt/video -name \*THM -print0 | xargs -0 rename -v -f 's/THM/thm/'
    

    Yup, that scans the whole drive every time, which takes care of stray files, manual tweaks, and suchlike. The THM files are useless thumbnails; I should just delete them.

    While I had the hood up, I listed the remaining space on the NAS drive and cleaned up a few misfeatures. I manually delete old video files / directories as needed, usually immediately after the script crashes for lack of room.

    The Sony HDR-AS30V can act as a USB memory device, but it dependably segfaults the ExFAT driver; I now transfer its MicroSD card to an adapter and jam it into the media slot on the monitor, where it works fine.

    Protip: always turn the AS30V on to verify the MicroSD card has seated correctly in its socket. Unfortunately, the socket can also hold Sony’s proprietary Memory Stick Micro cards (32 GB maximum capacity = roadkill), but the dual-use / dual-direction socket isn’t a snug fit around MicroSD cards. You (well, I) can insert a card so it looks fine, while sitting slightly canted and not making proper contact. The camera will kvetch about that and it’s easier to fix with the camera in hand.

    I’ve disabled USB device automounting, as I vastly prefer to handle them manually, so the script asks for permission in order to mount the drives. The transfer requires about an hour, so I’ve extended the time the sudo password remains active.

    The script lets both cards transfer data simultaneously; the Fly6 generally finishes first because it produces less data. That produces a jumbled progress display and the script waits for both drives to finish before continuing.

    The Bash source code as a GitHub Gist:

    #!/bin/sh
    thisdate=$(date –rfc-3339=date)
    echo Date is $thisdate
    date
    # MicroSD cards not automounted
    as30v=/mnt/AS30V
    fly6=/mnt/Fly6
    sudo mount -o uid=ed /dev/sdb1 /mnt/AS30V/
    sudo mount -o uid=ed /dev/sdc1 /mnt/Fly6/
    # IOmega NAS defined as /mnt/video in fstab
    sudo mount /mnt/video
    mkdir /mnt/video/$thisdate
    rsync -ahu –progress $as30v/MP_ROOT/100ANV01/ /mnt/video/$thisdate &
    pid1=$!
    rsync -ahu –progress $fly6 /mnt/video
    date
    rc2=$?
    echo Fly6 RC is $rc2
    echo Waiting for $as30v
    wait $pid1
    rc=$(( $rc2 + $? ))
    date
    echo Overall RC: $rc
    if [ $rc -eq 0 ] ; then
    echo Fix capitalized extensions
    find /mnt/video -name \*AVI -print0 | xargs -0 rename -v -f 's/AVI/avi/'
    find /mnt/video -name \*MP4 -print0 | xargs -0 rename -v -f 's/MP4/mp4/'
    find /mnt/video -name \*THM -print0 | xargs -0 rename -v -f 's/THM/thm/'
    echo Space remaining on NAS drive:
    df -h /mnt/video
    echo Remove files on AS30V
    rm $as30v/MP_ROOT/100ANV01/*
    echo Unmount cards and NAS
    sudo umount $as30v
    sudo umount $fly6
    sudo umount /mnt/video
    else
    echo Whoopsie: $rc
    fi
    view raw savevideo.sh hosted with ❤ by GitHub
  • Arduino Joystick

    A bag of sub-one-dollar resistive joysticks arrived from halfway around the planet:

    Arduino UNO - resistive joystick
    Arduino UNO – resistive joystick

    A quick-and-dirty test routine showed the sticks start out close to VCC/2:

    Welcome to minicom 2.7
    
    OPTIONS: I18n
    Compiled on Feb  7 2016, 13:37:27.
    Port /dev/ttyACM0, 10:23:45
    
    Press CTRL-A Z for help on special keys
    
    Joystick exercise
    Ed Nisley - KE4ZNU - May 2017
    00524 - 00513 - 1
    

    That’s from minicom on the serial port, as the Arduino IDE’s built-in serial monitor ignores bare Carriage Return characters.

    The joystick hat tilts ±25° from its spring-loaded center position, but the active region seems to cover only 15° of that arc, with a 5° dead zone around the center and 5° of overtravel at the limits. This is not a high-resolution instrument intended for fine motor control operations.

    The analog input values range from 0x000 to 0x3FF across the active region. Aim the connector at your tummy to make the axes work the way you’d expect: left / down = minimum, right / up = maximum.

    The delay(100) statements may or may not be needed for good analog input values, depending on some imponderables that seem not to apply for this lashup, but they pace the loop() to a reasonable update rate.

    Pushing the hat toward the PCB activates the simple switch you can see in the picture. It requires an external pullup resistor (hence the INPUT_PULLUP configuration) and reports low = 0 when pressed.

    Those are 0.125 inch (exactly!) holes on a 19.5×26.25 mm grid in a 26.5×34.25 mm PCB. Makes no sense to me, either.

    The trivial Arduino source code as a GitHub Gist:

    // Joystick exercise
    #define JOYX A0
    #define JOYY A1
    #define BUTTON 7
    int JoyX,JoyY;
    boolean Button;
    //– Helper routine for printf()
    int s_putc(char c, FILE *t) {
    Serial.write(c);
    }
    void setup() {
    Serial.begin (9600);
    fdevopen(&s_putc,0); // set up serial output for printf()
    Serial.println ("Joystick exercise");
    Serial.println ("Ed Nisley – KE4ZNU – May 2017");
    pinMode(BUTTON,INPUT_PULLUP);
    }
    void loop() {
    JoyX = analogRead(JOYX);
    delay(100);
    JoyY = analogRead(JOYY);
    delay(100);
    Button = digitalRead(BUTTON);
    printf("%05d – %05d – %1d\r",JoyX,JoyY,Button);
    }
  • Dropbox Tour: To Keep Learning, Click Cancel

    After copying a Digital Machinist column to my Dropbox folder, I went to the site to get the link, discovered they improved the UI, declined a Flash-based tour of the new features, and got this baffling confirmation dialog:

    Dropbox - tour exit dialog
    Dropbox – tour exit dialog

    So. Many. Wrongs.

  • Arduino vs. Significant Figures: BigNumber Library

    The BigNumber library wraps the bc arbitrary precision calculator into a set of Arduino routines that seem like a reasonable basis for DDS calculations requiring more than the half-dozen digits of a floating point number or the limited range of scaled fixed point numbers tucked into an long int.

    Treating programming as an experimental science produces some Arduino source code and its output as a GitHub Gist:

    // BigNumber exercise
    #include "BigNumber.h"
    //– 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 ("BigNumber exercise");
    Serial.println ("Ed Nisley – KE4ZNU – April 2017");
    #define WHOLES 10
    #define FRACTS 10
    printf("Fraction digits: %d\n",FRACTS);
    BigNumber::begin (FRACTS);
    char *pBigNumber;
    #define BUFFLEN (WHOLES + FRACTS)
    char NumString[BUFFLEN];
    BigNumber Tenth = "0.1"; // useful constants
    BigNumber Half = "0.5";
    BigNumber One = 1;
    BigNumber Two = 2;
    BigNumber ThirtyTwoBits = Two.pow(32);
    Serial.println(ThirtyTwoBits);
    BigNumber Oscillator = "125000000";
    Serial.println(Oscillator);
    BigNumber HertzPerCount;
    HertzPerCount = Oscillator / ThirtyTwoBits;
    Serial.println(HertzPerCount);
    BigNumber CountPerHertz;
    CountPerHertz = ThirtyTwoBits / Oscillator;
    Serial.println(CountPerHertz);
    BigNumber TestFreq = "60000";
    Serial.println(TestFreq);
    BigNumber DeltaPhi;
    DeltaPhi = TestFreq * CountPerHertz;
    Serial.println(DeltaPhi);
    long DeltaPhiL;
    DeltaPhiL = DeltaPhi;
    printf("Long: %ld\n",DeltaPhiL);
    Serial.println("0.1 Hz increment …");
    Serial.println(TestFreq + Tenth);
    DeltaPhi = (TestFreq + Tenth) * CountPerHertz;
    Serial.println(DeltaPhi);
    TestFreq = DeltaPhi * HertzPerCount;
    Serial.println(TestFreq);
    Serial.println("Rounding DeltaPhi up …");
    DeltaPhi += Half;
    Serial.println(DeltaPhi);
    TestFreq = DeltaPhi * HertzPerCount;
    Serial.println(TestFreq);
    pBigNumber = DeltaPhi.toString();
    printf("String: %04x → %s\n",pBigNumber,pBigNumber);
    free(pBigNumber);
    DeltaPhiL = DeltaPhi;
    printf("Unsigned: %ld\n",DeltaPhiL);
    pBigNumber = "59999.9";
    TestFreq = pBigNumber;
    Serial.println(TestFreq);
    DeltaPhi = TestFreq * CountPerHertz;
    Serial.println(DeltaPhi);
    Serial.println("Rounding DeltaPhi up …");
    DeltaPhi = TestFreq * CountPerHertz + Half;
    Serial.println(DeltaPhi);
    DeltaPhiL = DeltaPhi;
    int rc = snprintf(NumString,BUFFLEN,"%ld",DeltaPhiL);
    if (rc > 0 && rc < BUFFLEN) {
    printf("String length: %d\n",rc);
    }
    else {
    printf("Whoops: %d for %ld\n",rc,DeltaPhiL);
    strncpy(NumString,"123456789",sizeof(NumString));
    NumString[BUFFLEN-1] = 0;
    printf(" forced: %s\n",NumString);
    }
    printf("Back from string [%s]\n",NumString);
    DeltaPhi = NumString;
    Serial.println(DeltaPhi);
    TestFreq = DeltaPhi * HertzPerCount;
    Serial.println(TestFreq);
    }
    void loop () {
    }
    view raw BigNumTest.ino hosted with ❤ by GitHub
    BigNumber exercise
    Ed Nisley – KE4ZNU – April 2017
    Fraction digits: 10
    4294967296
    125000000
    0.0291038304
    34.3597383680
    60000
    2061584.3020800000
    Long: 2061584
    0.1 Hz increment …
    60000.1000000000
    2061587.7380538368
    60000.0998830384
    Rounding DeltaPhi up …
    2061588.2380538368
    60000.1144349536
    String: 045e → 2061588.2380538368
    Unsigned: 2061588
    59999.9
    2061580.8661061632
    Rounding DeltaPhi up …
    2061581.3661061632
    String length: 7
    Back from string [2061581]
    2061581
    59999.9037798624
    view raw BigNumTest.txt hosted with ❤ by GitHub

    All that happened incrementally, as you might expect, with the intent of seeing how it works, rather than actually doing anything.

    Some musings, in no particular order:

    The library soaks up quite a hunk of program space:

    Sketch uses 13304 bytes (43%) of program storage space. Maximum is 30720 bytes.
    

    I think you could cut that back a little by eliminating unused bc routines, like square root / exponential / modulus.

    That test code also blots up quite a bit of RAM:

    Global variables use 508 bytes (24%) of dynamic memory, leaving 1540 bytes for local variables. Maximum is 2048 bytes.
    

    All the BigNumber variables live inside the setup() function (or whatever it’s called in Arduino-speak), so they count as local variables. They’re four bytes each, excluding the dynamically allocated storage for the actual numbers at roughly a byte per digit. With 10 decimal places for all numbers, plus (maybe) an average of half a dozen integer digits, those ten BigNumbers soak up 200 = 10 × (4 + 16) bytes of precious RAM.

    You can load a BigNumber from an int (not a long) or a string, then export the results to a long or a string. Given that controlling a DDS frequency with a knob involves mostly adding and subtracting a specific step size, strings would probably work fine, using snprintf() to jam the string equivalent of a long into a BigNumber as needed.

    You must have about ten decimal places to hold enough significant figures in the HertzPerCount and CountPerHertz values. The library scale factor evidently forces all the numbers to have at least that many digits, with the decimal point stuck in front of them during string output conversions.

    The biggest integers happen in the Oscillator and ThirtyTwoBits values, with 9 and 10 digits, respectively.

    It looks useful, although I’m uncomfortable with the program space required. I have no way to estimate the program space for a simpleminded DDS controller, other than knowing it’ll be more than I estimate.

    While poking around, however, I discovered the Arduino compiler does provide (limited) support for long long int variables. Given a 64 bit unit for simple arithmetic operations, a simpler implementation of fixed point numbers may be do-able: 32 bits for the integer and fraction should suffice! More on that shortly.