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

  • LF Crystal Tester: LM75 Temperature Sensor

    A strip of NXP (née Philips plus Freescale, including the part of Motorola that didn’t become ON) LM75A I²C temperature sensors arrived from beyond the horizon. To see if they worked, I soldered thin wires directly to the SO-8 pins, entombed it in Kapton tape to prevent spitzensparken, and jammed it under the foam insulation atop the AD9850 DDS module:

    AD9850 DDS module with LM75A Temperature Sensor
    AD9850 DDS module with LM75A Temperature Sensor

    This turns out to be easier than screwing around with thermistors, because the chip reports the temperature directly in Celcius with ⅛ °C resolution. Classic LM75 chips from National (now absorbed by TI) had ½ °C resolution, but the datasheet shows the bits have an easily extensible format:

    LM75A Temperature Data Format
    LM75A Temperature Data Format

    Huh. Fixed-point data, split neatly on a byte boundary. Who’d’a thunk it?

    There’s a standard Arduino library using, naturally enough, floating point numbers, but I already have big fixed point numbers lying around and, with the I²C hardware up & running from the X axis DAC and OLED display, this was straightforward:

    Wire.requestFrom(LM75_ADDR,2);
    Temp.fx_32.high = Wire.read();
    Temp.fx_32.low = (uint32_t)Wire.read() << 24;
    PrintFixedPtRounded(Buffer,Temp,3);
    u8x8.drawString(0,ln,"DDS C          ");
    u8x8.drawString(16-strlen(Buffer),ln,Buffer);
    printf(",%s",Buffer);
    ln += 1;
    

    The next-to-last line squirts the temperature through the serial port to make those nice plots.

    Casually ignoring all I²C bus error conditions will eventually lead to heartache and confusion. In particular, the Basement Laboratory temperature must never fall below 0 °C, because I just plunk the two’s-complement temperature data into an unsigned fixed point number.

    Which produces the next-to-bottom line:

    DDS OLED with LM75 temperature
    DDS OLED with LM75 temperature

    Alas, the u8x8 font doesn’t include a degree symbol.

    Given sufficient motivation, I can now calibrate the DDS output against the GPS-locked 10 MHz standard to get a (most likely) linear equation for the oscillator frequency offset as a function of temperature. The DDS module includes a comparator to square up its sine wave, so an XOR phase detector or something based on filtering the output of an analog switch might be feasible.

  • Mailing Tube End Caps

    Faced with a need to send documents rolled up in a tube, rather than folded flat, I sawed off a suitable length of cardboard tube from the heap, then discovered a distinct lack of end caps.

    Well, once again, it’s 3D printing to the rescue:

    Mailing Tube Cap - top - Slic3r
    Mailing Tube Cap – top – Slic3r

    The small ribs probably don’t actually do anything, but seemed like a nice touch.

    They’re somewhat less boring from the bottom:

    Mailing Tube Cap - bottom - Slic3r
    Mailing Tube Cap – bottom – Slic3r

    The fancy spider supports that big flat top and provides some crush resistance. The flat flange should collect the edge of the packing tape wrapped around the ends.

    A firm shove installs them, so the size worked out perfectly:

    Mailing tube end cap - installed
    Mailing tube end cap – installed

    Add a wrap of tape to each end, affix the USPS label, and they went out with the next day’s mail, PETG hair and all.

    The OpenSCAD source code as a GitHub Gist:

    // Mailing tube end cap
    // Ed Nisley KE4ZNU – June 2017
    Layout = "Build";
    //- Extrusion parameters – must match reality!
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    Protrusion = 0.1;
    HoleWindage = 0.2;
    //- Screw sizes
    inch = 25.4;
    TubeID = 2 * inch;
    TubeWall = 0.1 * inch;
    CapInsert = 15.0;
    CapRim = 1.0;
    CapWall = 3*ThreadWidth;
    NumFlanges = 3;
    FlangeHeight = 3*ThreadThick;
    FlangeWidth = ThreadWidth/2;
    FlangeSpace = CapInsert / (NumFlanges + 1);
    OAHeight = CapInsert + CapRim;
    NumRibs = 3*4;
    NumSides = 3*NumRibs;
    //- Adjust hole diameter to make the size come out right
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides);
    }
    module TubeCap() {
    difference() {
    cylinder(d=TubeID,h=OAHeight,$fn=NumSides);
    translate([0,0,CapWall])
    cylinder(d=TubeID – 2*CapWall,h=OAHeight,$fn=NumSides);
    }
    for (i=[1:NumFlanges])
    translate([0,0,i*FlangeSpace])
    difference() {
    cylinder(d=TubeID + 2*FlangeWidth,h=FlangeHeight,$fn=NumSides);
    translate([0,0,-Protrusion])
    cylinder(d=TubeID – 2*CapWall,h=FlangeHeight + 2*Protrusion,$fn=NumSides);
    }
    for (i=[0:NumRibs-1])
    rotate(i*360/NumRibs)
    translate([0,-ThreadWidth,CapWall + ThreadThick])
    cube([TubeID/2 – CapWall/2,2*ThreadWidth,CapInsert + CapRim – CapWall – ThreadThick],center=false);
    translate([0,0,CapInsert]) {
    difference() {
    cylinder(d=TubeID + 2*TubeWall,h=CapRim,$fn=NumSides);
    translate([0,0,-Protrusion])
    cylinder(d=TubeID – 3*2*CapWall,h=2*CapRim,$fn=NumSides);
    }
    }
    }
    //- Build things
    if (Layout == "Show")
    TubeCap();
    if (Layout == "Build")
    translate([0,0,OAHeight])
    rotate([180,0,0])
    TubeCap();
  • Proto Board Holder: Revised Screw Mounts

    Improving the crystal tester’s (nonexistent) grounding requires a band of copper tape around the inside of the proto board holder. Rather than cut the tape lengthwise to fit the holder, a new one will be just tall enough:

    Proto Board - 80x120 - revised inserts - Slic3r
    Proto Board – 80×120 – revised inserts – Slic3r

    While I was at it, I deleted the washer recesses, because those didn’t work out well, and fiddled the screw holes to put the inserts in from the bottom:

    Proto Board - 80x120 - revised inserts - detail - Slic3r
    Proto Board – 80×120 – revised inserts – detail – Slic3r

    Although the overhang inside the holes will be ugly, I’ll epoxy the inserts flush with the bottom and nobody will ever know.

    The copper tape now makes a tidy ground strap:

    Crystal Tester - ground strap - rear
    Crystal Tester – ground strap – rear

    With a gap in the front to eliminate the obvious loop:

    Crystal Tester - ground strap - front gap
    Crystal Tester – ground strap – front gap

    The OpenSCAD source code as a GitHub Gist:

    // Test support frame for proto boards
    // Ed Nisley KE4ZNU – Jan 2017
    // June 2017 – Add side-mount bracket, inserts into bottom
    Layout = "Frame";
    ClampFlange = true;
    Channel = false;
    //- Extrusion parameters – must match reality!
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    Protrusion = 0.1;
    HoleWindage = 0.2;
    //- Screw sizes
    inch = 25.4;
    Tap4_40 = 0.089 * inch;
    Clear4_40 = 0.110 * inch;
    Head4_40 = 0.211 * inch;
    Head4_40Thick = 0.065 * inch;
    Nut4_40Dia = 0.228 * inch;
    Nut4_40Thick = 0.086 * inch;
    Washer4_40OD = 0.270 * inch;
    Washer4_40ID = 0.123 * inch;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Insert = [3.9,4.6,5.8];
    //- PCB sizes
    PCBSize = [80.0,120.0,1.6];
    PCBShelf = 1.0; // support rim under PCB
    Clearance = 2*[ThreadWidth,ThreadWidth,0];
    WallThick = 4.0;
    FrameHeight = IntegerMultiple(3/8 * inch,1.0);
    echo(str("Inner height: ",FrameHeight));
    ScrewOffset = 0.0 + Clear4_40/2;
    ScrewSites = [[-1,1],[-1,1]]; // -1/0/+1 = left/mid/right and bottom/mid/top
    OAHeight = FrameHeight + Clearance[2] + PCBSize[2];
    echo(str("OAH: ",OAHeight));
    FlangeExtension = 3.0;
    FlangeThick = IntegerMultiple(2.0,ThreadThick);
    Flange = PCBSize
    + 2*[ScrewOffset,ScrewOffset,0]
    + 2*[Washer4_40OD,Washer4_40OD,0]
    + [2*FlangeExtension,2*FlangeExtension,(FlangeThick – PCBSize[2])]
    ;
    echo(str("Flange: ",Flange));
    NumSides = 4*5;
    WireChannel = [Flange[0],15.0,3.0 + PCBSize[2]];
    WireChannelOffset = [Flange[0]/2,25.0,(FrameHeight + PCBSize[2] – WireChannel[2]/2)];
    //- Adjust hole diameter to make the size come out right
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides);
    }
    //- Build things
    if (Layout == "Frame")
    difference() {
    union() { // body block
    translate([0,0,OAHeight/2])
    cube(PCBSize + Clearance + [2*WallThick,2*WallThick,FrameHeight],center=true);
    for (x=[-1,1], y=[-1,1]) { // screw bosses
    translate([x*(PCBSize[0]/2 + ScrewOffset),
    y*(PCBSize[1]/2 + ScrewOffset),
    0])
    cylinder(r=Washer4_40OD,h=OAHeight,$fn=NumSides);
    }
    if (ClampFlange) // flange for work holder
    linear_extrude(height=Flange[2])
    hull()
    for (i=[-1,1], j=[-1,1]) {
    translate([i*(Flange[0]/2 – Washer4_40OD/2),j*(Flange[1]/2 – Washer4_40OD/2)])
    circle(d=Washer4_40OD,$fn=NumSides);
    }
    }
    for (x=[-1,1], y=[-1,1]) { // screw position indexes
    translate([x*(PCBSize[0]/2 + ScrewOffset),
    y*(PCBSize[1]/2 + ScrewOffset),
    -Protrusion])
    rotate(x*y*180/(2*6))
    PolyCyl(Clear4_40,(OAHeight + 2*Protrusion),6); // screw clearance holes
    translate([x*(PCBSize[0]/2 + ScrewOffset),
    y*(PCBSize[1]/2 + ScrewOffset),
    -Protrusion])
    rotate(x*y*180/(2*6))
    PolyCyl(Insert[OD],OAHeight – PCBSize[2] – 3*ThreadThick + Protrusion,6); // inserts
    if (false)
    translate([x*(PCBSize[0]/2 + ScrewOffset),
    y*(PCBSize[1]/2 + ScrewOffset),
    OAHeight – PCBSize[2]])
    PolyCyl(1.2*Washer4_40OD,(PCBSize[2] + Protrusion),NumSides); // washer recess
    }
    translate([0,0,OAHeight/2]) // through hole below PCB
    cube(PCBSize – 2*[PCBShelf,PCBShelf,0] + [0,0,2*OAHeight],center=true);
    translate([0,0,(OAHeight – (PCBSize[2] + Clearance[2])/2 + Protrusion/2)]) // PCB pocket on top
    cube(PCBSize + Clearance + [0,0,Protrusion],center=true);
    if (Channel)
    translate(WireChannelOffset) // opening for wires from bottom side
    cube(WireChannel + [0,0,Protrusion],center=true);
    }
    // Add-on bracket to hold smaller PCB upright at edge
    PCB2Insert = [3.0,4.9,4.1];
    PCB2OC = 45.0;
    if (Layout == "Bracket")
    difference() {
    hull() // frame body block
    for (x=[-1,1]) // bosses around screws
    translate([x*(PCBSize[0]/2 + ScrewOffset),0,0])
    cylinder(r=Washer4_40OD,h=OAHeight,$fn=NumSides);
    for (x=[-1,1]) // frame screw holes
    translate([x*(PCBSize[0]/2 + ScrewOffset),0,-Protrusion])
    rotate(x*180/(2*6))
    PolyCyl(Clear4_40,(OAHeight + 2*Protrusion),6);
    for (x=[-1,1]) // PCB insert holes
    translate([x*PCB2OC/2,(Washer4_40OD + Protrusion),OAHeight/2])
    rotate([90,0,0])
    cylinder(d=PCB2Insert[OD],h=2*(Washer4_40OD + Protrusion),$fn=6);
    }

  • 128×64 OLED Display: I²C Timings

    The OLED display has a noticeable delay between writing the first (double-size) line of text and the last line, which seemed odd:

    White 128x64 OLED Display - crystal tester
    White 128×64 OLED Display – crystal tester

    The top trace in this scope shot goes high while the code begins the display update, which involves converting the variable to strings, the characters to bitmaps, then writing the data to the display:

    IIC Timing - overall
    IIC Timing – overall

    The bottom trace shows I²C bus activity pretty much blots up all the time, with very little required for the computations in between the display writes for each text line.

    Near the leading edge of the top trace, the code computes the new delta phase value and the X axis DAC output corresponding to that frequency:

    TestCount.fx_64 = MultiplyFixedPt(ScanFreq,CtPerHz); // compute DDS delta phase
    TestCount.fx_32.low = 0; // truncate count to integer
    TestFreq.fx_64 = MultiplyFixedPt(TestCount,HzPerCt); // compute actual frequency
    
    Temp.fx_64 = (DAC_MAX * (ScanFreq.fx_64 - ScanFrom.fx_64)) / ScanWidth.fx_32.high;
    XAxisValue = Temp.fx_32.high;
    
    WriteDDS(TestCount.fx_32.high); // set DDS to new frequency
    XAxisDAC.setVoltage(XAxisValue,DAC_WR); // and set X axis to match
    

    The burst in the top trace shows the five SPI writes to the DDS (one pulse per byte, with the hardware handling the serialization) and the bottom trace shows four I²C bus writes to the DAC:

    IIC Timing - DDS to SPI - IIC to DAC
    IIC Timing – DDS to SPI – IIC to DAC

    A bit more detail shows writing each I²C byte to the DAC requires nine clock pulses (8 data, 1 ack):

    IIC Timing - DDS to SPI - IIC to DAC detail
    IIC Timing – DDS to SPI – IIC to DAC detail

    The I²C bus ticks along at 400 kHz, with each byte requiring 33.4 µs (including the mandatory downtime around each burst), so the DAC update requires about 100 µs. The MCP4725 datasheet suggests a three byte “fast mode” write, but there’s not much point in doing so for my simple needs.

    The display ticks along at the same pace with far more data.

    In round numbers, the entire display update hits 6 text lines (1 double-height + 4 single-height) × 16 characters / line × 64 pixels / character = 6144 pixels.

    The first scope shot shows the update requires something close to 90 ms, which allows for 2700 bytes = 90 ms / 33.4 µs, the equivalent of 21 k pixels. The SH1106 hardware includes an internal address counter, so there’s no need to transfer an address with each byte; I’m not sure where the factor-of-two overhead goes.

    In order to get a faster update, there’s a definite need for lazy screen updates: no writes when there’s no change.

    This probably doesn’t matter, because I can’t watch much faster, but it’s good to know the fancy fixed-point arithmetic isn’t the limiting factor.

  • AD8310 Log Amp Module: Sidesaddle Bracket

    This little bracket attaches to a proto board holder, with holes for M3 inserts to mount the AD8310 log amp module:

    PCB Side Bracket - 80x120
    PCB Side Bracket – 80×120

    Thusly:

    AD8310 module bracket on proto board holder - component side
    AD8310 module bracket on proto board holder – component side

    The OLED display looks a bit faded, which seems to be an interaction between matrix refresh and camera shutter: looks just fine in person!

    Not much to see from the other side:

    AD8310 module bracket on proto board holder - solder side
    AD8310 module bracket on proto board holder – solder side

    I should have included an offset to slide it a bit forward; then I could mount it on the other end with clearance for the Nano’s USB port. Maybe next time.

    The OpenSCAD source code as a GitHub Gist:

    // Test support frame for proto boards
    // Ed Nisley KE4ZNU – Jan 2017
    // June 2017 – Add side-mount bracket
    Layout = "Bracket";
    ClampFlange = true;
    Channel = false;
    //- Extrusion parameters – must match reality!
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    Protrusion = 0.1;
    HoleWindage = 0.2;
    //- Screw sizes
    inch = 25.4;
    Tap4_40 = 0.089 * inch;
    Clear4_40 = 0.110 * inch;
    Head4_40 = 0.211 * inch;
    Head4_40Thick = 0.065 * inch;
    Nut4_40Dia = 0.228 * inch;
    Nut4_40Thick = 0.086 * inch;
    Washer4_40OD = 0.270 * inch;
    Washer4_40ID = 0.123 * inch;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    Insert = [3.9,4.6,5.8];
    //- PCB sizes
    PCBSize = [80.0,120.0,1.6];
    PCBShelf = 1.5;
    Clearance = 2*[ThreadWidth,ThreadWidth,0];
    WallThick = 4.0;
    FrameHeight = 8.0;
    ScrewOffset = 0.0 + Clear4_40/2;
    ScrewSites = [[-1,1],[-1,1]]; // -1/0/+1 = left/mid/right and bottom/mid/top
    OAHeight = FrameHeight + Clearance[2] + PCBSize[2];
    echo(str("OAH: ",OAHeight));
    FlangeExtension = 3.0;
    FlangeThick = IntegerMultiple(2.0,ThreadThick);
    Flange = PCBSize
    + 2*[ScrewOffset,ScrewOffset,0]
    + 2*[Washer4_40OD,Washer4_40OD,0]
    + [2*FlangeExtension,2*FlangeExtension,(FlangeThick – PCBSize[2])]
    ;
    echo(str("Flange: ",Flange));
    NumSides = 4*5;
    WireChannel = [Flange[0],15.0,3.0 + PCBSize[2]];
    WireChannelOffset = [Flange[0]/2,25.0,(FrameHeight + PCBSize[2] – WireChannel[2]/2)];
    //- Adjust hole diameter to make the size come out right
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides);
    }
    //- Build things
    if (Layout == "Frame")
    difference() {
    union() { // body block
    translate([0,0,OAHeight/2])
    cube(PCBSize + Clearance + [2*WallThick,2*WallThick,FrameHeight],center=true);
    for (x=[-1,1], y=[-1,1]) { // screw bosses
    translate([x*(PCBSize[0]/2 + ScrewOffset),
    y*(PCBSize[1]/2 + ScrewOffset),
    0])
    cylinder(r=Washer4_40OD,h=OAHeight,$fn=NumSides);
    }
    if (ClampFlange) // flange for work holder
    linear_extrude(height=Flange[2])
    hull()
    for (i=[-1,1], j=[-1,1]) {
    translate([i*(Flange[0]/2 – Washer4_40OD/2),j*(Flange[1]/2 – Washer4_40OD/2)])
    circle(d=Washer4_40OD,$fn=NumSides);
    }
    }
    for (x=[-1,1], y=[-1,1]) { // screw position indexes
    translate([x*(PCBSize[0]/2 + ScrewOffset),
    y*(PCBSize[1]/2 + ScrewOffset),
    -Protrusion])
    rotate(x*y*180/(2*6))
    PolyCyl(Clear4_40,(OAHeight + 2*Protrusion),6); // screw clearance holes
    translate([x*(PCBSize[0]/2 + ScrewOffset),
    y*(PCBSize[1]/2 + ScrewOffset),
    OAHeight – PCBSize[2] – Insert[LENGTH]])
    rotate(x*y*180/(2*6))
    PolyCyl(Insert[OD],Insert[LENGTH] + Protrusion,6); // inserts
    translate([x*(PCBSize[0]/2 + ScrewOffset),
    y*(PCBSize[1]/2 + ScrewOffset),
    OAHeight – PCBSize[2]])
    PolyCyl(1.2*Washer4_40OD,(PCBSize[2] + Protrusion),NumSides); // washers
    }
    translate([0,0,OAHeight/2]) // through hole below PCB
    cube(PCBSize – 2*[PCBShelf,PCBShelf,0] + [0,0,2*OAHeight],center=true);
    translate([0,0,(OAHeight – (PCBSize[2] + Clearance[2])/2 + Protrusion/2)]) // PCB pocket on top
    cube(PCBSize + Clearance + [0,0,Protrusion],center=true);
    if (Channel)
    translate(WireChannelOffset) // opening for wires from bottom side
    cube(WireChannel + [0,0,Protrusion],center=true);
    }
    // Add-on bracket to hold smaller PCB upright at edge
    PCB2Insert = [3.0,4.9,4.1];
    PCB2OC = 45.0;
    if (Layout == "Bracket")
    difference() {
    hull() // frame body block
    for (x=[-1,1]) // bosses around screws
    translate([x*(PCBSize[0]/2 + ScrewOffset),0,0])
    cylinder(r=Washer4_40OD,h=OAHeight,$fn=NumSides);
    for (x=[-1,1]) // frame screw holes
    translate([x*(PCBSize[0]/2 + ScrewOffset),0,-Protrusion])
    rotate(x*180/(2*6))
    PolyCyl(Clear4_40,(OAHeight + 2*Protrusion),6);
    for (x=[-1,1]) // PCB insert holes
    translate([x*PCB2OC/2,(Washer4_40OD + Protrusion),OAHeight/2])
    rotate([90,0,0])
    cylinder(d=PCB2Insert[OD],h=2*(Washer4_40OD + Protrusion),$fn=6);
    }

  • 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: 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