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

  • Logitech Gamepad as EMC2 Pendant: Eagle Schematics for the Joggy Thing!

    Another pass at my Logitech Dual-Action Gamepad used as an EMC2 control pendant, but this time using an Eagle ULP (User Language Program) that converts a schematic into EMC2 HAL code.

    I tweaked Martin Schöneck’s original ULP a bit, added (some of the) new devices found in EMC2.4, added the corresponding Eagle symbols & devices to the library, then drew up a schematic based on my hand-hewn HAL code with some improvements. Ran the script and shazam the HAL code worked just fine (after a bit of debugging the infrastructure, of course).

    The new ULP and library are not quite ready for prime time, but this is a stick in the ground to mark some progress. You can certainly use the HAL code directly, without fiddling around in the schematic: stuff the whole program (at the end of the post) in your existing (but likely empty) custom_postgui.hal file.

    The schematic is, of course, much fluffier than the corresponding code, particularly because I chopped it into five easily printed pages. Here’s the Big Picture overview of what’s going on in each page; click the pix for bigger images.

    The servo thread interface device in the lower left provides the halui timing. The big block in the upper left has all the Logitech gamepad buttons, including the four big ones used for Z and A axis jogging. I changed the two left-rear buttons to activate the Abort signal rather than Estop, not that I use them all that much anyway.

    The two joystick knobs have pushbuttons, which I combine and use to toggle a flipflop that will select the jogging speed: fast or crawl.

    I also cut the jog deadband from 0.2 to 0.1, which makes the joysticks much more responsive.

    Logitech Gamepad HAL Schematic - Page 1
    Logitech Gamepad HAL Schematic – Page 1

    The big block on the left has all the gamepad’s analog axes. The HAT0X and HAT0Y axes correspond to the top-hat pushbuttons; they’re not really analog at all, although they take on -1.0 / 0.0 / + 1.0 floating point values. The window comparators determine which joystick axes are active, which comes in handy later on.

    Logitech Gamepad HAL Schematic - Page 2
    Logitech Gamepad HAL Schematic – Page 2

    The HAL jogging control has a single input that sets the default speed, but the proper value is vastly different depending on whether you’re jogging with linear or angular motion. This page picks out which ini file MAX_VELOCITY value to use, converts from units/sec to units/min, then does Cool Thing #1: scales the speed so that the fast/crawl speeds work out nicely.

    I use the buttons to jog rapidly from here to there, then creep up on the alignment point using the joysticks. Pressing the joysticks downward switches from Fast to Crawl speeds, which provides sort of a gearshift that’s useful for coarse / fine adjustments.

    The buttons run at two speeds:

    • Fast: the maximum speed for the axis
    • Crawl: 10% of that value

    The joysticks have a lower top speed:

    • Fast: half the maximum speed of the axis
    • Crawl: 10% of that value

    All those values go into the mux4 block and thence to the HAL jog speed control.

    Logitech Gamepad HAL Schematic - Page 3
    Logitech Gamepad HAL Schematic – Page 3

    This page does Cool Thing #2: prioritize the joystick axes and lock out the one that started moving last. The general idea is that it’s painfully easy to move the joysticks diagonally, which is great for gaming and terrible for milling machine control. A pair of flipflops for each joystick remember which axis started moving first.

    If you want to move diagonally, just press the buttons; they’re not locked out, so you can do what you want.

    Logitech Gamepad HAL Schematic - Page 4
    Logitech Gamepad HAL Schematic – Page 4

    The motion comes together on the last page, where scale blocks flip the direction of the Y and Z joystick axes so positive motion is upward. The multiplexers allow only the active axis of each joystick to reach the HAL analog jog inputs; you can vary the speed of that axis up to the maximum as you’d expect. The buttons drive the digital inputs that jog at that maximum speed; the Y and Z button directions get sorted out appropriately.

    Logitech Gamepad HAL Schematic - Page 5
    Logitech Gamepad HAL Schematic – Page 5

    Those five pages boil down into the following code, which I manually insert into my custom_postgui.hal file, along with the tool length probe pin definition.

    # HAL config file automatically generated by Eagle-CAD hal-write.ulp
    # (C) Martin Schoeneck.de 2008
    # Mods Ed Nisley 2010
    
    # Path: [/mnt/bulkdata/Project Files/eagle/projects/EMC2 HAL Configuration/]
    # ProjectName: [Logitech Gamepad]
    # File name: [/mnt/bulkdata/Project Files/eagle/projects/EMC2 HAL Configuration/Logitech Gamepad.hal]
    
    ####################################################
    # Load realtime and userspace modules
    loadrt constant	count=14
    loadrt and2	count=9
    loadrt flipflop	count=4
    loadrt mux2	count=5
    loadrt mux4	count=1
    loadrt not	count=8
    loadrt or2	count=9
    loadrt scale	count=7
    loadrt toggle	count=1
    loadrt wcomp	count=6
    
    ####################################################
    # Hook functions into threads
    addf toggle.0	servo-thread
    addf wcomp.1	servo-thread
    addf wcomp.2	servo-thread
    addf wcomp.3	servo-thread
    addf and2.0	servo-thread
    addf and2.4	servo-thread
    addf and2.3	servo-thread
    addf and2.2	servo-thread
    addf and2.1	servo-thread
    addf constant.6	servo-thread
    addf constant.5	servo-thread
    addf constant.4	servo-thread
    addf constant.3	servo-thread
    addf constant.2	servo-thread
    addf constant.1	servo-thread
    addf constant.0	servo-thread
    addf constant.7	servo-thread
    addf constant.8	servo-thread
    addf scale.1	servo-thread
    addf scale.2	servo-thread
    addf scale.3	servo-thread
    addf mux4.0	servo-thread
    addf mux2.0	servo-thread
    addf scale.4	servo-thread
    addf scale.0	servo-thread
    addf wcomp.5	servo-thread
    addf wcomp.4	servo-thread
    addf wcomp.0	servo-thread
    addf flipflop.1	servo-thread
    addf flipflop.0	servo-thread
    addf and2.5	servo-thread
    addf and2.6	servo-thread
    addf and2.7	servo-thread
    addf and2.8	servo-thread
    addf flipflop.2	servo-thread
    addf flipflop.3	servo-thread
    addf or2.4	servo-thread
    addf or2.8	servo-thread
    addf or2.7	servo-thread
    addf or2.6	servo-thread
    addf or2.5	servo-thread
    addf or2.3	servo-thread
    addf or2.2	servo-thread
    addf or2.1	servo-thread
    addf or2.0	servo-thread
    addf not.1	servo-thread
    addf not.2	servo-thread
    addf not.3	servo-thread
    addf not.4	servo-thread
    addf not.5	servo-thread
    addf not.6	servo-thread
    addf not.7	servo-thread
    addf not.0	servo-thread
    addf constant.9	servo-thread
    addf mux2.1	servo-thread
    addf mux2.2	servo-thread
    addf mux2.3	servo-thread
    addf mux2.4	servo-thread
    addf constant.10	servo-thread
    addf constant.11	servo-thread
    addf scale.5	servo-thread
    addf scale.6	servo-thread
    addf constant.12	servo-thread
    addf constant.13	servo-thread
    
    ####################################################
    # Set parameters
    
    ####################################################
    # Set constants
    setp constant.0.value	+0.02
    setp constant.1.value	-0.02
    setp constant.2.value	60
    setp constant.3.value	1.00
    setp constant.4.value	0.10
    setp constant.5.value	0.50
    setp constant.6.value	0.10
    setp constant.7.value	+0.5
    setp constant.8.value	-0.5
    setp constant.9.value	0.0
    setp constant.10.value	[TRAJ]MAX_LINEAR_VELOCITY
    setp constant.11.value	[TRAJ]MAX_ANGULAR_VELOCITY
    setp constant.12.value	-1.0
    setp constant.13.value	0.1
    
    ####################################################
    # Connect Modules with nets
    net a-button-minus input.0.btn-trigger or2.2.in0 halui.jog.3.minus
    net a-button-plus input.0.btn-thumb2 or2.2.in1 halui.jog.3.plus
    net a-buttons-active or2.2.out or2.3.in0 or2.4.in1
    net a-disable not.7.out and2.5.in1
    net a-enable flipflop.3.out not.7.in mux2.4.sel
    net a-jog wcomp.2.in input.0.abs-z-position mux2.4.in1
    net a-knob-active not.2.out or2.4.in0 and2.7.in1
    net a-knob-inactive wcomp.2.out not.2.in and2.6.in1
    net a-select and2.8.in0 and2.7.out
    net a-set flipflop.3.set and2.8.out
    net angular_motion or2.4.out mux2.0.sel
    net any-buttons-active mux4.0.sel0 or2.8.out
    net az-buttons-active or2.3.out or2.8.in1
    net az-reset flipflop.2.reset and2.6.out flipflop.3.reset
    net button-crawl scale.4.out mux4.0.in3
    net button-fast scale.2.out mux4.0.in1 scale.4.in
    net jog-crawl toggle.0.out mux4.0.sel1
    net jog-speed halui.jog-speed mux4.0.out
    net knob-crawl mux4.0.in2 scale.3.out
    net knob-fast mux4.0.in0 scale.1.out scale.3.in
    net n_1 constant.10.out mux2.0.in0
    net n_2 and2.0.in0 input.0.btn-top2
    net n_3 and2.0.in1 input.0.btn-base
    net n_4 and2.0.out halui.abort
    net n_5 halui.mode.manual input.0.btn-base3
    net n_6 wcomp.0.max wcomp.1.max wcomp.2.max wcomp.3.max constant.0.out
    net n_7 halui.program.resume input.0.btn-base4
    net n_8 wcomp.0.min wcomp.1.min wcomp.2.min wcomp.3.min constant.1.out
    net n_9 mux2.0.in1 constant.11.out
    net n_10 constant.12.out scale.5.gain scale.6.gain
    net n_11 input.0.btn-base5 or2.0.in0
    net n_12 input.0.btn-base6 or2.0.in1
    net n_13 constant.9.out mux2.1.in0 mux2.2.in0 mux2.3.in0 mux2.4.in0
    net n_14 mux2.1.out halui.jog.0.analog
    net n_15 toggle.0.in or2.0.out
    net n_16 constant.2.out scale.0.gain
    net n_17 constant.5.out scale.1.gain
    net n_18 constant.3.out scale.2.gain
    net n_19 constant.4.out scale.3.gain
    net n_20 scale.4.gain constant.6.out
    net n_21 halui.jog.1.analog mux2.2.out
    net n_22 mux2.2.in1 scale.5.out
    net n_23 scale.6.out mux2.3.in1
    net n_24 constant.13.out halui.jog-deadband
    net n_25 wcomp.4.max constant.7.out wcomp.5.max
    net n_26 constant.8.out wcomp.4.min wcomp.5.min
    net n_27 mux2.3.out halui.jog.2.analog
    net n_28 halui.jog.3.analog mux2.4.out
    net vel-per-minute scale.0.out scale.1.in scale.2.in
    net vel-per-second mux2.0.out scale.0.in
    net x-buttons-active or2.7.in0 or2.5.out
    net x-disable not.4.out and2.4.in1
    net x-enable not.4.in flipflop.0.out mux2.1.sel
    net x-hat-jog wcomp.4.in input.0.abs-hat0x-position
    net x-hat-minus wcomp.4.under or2.5.in1 halui.jog.0.minus
    net x-hat-plus or2.5.in0 wcomp.4.over halui.jog.0.plus
    net x-jog wcomp.0.in input.0.abs-x-position mux2.1.in1
    net x-knob-active not.0.out and2.1.in0
    net x-knob-inactive wcomp.0.out not.0.in and2.2.in0 and2.3.in0
    net x-set and2.1.out flipflop.0.set
    net xy-buttons-active or2.7.out or2.8.in0
    net xy-reset flipflop.0.reset and2.2.out flipflop.1.reset
    net y-buttons-active or2.6.out or2.7.in1
    net y-disable not.5.out and2.1.in1
    net y-enable flipflop.1.out not.5.in mux2.2.sel
    net y-hat-jog input.0.abs-hat0y-position wcomp.5.in
    net y-hat-minus wcomp.5.under or2.6.in1 halui.jog.1.plus
    net y-hat-plus or2.6.in0 wcomp.5.over halui.jog.1.minus
    net y-jog wcomp.1.in input.0.abs-y-position scale.5.in
    net y-knob-active not.1.out and2.3.in1
    net y-knob-inactive not.1.in wcomp.1.out and2.2.in1
    net y-select and2.4.in0 and2.3.out
    net y-set flipflop.1.set and2.4.out
    net z-button-minus input.0.btn-thumb or2.1.in0 halui.jog.2.minus
    net z-button-plus input.0.btn-top or2.1.in1 halui.jog.2.plus
    net z-buttons-active or2.1.out or2.3.in1
    net z-disable not.6.out and2.8.in1
    net z-enable not.6.in flipflop.2.out mux2.3.sel
    net z-jog wcomp.3.in input.0.abs-rz-position scale.6.in
    net z-knob-active not.3.out and2.5.in0
    net z-knob-inactive not.3.in wcomp.3.out and2.7.in0 and2.6.in0
    net z-set and2.5.out flipflop.2.set
    

    The ULP script that eats the schematic and poots out the HAL code:

    /******************************************************************************
     * HAL-Configurator
     *
     * Author: Martin Schoeneck 2008
     * Additional gates & tweaks: Ed Nisley KE4ZNU 2010
     *****************************************************************************/
    #usage "<h1>HAL-Configurator</h1>Start from a Schematic where symbols from hal-config.lbr are used!";
    
    string output_path =    "./";
    string dev_loadrt =     "LOADRT";
    string dev_loadusr =    "LOADUSR";
    string dev_thread =     "THREAD";
    string dev_parameter =  "PARAMETER";
    
    string dev_names[] = {
    "CONSTANT",								// must be first entry to make set_constants() work
    "ABS",				// 2.4
    "AND2",
    "BLEND",			// 2.4
    "CHARGE-PUMP",		// 2.4
    "COMP",
    "CONV_S32_FLOAT",	// 2.4
    "DDT",				// 2.4
    "DEADZONE",			// 2.4
    "DEBOUNCE",			// 2.4
    "EDGE",
    "ENCODER",			// 2.4
    "ENCODER-RATIO",	// 2.4
    "ESTOP-LATCH",
    "FLIPFLOP",
    "FREQGEN",			// 2.4
    "LOWPASS",
    "MULT2",			// 2.4
    "MUX2",
    "MUX4",				// 2.4
    "MUX8",				// 2.4
    "NEAR",				// 2.4
    "NOT",
    "ONESHOT",
    "OR2",
    "SAMPLER",			// 2.4
    "SCALE",			// 2.4
    "SELECT8",			// 2.4
    "SUM2",
    "TIMEDELAY",		// 2.4
    "TOGGLE",			// 2.4
    "WCOMP",			// 2.4
    "XOR2",				// 2.4
    ""					// end flag
    };
    
    string init = "# HAL config file automatically generated by Eagle-CAD hal-write.ulp\n# (C) Martin Schoeneck.de 2008\n# Mods Ed Nisley 2010\n";
    
    /*******************************************************************************
     * Global Stuff
     ******************************************************************************/
    
    string FileName;
    string ProjectPath;
    string ProjectName;
    
    void Info(string Message) {
    	dlgMessageBox(";<b>Info</b><p>\n" + Message);
    }
    
    void Warn(string Message) {
    	dlgMessageBox("!<b>Warning</b><p>\n" + Message + "<p>see usage");
    }
    
    void Error(string Message) {
    	dlgMessageBox(":<hr><b>Error</b><p>\n" + Message + "<p>see usage");
    	exit(1);
    }
    
    string replace(string str, char a, char b) {
    	// in string str replace a with b
    	int pos = -1;
    	do {
    		// find that character
    		pos = strchr(str, a);
    		// replace if found
    		if(pos >= 0) {
    			str[pos] = b;
    		}
    	} while(pos >= 0);
    
    	return str;
    }
    
    // the part name contains an index and is written in capital letters
    string get_module_name(UL_PART P) {
    	// check module name, syntax: INDEX:NAME
    	string mod_name = strlwr(P.name);
    	// split string at the : if exists
    	string a[];
    	int c = strsplit(a, mod_name, ':');
    	mod_name = a[c-1];
    	// if name starts with '[' we need uppercase letters
    	if(mod_name[0] == '[') {
    		mod_name = strupr(mod_name);
    	}
    
    	return mod_name;
    }
    
    string comment(string mess) {
    	string str = "\n\n####################################################\n";
    	if(mess != "") {
    		str += "# " + mess + "\n";
    	}
    
    	return str;
    }
    
    // if this is a device for loading a module, load it (usr/rt)
    string load_module(UL_PART P) {
    	string str = "";
    
    	// it's a module if the device's name starts with LOADRT/LOADUSR
    	if((strstr(P.device.name, dev_loadrt) == 0) ||
    	   (strstr(P.device.name, dev_loadusr) == 0)) {
    
    		// now add the string to our script
    		str += P.value + "\n";
    	}
    
    	return str;
    }
    
    // count used digital gates (and, or, etc) and load module if neccessary
    string load_blocks() {
    	string str = "";
    
    	int index;
    
    	int dev_counters[];
    	string dname[];
    
    	// count the gates that are used
    	schematic(S) { S.parts(P) {
    		strsplit(dname,P.device.name,'.');		// extract first part of name
    		if ("" != lookup(dev_names,dname[0],0)) {
    			for (index = 0;  (dname[0] != dev_names[index]) ; index++) {
    				continue;
    			}
    			dev_counters[index]++;
    		}
    	} }
    
    // force lowercase module names...
    
    	for (index = 0; ("" != dev_names[index]) ; index++) {
    		if (dev_counters[index]) {
    			sprintf(str,"%sloadrt %s\tcount=%d\n",str,strlwr(dev_names[index]),dev_counters[index]);
    		}
    	}
    
    	return str;
    }
    
    string hook_function(UL_NET N) {
    	string str = "";
    
    	// is this net connected to a thread (work as functions here)?
    	int    noclkpins       = 0;
    	string thread_name     = "";  // this net should be connected to a thread
    	string thread_position = "";
    	N.pinrefs(PR) {
    		// this net is connected to a clk-pin
    		if(PR.pin.function == PIN_FUNCTION_FLAG_CLK) {
    			// check the part: is it a thread-device?
    			if(strstr(PR.part.device.name, dev_thread) == 0) {
    				// we need the name of the thread
    				thread_name = strlwr(PR.part.name);
    				// and we need the position (position _ is ignored)
    				thread_position = strlwr(PR.pin.name);
    				thread_position = replace(thread_position, '_', ' ');
    			}
    		} else {
    			// no clk-pin, this is no function-net
    			noclkpins++;
    			break;
    		}
    	}
    
    	// found a thread?
    	if(noclkpins == 0 && thread_name != "") {
    		// all the other pins are interesting now
    		N.pinrefs(PR) {
    			// this pin does not belong to the thread
    			if(strstr(PR.part.device.name, dev_thread) != 0) {
    				// name of the pin is name of the function
    				//string function_name = strlwr(PR.pin.name);
    				string function_name = strlwr(PR.instance.gate.name);
    				// if functionname starts with a '.', it will be appended to the modulename
    				if(function_name[0] == '.') {
    					// if the name is only a point, it will be ignored
    					if(strlen(function_name) == 1) {
    						function_name = "";
    					}
    					function_name = get_module_name(PR.part) + function_name;
    				}
    				str += "addf " + function_name + "\t" + thread_name + "\t" + thread_position + "\n";
    			}
    		}
    	}
    
    	return str;
    }
    
    string set_parameter(UL_NET N) {
    	string str = "";
    
    	// is this net connected to a parameter-device?
    	int    nodotpins       = 0;
    	string parameter_value = "";
    	N.pinrefs(PR) {
    		// this net is connected to a dot-pin
    		if(PR.pin.function == PIN_FUNCTION_FLAG_DOT) {
    			// check the part: is it a parameter-device?
    //			str += "** dev name [" + PR.part.device.name + "] [" + dev_parameter + "]\n";
    			if(strstr(PR.part.device.name, dev_parameter) == 0) {
    				// we need the value of that parameter
    				parameter_value = PR.part.value;
    //				str += "**  value [" + PR.part.value +"]\n";
    			}
    		} else {
    			// no clk-pin, this is no function-net
    			nodotpins++;
    			break;
    		}
    	}
    
    	// found a parameter?
    	if(nodotpins == 0 && parameter_value != "") {
    		// all the other pins are interesting now
    		N.pinrefs(PR) {
    //			str += "** dev name [" + PR.part.device.name + "] [" + dev_parameter + "]\n";
    			// this pin does not belong to the parameter-device
    			if(strstr(PR.part.device.name, dev_parameter) != 0) {
    				// name of the pin is name of the function
    				//string parameter_name = strlwr(PR.pin.name);
    				string parameter_name = strlwr(PR.instance.gate.name);
    				// if functionname starts with a '.', it will be appended to the modulename
    //				str += "** param (gate) name [" + parameter_name + "]\n";
    				if(parameter_name[0] == '.') {
    					// if the name is only a point, it will be ignored
    					if(strlen(parameter_name) == 1) {
    						parameter_name = "";
    					}
    					parameter_name = get_module_name(PR.part) + parameter_name;
    //					str += "** param (part) name [" + parameter_name + "]\n";
    				}
    				str += "setp " + parameter_name + "\t" + parameter_value + "\n";
    			}
    		}
    	}
    
    	return str;
    }
    
    // if this is a 'constant'-device, set its value
    // NOTE: this is hardcoded to use the first entry in the dev_names[] array!
    string set_constants(UL_PART P) {
    	string str = "";
    
    	// 'constant'-device?
    	if(strstr(P.device.name, dev_names[0]) == 0) {
    		str += "setp " + get_module_name(P) + ".value\t" + P.value + "\n";
    	}
    
    	return str;
    }
    
    string connect_net(UL_NET N) {
    	string str = "";
    
    	// find all neccessary net-members
    	string pins = "";
    	N.pinrefs(P) {
    		// only non-functional pins are connected
    		if(P.pin.function == PIN_FUNCTION_FLAG_NONE) {
    			string pin_name =  strlwr(P.pin.name);
    			string part_name = strlwr(P.part.name);
    			pin_name =  replace(pin_name,  '$', '_');
    			part_name = replace(part_name, '$', '_');
    			pins += part_name + "." + pin_name + " ";
    		}
    	}
    
    	if(pins != "") {
    		string net_name = strlwr(N.name);
    		net_name = replace(net_name, '$', '_');
    		str += "net " + net_name + " " + pins + "\n";
    	}
    
    	return str;
    }
    
    /*******************************************************************************
     * Main program.
     ******************************************************************************/
    // is the schematic editor running?
    if (!schematic) {
    	Error("No Schematic!<br>This program will only work in the schematic editor.");
    }
    
    schematic(S) {
    	ProjectPath = filedir(S.name);
    	ProjectName = filesetext(filename(S.name), "");
    }
    
    // build configuration
    string cs = init + "\n\n";
    
    FileName = ProjectPath + ProjectName + ".hal";
    
    cs += "# Path: [" + ProjectPath + "]\n";
    cs += "# ProjectName: [" + ProjectName + "]\n";
    //cs += "# File name: [" + FileName + "]\n\n";
    
    // ask for a filename: where should we write the configuration?
    
    FileName = dlgFileSave("Save Configuration", FileName, "*.hal");
    
    if(!FileName) {
    	exit(0);
    }
    
    cs += "# File name: [" + FileName + "]\n\n";
    
    schematic(S) {
    	// load modules
    	cs += comment("Load realtime and userspace modules");
    	S.parts(P) {
    		cs += load_module(P);
    	}
    
    	// load blocks
    	cs += load_blocks();
    
    	// add functions
    	cs += comment("Hook functions into threads");
    	S.nets(N) {
    		cs += hook_function(N);
    	}
    
    	// set parameters
    	cs += comment("Set parameters");
    	S.nets(N) {
    		cs += set_parameter(N);
    	}
    
    	// set constant values
    	cs += comment("Set constants");
    	S.parts(P) {
    		cs += set_constants(P);
    	}
    
    	// build nets and connect them
    	cs += comment("Connect Modules with nets");
    	S.nets(N) {
    		cs += connect_net(N);
    	}
    }
    
    // open/overwrite the target file to save the configuration
    output(FileName, "wt") {
    	printf(cs);
    }
    

    Most of that script is Martin’s work; I just cleaned it up. You can download it by hovering over the code to make the little toolbar pop up near the upper-right corner of the text, then:

    • click a little button to copy it to the clipboard or
    • click another little button to view the source, then save that file

    You’ll also need the Eagle library that goes along with the script, but WordPress doesn’t like .lbr files. Here’s the hal-config-2.4.lbr file with a totally bogus odt extension. Download it, rename it to remove the .odt extension, and it’s all good.

    There is basically no documentation for any of this. I figured out what to do by looking at the source and Martin’s sample schematic, but now you have two sample schematics: the situation is definitely improving!

  • C Constants Aren’t Automatically Promoted

    Quick, what’s wrong with this code snippet…

    #define MAX_INTERVAL	(60*1000)
    
    unsigned long int LoggedTime;
    unsigned long int NowTime;
    
    byte LogNeeded;
    
    ... snippage ...
    
    LogNeeded = (NowTime >= (LoggedTime + MAX_INTERVAL)); 
    

    Yeah, that constant up there should look like this:

    #define MAX_INTERVAL (60*1000ul)

    Took me a while to figure that out. Again.

  • Trinity Rules: LyX and LaTEX Setup

    The preamble turns on line numbers for proofing; remove those lines to turn them off.

    \usepackage{ragged2e}
    \usepackage{lastpage}
    \usepackage{url}
    \usepackage{dvipost}
    \usepackage{breakurl}
    \usepackage[labelfont={bf,sf}]{caption}
    \usepackage{listings}
    \usepackage{color}
    \usepackage{lineno}
    \linenumbers
    \renewcommand{\bottomfraction}{0.7}
    \pagestyle{fancyplain}
    \fancyhf{}
    \lhead{\fancyplain{}{Trinity College Home Robot Contests}}
    \rhead{\fancyplain{}{2011 Rules}}
    \lfoot{\fancyplain{Modified \today}{Modified \today}}
    \cfoot{Copyright 2010 by Trinity College}
    \rfoot{\fancyplain{\thepage\ of \pageref{LastPage}}{\thepage\ of \pageref{LastPage}}}
    \RaggedRight
    \dvipostlayout
    \dvipost{cbstart color push Blue}
    \dvipost{cbend color pop}
    

    There’s no obvious documentation for the Document → Settings  → PDF Properties → Addition Options values, but these make the crossref links look better:

    urlcolor=blue,linkcolor=blue
    

    For what it’s worth, LyX is still the right hammer for this job, although when something goes wrong, the error messages are truly oracular…

    Memo to Self: no line numbers in the final version!

  • Arduino Mega: Showstopper Workaround

    The discussion following that post gave me enough impetus to figure this out. What I have here is not a complete solution, but it seems to solve the immediate problem.

    Downside: this will not survive the next regular system update that touches the gcc-avr package (yes, it’s the avr-gcc compiler and the gcc-avr package). Hence, I must write down the details so I can do it all over again…

    To review:

    The problem is that the avr-gcc cross-compiler produces incorrect code for Atmega1280-class chips with more than 64 KB of Flash space: a register isn’t saved-and-restored around a runtime routine that alters it. Simple sketches (seem to) run without problems, but sketches that instantiate objects crash unpredictably. Because Arduino sketches depend heavily on various objects (like, oh, the Serial routines), nontrivial sketches don’t work.

    The workaround is to patch the library routine that invokes the constructors, as detailed in that gcc bug report, to push / pop r20 around the offending constructors. The patch tweaks two spots in the libgcc.S source file, which then gets built into an assortment of chip-specific libgcc.a files during the compile.

    I was highly reluctant to do that, simply I’ve already installed the various gcc packages using pacman (the Arch Linux package manager) and really didn’t want to screw anything up by recompiling & reinstalling gcc from source. It’s certainly possible to update just the avr portion, but I don’t know exactly how to do that and doubt that I could get it right the first time… and the consequences of that catastrophe I don’t have time to deal with.

    So I elected to build the avr cross-compiler from source, verify that the as-built libgcc.a file was identical to the failing one, apply the patch, recompile, then manually insert the modified file in the right spot(s) in my existing installation. This is less manly than doing everything automagically, but has a very, very limited downside: I can easily back out the changes.

    Here’s how that went down…

    The instructions there (see the GCC for the AVR target section) give the overview of what to do. The introduction says:

    The default behaviour for most of these tools is to install every thing under the /usr/local directory. In order to keep the AVR tools separate from the base system, it is usually better to install everything into /usr/local/avr.

    Arch Linux has the tools installed directly in /usr, not /usr/local or /usr/local/avr, so $PREFIX=/usr. Currently, they’re at version 4.5.1, which is typical for Arch: you always get the most recent upstream packages, warts and all.

    Download the gcc-g++ (not gcc-c++ as in the directions) and gcc-core tarballs (from there or, better, the gnu mirrors) into, say, /tmp and unpack them. They’ll both unpack into /tmp/gcc-4.5.1, wherein you create and cd into obj-avr per the directions.

    I opted to feed in the same parameters as the Arch Build System used while installing the original package, rather than what’s suggested in the directions. That’s found in this file:

    /var/abs/community/gcc-avr/PKGBUILD
    

    Which contains, among other useful things, this lump of command-line invocation:

    ../configure --disable-libssp \
                   --disable-nls \
                   --enable-languages=c,c++ \
                   --infodir=/usr/share/info \
                   --libdir=/usr/lib \
                   --libexecdir=/usr/lib \
                   --mandir=/usr/share/man \
                   --prefix=/usr \
                   --target=avr \
                   --with-gnu-as \
                   --with-gnu-ld \
                   --with-as=/usr/bin/avr-as \
                   --with-ld=/usr/bin/avr-ld
    

    Yes, indeed, $PREFIX will wind up as /usr

    Feeding that into ./configure produces the usual torrent of output, ending in success after a minute or two. Firing off the make step is good for 15+ minutes of diversion, even on an 11-BogoMIPS dual-core box. I didn’t attempt to fire up threads for both cores, although I believe that’s a simple option.

    The existing compiler installation has several libgcc.a files, each apparently set for a specific avr chip:

    [ed@shiitake tmp]$ find /usr/lib/gcc/avr/4.5.1/ -name libgcc.a
    /usr/lib/gcc/avr/4.5.1/libgcc.a
    /usr/lib/gcc/avr/4.5.1/avr35/libgcc.a
    /usr/lib/gcc/avr/4.5.1/avr3/libgcc.a
    /usr/lib/gcc/avr/4.5.1/avr51/libgcc.a
    /usr/lib/gcc/avr/4.5.1/avr4/libgcc.a
    /usr/lib/gcc/avr/4.5.1/avr6/libgcc.a
    /usr/lib/gcc/avr/4.5.1/avr5/libgcc.a
    /usr/lib/gcc/avr/4.5.1/avr31/libgcc.a
    /usr/lib/gcc/avr/4.5.1/avr25/libgcc.a
    

    The key to figuring out which of those files need tweaking lies there, which says (I think) that the Atmega1280 is an avr5 or avr51. Because I have an Arduino Mega that’s affected by this bug, I planned to tweak only these files:

    /usr/lib/gcc/avr/4.5.1/libgcc.a
    /usr/lib/gcc/avr/4.5.1/avr51/libgcc.a
    /usr/lib/gcc/avr/4.5.1/avr5/libgcc.a
    

    I have no idea what the top-level file is used for, but … it seemed like a good idea.

    Now, I innocently expected that the libgcc.a files for a 4.5.1 installation would match the freshly compiled files for a 4.5.1-from-source build, but that wasn’t the case. I don’t know what the difference might be; perhaps there’s an embedded path or timestamp or whatever that makes a difference?

    The Arch Linux standard installation of gcc 4.5.1 has these files:

    $ find /usr/lib/gcc/avr/4.5.1/ -iname libgcc.a -print0 | xargs -0 ls -l
    -rw-r--r-- 1 root root 2251078 Sep  4 16:26 /usr/lib/gcc/avr/4.5.1/avr25/libgcc.a
    -rw-r--r-- 1 root root 2256618 Sep  4 16:26 /usr/lib/gcc/avr/4.5.1/avr31/libgcc.a
    -rw-r--r-- 1 root root 2252506 Sep  4 16:26 /usr/lib/gcc/avr/4.5.1/avr35/libgcc.a
    -rw-r--r-- 1 root root 2256310 Sep  4 16:26 /usr/lib/gcc/avr/4.5.1/avr3/libgcc.a
    -rw-r--r-- 1 root root 2250930 Sep  4 16:26 /usr/lib/gcc/avr/4.5.1/avr4/libgcc.a
    -rw-r--r-- 1 root root 2251846 Sep 27 12:58 /usr/lib/gcc/avr/4.5.1/avr51/libgcc.a
    -rw-r--r-- 1 root root 2251550 Sep 27 12:58 /usr/lib/gcc/avr/4.5.1/avr5/libgcc.a
    -rw-r--r-- 1 root root 2252458 Sep  4 16:27 /usr/lib/gcc/avr/4.5.1/avr6/libgcc.a
    -rw-r--r-- 1 root root 2251474 Sep 27 12:57 /usr/lib/gcc/avr/4.5.1/libgcc.a
    

    The compilation-from-source using the gcc 4.5.1 tarballs has these files:

    $ pwd
    /tmp/gcc-4.5.1/obj-avr
    $ find -iname libgcc.a -print0 | xargs -0 ls -l
    -rw-r--r-- 1 ed ed 2250258 Sep 27 15:51 ./avr/avr25/libgcc/libgcc.a
    -rw-r--r-- 1 ed ed 2255798 Sep 27 15:51 ./avr/avr31/libgcc/libgcc.a
    -rw-r--r-- 1 ed ed 2251686 Sep 27 15:51 ./avr/avr35/libgcc/libgcc.a
    -rw-r--r-- 1 ed ed 2255490 Sep 27 15:51 ./avr/avr3/libgcc/libgcc.a
    -rw-r--r-- 1 ed ed 2250110 Sep 27 15:51 ./avr/avr4/libgcc/libgcc.a
    -rw-r--r-- 1 ed ed 2251838 Sep 27 15:51 ./avr/avr51/libgcc/libgcc.a
    -rw-r--r-- 1 ed ed 2251550 Sep 27 15:51 ./avr/avr5/libgcc/libgcc.a
    -rw-r--r-- 1 ed ed 2251638 Sep 27 15:52 ./avr/avr6/libgcc/libgcc.a
    -rw-r--r-- 1 ed ed 2251474 Sep 27 15:52 ./avr/libgcc/libgcc.a
    -rw-r--r-- 1 ed ed 2250258 Sep 27 15:51 ./gcc/avr25/libgcc.a
    -rw-r--r-- 1 ed ed 2255798 Sep 27 15:51 ./gcc/avr31/libgcc.a
    -rw-r--r-- 1 ed ed 2251686 Sep 27 15:51 ./gcc/avr35/libgcc.a
    -rw-r--r-- 1 ed ed 2255490 Sep 27 15:51 ./gcc/avr3/libgcc.a
    -rw-r--r-- 1 ed ed 2250110 Sep 27 15:51 ./gcc/avr4/libgcc.a
    -rw-r--r-- 1 ed ed 2251838 Sep 27 15:51 ./gcc/avr51/libgcc.a
    -rw-r--r-- 1 ed ed 2251550 Sep 27 15:51 ./gcc/avr5/libgcc.a
    -rw-r--r-- 1 ed ed 2251638 Sep 27 15:52 ./gcc/avr6/libgcc.a
    -rw-r--r-- 1 ed ed 2251474 Sep 27 15:52 ./gcc/libgcc.a
    

    The top-level files have the same size, but are not identical:

    $ diff ./avr/libgcc/libgcc.a ./gcc/libgcc.a
    Binary files ./avr/libgcc/libgcc.a and ./gcc/libgcc.a differ
    

    Haven’t a clue what’s going on with different files in different spots, but I saved the existing files in the installed tree as *.base and copied the new ones from ./gcc/avr* into place. While there are many ways to crash a program, the AnalogInOutSerial demo program ran correctly on a Duemilanova (presumably with the existing libgcc.a) and failed on the Mega (with the recompiled libgcc.a). Save those files as *.rebuild just in case they come in handy.

    Manually change the libgcc.S source file (it’s only four lines, I can do this), recompile, and the build process recompiles only the affected files; that’s comforting. Copy those into the installed tree and, lo and behold, the demo program now runs on both the Duemilanova and the Mega.

    While it’s too soon to declare victory, the hardware bringup program I’m writing also works, so the initial signs are good.

    Thanks to Mark Stanley for blasting me off dead center on this. I didn’t do a complete install, but he got me thinking how to make the least disruptive change…

    And a tip o’ the cycling helmet to the whole Free Software collective for making a mid-flight patch like this both feasible and possible: Use The Source!

  • Arduino Mega: Showstopper!

    I planned to use an Arduino Mega for an upcoming Circuit Cellar project, but … it doesn’t work. Well, it works, but under very limited circumstances.

    The problem manifests itself as a complete crash / lockup under very straightforward conditions: attempting to use the serial output will suffice. This unmodified example sketch fails: AnalogInOutSerial.

    After considerable Googling, there’s the showstopper: the gcc-avr compiler fails to save-and-restore a register that gets clobbered by the object constructors. Simple code doesn’t instantiate any objects, so it works fine. The serial failure is just a symptom, which means the various workarounds suggested in the forums don’t fix the general case.

    The patch offered for gcc-avr is basically four lines (a pair of save / restores on R20), but requires recompiling what seems to be the entire AVR toolchain from source. That, alas, lies far beyond my capabilities… I could probably figure out enough to recompile it, but I’m very uncertain I could accomplish that without screwing up the main gcc compiler or the setup thereof.

    It is not clear to me that the many claims of “it works on this version” are correct. From the nature of the problem, the failures depend critically on addresses occupied, final layout of the program / data in Flash, and (most likely) the execution path. The “working” configurations / systems may simply not fail using the sample programs.

    This is on Arch Linux, for what it’s worth, with gcc-avr 4.5.1.

    If anybody can walk me through the process of rebuilding whatever must be rebuilt, preferably in a safe place, perhaps I can manually stuff the new file(s) into the proper spots(s) to replace the incorrect ones…

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