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

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

  • Arduino Mega 1280: PWM-to-Timer Assignment

    The Arduino Mega has five hardware Timers, each with up to three PWM outputs. Most of the outputs go to headers, but the correspondence between Timer hardware and Arduino PWM number is not obvious.

    Herewith…

    PWM Hardware
    13 OC1C
    12 OC1B
    11 OC1A
    10 OC2A
    9 OC2B
    8 OC4C
    7 OC4B
    6 OC4A
    5 OC3A
    4 OC0B
    3 OC3C
    2 OC3B

    Although the Mega schematic shows PWM0 and PWM1, they don’t really exist; they’re actually the serial I/O bits for the FT232 USB converter.

    PWM4 uses Timer 0: OC0B. Don’t mess with Timer 0’s prescaler to get higher speeds; it’ll wreck the millis() function and the firmware’s timekeeping in general.

    Timer 5 doesn’t come to a header. If you desperately need three more PWM outputs (that aren’t supported by the Arduino runtime), you could affix three fine wires to pins 38, 39, and 40 of that TQFP. Good luck with that…

    Some notes about fiddling with the Arduino PWM setup: changing the frequency and going much faster. The new Timers have different numbers, but the same considerations apply. As before, Thou Shalt Not Mess with Timer 0!

    Consult the Fine Manual for the ATmega1280 chip to get all the details.

  • Useful Minicom Defaults

    I usually figure this stuff out for each minicom setup; now I can just refer here and get it right the first time.

    Run minicom -s the first time to set the internal configs:

    • Port typically /dev/ttyUSB0 for USB-serial adapter
    • 19200 8N1 for lack of anything smarter
    • No hardware flow control
    • Software flow control
    • Clear modem Init and Reset strings to blank
    • No DCD line
    • Set colors as below

    Your favorite colors will differ; these are Old-Skool:

    • Menu: white on blue
    • Status: black on cyan
    • Terminal: green on black

    Save that as the overall system default.

    Set command-line defaults in ~/.bashrc:

    • -m to use Alt+key rather than Ctrl-A and then the key
    • -c on to see those pretty colors

    Which will look like

    MINICOM='-m -c on'
    export MINICOM
    

    And then it Just Works…

  • OpenOffice 3.2 Graphic File Link Hackage

    OpenOffice normally stores graphic file links relative to the location of the ODT document file. It’s an option at Tools -> Options -> Load/Save -> General, where you check Save URLs relative to file system.

    That generally works well, as long as you keep all the graphics either in the same directory or in a subdirectory, which is our general practice. Note that this doesn’t apply if you embed the image files into the document, which works fine for one-pagers and dies horribly for lengthy graphics-intense documents.

    (Yes, I know OOo is not a page layout program. Sometimes other considerations get in the way. Work with me on this, OK?)

    It’s easy to confuse the program: copy the ODT file somewhere else and, shazam, the links either break or get weird. In a recent case, the links somehow wound up holding the entire path from the root directory through /home, down through an NFS mount, and out to the actual file. Not only was it un-pretty, the links basically didn’t work from any other account on any other machine because you really can’t reach through another user’s account to your files.

    This is tedious, at best, to fix up within OpenOffice, because you can’t do a find-and-replace on the file names.

    So.

    In OOo, click through Tools -> Options -> Load/Save -> General. Uncheck the Size optimization for ODF format option to force the XML file to become human-readable. Otherwise, OO stores everything as one huge line. While you’re there, make sure Save URLs relative to file system is checked.

    Save the file again to get readable XML.

    Create /tmp/work, copy the ODT file therein, apply unzip to it. That extracts the contents, including the all-important  content.xml containing your document’s text & links.

    Edit content.xml with the text editor (not a word processor like OOo!) of your choice. Bulk-change the garbage paths to something meaningful. For example, we had all the images in Tweaked, a subdirectory below the document directory, so the desired file links looked like ../Tweaked/image-file-name.jpg.

    Save the file and stuff it back into the ODT file using zip -vi document.odt content.xml

    That produced some odd error messages that didn’t seem to have any effect:

    	zip warning: undefined bits used in flags = 0x0808: layout-cache
    	zip warning: undefined bits used in flags = 0x0808: content.xml
    	zip warning: undefined bits used in flags = 0x0808: styles.xml
    	zip warning: undefined bits used in flags = 0x0808: Thumbnails/thumbnail.png
    	zip warning: undefined bits used in flags = 0x0808: settings.xml
    	zip warning: undefined bits used in flags = 0x0808: META-INF/manifest.xml
    updating: content.xml
    	zip warning: Local Entry CRC does not match CD: content.xml
    	(in=54496) (out=7765) (deflated 86%)
    total bytes=91294, compressed=17644 -> 81% savings
    

    OOo stores the file timestamps within the ODT file in UTC, confusing the daylights out of zip, which assumes they’re in local time. Being at UTC-4 right now, I couldn’t simply freshen or update a recently created ODT file.

    Copy the modified ODT file back where it came from, make sure the graphic files are where you promised they’d be, and open the document.

    Everything should be just fine.

  • OpenOffice 3.2 Graphics Cache Settings

    The default OpenOffice 3.2 graphics cache is probably large enough for ordinary documents. However, I put together a 9-page illustrated biography for a birthday party last year using (most likely) OOo 3.1 and that file dragged OOo 3.2 to its knees.

    The default cache settings are something like

    • 20 MB
    • 2 MB per object
    • 20 objects

    Crank those to

    • 256 MB
    • 5 MB per object
    • 50 objects

    Much better!

    I’m sure that depends on what you’re doing and how much memory your PC has, but when OOo gets really pokey on a graphics-intensive document, check the cache.

  • Epson R380 Printer: Resetting the Waste Ink Counters

    So a few days after topping off the continuous ink tanks on my Epson R380 printer, we had a series of thunderstorms that prompted me to turn everything off. Upon turning the printer back on, its fancy LCD panel showed a message along the lines of

    Service is required. Contact Epson Customer Service.

    Oddly, it continued to print perfectly with no further complaints. The error message appeared only at power-on, then politely went away when I pressed the OK button.

    Well, that puppy is long out of warranty, even if I wasn’t using a continuous ink system, soooo… what to do? The printer produces absolutely no diagnostic codes other than that error message.

    A bit of searching gave me the Maintenance Manual for that family of printers. That message isn’t among the ones listed.

    Further searching suggests that at least one of the two waste ink pads / tanks is nearly full and that ignoring the problem will cause the printer to shut itself down, lest it dribble ink. The listed messages warn that the printer is approaching the “end of its service life”, which isn’t the message I saw, but it’s close enough.

    The Maintenance Manual suggests that it’ll be cheaper and better to simply buy a new printer, as replacing the waste ink tanks may cost more than the printer is worth. The website points out that providing a customer-replaceable tank would drive up the cost of the printer, because most customers would buy a new printer before filling the tank.

    In order to get to the waste ink tank, you must remove:

    • Paper Support
    • Printer Cover
    • Front Cover
    • Right Housing
    • Left Housing
    • USB Housing
    • Upper Housing
    • Panel Unit
    • EMI Frame

    I can see why it might take a trained tech a few hours to get all that done… and then reassemble in reverse order.

    The Epson website has a link to a program that will reset the waste ink counters for one of the tanks. Downloaded & ran it on the Token Windows Laptop; it tells me there’s no problem.

    Hmmm

    So I ordered an external waste ink tank from the usual eBay supplier. The hardware is grossly overpriced ($20 delivered) for what it is (large tube with sealed endcaps, some tubing & barb fittings, a syringe), but the deal includes links to programs that will reset the counters. I found several of those programs by myself, so it’s not as if you must actually spend money to reset the printer’s counters. I figured this was in the nature of a learning experience.

    Turns out that the programs are provided by parties having, shall we say, long-term interests that may not coincide with mine. To wit, I’d be batshit crazy to run those programs on a PC I cared about.

    [Update: Something like that.]

    The various program files all passed a ClamAV virus scan, but that doesn’t mean anything these days.

    So, during the next hour:

    • Boot System Rescue CD on my oldest Token Windows Laptop
    • Run partimage to back up the Windows partition to another partition
    • Disconnect from the house LAN
    • Reboot in Windows, which evidently hasn’t seen the light of day in about a year
    • Stifle bleating requests for updates
    • Copy the programs from a USB stick, install as needed
    • Reset one of the ink counters (more on this below)
    • Reboot in SRC
    • Restore the partition from the backup

    All that is straightforward and I’ve written about it earlier. Search the blog for more info using the obvious keywords.

    I attempted to restore the drive’s Master Boot Record from the partition backup file, but partimage complained that the drive size in the backed-up MBR did not match the existing drive size, which suggests something tinkered with the drive’s MBR between the backup and the restore.

    Hmmm….

    You might want to do a bit of reading on Boot Sector Viruses at this point. I have no other evidence to suggest that’s what’s going on, other than to remind you that programs need not do only what they say they’ll do.

    Given all that, I figured this was a great time to update the Token Windows Laptop to Xubuntu 10.04, which installed Grub2 in the MBR and wiped away anything placed therein. The box is heavily multi-booted: Dell Diags, XP, Puppy, and now Xubuntu 10.04.

    Without naming names or providing links:

    • The Russian program seems to not include the R380, but it does include others in that family. I elected to not reset the counters using that program.
    • The Chinese program seems to be a bootleg copy of the Official Epson Adjustment Program, although it’s rife with misspellings and grammatical errors. I told it to reset the “Main Pad” counter and give me a dump of the EEPROM.

    The Main Pad had 16008 counts of the maximum 16200, while the Platen Pad had only 3019 of 54513. Those names do not correspond to anything in the Maintenance Manual, but I suspect the Main Pad is the Waste Ink Tray at the head-cleaning station and the Platen Pad is the Waste Ink Pad running across the printer to catch the overspray from borderless prints.

    Resetting the Main Pad counter to zero cleared the error message; the printer is perfectly happy now. I’ll install the external waste ink tank when I clear the workbench after building the next GPS interface for our HTs.

    The program reported 9922 pages printed. Figuring 7 bottles of ink at 250 ml each, that’s 0.18 ml per page. That’s a slight overestimate because the ≈50 ml tanks were just topped off, but it’s close enough. I’m guessing head cleaning consumed much of that ink, as the printer does plenty of that, and the number of pages seems close to half the number of counts.

    Perhaps it performs a cleaning when more than X minutes has elapsed since the previous print job? That would account for the high number of cleanings; most print jobs are a few pages, at most.

    En passant, I found some totally unofficial ink cartridge capacity numbers:

    • Standard T078x: 7 ml @ $13 = $1857 / liter
    • Large T077x: 11 ml @ $20 = $1818 / liter

    [Update: corrected typo from ml to liter]

    Ain’t that impressive? I love the savings they give you with higher-capacity cartridges …