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

  • 3D Printed Chain Mail Armor: Cosplay Edition

    Starting from the improved chain mail link design, extend the top bars enough to clear the cross links, then bridge across them to form a flat cap:

    Chain Mail - Armor and Link
    Chain Mail – Armor and Link

    The OpenSCAD code makes the links as small as they can possibly be, based on the bar size and clearances, then rounds up to a multiple of the thread width so the flat cap will fill properly. Given the extrusion thread dimensions and the bar sizes, the OpenSCAD code computes everything else: the link model matches the slicer settings that define the printer’s output.

    Given:

    • Thread: 0.4 mm wide x 0.2 mm thick
    • Bar: 6 thread wide x 4 thread thick = 2.4 x 0.8 mm
    • Clearances: 2 thread horizontal x 5 thread vertical = 0.8 x 1.0 mm

    All the links measure 15.6 mm from side to side, the short connecting links are 2.6 mm tall, and the flat caps are 4.4 mm tall. Interlinked links sit 8.2 mm on center = half the link side plus one thread width clearance, which is 16.4 mm on center for adjacent links.

    Duplicated appropriately, the caps resemble turtle armor:

    Chain Mail - Flat Armor
    Chain Mail – Flat Armor

    Which look about the same in real life, minus the cheerful colors:

    Armor Buttons - on platform - side
    Armor Buttons – on platform – side

    Now, however, you can plunk an armor button atop the cap:

    Chain Mail Armor - 4 sided
    Chain Mail Armor – 4 sided

    With any number of sides:

    Chain Mail Armor - 6 sided
    Chain Mail Armor – 6 sided

    Up to a truncated cone:

    Chain Mail Armor - 24 sided
    Chain Mail Armor – 24 sided

    The flat tip makes the button more durable and user-friendly, but you can make it a bit more pointy if you favor that sort of thing. The button adds 6 mm to the link base, making armor links 10.4 mm tall.

    Other printable stuff could fit on that cap: letters, decorations, widgets, whatever.

    I think square armor buttons look ever so imposing when they’re arrayed in a sheet:

    Chain Mail Armor - square - 4 sided
    Chain Mail Armor – square – 4 sided

    The general idea being that you could attach the armor sheet to a cloth / leather backing to form a gauntlet or greave; the border of bottom links around the button array should serve for that purpose.

    The plastic prints just like the model and pops off the M2’s platform ready to use, with no finishing required:

    Chain Mail Armor - square on desk
    Chain Mail Armor – square on desk

    The two-color effect came from hot-swapping black filament as the red PLA ran out. The 6×6 armor button array and the 7×7 connecting link array holding it together required 14 meters of filament and I guesstimated the red spool held 9 meters: I was ready when the last of the red vanished just after completing the bridging layer under the flat caps. Filament swaps work reasonably well; I’d hate to do that on a production basis.

    If you don’t mind my saying so, everybody thinks it’s spectacular:

    Chain Mail Armor - square on arm
    Chain Mail Armor – square on arm

    The sheet has a definite “grain” defined by the orientation of the bottom links, making it far more bendy in one direction than the other:

    Chain Mail Armor - square rolled
    Chain Mail Armor – square rolled

    The sheet layout orients the more-bendy direction along the M2’s (longer) Y axis, so that sheets can wrap snugly around your arm (or leg) and extend straight-ish along the bones in the other direction. That should be configurable, I think.

    There’s an option to rotate the links by 45° to produce diamond-theme arrays:

    Chain Mail Armor - diamond - 8 sided
    Chain Mail Armor – diamond – 8 sided

    Which would make good patch armor, if you’re into that sort of thing:

    Chain Mail Armor - diamond on hand
    Chain Mail Armor – diamond on hand

    Those have octagonal buttons, which IMHO don’t look nearly as crisp as the four-sided version.

    Ah! I should generalize the diamond rotation option to select all four useful rotations.

    The 6×6 square sheet requires three hours on the M2, with the intial print time estimates being low by nearly a factor of two. The M2 has a 200×250 mm platform and I’ll definitely try a full-size array just to see how it works.

    The OpenSCAD source code, which stands badly in need of refactoring:

    // Chain Mail Armor Buttons
    // Ed Nisley KE4ZNU - November 2014
    
    Layout = "Build";			// Link Button LB Build
    
    //-------
    //- Extrusion parameters must match reality!
    //  Print with 1 shell and 2+2 solid layers
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    HoleWindage = 0.2;
    
    Protrusion = 0.1;			// make holes end cleanly
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    //-------
    // Dimensions
    
    //- Set maximum sheet size
    
    SheetSizeX = 70;
    SheetSizeY = 80;
    
    //- Diamond or rectangular sheet?
    
    Diamond = false;					// true = rotate 45 degrees, false = 0 degrees for square
    ArmorButton = true;					// true = build button atop cap
    
    // Link bar sizes
    
    BarWidth = 6 * ThreadWidth;
    BarThick = 4 * ThreadThick;
    
    BarClearance = 5*ThreadThick;		// vertical clearance above & below bars
    
    //-- Compute link sizes from those values
    
    // Absolute minimum base link: bar width + corner angle + build clearance around bars
    //  rounded up to multiple of thread width to ensure clean filling
    BaseSide = IntegerMultiple((4*BarWidth + 2*BarWidth/sqrt(2) + 3*(2*ThreadWidth)),ThreadWidth);
    
    BaseHeight = 2*BarThick + BarClearance;           // both bars + clearance
    
    echo(str("BaseSide: ",BaseSide," BaseHeight: ",BaseHeight));
    
    BaseOutDiagonal = BaseSide*sqrt(2) - BarWidth;
    BaseInDiagonal = BaseSide*sqrt(2) - 2*(BarWidth/2 + BarWidth*sqrt(2));
    
    echo(str("Outside diagonal: ",BaseOutDiagonal));
    
    //- On-center distance measured along coordinate axis
    
    LinkOC = BaseSide/2 + ThreadWidth;
    
    LinkSpacing = Diamond ? (sqrt(2)*LinkOC) : LinkOC;
    echo(str("Base spacing: ",LinkSpacing));
    
    //- Compute how many links fit in sheet
    
    MinLinksX = ceil((SheetSizeX - (Diamond ? BaseOutDiagonal : BaseSide)) / LinkSpacing);
    MinLinksY = ceil((SheetSizeY - (Diamond ? BaseOutDiagonal : BaseSide)) / LinkSpacing);
    echo(str("MinLinks X: ",MinLinksX," Y: ",MinLinksY));
    
    NumLinksX = ((0 == (MinLinksX % 2)) && !Diamond) ? MinLinksX + 1 : MinLinksX;
    NumLinksY = ((0 == (MinLinksY % 2) && !Diamond)) ? MinLinksY + 1 : MinLinksY;
    echo(str("Links X: ",NumLinksX," Y: ",NumLinksY," Total: ",NumLinksX*NumLinksY));
    
    //- Armor button base
    
    CapThick = BarThick;
    
    ButtonHeight = BaseHeight + BarClearance + CapThick;
    echo(str("ButtonHeight: ",ButtonHeight));
    
    //- Armor ornament size & shape
    
    ArmorSides = 4;
    ArmorAngle = true ? 180/ArmorSides : 0;			// rotate half a side?
    
    ArmorThick = IntegerMultiple(6,ThreadThick);	// keep it relatively short
    
    ArmorOD = 1.1 * BaseSide;						// tune for best fit at base
    
    ArmorID = 10 * ThreadWidth;						// make the tip wide & strong
    
    //-------
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      RangeX = floor(95 / Space);
      RangeY = floor(125 / Space);
    
    	for (x=[-RangeX:RangeX])
    	  for (y=[-RangeY:RangeY])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    //-------
    // Create base link
    
    module BaseLink() {
    
    render()
    	rotate(Diamond ? 45 : 90)			// 90 = more bendy around X axis
    		difference() {
    			translate([0,0,BaseHeight/2]) {
    				difference(convexity=2) {
    					intersection() {		// outside shape
    						cube([BaseSide,BaseSide,BaseHeight],center=true);
    						rotate(45)
    							cube([BaseOutDiagonal,BaseOutDiagonal,BaseHeight],center=true);
    					}
    					intersection() {		// inside shape
    						cube([(BaseSide - 2*BarWidth),
    							  (BaseSide - 2*BarWidth),
    							  (BaseHeight + 2*Protrusion)],
    							 center=true);
    						rotate(45)
    							cube([BaseInDiagonal,
    								  BaseInDiagonal,
    								  (BaseHeight +2*Protrusion)],
    								 center=true);
    					}
    				}
    			}
    
    			translate([0,0,(BaseHeight/2 + BarThick)])
    				cube([(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
    					  (2*BaseSide),
    					  BaseHeight],
    					 center=true);
    			translate([0,0,(BaseHeight - BaseHeight/2 - BarThick)])
    				cube([(2*BaseSide),
    					  (BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
    					  BaseHeight],
    					 center=true);
    		}
    }
    
    //-------
    // Create button link
    
    module ButtonLink() {
    
    render()
    	rotate(Diamond ? 45 : 90)			// 90 = more bendy around X axis
    		union() {
    			difference() {
    				translate([0,0,ButtonHeight/2])		// outside shape
    					intersection() {
    						cube([BaseSide,BaseSide,ButtonHeight],center=true);
    						rotate(45)
    							cube([BaseOutDiagonal,BaseOutDiagonal,ButtonHeight],center=true);
    					}
    				translate([0,0,(BaseHeight + BarClearance - Protrusion)/2])
    					intersection() {		// inside shape
    						cube([(BaseSide - 2*BarWidth),
    								(BaseSide - 2*BarWidth),
    								(BaseHeight + BarClearance + Protrusion)],
    								center=true);
    						rotate(45)
    							cube([BaseInDiagonal,
    									BaseInDiagonal,
    									(BaseHeight + BarClearance + Protrusion)],
    									center=true);
    				}
    
    				translate([0,0,((BarThick + 2*BarClearance)/2 + BarThick)])
    					cube([(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
    						(2*BaseSide),
    						BarThick + 2*BarClearance],
    						center=true);
    
    				translate([0,0,(BaseHeight/2 - BarThick)])
    					cube([(2*BaseSide),
    						(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
    						BaseHeight],
    						center=true);
    
    			}
    
    			if (ArmorButton)
    				translate([0,0,(ButtonHeight - Protrusion)])		// armor on cap
    					rotate(ArmorAngle)
    					cylinder(d1=ArmorOD,
    							 d2=ArmorID,
    							 h=(ArmorThick + Protrusion),
    							 $fn=ArmorSides);
    		}
    }
    
    //-------
    // Build it!
    
    ShowPegGrid();
    
    if (Layout == "Link") {
    	BaseLink();
    }
    
    if (Layout == "Button") {
    	ButtonLink();
    }
    
    if (Layout == "LB") {
    	ButtonLink();
    	translate([LinkSpacing,LinkSpacing,0])
    		BaseLink();
    }
    
    if (Layout == "Build") {
    	for (ix = [0:(NumLinksX - 1)],
    		 iy = [0:(NumLinksY - 1)])
    			assign(x = (ix - (NumLinksX - 1)/2)*LinkSpacing,
    				   y = (iy - (NumLinksY - 1)/2)*LinkSpacing)
    			translate([x,y,0])
    			color([(ix/(NumLinksX - 1)),(iy/(NumLinksY - 1)),1.0])
    				if (Diamond)
    					if ((ix + iy) % 2)						// armor at odd,odd & even, even points
    						ButtonLink();
    					else
    						BaseLink();							// connectors otherwise
    				else
    					if ((iy % 2) && (ix % 2))				// armor at odd,odd points
    						ButtonLink();
    					else if ((!(iy % 2) && !(ix % 2)))		// connectors at even,even points
    						BaseLink();
    }
    
    
  • Improved Chain Mail Link

    The rectangular posts in my chain mail resemble Zomboe’s original design, but with dimensions computed directly from the bar (and, thus, thread) widths and thicknesses to ensure good fill and simple bridging:

    Chain Mail Link
    Chain Mail Link

    They fit together well, but the angled post edges make the bridge threads longer than absolutely necessary along the outside edge of each link:

    Chain Mail Sheet - detail
    Chain Mail Sheet – detail

    A bit of fiddling produces a squared-off version:

    Chain Mail Link - Improved Posts
    Chain Mail Link – Improved Posts

    Which nest together like this:

    Chain Mail - Improved Posts - Bottom View
    Chain Mail – Improved Posts – Bottom View

    Now all the bridge threads have the same length, which should produce better results.

    The OpenSCAD source code for the link:

    module BaseLink() {
    
    	render(convexity=2)
    		difference() {
    			translate([0,0,BaseHeight/2]) {
    				difference(convexity=2) {
    					intersection() {		// outside shape
    						cube([BaseSide,BaseSide,BaseHeight],center=true);
    						rotate(45)
    							cube([BaseOutDiagonal,BaseOutDiagonal,BaseHeight],center=true);
    					}
    					intersection() {		// inside shape
    						cube([(BaseSide - 2*BarWidth),
    							  (BaseSide - 2*BarWidth),
    							  (BaseHeight + 2*Protrusion)],
    							 center=true);
    						rotate(45)
    							cube([BaseInDiagonal,
    								  BaseInDiagonal,
    								  (BaseHeight +2*Protrusion)],
    								 center=true);
    					}
    				}
    			}
    
    			translate([0,0,(BaseHeight/2 + BarThick)])
    				cube([(BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
    					  (2*BaseSide),
    					  BaseHeight],
    					 center=true);
    			translate([0,0,(BaseHeight - BaseHeight/2 - BarThick)])
    				cube([(2*BaseSide),
    					  (BaseSide - 2*BarWidth - 2*BarWidth/sqrt(2)),
    					  BaseHeight],
    					 center=true);
    		}
    }
    
  • Windows Driver Update: Root Canal Edition

    This is not what you want to see on the monitor displaying the dental X-ray images guiding your dentist during a root canal:

    Epson Driver Update - X-Ray Screen
    Epson Driver Update – X-Ray Screen

    Yup, exactly what you’d expect:

    Epson Driver Update - X-Ray Screen - Detail
    Epson Driver Update – X-Ray Screen – Detail

    They dismissed the message and continued the mission.

    You’d think that for as much as they’re surely paying for that software package, it would hold off all the updates until after office hours…

    [Those quick on the RSS feed saw this in mid-November, after a finger fumble while typing the date dropped it into the past…]

  • 3D Printed Chain Mail Again

    Everybody likes chain mail, so I made a few big sheets:

    Chain Mail Sheet
    Chain Mail Sheet

    That’s a nominal 150 mm on the X axis and 200 mm on the Y, which pretty well fills the M2’s 8×10 inch platform after Slic3r lays a few skirt threads around the outside. All 192 links require a bit under four hours to print: all those short movements never let the platform get up to full speed.

    Look no further for a brutal test of platform alignment and adhesion. The platform is slightly too high in the left front corner and, no surprise, slightly too low in the right rear. The skirt thread varies from 0.15 to 0.27 around the loop.

    Hairspray works wonder to glue down all those little tiny links. They pop off the platform quite easily after it cools under 50 °C, with no need for any post-processing.

    This version of the OpenSCAD code correctly figures the number of links to fill a given width & length; the old code didn’t get it quite right.

    Coloring the links makes the whole thing easier to look at:

    Chain Mail Sheet - detail
    Chain Mail Sheet – detail

    The real world version comes out in red PLA that saturates Sony imagers:

    Chain Mail - flexed
    Chain Mail – flexed

    It really is that flexible!

    The OpenSCAD source code:

    // Chain Mail Sheet
    // For Slic3r and M2 printer
    // Ed Nisley KE4ZNU - Apr 2013
    //   Oct 2013 - larger links, better parameterization
    //   Nov 2014 - fix size calculation, add coloration
    
    Layout = "Show";			// Link Build Show
    
    //-------
    //- Extrusion parameters must match reality!
    //  Print with +0 shells and 6 solid layers
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    HoleWindage = 0.2;
    
    Protrusion = 0.1;			// make holes end cleanly
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    //-------
    // Dimensions
    
    BarThreads = 6;
    BarWidth = BarThreads * ThreadWidth;
    
    BarThick = 4 * ThreadThick;
    
    LinkSquare = IntegerMultiple(2.5*BarThreads,ThreadWidth);
    LinkHeight = 2*BarThick + 4*ThreadThick;           // bars + clearance
    
    echo(str("Link height: ",LinkHeight));
    
    LinkOutDiagonal = LinkSquare*sqrt(2) - BarWidth;
    LinkInDiagonal = LinkSquare*sqrt(2) - 2*(BarWidth/2 + BarWidth*sqrt(2));
    
    echo(str("Outside diagonal: ",LinkOutDiagonal));
    
    LinkSpacing = 0.60 * LinkOutDiagonal;		// totally empirical
    echo(str("Link spacing: ",LinkSpacing));
    
    SheetSizeX = 150;
    SheetSizeY = 200;
    
    NumLinksX = floor((SheetSizeX - LinkOutDiagonal) / LinkSpacing) + 1;
    NumLinksY = floor((SheetSizeY - LinkOutDiagonal) / LinkSpacing) + 1;
    
    echo(str("Links X: ",NumLinksX," Y: ",NumLinksY," Total: ",NumLinksX*NumLinksY));
    
    //-------
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      RangeX = floor(95 / Space);
      RangeY = floor(125 / Space);
    
    	for (x=[-RangeX:RangeX])
    	  for (y=[-RangeY:RangeY])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    //-------
    // Create basic link
    
    module Link() {
        render()
    	rotate(45)
    		difference(convexity=2) {
    			translate([0,0,LinkHeight/2]) {
    				difference(convexity=2) {
    					intersection() {		// outside shape
    						cube([LinkSquare,LinkSquare,LinkHeight],center=true);
    						rotate(45)
    							cube([LinkOutDiagonal,LinkOutDiagonal,LinkHeight],center=true);
    					}
    					intersection() {		// inside shape
    						cube([(LinkSquare - 2*BarWidth),(LinkSquare - 2*BarWidth),(LinkHeight + 2*Protrusion)],center=true);
    						rotate(45)
    							cube([LinkInDiagonal,LinkInDiagonal,(LinkHeight +2*Protrusion)],center=true);
    					}
    				}
    			}
    			for (i=[-1,1]) {				// create bars
    				translate([0,-i*(sqrt(2)*BarWidth/2),BarThick])
    					rotate(45 + 180*(i+1)/2)
    						cube([LinkOutDiagonal,LinkOutDiagonal,LinkHeight]);
    				translate([i*(sqrt(2)*BarWidth/2),0,-BarThick])
    					rotate(135 + 180*(i+1)/2)
    						cube([LinkOutDiagonal,LinkOutDiagonal,LinkHeight]);
    			}
    		}
    }
    
    //-------
    // Build it!
    
    ShowPegGrid();
    
    if (Layout == "Link") {
      Link();
    
    }
    
    if (Layout == "Build" || Layout == "Show") {
    	for (ix=[-(NumLinksX/2 - 0):(NumLinksX/2 - 1)])
    		for (iy=[-(NumLinksY/2 - 0):(NumLinksY/2 - 1)])
    			translate([ix*LinkSpacing + LinkSpacing/2,iy*LinkSpacing + LinkSpacing/2,0])
    				if (Layout == "Show")
    					color([0.5+(ix/NumLinksX),0.5+(iy/NumLinksY),1.0]) Link();
    				else Link();
    }
    
  • Kenmore 158: Bubble Sorted Motor Current Sampling

    Because the ET227 transistor acts as a current limiter, the motor current waveform has flat tops at the level set by the DAC voltage. However, the current depends strongly on the temperature of all those transistor junctions, with some commutation noise mixed in for good measure, so the firmware must measure the actual current to know what’s going on out there.

    Here’s one way to pull that off:

    Motor current - ADC sample timing
    Motor current – ADC sample timing

    The upper waveform shows the motor current sporting flat tops at 650 mA.

    The lower waveform marks the current measurement routine, with samples taken just before the falling edge of the first nine pulses. The (manually tweaked) delay between the samples forces them to span one complete cycle of the waveform, but they’re not synchronized to the power line. Remember that the motor runs from a full wave rectifier, so each “cycle” in that waveform is half of a normal power line cycle.

    Given an array containing those nine samples, the routine must return the maximum value of the waveform, ignoring the little glitch at the start of the flat top and taking into consideration that the waveform won’t have a flat top (or much of a glitch) when the current “limit” exceeds the maximum motor current.

    After a bit of fumbling around with the scope and software, the routine goes like this:

    • Collect samples during one current cycle
    • Sort in descending order
    • Ignore highest sample
    • Return average of next two highest samples

    Given that the array has only nine samples, I used a quick-and-dirty bubble sort. The runt pulse at the end of the series in the bottom waveform brackets the sort routine, so it’s not a real time killer.

    Seeing as how this is one of the very few occasions I’ve had to sort anything, I wheeled out the classic XOR method of exchanging the entries. Go ahead, time XOR against swapping through a temporary variable; it surely doesn’t make any difference at all on an 8-bit microcontroller.

    The sampling code, with all the tracing stuff commented out:

    //------------------
    // Sample current along AC waveform to find maximum value
    //	this is blocking, so don't call it every time around the main loop!
    
    #define NUM_I_SAMPLES	9
    
    unsigned int SampleCurrent(byte PinNum) {
    	
    unsigned int Samples[NUM_I_SAMPLES];
    unsigned int AvgSample;
    byte i,j;
    	
    //	digitalWrite(PIN_SYNC,HIGH);
    	for (i=0; i < NUM_I_SAMPLES; i++) {				// collect samples
    //		digitalWrite(PIN_SYNC,HIGH);
    		Samples[i] = ReadAI(PinNum);
    //		digitalWrite(PIN_SYNC,LOW);
    		delayMicroseconds(640);
    	}
    //	digitalWrite(PIN_SYNC,LOW);
    	
    //	digitalWrite(PIN_SYNC,HIGH);							// mark start of sorting
    	for (i=0; i < (NUM_I_SAMPLES - 1); i++)
    		for (j=0 ; j < (NUM_I_SAMPLES - 1 - i); j++)
    			if (Samples[j] < Samples[j+1]) {
    				Samples[j] ^= Samples[j+1];					// swap entries!
    				Samples[j+1] ^= Samples[j];
    				Samples[j] ^= Samples[j+1];
    			}
    //	digitalWrite(PIN_SYNC,LOW);								// mark end of sorting
    	
    //	printf("Samples: ");
    //	for (i=0; i < NUM_I_SAMPLES; i++)
    //		printf("%5d,",Samples[i]);
    	
    	AvgSample = (Samples[1] + Samples[2])/2;				// discard highest sample
    //	printf(" [%5d]\r\n",AvgSample);
    	
    	return AvgSample;
    
    }
    
  • Kenmore 158: Motor RPM Sensor Deglitching

    The setscrew in the motor pulley lies directly in the path of the photosensor:

    TCTR5000 Motor RPM Sensor - side view
    TCTR5000 Motor RPM Sensor – side view

    Which produces a glitch in the rising edge of the digital output as the pulley rotates from the dark to the light section:

    Motor Sensor - Rising Edge Glitch
    Motor Sensor – Rising Edge Glitch

    The RPM signal goes to Arduino pin D2, where each falling edge triggers an interrupt handler:

    const byte PIN_MOTOR_REV = 2;		// DI - IRQ 0 (must be D2)
    
    ... snippage...
    
    void setup() {
    ... snippage ...
    
        pinMode(PIN_MOTOR_REV,INPUT_PULLUP);
        attachInterrupt((PIN_MOTOR_REV - 2),ISR_Motor,FALLING);			// one IRQ / motor revolution
    
     ... snippage ...
    }
    

    The maximum motor speed is about 11 kRPM, so interrupts should be at least 5.5 ms apart and the digital input should be low. If that’s true, then the code updates a bunch of useful information:

    struct pulse_t {
     byte Counter;
     unsigned long TimeThen;
     unsigned long Period;
     word RPM;
     byte State;
    };
    
    struct pulse_t Motor;
    
    ... snippage ...
    
    //------------------
    // ISR to sample motor RPM sensor timing
    
    void ISR_Motor(void) {
    
    static unsigned long Now;
    
    	digitalWrite(PIN_SYNC,HIGH);
    
    	Now = micros();
    
    	if ((5000ul < (Now - Motor.TimeThen)) && !digitalRead(PIN_MOTOR_REV) ) {	// discard glitches
    		Motor.Counter++;
    		Motor.Period = Now - Motor.TimeThen;
    		Motor.TimeThen = Now;
    		Motor.State = digitalRead(PIN_MOTOR_REV);		// always zero in a Physics 1 world
    	}
    
    	digitalWrite(PIN_SYNC,LOW);
    	return;
    }
    

    The scope trace shows that the handler takes about 7 µs to get control after the glitch (the left cursor should be on the falling edge, not the rising edge), so the input read occurs when the sensor output is over 4.5 V, causing the handler to discard this spurious interrupt.

    Because Motor.Period is a four-byte unsigned long, the Arduino’s CPU must handle it in chunks. Rather than disable interrupts around each use, it’s better to read the value until two successive copies come back identical:

    //------------------
    // Return current microsecond period without blocking ISR
    
    unsigned long ReadTime(struct pulse_t *pTime) {
    
    unsigned long Sample;
    
    	do {
    		Sample = pTime->Period;				// get all four bytes
    	} while (Sample != pTime->Period);		//  repeat until not changed by ISR while reading
    
    	pTime->Counter = 0;						// this is a slight race condition
    
    	return Sample;
    }
    

    Because the interrupts don’t happen that often, the loop almost always executes only one time. On rare occasions, it’ll go back for another two values.

    Converting the pulley rotation period into revolutions per minute goes like this:

    		Motor.RPM = 60000000ul/ReadTime(&Motor);		// one (deglitched) pulse / rev
    

    That’s easier than hiding the setscrew and it also discards any other glitches that may creep into D2

  • Kenmore 158: Power Switch Timing

    The crash test dummy sewing machine now has a cheerful red momentary pushbutton in the same spot the original machine sported a 120 VAC push-on/push-off power switch:

    Kenmore 158 - Digital Power Switch
    Kenmore 158 – Digital Power Switch

    It’s held in place by a dab of epoxy on the bottom. The threads aren’t quite long enough to engage the ring, so another dab of epoxy holds that in place. In the unlikely event I must replace the button, I’ll deploy a punch and hammer it out from the top; the slick paint on the sides of the straight-sided hole doesn’t provide much griptivity.

    The button connects in parallel with the GX270’s front-panel button and the one on the Low Voltage Interface Board, so it operates exactly the same way. My original code didn’t include a delay before turning the power off, which meant that brushing the switch while doing something else would kill the power.

    This is not to be tolerated…

    You (well, I) must now hold the button down for one second to turn the power off. Releasing it before the deadline has no effect, other than blinking the green power LED on the front panel a few times.

    The routine maintains a timer that allows it to yield control to the mainline code, rather than depend on a blocking timer that would screw up anything else that’s in progress:

    //------------------
    // Handle shutdown timing when power button closes
    // Called every time around the main loop
    
    void TestShutdown(void) {
    	
    	if (LOW == digitalRead(PIN_BUTTON_SENSE)) {			// power button pressed?
    		if (ShutdownPending) {
    			if (1000ul < (millis() - ShutdownPending)) {
    				printf("Power going off!\r\n");
    				digitalWrite(PIN_ENABLE_AC,LOW);
    				digitalWrite(PIN_ENABLE_ATX,LOW);
    				while(true) {
    					delay(20);
    					TogglePin(PIN_PWR_G);				// show we have shut down
    				}
    			}
    		}
    		else {
    			ShutdownPending = millis();					// record button press time
    			printf("Shutdown pending...\r\n");
    		}
    	}
    	else {
    		if (ShutdownPending) {
    			ShutdownPending = 0ul;						// glitch or button released
    			printf("Shutdown cancelled\r\n");
    		}
    	}
    }
    

    The normal Arduino bootloader imposes a similar delay while turning the power on, which means that you can’t accidentally light the machine up by bumping the switch. All in all, it’s much more user-friendly this way.