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: Machine Shop

Mechanical widgetry

  • Erasing a Hole

    Turning the plug OD
    Turning the plug OD

    The scrap pile disgorged a chunk of aluminum plate exactly the correct size for a heat spreader that will mate eight power FETS to that heatsink. The catch: a 1-1/4-inch deep hole tapped 1/4-20 for about 3/4 inch at almost the right spot along one end. Rather than sawing off Yet Another Chunk from the original plate, I figured it’d be more useful to just plug the hole.

    Note that this is somewhat different than the situation described there, where I screwed up by putting a hole in the wrong place. Here, I’m just being a cheapskate by making a piece of junk good enough to use in a project, rather than having it kick around in the scrap pile for another decade.

    Anyway.

    I turned a 3/8-inch diameter aluminum rod down to 1/4 inch for the threaded part and a bit under 0.200 inch to fit into the partially threaded end.

    A real machinist would single-point the thread, but I just screwed a die over it. The narrow end is slightly larger than the minor thread diameter, which helped get things started. Then a trial fit, saw off the excess on the skinny end, and apply a touch of the file to shape the end to mate with the hole’s drill-point bottom:

    Threaded hole plug
    Threaded hole plug
    Plug epoxied in place
    Plug epoxied in place

    I buttered up the plug with a generous helping of JB Weld epoxy and screwed it in. Toward the end of that process, the air trapped in the end became exceedingly compressed, to the extent I had to stop after each quarter-turn to let it ooze outward; eventually the hole gave off a great pffft as the remaining air pooted out. Unscrewed slightly to suck some epoxy back in, screwed it tight, and let it cure overnight.

    Squared-up block with plugged hole
    Squared-up block with plugged hole

    Sawed off the plug, filed the rubble more-or-less smooth, then squared it in the Sherline mill. The heatsink prefers to sit on a nice, smooth metal surface, so I flycut the other side of the block to get rid of a few dings and the entire anodized layer while I was at it.

    The epoxy ring doesn’t have a uniform width, because you’re looking at a cross section of the thread. The skinny part is the crest of the plug thread, the wide part is along one flank. Barely a Class 1A fit, methinks.

    New hole
    New hole

    Locate the midpoint of the block’s end, center-drill, then poke a new #29 hole 20 mm deep (I really do prefer metric!) for an 8-32 screw. The plug didn’t move at all during this process, pretty much as you’d expect. The chips came out of this hole in little crumbles, rather than the long stringy swarf from the solid aluminum on the other end.

    Using a simple peck drill canned cycle is just downright wonderful:

    G83 Z-20 R1 Q3 F100
    

    The rule of thumb is 3000 RPM with a feed 100 times the drill diameter. In this case, the drill is about 3 mm and calls for 300 mm/min, but the Sherline is happier with slower feeds. Maybe if I was doing production work, I’d push it harder.

    A real machinist would have a milling machine with a servo-driven spindle for rigid tapping, but I just screwed an ordinary hand tap into the holes.

    A bit of futzing converted a pair of solderless connectors into clips that capture the hooks on the ends of the heatsink’s springy wiffletree to secure the spreader to the heatsink. You can see the flycut surface peeking out from below the end of the heatsink. I should hit it with some fine abrasive to polish it out, but I think heatsink compound alone will do the trick.

    Heat spreader on heatsink
    Heat spreader on heatsink

    The next step: drilling-and-tapping eight more blind holes along the sides for the FETs. It’d be really neat to have a servo spindle…

  • Bicycle Tube: Complete Failure

    Glass shard in tire
    Glass shard in tire

    On my way back from a ride around the block the back tire went pfft thump thump thump. I’m 1.5 miles from home: fix or walk?

    The first step: always examine the tire to find the puncture, before you move too far. Finding something sticking out of the tire means you’re well on your way to fixing the flat. Lose the entry point and you’re left to blow up the tire and listen for escaping wind. So I picked up the butt end of the bike, spun the wheel, and this little gem heaved into view…

    That area of the road has seen several collisions in recent months that left the shoulder littered with broken automotive glass. The shard in my tire glistened like a diamond, because one side was flat and mirrored; perhaps it’s from a headlamp reflector or side mirror. The pointy end went into the tire, of course…

    Glass fragment and puncture
    Glass fragment and puncture

    Well, a single-point failure like that is easy to fix, so:

    • remember that the hole is a few inches spinward of the label
    • shift to small chainring / small sprocket
    • get the tool bag out
    • lay the bike down (it’s a recumbent, this is no big deal)
    • release the rear brake
    • release the skewer and whack the hub out of the dropouts
    • apply tire irons to get the tire off
    • pop the tube out and examine the innards

    No pix of any of that, but suffice it to say I was astonished to discover that the glass penetrated the Marathon tire’s Kevlar belt just barely far enough to poke the Slime tire liner, but not enough to leave more than a hint of a mark on the tube. Definitely not a puncture and certainly nothing that would account for a sudden flat.

    That glass shard is not why the tire went flat! Tire liners FTW!

    Examining the rest of the tube revealed this situation a few inches anti-spinward of the glass fragment.

    Failed tube rubber
    Failed tube rubber

    There’s a row of holes across the tube, with no corresponding tire or liner damage at all. As nearly as I can tell, the tube rubber simply pulled apart across that line, all at once, and the air went pfft just like you’d expect.

    That’s not survivable, but I don’t carry a spare tube (well, two spare tubes: 700x35C rear and 20×1.25 front) on rides around the block. Long bike tours? Yup, spare tires & tubes because I’m that type of guy.

    Anyway, I’ve got the tube in hand, so what’s to lose? Scuff it up with the sandpaper and yipes

    Tube after scuffing
    Tube after scuffing

    What’s not obvious in the picture is that all those little spots around the big holes are pinholes. The whole area of the tube must have gotten just barely enough rubber to cover the mold.

    I know as well as you do this isn’t going to have a happy outcome, but I slobber on the cement, let it dry, squash on a big patch, install the tube & tire, fire a 16-gram CO2 cartridge into it, and … it doesn’t seal.

    The tube is several-many years old, probably from whoever was supplying Nashbar at the time, and it served well, so it gets a pass. I’d rather tubes fail in the garage than on the road and sometimes they do, but that’s not the usual outcome.

    My ladies were out gardening at the time and a long wheelbase ‘bent isn’t the sort of thing you can stuff into a friend’s car. Not to mention that my ladies had the magic phone.

    So I walked home.

    Sometimes a man’s gotta do what a man’s gotta do.

    Memo to Self: Schwalbe tube at 8910. Reversed(*) the Marathon’s direction.

    (*)They’re directional, but when they get about halfway worn I don’t see that it makes much difference. The rear tire on my bikes wears asymmetrically: probably too many tools in the left underseat bag.

  • Soap Dispenser Pump Lube Job

    When I replaced the kitchen counter & installed a new sink, I added a soap dispenser, mostly because the stainless steel sink had three holes that needed filling. After nigh onto a decade, the dispenser pump is now getting sticky: difficult to push down and reluctant to pop up.

    Soap dispenser pump
    Soap dispenser pump

    The problem seemed to be that the O-ring wasn’t sliding nicely along the internal bore.

    The catch is that both ends have ball check valves, so you can’t just squirt lube into the bore. I tried prying the thing apart, but the snap-together cap has a really aggressive closure.

    So I shoved the exit valve ball (on the left of the picture) out of the way with a pin punch, wedged it into the end of the spring, and squirted the least amount of silicone lube I could manage into the pump. A bit of fiddling un-wedged the ball and got it back in position.

    The pump works fine now, but I have my doubts as to how long the lube will last with continuous exposure to soap and constant sliding.

    The thing probably needs a new O-ring and I’m certain of two facts:

    1. Getting the pump apart will wreck it
    2. The O-ring isn’t a standard size
  • Shop Assistant: First Thread

    My shop assistant came home with a five-dollar tag sale find: either a genuine antique car horn or a reasonable facsimile lashed together by an underemployed Pakistani shipbreaker. The original rubber bulb had long since rotted away, but the brass reed worked fine and the horn gave off a mighty honk! when given sufficient wind.

    She bought a replacement bulb with hardware definitely made by the shipbreakers, knowing full well that the internal thread on the end of the new bulb’s brass stem couldn’t possibly match up with the external thread on the old horn. We sketched out some possibilities and decided to make a bushing over the horn’s stem with an internal thread: easier than a very short, perilously thin, double-threaded adapter ring.

    She measured various dimensions of both pieces and we consulted Machinery’s Handbook. The horn has a really crusty 32-tpi thread somewhere between 1/2 and 9/16 inch, which is not standard at all. Heck, it’s not even metric. (#include standard-metric-goodness-rant)

    Horn fitting
    Horn fitting

    The fitting also has an internal pipe thread (!) for the brass reed assembly. We eventually filed a few bits off the reed’s mounting dingus in order to clear the final bushing ID.

    Some poking forced the scrap pile to disgorge an aluminum cylinder of exactly the right size for the bushing, with a nice half-inch hole right down the middle. Using a half-inch bolt with a center-drilled end as a mandrel, we brass-hammered it to line up pretty true, and she cleaned off the OD while learning about the quick-change gearbox; a round-nose bit at 104 tpi puts a nice zeepy (her term) finish on aluminum.

    We left it stout, rather than trying to turn it down to a thin and elegant shell, because that was the easiest way to get things done. She’ll epoxy it to the horn stem and apply some Loctite to the horn bushing.

    A lot of rummaging in the tool cabinet’s recesses produced a taper-shank drill slightly larger than the bulb stem. She drilled out most of the cylinder’s guts, leaving just enough for the threads at the far end, counting 1/10-inch turns on the tailstock all the way.

    Shop Assistant Making Swarf
    Shop Assistant Making Swarf

    That pile of razor-edged swarf is now prized possession…

    She bored out the narrow end to what seemed like the right minor diameter, given that we really didn’t have anything more than a guesstimate of the thread dimensions. I figured we could just continue threading, eating away at the ID, until it fit.

    I don’t do a lot of internal threading, but we found a suitable threading tool, lined things up, and she learned about single-point threading by cutting a thread to match that horn. No measurements worth mentioning; this wasn’t the sort of job requiring a Go-NoGo gage.

    I stayed away while she completed the threading, apart from consoling her when she discovered why you shouldn’t hand-rotate the chuck with the quick-change gearbox disconnected. We picked up the thread again and she completed the mission.

    Here’s the raw thread before beveling the entrance.

    Horn Bushing
    Horn Bushing

    And then it fit! Verily, the horn itself was the Go-NoGo gage.

    Horn in bushing
    Horn in bushing

    This was the second part she’s turned on the lathe; I’d say she’s doing just fine.

    Now, that was some Quality Shop Time…

  • Resistance Soldering: Electrodes

    With transformer, circuitry, and firmware in hand, the final step is to get juice to the workpiece. Resistance soldering depends on passing a high current through a relatively low resistance: the power varies as the square of the current, so more current is much better.

    The catch is that the transformer produces a relatively low voltage, so the initial circuit resistance must be exceedingly low. With 5 Vrms from the transformer secondary, a mere 0.5 Ω in the secondary circuit limits the maximum current to 10 A. Even with most of that in the joint, it’s not gonna work as you expect.

    I scrounged some very flexible 6-conductor signal cable with several shields that, with everything conductive crimped-and-soldered together on both ends, worked out to 1 mΩ/foot. A pair of 7-foot leads with copper lugs swaged-and-soldered on each end, bolted together to the transformer secondary, produced 280 A at 4.1 V: a bit over a kilowatt. The secondary winding and lugs evidently contribute 4 mΩ of resistance to the total.

    CAUTION – That much current makes the cables twitch in their own magnetic field. If you wear finger rings, bracelets, or metallic body jewelry, remove it. A metallic ring looks like a single shorted secondary winding that can couple magnetic flux from the cables and get surprisingly hot surprisingly quickly.

    Don’t put the rings in your pocket, either. Your pants are not a good magnetic shield.

    With that in mind, some electrodes:

    Electrodes
    Electrodes

    The black block is a slab of machinable graphite clamped to a brass plate that probably served as a wall bracket in a previous life. It serves as a nice base for most operations: conductive, non-sticky for solder, doesn’t produce nasty arc scorches. A cable from the secondary bolts firmly to the brass plate and the vast surface area provides a low-resistance contact. The cheesy plastic clamps work fine: the block doesn’t get too hot.

    The huge electrode comes from a carbon-arc spotlight. It’s actually too long, with too much resistance, and doesn’t work well at all.

    The tiny electrode is a steel welding rod (for gas welding). It works well for very small setups, but has essentially no resistance and requires low duty cycles.

    The Goldilocks electrode in the middle is a length of 5/32-inch carbon gouging rod (for arc welding). It has a copper coating that tends to burn off near the tip, but the overall resistance remains low enough that the joint heats well. The middle glows yellow-hot if you overdo things, hence the discolored section.

    To date, I’ve used a few inches off the end of one 12-inch rod. There are 49 more rods in the package. If you build one of these things and don’t want to pass a similar box along to your heirs, drop me a note and I’ll send you a rod.

    The scrap box emitted a sturdy cardboard tube that slipped over the cable so well that I simply gave up thinking about making an actual handle with a contact switch and all that stuff.

    Resistance soldering electrode handle
    Resistance soldering electrode handle

    All the electrodes terminate in homebrew clamps made from copper lugs that bolt to the transformer’s secondary terminals with 10-32 machine screws. The gouging rod has steel rings (forged from husky wire) holding the lug closed around the rod; they’re a pain to (re)move, but ensure very solid contact. The cable termination is swaged-and-soldered.

    AA Cell Clamping Pliers
    AA Cell Clamping Pliers
    Center Electrode - Front Detail
    Center Electrode – Front Detail

    I have not yet conjured up a pair of tweezers, as almost everything I’ve done has been suitable for pressing against the carbon plate.

    One exception: a pair of snap-ring pliers became a clamp for AA cell positive terminals and worked well in that capacity, along with a repurposed oil burner tungsten electrode that even provided its own ceramic handle.

    If I ever get around to building tweezers, I’d probably use chunks of that tungsten rod. It’d be easy to put a contact switch in there, too.

    Right after I got it working, I grabbed some copper junk and tried it out:

    Resistance soldering test pieces
    Resistance soldering test pieces

    Notice that there’s not enough heat in the surrounding metal to discolor it. I’m not always that lucky good, but it’s possible.

    The copper wire was instructive: even though copper is a great conductor, the joint is the lowest-resistance part of the circuit and gets all the heat. If you think of it as a parallel circuit, the ring has a relatively high resistance and sees much less current.

    Cleanliness and good joint preparation are vital, because any nontrivial resistance will reduce the heat to zilch. The tip of the carbon electrode sometimes acquires an insulating flux coating; a swipe on a file solves that problem

    Solder foil works well, because the current passes through the solder and starts the melting process in the middle of the joint. That’s easier than fiddling with solder wire, although your mileage may vary depending on what the joint looks like.

    Projects done with the equipment you’ve seen…

    It’s a great tool; I wonder how I got along without it.

    Now, I really must put that widowmaker breadboard inside an enclosure. There’s a dead dehumidifier near the bench (slated to contribute its compressor as a vacuum pump for a hold-down chuck on the mill) with a case that just might be the right size…

  • Resistance Soldering: Firmware

    Here’s the firmware driving the Atmel 89C2051 in my resistance soldering gizmo. You could substitute any 8051-style microcontroller without much difficulty at all. With a bit more difficulty, you could even use an Arduino Pro Mini.

    As you already know from recent posts, I’d jettison all the fancy gate control circuity and use a single bit driving a triac optoisolator. Redefine one of the GATE_CLAMP_x or GATE_DRIVE_x bits accordingly, then toss all but the last six Pat* timing structures overboard. That gets you duty cycle control in 1/6 increments: the triac will be turned on for one to six cycles of every six.

    You’ll probably have a serial LCD with a standard bit rate, so change the OSCILLATOR_FREQ to match your 11.0592 MHz crystal. Those of you in 50 Hz land can get 1/5 duty cycle control after you set LINE_FREQ accordingly; change the Pat* entries, too.

    Operation is straightforward: one pair of those fancy keyboard switches selects the triac trigger pulse sequence, the other selects the total burn time in 0.1 second units. You should use only patterns 11 through 16, which are the 1/6 through 6/6 duty cycles.

    Note: patterns 1 through 10 perform weird sub-cycle triggering to illustrate various topics I covered in the columns and shouldn’t be used for anything else. You don’t want to hammer your transformer with a series of half-cycle pulses, for example, because you don’t want to magnetize the core with a DC bias.

    Use the SDCC compiler to get the corresponding HEX file. I have not, in actual point of fact, recompiled this since burning the original microcontroller, so I’d expect the new HEX (actually, *.ihx) file to be completely different due to nothing more than improved optimizations and suchlike. Let me know how it works out, but, by and large, you’re on your own.

    I have a couple of tubes of 2051s, so if you want to build one of these gizmos and don’t want to use another 8051-family micro (or, better, port the code to an Arduino), send me a small box stuffed with as many dollar bills as you think appropriate and I’ll send you a programmed chip. No warranty, express or implied: you’re really on your own after that. I highly recommend that you do not take me up on this offer, OK?

    Herewith, The Source…

    // Resistance soldering unit control program
    // Circuit Cellar - June 2008
    // Ed Nisley KE4ZNU
    
    #include <8051.h>
    #include <stdio.h>
    
    //-------------------------------------------------------------------------------------------
    // I/O bits
    
    // The bit patterns in the TriacEvent_t struct match the output bit locations
    // These are low-active at the output pins, but we think of them as 1=ON here
    
    #define GATE_CLAMP_LOW		P1_4		// out - clamp low-going gate pulses to zero
    #define GATE_CLAMP_LOW_MASK	0x10
    #define GATE_CLAMP_LOW_BIT	4
    
    #define GATE_CLAMP_HIGH		P1_5		// out - clamp high-going gate pulses to zero
    #define GATE_CLAMP_HIGH_MASK	0x20
    #define GATE_CLAMP_HIGH_BIT	5
    
    #define GATE_DRIVE_LOW		P1_6		// out - drive gate low
    #define GATE_DRIVE_LOW_MASK	0x40
    #define GATE_DRIVE_LOW_BIT	6
    
    #define GATE_DRIVE_HIGH		P1_7		// out - drive gate high
    #define GATE_DRIVE_HIGH_MASK	0x80
    #define GATE_DRIVE_HIGH_BIT	7
    
    #define GATE_BIT_MASK		0xf0		// overall bitmask for these output bits
    #define GATE_BIT_PORT		P1		// which port they're on
    
    #define BUTTON_CONTACT		P1_0		// in - high when tip contact active
    #define BUTTON_FIRE		P1_1		// in - high when foot switch pressed
    #define TRACE2			P1_2		// out - toggled in loops and so forth
    #define TRACE3			P1_3		// out - toggled in IRQ handlers
    
    #define TRACE0			P3_0		// out -- toggled during burn sequence
    
    #define LINE_SYNC			P3_2		// in - INT0 from line monitor optoisolator
    #define LINE_SYNC_EDGE		IE0		//      interrupt edge detect bit
    #define LINE_SYNC_EDGE_ENABLE	IT0		//      enable edge detection for this IRQ
    
    #define BUTTON_TIME_INC		P3_3		// in - high to increment time
    #define BUTTON_TIME_DEC		P3_4		// in - high to decrement time
    #define BUTTON_PATTERN_INC	P3_5		// in - high to increment pattern index
    #define BUTTON_PATTERN_DEC	P3_7		// in - high to decrement pattern index
    
    #define TRACE_SERIAL		0		// nonzero to trace serial operations
    #define TRACE_PATTERN		1		// nonzero to trace pattern start
    
    //-------------------------------------------------------------------------------------------
    // Triac control timings
    // The ratio LINE_FREQ / TRIAC_PATTERN_CYCLES should be 10 to make decimal seconds work out nicely
    //  ... so those of you in 50-Hz land will have only five cycles per pattern...
    
    #define OSCILLATOR_FREQ		12.0000E6				// crystal frequency
    #define TIMER_TICK_FREQ		(OSCILLATOR_FREQ / 12)		// CPU instruction cycle frequency, Hz
    
    #define LINE_FREQ			60		// power line frequency, Hz
    
    #define TRIAC_PATTERN_FREQ	10		// Patterns per second
    
    #define TRIAC_PATTERN_CYCLES	(LINE_FREQ / TRIAC_PATTERN_FREQ)	// Power cycles per pattern
    
    // Because the VFL display requires about three stop bits at 9600 b/s,
    //  serial output must be paced at no more than 13 chars per power-line cycle
    //  so 8 chars per cycle (one per phase) works out perfectly well
    // If you want more phases or faster data, you must adjust accordingly
    // As it turns out, my VFL doesn't use standard serial rates anyway, but the thought was nice...
    //  and the pacing still gives a full update in about 50 chars / (8 chars / cycle) = 6 cycles = 1/10 sec
    
    #define PHASES_PER_CYCLE	8					// phases per line cycle
    
    #define PHASE_TICKS	(TIMER_TICK_FREQ / (PHASES_PER_CYCLE * LINE_FREQ))	// ticks per phase
    
    #define PHASES_PER_PATTERN	(PHASES_PER_CYCLE * TRIAC_PATTERN_CYCLES)
    
    #define TIMER_OVERHEAD		20					// IRQ handler overhead ticks
    
    #define LINE_SYNC_DELAY		(410E-6 * TIMER_TICK_FREQ)	// zero-crossing detection delay in ticks
    
    // Event records contain
    //  match EventPhase to the Phase timer: when it matches, then output bits happen
    //   0 = start of first cycle, max value = PHASES_PER_PATTERN-1
    //  output bits correctly aligned, but 1=active so they must be flipped before output
    
    typedef struct {
    	unsigned char EventPhase;
    	unsigned char TriacBits;
    } TriacEvent_t;
    
    #define PB(t,b) {t,b}
    
    TriacEvent_t __code Pat0[] = {		// 0 - all drivers off, always
    	PB(0,0)
    	};
    
    TriacEvent_t __code Pat1[] = {		// 1 - single high trigger
    	PB(0,GATE_DRIVE_HIGH_MASK),		//     similar to Figure 1 in April 2008 column
    	PB(1,0)
    	};
    
    TriacEvent_t __code Pat2[] = {		// 2 - single high, clamped second half-cycle
    	PB(0,GATE_DRIVE_HIGH_MASK),
    	PB(1,0),
    	PB(4,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK),
    	PB(8,0)
    	};
    
    TriacEvent_t __code Pat3[] = {		// 3 - peak +V half-cycle, then one full cycle, clamped
    	PB(2,GATE_DRIVE_HIGH_MASK),		//     similar to Figure 3 in April 2008 column
    	PB(3,0),
    	PB(4,GATE_DRIVE_LOW_MASK),
    	PB(8,GATE_DRIVE_HIGH_MASK),
    	PB(12,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK),
    	PB(14,0)
    	};
    
    TriacEvent_t __code Pat4[] = {		// 4 - peak +V half-cycle, then half cycle, clamped
    	PB(2,GATE_DRIVE_HIGH_MASK),		//     this gives one complete cycle
    	PB(3,0),
    	PB(4,GATE_DRIVE_LOW_MASK),
    	PB(6,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK),
    	PB(16,0)
    	};
    
    TriacEvent_t __code Pat5[] = {		// 5 - similar to 3 with additional half cycle
    	PB(2,GATE_DRIVE_HIGH_MASK),		//     this gives two complete cycles
    	PB(3,0),
    	PB(4,GATE_DRIVE_LOW_MASK),
    	PB(8,GATE_DRIVE_HIGH_MASK),
    	PB(12,GATE_DRIVE_LOW_MASK),
    	PB(14,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK),
    	PB(20,0)
    	};
    
    TriacEvent_t __code Pat6[] = {		// 6 -
    	PB(0,0)
    	};
    
    TriacEvent_t __code Pat7[] = {		// 7 -
    	PB(0,0)
    	};
    
    TriacEvent_t __code Pat8[] = {		// 8 -
    	PB(0,0)
    	};
    
    TriacEvent_t __code Pat9[] = {		// 9 -
    	PB(0,0)
    	};
    
    TriacEvent_t __code Pat10[] = {		// 10 -
    	PB(0,0)
    	};
    
    TriacEvent_t __code Pat11[] = {		// 11 - 1/6: 1 0 0 0 0 0
    	PB(2,GATE_DRIVE_HIGH_MASK),
    	PB(3,GATE_DRIVE_LOW_MASK),
    	PB(7,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK)
    	};
    
    TriacEvent_t __code Pat12[] = {		// 12 - 2/6: 1 0 0 1 0 0
    	PB(2,GATE_DRIVE_HIGH_MASK),
    	PB(3,GATE_DRIVE_LOW_MASK),
    	PB(7,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK),
    	PB(26,GATE_DRIVE_HIGH_MASK),
    	PB(27,GATE_DRIVE_LOW_MASK),
    	PB(31,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK)
    	};
    
    TriacEvent_t __code Pat13[] = {		// 13 - 3/6: 1 0 1 0 1 0
    	PB(2,GATE_DRIVE_HIGH_MASK),
    	PB(3,GATE_DRIVE_LOW_MASK),
    	PB(7,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK),
    	PB(18,GATE_DRIVE_HIGH_MASK),
    	PB(19,GATE_DRIVE_LOW_MASK),
    	PB(23,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK),
    	PB(34,GATE_DRIVE_HIGH_MASK),
    	PB(35,GATE_DRIVE_LOW_MASK),
    	PB(39,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK)
    	};
    
    TriacEvent_t __code Pat14[] = {		// 14 - 4/6: 1 1 0 1 1 0
    	PB(2,GATE_DRIVE_HIGH_MASK),
    	PB(3,GATE_DRIVE_LOW_MASK),
    	PB(7,GATE_DRIVE_HIGH_MASK),
    	PB(11,GATE_DRIVE_LOW_MASK),
    	PB(15,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK),
    	PB(26,GATE_DRIVE_HIGH_MASK),
    	PB(27,GATE_DRIVE_LOW_MASK),
    	PB(31,GATE_DRIVE_HIGH_MASK),
    	PB(35,GATE_DRIVE_LOW_MASK),
    	PB(39,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK)
    	};
    
    TriacEvent_t __code Pat15[] = {		// 15 - 5/6: 1 1 1 1 1 0
    	PB(2,GATE_DRIVE_HIGH_MASK),
    	PB(3,GATE_DRIVE_LOW_MASK),
    	PB(7,GATE_DRIVE_HIGH_MASK),
    	PB(11,GATE_DRIVE_LOW_MASK),
    	PB(15,GATE_DRIVE_HIGH_MASK),
    	PB(19,GATE_DRIVE_LOW_MASK),
    	PB(23,GATE_DRIVE_HIGH_MASK),
    	PB(27,GATE_DRIVE_LOW_MASK),
    	PB(31,GATE_DRIVE_HIGH_MASK),
    	PB(35,GATE_DRIVE_LOW_MASK),
    	PB(39,GATE_CLAMP_LOW_MASK | GATE_CLAMP_HIGH_MASK)
    	};
    
    TriacEvent_t __code Pat16[] = {		// 16 - 6/6: 1 1 1 1 1 1
    	PB(2,GATE_DRIVE_HIGH_MASK),
    	PB(3,GATE_DRIVE_LOW_MASK),
    	PB(7,GATE_DRIVE_HIGH_MASK),
    	PB(11,GATE_DRIVE_LOW_MASK),
    	PB(15,GATE_DRIVE_HIGH_MASK),
    	PB(19,GATE_DRIVE_LOW_MASK),
    	PB(23,GATE_DRIVE_HIGH_MASK),
    	PB(27,GATE_DRIVE_LOW_MASK),
    	PB(31,GATE_DRIVE_HIGH_MASK),
    	PB(35,GATE_DRIVE_LOW_MASK),
    	PB(39,GATE_DRIVE_HIGH_MASK),
    	PB(43,GATE_DRIVE_LOW_MASK),
    	PB(47,GATE_DRIVE_HIGH_MASK)
    	};
    
    __code TriacEvent_t * __code pTriacPatterns[] = {	// pointers to patterns
    	Pat0,Pat1,Pat2,Pat3,Pat4,Pat5,Pat6,Pat7,Pat8,Pat9,
    	Pat10,Pat11,Pat12,Pat13,Pat14,Pat15,Pat16
    };
    
    // Number of duty-cycle patterns (0 = all off)
    #define TRIAC_NUM_PATTERNS	(sizeof(pTriacPatterns) / sizeof( __code * ))
    
    unsigned char PatternSelection = 11;			// default selected entry in pTriacPatterns
    
    //-------------------------------------------------------------------------------------------
    // These are mostly the (dreaded) global variables modified by the IRQ handlers
    // The initial values set up for the first line sync IRQ
    
    // Phase and output control
    
    volatile unsigned char Phase = 0xff;			// phase within each pattern, init to -1
    volatile unsigned char EventIndex;				// step through TriacEvents records
    
    typedef enum {BURN_IDLE, BURN_CONTACT, BURN_ACTIVE, BURN_DONE} BurnState_t;
    
    BurnState_t BurnState = BURN_IDLE;				// what we're doing right now
    
    volatile __bit BurnEnable;					// true when output is active
    
    typedef struct {
    	unsigned char Seconds;
    	unsigned char Tenths;
    } Time_t;
    
    volatile Time_t BurnTime = {1,0};			// displayable burn time
    Time_t FullTime = {1,0};				// ditto, selected time
    
    volatile unsigned char Cycles;			// power-line cycle counter, wraps each second
    
    // Serial output
    
    #define SERIAL_RATE	62500				// which depends on the crystal, of course!
    
    volatile char SerialCharOut;				// char to send
    volatile bit SerialOutReady;				// 1 for new char, 0 when sent
    
    unsigned char Heartbeat;				// simple activity indicator
    __code unsigned char ActivityChar[8] = {"-\\|/-\\|/"};	// remember \\ is just one character
    
    // Button debouncing accumulators
    // Incremented when pressed, zeroed when released... ticks = power-line cycles
    // I used Hall-effect switches that don't bounce, so the initial delay is very short
    //  For real debouncing set ON much longer
    
    #define BUTTON_ON		3				// if continuously pressed this long, it's on!
    
    volatile unsigned char Button_Time_Inc;
    volatile unsigned char Button_Time_Dec;
    volatile unsigned char Button_Pattern_Inc;
    volatile unsigned char Button_Pattern_Dec;
    
    volatile unsigned char Button_Contact;
    volatile unsigned char Button_Fire;
    
    //-------------------------------------------------------------------------------------------
    // Utilities
    
    #define SEC_TENTHS(s,t) (60*s + 6*t)			// convert seconds+tenths to cycles
    
    void Delay(unsigned char CycleCount) {			// rough-and-ready delay by power-line cycles
    unsigned char PrevCycle;
    
    	PrevCycle = Cycles;
    
    	while (CycleCount) {
    		if (PrevCycle != Cycles) {
    			CycleCount--;
    			PrevCycle = Cycles;
    		}
    	}
    
    }
    
    void IncrementTime(Time_t *pTime) {
    
    	if (pTime->Tenths >= 9) {
    		pTime->Tenths = 0;
    		pTime->Seconds++;			// wraps at 255 and we don't care at all
    	}
    	else {
    		pTime->Tenths++;
    	}
    }
    
    void DecrementTime(Time_t *pTime) {
    
    	if (pTime->Tenths) {
    		pTime->Tenths--;
    	}
    	else {
    		if (pTime->Seconds) {		// saturate at 0.0
    			pTime->Tenths = 9;
    			pTime->Seconds--;
    		}
    	}
    
    	if ((pTime->Tenths == 0) && (pTime->Seconds == 0)) {
    		pTime->Tenths = 1;
    	}
    
    }
    
    unsigned char TestButton(unsigned char Button) {
    
    	return (Button >= BUTTON_ON);
    
    }
    
    //-------------------------------------------------------------------------------------------
    // Display controls
    // Rows and columns start at zero, of course
    
    #define DISP_ROWS	2
    #define DISP_COLS	20
    
    #define DISP_POS_CMD	'\x10'
    
    void InitDisplay(void) {
    
    	puts("Ed\x1f\x11\x14");				// filler, reset, no scroll, no cursor
    
    }
    
    void SetCursor(unsigned char row, unsigned char col) {
    
    	putchar(DISP_POS_CMD);
    	putchar(row * DISP_COLS + col);
    
    }
    
    void RefreshDisplay(void) {
    
    	SetCursor(1,19);
    	putchar(ActivityChar[Heartbeat++ & 0x07]);
    
    //	SetCursor(0,0);
    	if (BurnEnable) {
    //                       012345 67 89
    		printf_tiny("BURN %d.%d  ",BurnTime.Seconds,BurnTime.Tenths);
    	}
    	else {
    		printf_tiny("Time %d.%d  ",FullTime.Seconds,FullTime.Tenths);
    	}
    
    	SetCursor(0,10);
    //                 abcde f0123
    	printf_tiny("Patt %d ",PatternSelection);
    
    	SetCursor(1,0);
    	if (TestButton(Button_Contact))
    //                0123456789abcdef0123
    		puts("<<Ready!>>");
    	else
    		puts("No contact");
    
    }
    
    //-------------------------------------------------------------------------------------------
    // Handler for line-sync input
    // Synchronize triac bits to power line positive half-cycle
    // Forces Timer 0 interrupt after exit to ensure synchronization
    //  Timer 0 will not be running when this IRQ occurs!
    
    void LineSyncIRQ(void) __interrupt (0) __using(1) {
    
    	TRACE3 = 0;
    
    	if (Phase >= (PHASES_PER_PATTERN - 1)) {	// if this is line sync after last pattern phase IRQ
    		TRACE0 = 0;
    		Phase = 0xff;				// preload to get 0 on PhaseIRQ fallthru
    		EventIndex = 0;				// restart pattern on that phase
    
    		if ((!BurnEnable) && BurnState == BURN_ACTIVE) {	// if should be active
    			BurnEnable = 1;			//  then allow startup on this cycle
    #if TRACE_PATTERN
    			TRACE2 = 1;				// flag pattern startup
    #endif
    		}
    
    		if (BurnEnable) {				// outputs active?
    			if (BurnTime.Tenths) {			// tick time backwards
    				BurnTime.Tenths--;		// continue running on 0.1 -> 0.0 tick
    			}
    			else {
    				if (BurnTime.Seconds) {
    					BurnTime.Tenths = 9;
    					BurnTime.Seconds--;
    			     }
    			     else BurnEnable = 0;		// off after 1 tick for 0.0
    			}
    		}
    	}
    
    	if ((Phase % PHASES_PER_CYCLE) == (PHASES_PER_CYCLE - 1)) {	// if line sync after last phase in cycle
    		TRACE0 = 0;					// make a blip
    		TRACE0 = 1;
    		TRACE0 = 0;					// then leave low
    	}
    
    	TF0 = 1;				// always force Timer 0 interrupt immediately after us
    
    // Tick cycle counter
    
    	if (Cycles < (LINE_FREQ - 1)) {
    		Cycles++;
    	}
    	else {
    		Cycles = 0;
    	}
    
    // Sample switches & twiddle debounce accumulators
    // Buttons are rarely pressed, so that case goes pretty quickly
    // To get even faster, skip it all if burn is in progress
    // Remember that I'm using +active Hall-effect switches
    
    	if (BUTTON_TIME_INC) {						// switch pressed?
    		Button_Time_Inc += (Button_Time_Inc < 255);	// yes, increment and saturate
    	}
    	else {
    		Button_Time_Inc = 0;					// no, flush
    	}
    
    	if (BUTTON_TIME_DEC) {
    		Button_Time_Dec += (Button_Time_Dec < 255);
    	}
    	else {
    		Button_Time_Dec= 0;
    	}
    
    	if (BUTTON_PATTERN_INC) {
    		Button_Pattern_Inc += (Button_Pattern_Inc < 255);
    	}
    	else {
    		Button_Pattern_Inc= 0;
    	}
    
    	if (BUTTON_PATTERN_DEC) {
    		Button_Pattern_Dec += (Button_Pattern_Dec < 255);
    	}
    	else {
    		Button_Pattern_Dec = 0;
    	}
    
    	if (BUTTON_CONTACT) {					// active high
    		Button_Contact += (Button_Contact < 255);
    	}
    	else {
    		Button_Contact = 0;
    	}
    
    	if (BUTTON_FIRE) {					// active high
    		Button_Fire += (Button_Fire < 255);
    	}
    	else {
    		Button_Fire = 0;
    	}
    
    #if TRACE_PATTERN
    	TRACE2 = 0;
    #endif
    
    	TRACE3 = 1;
    
    }
    
    //-------------------------------------------------------------------------------------------
    // Handler for Timer 0
    // This meters out the triac control bits
    // Turns off Timer 0 during last phase in each cycle, so line-sync IRQ will re-sync us
    
    void PhaseIRQ(void) __interrupt (1) __using(1) {
    
    TriacEvent_t *pEvent;
    
    	TRACE3 = 0;
    
    	TR0 = 0;						// Reload phase timer
    	if (++Phase) {					// step to next phase. Is it nonzero?
    		TH0 = ((int)(-(PHASE_TICKS - TIMER_OVERHEAD)) >> 8) & 0xff;		// nonzero = normal tick
    		TL0 =  (int)(-(PHASE_TICKS - TIMER_OVERHEAD)) & 0xff;
    	}
    	else {
    		TH0 = ((int)(-(PHASE_TICKS - LINE_SYNC_DELAY)) >> 8) & 0xff;	// zero = after line sync
    		TL0 =  (int)(-(PHASE_TICKS - LINE_SYNC_DELAY)) & 0xff;
    	}
    	TR0 = 1;						// and start it up
    
    	if (! BurnEnable) {				// if outputs should not be active
    		GATE_BIT_PORT |= GATE_DRIVE_HIGH_MASK | GATE_DRIVE_LOW_MASK;	// force drive off
    		GATE_BIT_PORT &= ~(GATE_CLAMP_HIGH_MASK | GATE_CLAMP_LOW_MASK);	// force clamp on
    	}
    
    	pEvent = pTriacPatterns[PatternSelection] + EventIndex;
    
    	if (Phase == pEvent->EventPhase) {		// event time match?
    		if (BurnEnable) {				// change outputs only if in active burn time
    			GATE_BIT_PORT ^= GATE_BIT_MASK & (GATE_BIT_PORT ^ ~(pEvent->TriacBits));
    		}
    		EventIndex++;				// step to next event in pattern list
    	}
    
    	if ((Phase % PHASES_PER_CYCLE) == (PHASES_PER_CYCLE - 1)) {	// if now in last phase of cycle
    		TR0 = 0;					//  ... next line sync will restart timer
    		TRACE0 = 1;					// short blip to mark this point
    		TRACE0 = 0;
    		if (Phase == (PHASES_PER_PATTERN - 1)) {
    			TRACE0 = 1;				// flag final transition of pattern
    		}
    	}
    
    	if (SerialOutReady) {				// if char ready to send
    		SBUF = SerialCharOut;			//   do it (TI will always be clear!)
    		SerialOutReady = 0;			//   and mark it as gone
    	}
    
    	TRACE3 = 1;
    
    }
    
    //-------------------------------------------------------------------------------------------
    // Serial character I/O
    // This is utterly crude...
    
    /************
    char getchar(void) {
    
    	if (RI) {
    		RI = 0;
    		return SBUF;
    	}
    	else {
    		return (char) 0;
    	}
    
    }
    *****************/
    
    // Output must be synced to the phase IRQs to properly pace the chars to the VFL display...
    // So we hand this off to the Timer0 IRQ
    
    void putchar(char c) {
    
    	while (SerialOutReady) {
    #if TRACE_SERIAL
    		TRACE2 = ! TRACE2;
    #else
    		continue;
    #endif
    	}
    
    #if TRACE_SERIAL
    	TRACE2 = 1;
    #endif
    
    	SerialCharOut = c;
    	SerialOutReady = 1;
    
    	return;
    
    }
    
    //-------------------------------------------------------------------------------------------
    
    void main(void) {
    
    __bit SomethingChanged;
    
    // Set up hardware
    
    	TCON = 0;				// Timers off, software control
    	PCON |= SMOD;			// double the serial bit rate
    
    	TMOD = 0x21;			// Timer 1 = 8 bit auto-reload, Timer 0 = 16-bit
    
    	TL1 = TH1 = 256 - ((2 * OSCILLATOR_FREQ) / (32 * 12 * SERIAL_RATE));
    
    	SCON = 0x50;			// serial mode 1
    	TR1 = 1;				// start Timer 1
    
    // Sync to incoming power-line signal
    // Timer 0 is off so line-sync will start normally
    
    	LINE_SYNC_EDGE_ENABLE = 1;	// make INT0 edge-triggered
    	LINE_SYNC_EDGE = 0;
    
    	while (!LINE_SYNC_EDGE) {	// hang until first edge
    		TRACE3 = !TRACE3;
    	}
    	TRACE3 = 1;
    
    	IE = 0x83;				// Ints enabled, Timer0 IRQ enabled, INT0 enabled
    
    	InitDisplay();			// set up the display
    	SetCursor(0,0);			// don't know why this is needed the first time, but it is...
    
    //          0123456789abcdef0123
    	puts("CC June 08\r\n"
               "Ed Nisley 20 Feb 08");
    
    	Delay(SEC_TENTHS(3,0));
    
    	InitDisplay();			// clear the decks!
    
    // Get sane input to start... just keep rewriting the message, it's shorter
    
    	while (TestButton(Button_Fire)) {
    		SetCursor(0,0);
    //			0123456789abcdef0123
    		puts("Release tip switch!");
    	}
    
    	InitDisplay();
    
    // Repeat forever...
    
    	while (1) {
    
    // If nothing else happens, update the display about twice a second
    
    		SomethingChanged = !(Cycles % (LINE_FREQ / 2));
    
    // Handle timing and pattern-selection buttons
    
    		if (TestButton(Button_Time_Inc)) {
    			IncrementTime(&FullTime);
    			SomethingChanged = 1;
    		}
    
    		if (TestButton(Button_Time_Dec)) {
    			DecrementTime(&FullTime);
    			SomethingChanged = 1;
    		}
    
    		if (TestButton(Button_Pattern_Inc) && (PatternSelection < (TRIAC_NUM_PATTERNS - 1))) {
    			PatternSelection++;
    			SomethingChanged = 1;
    		}
    
    		if (TestButton(Button_Pattern_Dec) && PatternSelection) {
    			PatternSelection--;
    			SomethingChanged = 1;
    		}
    
    // Convert contact & footswitch buttons into output control
    // Ignore nearly all the ugly race conditions...
    
    		switch (BurnState) {
    		case BURN_IDLE :
    			if (TestButton(Button_Contact)) {		// first we need contact
    				BurnState = BURN_CONTACT;
    				SomethingChanged = 1;
    			}
    			break;
    		case BURN_CONTACT :
    			if (!TestButton(Button_Contact)) {		// no contact = restart
    				BurnState = BURN_IDLE;
    				SomethingChanged = 1;
    			}
    			else if (TestButton(Button_Fire)) {		// foot switch active?
    				BurnTime.Tenths = FullTime.Tenths;	// set up burn duration
    				BurnTime.Seconds = FullTime.Seconds;
    				BurnState = BURN_ACTIVE;
    				while (!BurnEnable) {			// wait for IRQ to activate burning
    					continue;
    				}
    				SomethingChanged = 1;
    			}
    			break;
    		case BURN_ACTIVE :
    			if (!TestButton(Button_Contact)) {		// no contact = restart
    				BurnState = BURN_IDLE;
    				BurnEnable = 0;
    			}
    			else if (!BurnEnable) {				// burn completed?
    				BurnState = BURN_DONE;
    			}
    			SomethingChanged = 1;				// always update display
    			break;
    		case BURN_DONE :
    			if (!TestButton(Button_Fire)) {		// foot switch released?
    				BurnState = BURN_IDLE;
    				SomethingChanged = 1;
    			}
    			break;
    		default :
    			BurnEnable = 0;
    			BurnState = BURN_IDLE;
    			SomethingChanged = 1;
    		}
    
    // Update display if anything interesting happened
    
    		if (SomethingChanged) {
    			RefreshDisplay();
    		}
    
    	}
    
    }
    
    
  • Resistance Soldering: Circuitry

    Because I wanted to discuss triac triggering for inductive loads, the triggering circuitry & firmware turned out to be absurdly complex. A quartet of transistors provides source and sink current, as well as source and sink clamps, with 1/8 cycle timing resolution. The transistors and their power supply must be optically isolated from the microcontroller, of course.

    None of this triggering circuitry is quite what you want, but it’ll get you started in the right direction…

    This schematic shows the driver circuitry, triac, transformer, and suchlike.

    Triac Drive Schematic
    Triac Drive Schematic

    The weird +4 V supply comes directly from the small multi-tap transformer harvested from the ‘waver; your supply will certainly be different.

    The 100 mΩ resistor in the primary is there strictly for current monitoring while debugging the thing. If you’re not doing that, leave it out.

    The optocoupler in the lower right sends the zero-crossing time back to the microcontroller; it is vitally important that you get the phase correct on this one, as the firmware is doing triggering in all four quadrants and the triac doesn’t take kindly to pulses 180 degrees out of phase.

    The microcontroller side looks pretty much like any 8051-based circuit.

    Timing Controller Schematic
    Timing Controller Schematic

    I used a surplus VFL display with a serial input that required the 12.000 MHz crystal. That had the useful benefit of giving exact 1 µs instruction timing, but otherwise I’d have gone with a 11.0592 MHz crystal to get normal serial output bit timings.

    The pushbuttons (lower left) are weird Hall-effect keyboard switches that are either open or pulled to the power supply; they do not have a low-active state. As a result, the resistors pull the inputs down in the inactive state. These switches don’t bounce, which simplified the firmware a bit. If you use mechanical switches, you must add a debouncing routine.

    The Enable switch (upper right) provides positive control over the gate drive signals: when it’s open, the triac cannot fire.

    The Contact switch (upper middle) seemed like a good idea: it’s supposed to close only when the electrodes are making firm contact. I never got around to building such a switch and it turns out to be unnecessary, so it’s bypassed by a toggle switch on the circuit board.

    The Foot switch (lower middle) is absolutely vital: you get everything set up with electrodes properly arranged, then step on the switch. The microcontroller handles the timing, the heat goes off, and then you lift your foot at your leisure… when the joint is cool.

    Here’s what all that looks like, all screwed to a piece of plywood in genuine breadboard mode:

    Timing control and triac trigger circuitry
    Timing control and triac trigger circuitry

    Straight up: this is a lethally stupid way to build the thing. Put it inside a container of some sort, so you can’t drop anything conductive across the exposed primary components. OK?

    Now, the reason I say none of this is what you want is because all resistance soldering requires is just turning the triac on for a while, then turning it off. I think duty-cycle control would be helpful, but sub-cycle timing is definitely not required.

    So, by and large, were I to rebuild this, I’d jettison the entire triac triggering board and replace it with a simple optoisolated triac trigger IC (perhaps a MOC3022, of which I have a bag, or a TLP3042), then modify the firmware to flick a single output bit to turn on the heat.

    You can download the schematics, simulation models, and source code from the Circuit Cellar FTP site: Issues 213 and 215.

    Tomorrow: the firmware.