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

  • Dripworks Mainline Pipe Clamp

    Dripworks Mainline Pipe Clamp

    This is laid in against a need I hope never occurs:

    Dripworks 0.75 inch pipe clamp
    Dripworks 0.75 inch pipe clamp

    It’s intended to clamp around one of the Dripworks mainline pipes carrying water from the pressure regulator to the driplines in the raised beds, should an errant shovel or fork find the pipe.

    It descends from a long line of soaker hose clamps, with a 25 mm ID allowing for a silicone tape wrap as a water barrier.

    The solid model has no surprises:

    Dripworks Mainline Clamp - build view
    Dripworks Mainline Clamp – build view

    The OpenSCAD source code as a GitHub Gist:

    // Dripworks 3/4 inch mainline clamp
    // Ed Nisley KE4ZNU 2021-06
    Layout = "Build"; // [Hose,Block,Show,Build]
    HoseOD = 25.0;
    TestFit = false; // true to build test fit slice from center
    //- Extrusion parameters must match reality!
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    inch = 25.4;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    ID = 0;
    OD = 1;
    LENGTH = 2;
    //———-
    // Dimensions
    // Hose lies along X axis
    Hose = [200,HoseOD,HoseOD]; // X = longer than anything else
    NumScrews = 2; // screws along each side of cable
    WallThick = 3.0; // Thinnest printed wall
    PlateThick = 1.5; // Stiffening plate thickness
    // 8-32 stainless screws
    Screw = [4.1,8.0,50.0]; // OD = head LENGTH = thread length
    Washer = [4.4,9.5,1.0];
    Nut = [4.1,9.7,3.3];
    Block = [30.0,Hose.y + 2*Washer[OD],HoseOD + 2*WallThick]; // overall splice block size
    echo(str("Block: ",Block));
    ScrewMinLength = Block.z + 2*PlateThick + 2*Washer.z + Nut.z; // minimum screw length
    echo(str("Screw min length: ",ScrewMinLength));
    Kerf = 1.0; // cut through middle to apply compression
    CornerRadius = Washer[OD]/2;
    ScrewOC = [(Block.x – 2*CornerRadius) / (NumScrews – 1),
    Block.y – 2*CornerRadius,
    2*Block.z // ensure complete holes
    ];
    echo(str("Screw OC: x=",ScrewOC.x," y=",ScrewOC.y));
    //———————-
    // Useful routines
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(d=(FixDia + HoleWindage),h=Height,$fn=Sides);
    }
    // Hose shape
    // This includes magic numbers measured from reality
    module HoseProfile() {
    NumSides = 12*4;
    rotate([0,-90,0])
    translate([0,0,-Hose.x/2])
    resize([Hose.z,Hose.y,0])
    cylinder(d=Hose.z,h=Hose.x,$fn=NumSides);
    }
    // Outside shape of splice Block
    // Z centered on hose rim circles, not overall thickness through center ridge
    module SpliceBlock() {
    difference() {
    hull()
    for (i=[-1,1], j=[-1,1]) // rounded block
    translate([i*(Block.x/2 – CornerRadius),j*(Block.y/2 – CornerRadius),-Block.z/2])
    cylinder(r=CornerRadius,h=Block.z,$fn=4*8);
    for (i = [0:NumScrews – 1], j=[-1,1]) // screw holes
    translate([-(Block.x/2 – CornerRadius) + i*ScrewOC.x,
    j*ScrewOC.y/2,
    -(Block.z/2 + Protrusion)])
    PolyCyl(Screw[ID],Block.z + 2*Protrusion,6);
    cube([2*Block.x,2*Block.y,Kerf],center=true); // slice through center
    }
    }
    // Splice block less hose
    module ShapedBlock() {
    difference() {
    SpliceBlock();
    HoseProfile();
    }
    }
    //———-
    // Build them
    if (Layout == "Hose")
    HoseProfile();
    if (Layout == "Block")
    SpliceBlock();
    if (Layout == "Show") {
    difference() {
    SpliceBlock();
    HoseProfile();
    }
    color("Green",0.25)
    HoseProfile();
    }
    if (Layout == "Build") {
    SliceOffset = TestFit && !(NumScrews % 2) ? ScrewOC.x/2 : 0;
    intersection() {
    translate([SliceOffset,0,Block.z/4])
    if (TestFit)
    cube([ScrewOC.x/2,4*Block.y,Block.z/2],center=true);
    else
    cube([4*Block.x,4*Block.y,Block.z/2],center=true);
    union() {
    translate([0,0.6*Block.y,Block.z/2])
    ShapedBlock();
    translate([0,-0.6*Block.y,Block.z/2])
    rotate([0,180,0])
    ShapedBlock();
    }
    }
    }

  • Sticky Trap Screen Frames

    Sticky Trap Screen Frames

    The objective being to reduce the number of onion maggots in Mary’s Vassar Farm plot without chemical agents, I conjured sticky trap screen frames from the vasty digital deep:

    Sticky Trap - first production run
    Sticky Trap – first production run

    Each one contains half a sheet of yellow sticky plastic, which is easy enough to cut before peeling off the protective covering sheets. The cage is half-inch galvanized hardware cloth snipped with hardened diagonal cutters. A bead of acrylic adhesive around the base holds the cage in place

    Although you can deploy sticky sheets without cages, they tend to attract and affix beneficial critters: butterflies, small birds, furry critters, toads, gardeners, and the like. We don’t know how effective the cages will be, but they seemed better than nothing.

    They mount on ski poles cut in half:

    Sticky Trap - ski pole installed
    Sticky Trap – ski pole installed

    And on fence posts around the perimeter:

    Sticky Trap - angle bracket installed
    Sticky Trap – angle bracket installed

    To my untrained eye, some of those doomed critters are, indeed, onion maggot flies. The rest seem to be gnats and other nuisances, so IMO we’re applying population pressure in the right direction.

    Each base-and-cap frame takes about three hours to print, so I did them one at a time over the course of a few days while applying continuous product improvement.

    The sheets rest on small V blocks intended to keep them centered within the cage:

    Sticky Sheet Cage - angle bracket - solid model
    Sticky Sheet Cage – angle bracket – solid model

    The ski pole attachment must build with the cap on top, but it bridges well enough for the purpose:

    Sticky Sheet Cage - ski pole - solid model
    Sticky Sheet Cage – ski pole – solid model

    The overhanging hooks on the blocks (just barely) engage the grid to keep the lid in place, while remaining short enough to not droop too badly. You could probably delete the hooks from the bottom plate, but they align the cage while the adhesive cures.

    The sheets tend to bend in the middle, so I’ll stick a thin slat or two vertically to keep them straight.

    The OpenSCAD source code as a GitHub Gist:

    // Sticky Sheet Cage
    // Ed Nisley KE4ZNU May 2021
    Layout = "Build"; // [Build, Show, Cap, Attachment]
    Bracket = "Ski"; // [Angle, Ski, Post]
    //- Extrusion parameters must match reality!
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    inch = 25.4;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    //———————-
    // Dimensions
    Sheet = [1,100,150]; // sticky sheet
    Grid = 0.5*inch;
    Cage = [2*Grid + 5.0, 8*Grid + 5.0, 12*Grid + 2.0]; // grid wire cage bent around sheet
    CageRad = 2.5; // wire bending radius
    CageThick = 2.0; // grid thickness
    WallThick = 3.0; // min wall and bottom thickness
    Recess = 5.0; // inset to capture cage edge
    Plate = [Cage.x,Cage.y,Recess] + [2*WallThick,2*WallThick,WallThick];
    PlateRad = 5.0;
    SkiPole = [20.0,20.0 + 2*WallThick,50];
    AnglePlate = [30,30,50];
    ScrewClear = 5.0;
    BuildGap = 5.0;
    //———————-
    // Useful routines
    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);
    }
    //———————-
    // Pieces
    module Cap() {
    union() {
    difference() {
    hull()
    for (i=[-1,1], j=[-1,1])
    translate([i*(Plate.x/2 – PlateRad),j*(Plate.y/2 – PlateRad),0])
    cylinder(r=PlateRad,h=Plate.z,$fn=12);
    translate([0,0,Plate.z – Recess])
    hull()
    for (i=[-1,1], j=[-1,1])
    translate([i*(Cage.x/2 – CageRad),j*(Cage.y/2 – CageRad),0])
    cylinder(r=CageRad,h=Plate.z,$fn=12);
    }
    difference() {
    Strut = Cage.x – 2*CageThick;
    Latch = [Cage.x,WallThick,0.75*Plate.z];
    union() {
    for (j=[-1,1])
    translate([0,j*2.5*Grid,Plate.z])
    cube([Strut,WallThick,2*Plate.z],center=true);
    for (j=[-1,1])
    translate([0,j*2.5*Grid,2*Plate.z – Latch.z/2])
    cube(Latch,center=true);
    }
    translate([0,0,2*Plate.z + (Cage.z – Sheet.z)/4])
    rotate([0,45,0])
    cube([Strut/sqrt(2),Plate.y,Strut/sqrt(2)],center=true);
    }
    }
    }
    module Attachment() {
    if (Bracket == "Angle") {
    translate([0,Plate.y/2,0])
    rotate(45)
    difference() {
    union() {
    cube(AnglePlate,center=false);
    rotate(-45)
    translate([0,WallThick,Plate.z/2])
    cube([Plate.x – 2*PlateRad,4*WallThick,Plate.z],center=true);
    }
    translate([WallThick,WallThick,-Protrusion])
    cube(AnglePlate + [0,0,2*Protrusion],center=false);
    translate([AnglePlate.x/2,-Protrusion,2*AnglePlate.z/3])
    rotate([-90,0,0])
    PolyCyl(ScrewClear,2*AnglePlate.x,6);
    translate([-Protrusion,AnglePlate.x/2,1*AnglePlate.z/3])
    rotate([90,0,90])
    PolyCyl(ScrewClear,2*AnglePlate.x,6);
    }
    }
    else if (Bracket == "Ski") {
    translate([0,Plate.y/2 + SkiPole[OD]/2,0])
    difference() {
    union() {
    PolyCyl(SkiPole[OD],SkiPole[LENGTH],24);
    translate([0,-3*WallThick,Plate.z/2])
    cube([Plate.x – 2*PlateRad,4*WallThick,Plate.z],center=true);
    }
    translate([0,0,-2*WallThick])
    PolyCyl(SkiPole[ID],SkiPole[LENGTH],24);
    }
    }
    }
    //———————-
    // Build it
    if (Layout == "Cap")
    Cap();
    if (Layout == "Attachment") {
    Attachment();
    }
    if (Layout == "Show") {
    translate([0,0,Sheet.z/2 + Plate.z])
    color("Yellow")
    cube(Sheet,center=true);
    Cap();
    Attachment();
    translate([0,0,Sheet.z + 2*Plate.z])
    rotate([180,0,0])
    Cap();
    }
    if (Layout == "Build") {
    translate([-(Plate.x/2 + BuildGap),0,0]) {
    Cap();
    Attachment();
    }
    translate([(Plate.x/2 + BuildGap),0,0])
    Cap();
    }

  • Deer Fence Hangers

    Deer Fence Hangers

    For what should be obvious reasons, we armored Mary’s “kitchen garden” with buried concrete blocks and deer fence. I secured the fence to 7 foot plastic-coated steel-core posts strapped to shorter stakes supporting the lower wire fence, using cable ties we both knew wouldn’t survive exposure to the sun.

    As part of the spring garden prep, I summoned proper supports from the vasty digital deep:

    Deer Fence Hanger - Build view
    Deer Fence Hanger – Build view

    The general idea is to plunk one atop each post and tangle wrap the netting through the hooks, thusly:

    Deer Fence Hanger - installed
    Deer Fence Hanger – installed

    The garden looks like we killed an entire chess set and impaled their carcasses as a warning to others of their kind, but the fence now hangs neatly from the top of the posts rather than drooping sadly.

    Each one of those things takes nigh onto two hours to emerge from the M2, so I printed them one by one over the course of a few days while making continuous product improvements.

    The “natural” PETG isn’t UV stabilized, either, but it ought to last longer than those little bitty nylon cable ties. We shall see.

    The OpenSCAD source code as a GitHub Gist:

    // Deer Fence Hangers
    // Ed Nisley KE4ZNU May 2021
    Layout = "Show"; // [Build, Show, Cap, Hook]
    // net grid spacing
    NetOC = 55.0; // [40.0:5.0:70.0]
    // stake OD
    PoleDia = 23.0; // [20.0:30.0]
    //- Extrusion parameters must match reality!
    /* [Hidden] */
    ThreadThick = 0.25;
    ThreadWidth = 0.40;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    inch = 25.4;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    //———————-
    // Dimensions
    Notch = 5.0; // hook engagement
    WallThick = 3.0; // min wall and end thickness
    Shell = [PoleDia,PoleDia + 2*WallThick,NetOC + 2*Notch];
    HookBlock = [10.0,Shell.y/4,2*Notch]; // hanger inside length
    LegendBlock = [0.7*Shell.z,Shell.y/2,2*ThreadThick]; // legend size
    //———————-
    // Useful routines
    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);
    }
    //———————-
    // Pieces
    module Hook() {
    //%Cap();
    translate([Shell[OD]/2 – Protrusion,HookBlock.y/2,0])
    rotate([90,0,0])
    linear_extrude(height=HookBlock.y)
    difference() {
    scale([1,2])
    intersection() {
    circle(r=HookBlock.x);
    square(HookBlock.x,center=false);
    }
    square(Notch,center=false);
    }
    }
    module Cap() {
    difference() {
    rotate(180/6)
    PolyCyl(Shell[OD],Shell[LENGTH],6);
    translate([0,0,-WallThick])
    rotate(180/24)
    PolyCyl(Shell[ID],Shell[LENGTH],24);
    translate([-Shell[OD]/2,0,Shell[LENGTH]/2])
    rotate([0,90,0])
    cube(LegendBlock,center=true);
    }
    translate([-(Shell[OD]/2 – LegendBlock.z/2),0,Shell[LENGTH]/2])
    rotate([0,-90,0])
    resize(0.8*LegendBlock,auto=[true,true,false])
    linear_extrude(height=LegendBlock.z)
    text(text=str(NetOC," ",PoleDia),
    size=6,spacing=1.00,font="Bitstream Vera Sans:style=Bold",
    halign="center",valign="center");
    }
    module Hanger() {
    Cap();
    for (k=[0,1])
    translate([0,0,k*Shell.z])
    for (a=[-1:1])
    rotate([k*180,0,a*60])
    Hook();
    }
    //———————-
    // Build it
    if (Layout == "Cap")
    Cap();
    if (Layout == "Hook")
    Hook();
    if (Layout == "Show")
    Hanger();
    if (Layout == "Build")
    translate([0,0,Shell[LENGTH]])
    rotate([180,0,0])
    Hanger();

  • Tour Easy: Bafang Mid-drive vs. Cateye Cadence Sensor

    Tour Easy: Bafang Mid-drive vs. Cateye Cadence Sensor

    For inscrutable reasons, the Bafang 500C display includes all stopped time in its average trip speed. While that is, in fact, the average speed over the entire trip, the Cateye cyclocomputers we’ve been using forever stop averaging after a few seconds at 0 mph.

    Bonus: Although the Bafang BBS02 motor knows the pedal cadence, it’s not part of the display.

    The Bafang BBS02 bottom bracket shaft put its pedal cranks much farther from the Tour Easy’s frame than the Shimano cranks, to the extent that the existing Cateye cadence sensor position just wasn’t going to work, so I printed a simple clip to fit over the motor’s “fixing plate”:

    Tour Easy Bafang BBS02 motor
    Tour Easy Bafang BBS02 motor

    It turns out putting a magnetic sensor immediately next to the winding end of a high-current three-phase motor isn’t the brightest idea I’ve ever had. The Cateye cadence display spent most of its time maxed out at 199 rpm, far faster than Mary can spin for, well, a single revolution.

    A somewhat more complex mount put the sensor roughly where it used to be:

    Cateye Cadence Sensor mount - installed
    Cateye Cadence Sensor mount – installed

    It looks precarious, but it spent nigh onto two decades there without incident, so we have precedent.

    Those are the original 165 mm Shimano cranks, because the 170 mm Bafung cranks threatened to lock out her knees. More on this in a while, as it’s a more complex issue than it may appear.

    The solid model looks about like you’d expect:

    Cateye Cadence Sensor mount - solid model
    Cateye Cadence Sensor mount – solid model

    The OpenSCAD code replaces the simple clip in the original GitHub Gist:

    // Cateye cadence sensor bracket
    
    LockRingDia = [44.0,46.0];
    LockRingLen = [4.0,6.5];
    LockRingOAD = LockRingDia[1] + 2*WallThick;
    LockRingOAL = LockRingLen[0] + LockRingLen[1];
    
    Notches = 16;
    SensorAngle = 3*360/Notches;
    SensorBase = 10.0;
    
    module Cateye() {
    
        difference() {
            union() {
                cylinder(d=LockRingOAD,h=LockRingOAL,$fn=Notches);
                translate([LockRingOAD/2 + LockRingOAL/2 - WallThick/2,0,LockRingOAL/2])
                    cube([LockRingOAL + WallThick,2*WallThick + Kerf,LockRingOAL],center=true);
          rotate(SensorAngle)
                    translate([LockRingOAD/2 + SensorBase - WallThick/2,0,LockRingOAL/2])
                        cube([2*SensorBase + WallThick,2*WallThick,LockRingOAL],center=true);
            }
            translate([0,0,LockRingLen[0]])
                PolyCyl(LockRingDia[1],LockRingOAL,Notches);
            translate([0,0,-Protrusion])
                PolyCyl(LockRingDia[0],2*LockRingOAL,Notches);
    
            translate([LockRingDia[0],0,0])
                cube([2*LockRingDia[0],Kerf,4*LockRingOAL],center=true);
            translate([LockRingOAD/2 + LockRingOAL/2,2*WallThick,LockRingOAL/2])
                rotate([90,0,0])
                    PolyCyl(3.0,4*WallThick,6);
    
            rotate(SensorAngle)
                translate([LockRingOAD/2 + 2*SensorBase - SensorBase/2,2*WallThick,LockRingOAL/2])
                    rotate([90,0,0])
                        PolyCyl(3.0,4*WallThick,6);
        }
    
    }
    
  • Bafang USB Programming Adapter

    Bafang USB Programming Adapter

    Changing (“programming”) the Bafang BBS02 motor controller parameters requires a USB-to-serial adapter with a connector matching the end of the cable from the motor to the display. While you can buy such things directly from the usual randomly named Amazon sellers, I happen to have a wide variety of bare adapter boards, so I just bought a display extender cable and cut it in half to get the connector; you can apparently buy pigtailed connectors (for more than the price of an extender) if you dislike cutting cables in half.

    Various documents provide versions of the canonical illustration of the motor end of the display cable, as ripped from Penoff’s original documentation:

    Bafang BBS02 display cable pinout
    Bafang BBS02 display cable pinout

    The pin colors correspond to the wiring inside the motor cable, but the extender uses different colors, because nobody will ever know:

    Bafang programmer - wire colors
    Bafang programmer – wire colors

    A bit of work with a continuity meter gave the pinout:

    Bafang BBS02 display extender - wire colors
    Bafang BBS02 display extender – wire colors

    Don’t trust stuff you read on the Intertubes: make your own measurements and draw your own diagrams!

    You want the cable end carrying the sockets to mate with the pins on the motor cable (coming in from the left):

    Bafang programmer - cable ends
    Bafang programmer – cable ends

    Soldering the cable to a known-counterfeit FTDI USB adapter went swimmingly:

    Bafang programmer - USB adapter wiring
    Bafang programmer – USB adapter wiring

    Note that the yellow-blue connection carries the full 48 V from the battery and may or may not have any current limiting / fusing / protection, so be a little more careful than usual in your wiring layout.

    The red jumper from DTR to CTS, shown in all the Amazon and eBay listIngs, turns out to be unnecessary.

    A quick and dirty case (eventually held together with generous hot-melt glue blobs) protects the PCB and armors the cables:

    Bafang USB-serial adapter interior
    Bafang USB-serial adapter interior

    The solid model over on the right looks about like you’d expect:

    Bafang Battery Mount - complete build view
    Bafang Battery Mount – complete build view

    Most of the instructions will tell you to hot-plug the cable to the motor with the battery connected, which strikes me as foolhardy; not all of those pins make contact in the right order, which means you will slap 50-odd volts across the wrong parts of the circuitry.

    Instead:

    • Disconnect the battery
    • Unplug the display
    • Plug the adapter cable into the motor connector
    • Plug the USB cable into the Token Windows Laptop
    • Reconnect the battery
    • Fire up the “programming” routine
    • Send the new configuration to the motor controller
    • Disconnect the battery
    • Unplug the adapter cable
    • Reconnect the display cable
    • Reconnect the battery

    Makes more sense to me, even if it’s more tedious.

    Tuck this OpenSCAD source code for the case into the original program that produces the battery mounts:

    Layout = "Build";               // [Frame,Block,Show,Build,Bushing,Cateye,Case]
    
    … snippage …
    
    // Programming cable case
    
    ProgCavity = [70.0,19.0,10.0];
    ProgBlock = [85.0,25.0,15.0];
    ProgCableOD = 4.0;
    
    module ProgrammerCase() {
    
        difference() {
            hull() {
                for (i=[-1,1], j=[-1,1])
                    translate([i*(ProgBlock.x/2 - CornerRadius),j*i*(ProgBlock.y/2 - CornerRadius),-ProgBlock.z/2])
                        cylinder(r=CornerRadius,h=ProgBlock.z,$fn=12);
                }
            translate([-ProgBlock.x,0,0])
                rotate([0,90,0])
                    PolyCyl(ProgCableOD,3*ProgBlock.x,6);
            cube(ProgCavity,center=true);
        }
    }
    
    // Half case sections for printing
    
    module HalfCase(Section = "Upper") {
    
        intersection() {
           translate([0,0,ProgBlock.z/4])
                cube([2*ProgBlock.x,2*ProgBlock.y,ProgBlock.z/2],center=true);
            if (Section == "Upper")
                translate([0,0,-Kerf/2])
                    ProgrammerCase();
            else
                translate([0,0,ProgBlock.z/2])
                    ProgrammerCase();
        }
    }
    
    … snippage …
    
    // tuck this into the Build conditional
    
        translate([0,3*Block.x,0]) {
    
            translate([gap*ProgBlock.x/2,0,ProgBlock.z/2])
                rotate([180,0,0])
                    HalfCase("Upper");
            translate([-gap*ProgBlock.x/2,0,0])
                HalfCase("Lower");
    

  • Pixel 3a Boot Failure

    Pixel 3a Boot Failure

    Attempting to rub a smudge off the phone’s screen while it was booting causes problems:

    Pixel 3a boot fail
    Pixel 3a boot fail

    Rebooting that sucker cleared the problem, so it wasn’t permanently fatal.

    Whew!

  • Sherline CNC Driver Step Pulse Width Puzzle

    Sherline CNC Driver Step Pulse Width Puzzle

    Long long ago, as part of tidying up the power distribution inside the Sherline CNC controller PCB, I wrote a cleanroom reimplementation of its PIC firmware and settled on a 25 µs Step pulse width with a minimum 50 µs period:

    [PARPORT]
    ADDRESS = 0x378
    RESET_TIME = 10000
    STEPLEN = 25000
    STEPSPACE = 25000
    DIRSETUP = 50000
    DIRHOLD = 50000
    

    Even shorter values for the Direction signal worked with the initial pncconf setup for the Mesa 5I25 FPGA card:

    DIRSETUP   = 25000
    DIRHOLD    = 25000
    STEPLEN    = 25000
    STEPSPACE  = 25000
    
    

    After thrashing through enough of the Kicad-to-HAL converter to get a HAL file sufficiently tasty to prevent LinuxCNC from spitting it out, the X and A axes moved with a gritty sound and the two other axes were pretty much inert.

    After eliminating everything else, including having Tiny Scope™ confirm the pulses were exactly the right duration, I increased them by 10 µs:

    DIRSETUP   = 35000
    DIRHOLD    = 35000
    STEPLEN    = 35000
    STEPSPACE  = 35000
    

    After which, all the axes suddenly worked perfectly.

    At some point along the way, I (re)discovered that Sherline Step pulses are active-low, although in practical terms getting the pulse upside-down just delays the active edge by its width. Given that the Sherline’s top speed is 24 inch/min = 0.4 inch/s, the minimum step period is 156 µs and even a wrong-polarity step should work fine.

    For the record, here’s a perfectly good Step pulse:

    Mesa 5I25 35us active-low Step pulse
    Mesa 5I25 35us active-low Step pulse

    Gotta wipe off that screen more often …