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.

Author: Ed

  • Generic I²C 128×64 OLED Displays: Beware Swapped VCC and GND

    A batch of 1.3 inch white I²C OLED displays arrived from halfway around the planet, so I figured I could run a quick acceptance test by popping them into the socket on the crystal tester proto board:

    White 1.3 inch OLED on crystal tester
    White 1.3 inch OLED on crystal tester

    The first one flat-out didn’t work, as in not at all. The original display continued to work fine, so I compared the old & new displays:

    OLED Modules - pinout difference
    OLED Modules – pinout difference

    Yup, swapped VCC and GND pins. I should be used to that by now.

    I rewired the socket, tried the new displays, undid the change, popped the original display in place, and all is right with the world. Somewhat to my surprise, all five new displays worked, including the one I’d insulted with reversed power.

  • Goslings at Vassar Farm Pond

    I watched the Canada Goose family paddling around the pond:

    Goslings at Vassar Farm Pond - 2017-06-04 - family
    Goslings at Vassar Farm Pond – 2017-06-04 – family

    A hiker on the trail around the pond brought them to DEFCON 4:

    Goslings at Vassar Farm Pond - 2017-06-04 - alert
    Goslings at Vassar Farm Pond – 2017-06-04 – alert

    The little ones aren’t triphibans yet, but they know the drill:

    Goslings at Vassar Farm Pond - 2017-06-04 - wing exercise
    Goslings at Vassar Farm Pond – 2017-06-04 – wing exercise

    Maybe he only does that when Mom’s not watching?

  • Under-cabinet LED Strip IR Sensor: Re-aimed

    The under-cabinet LED strips work wonderfully well, except that the IR sensor seemed rather hypersensitive, so I added a small reflector made of shiny steel:

    Under-cabinet light - IR sensor mirror
    Under-cabinet light – IR sensor mirror

    Even though I rounded those corners and deburred the edges, it does look a bit threatening, doesn’t it?

    It moves the sensor’s hotspot back about half a foot, which seems Good Enough to eliminate false triggering from normal activity over the cutting board.

  • Teledyne 732TN-5 Relay: Zowie!

    The first pass at the crystal tester used a manual jumper to switch the 33 pF series capacitor in / out of the circuit:

    Quartz crystal resonance test fixture
    Quartz crystal resonance test fixture

    With an Arduino close at hand, however, a relay makes somewhat more sense. For long-forgotten reasons, I have a small fortune in Teledyne 732TN-5 relays intended for RF switching:

    Teledyne 732TN-5 Relay
    Teledyne 732TN-5 Relay

    The 7820 date code on the side suggests they’ve been in the heap basically forever, although some fractions of Teledyne still exist and you can apparently buy the same relay today at 50 bucks a pop. It’s definitely overqualified for this job and you can surely get away with an ordinary DIP DPDT (or, heck, even SPST) relay.

    It seems I picked a hyper-bright white LED: the red ink tones it down a bit. Black might be more effective. A diffused LED may be in order.

    The “TN” suffix indicates a built-in transistor driver with a catch diode on the relay coil, so the relay needs power, ground, and a current drive into the transistor’s base terminal:

    Teledyne 732TN relay - drive schematic
    Teledyne 732TN relay – drive schematic

    Even with the internal catch diode, I ran the +5 V power through a 12 Ω resistor to a 10 µF cap in hopes of isolating the inevitable switching transients from the DDS and log amp. As a result, the turn-on transient isn’t much of a transient at all:

    Teledyne 732TN Relay - turn-on transient
    Teledyne 732TN Relay – turn-on transient

    The 560 mV drop suggests a 47 mA coil current through the 12 Ω resistor, just about spot on for a 100 Ω coil.

    The energy stored in the coil makes the turn-off transient much steeper:

    Teledyne 732TN Relay - turn-off transient
    Teledyne 732TN Relay – turn-off transient

    Note the 1.5 µs delay from the falling control input to the relay opening. Granted, it’s running at 4.7 V, not the rated 5 V, but that’s still rather peppy. The turn-on delay seems to be about the same, making the datasheet’s “6 ms nominal” operating time look rather conservative.

    Dang, that’s a nice gadget!

  • 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: 1.3 inch I²C OLED FTW

    A white 1.3 inch I²C OLED turns out to be much more readable than the yellow-blue 0.96 inch version:

    Arduino with OLED - white 1.3 inch
    Arduino with OLED – white 1.3 inch

    Of course, after you make it readable, you immediately make room to cram more data on it:

    White 1.3 inch OLED on crystal tester
    White 1.3 inch OLED on crystal tester

    That’s on the proto board with the Arduino and AD9850 DDS ticking away on the left; the bright red MCP4725 DAC will eventually drive the scope’s X axis. Shifting the display to the I²C interface and cleaning up my SPI initialization code worked wonders: the DDS now steps a sine wave at 0.1 Hz (pretty nearly) intervals from 57.0 to 60.3 Hz.