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

  • Bash File Name Chopping for Gnuplot

    Just so I can remember it for next time, this plot:

    PI-Loop-ErrDrive
    PI-Loop-ErrDrive

    Came from a dataset with a zillion lines like this:

    #Set	Temp	TZone	TErr	Int	PDrive	sPWM	Time
    30.0	15.7	3	-14.30	0.000	-1.000	-255	0
    30.0	15.7	3	-14.30	0.000	-1.000	-255	142
    30.0	15.7	3	-14.30	0.000	-1.000	-255	245
    30.0	15.7	3	-14.30	0.000	-1.000	-255	348
    

    Using this Bash script to allow many different file names:

    #!/bin/sh
    export GDFONTPATH="/usr/share/fonts/truetype/"
    base=${1%%.*}
    echo Base name: ${base}
    ofile=${base}.png
    echo Output file: ${ofile}
    gnuplot << EOF
    #set term x11
    set term png font "arialbd.ttf" 18 size 950,600
    set output "${ofile}"
    set title "Peltier Test - Loop Tuning"
    set key noautotitles
    unset mouse
    set bmargin 4
    set grid xtics ytics
    set xlabel "Time - sec"
    #set format x "%4.0f"
    #set xrange [5000:7500]
    #set xtics 0,5
    set mxtics 2
    set ytics nomirror autofreq
    set ylabel "Various"
    set format y "%5.1f"
    set yrange [-2:2]
    #set mytics 2
    #set y2label "PWM"
    #set format y2 "%3.0f"
    #set y2range [0:255]
    #set y2tics 32
    #set rmargin 9
    set datafile separator "\t"
    #set label 1 "HP + LP" at 0.25,-14 font "arialbd,14"
    plot	\
        "$1" using (\$8/1000):4 with lines lt 3 title "Error" ,\
        "$1" using (\$8/1000):6 with lines lt 4 title "Drive"
    #    "$1" using 4 with lines lt 3 title "Error" ,\
    #    "$1" using 6 with lines lt 4 title "Drive"
    #    "$1" using (\$8/1000):1 with lines lt 3 title "Setpoint" ,\
    #    "$1" using (\$8/1000):2 with lines lt 4 title "Temp C"
    EOF
    

    There’s quite some other cruft in there, but the first part I must remember is right up at the top, where the magic incantation

    base=${1%%.*}

    chops off the file extension. Of course, that doesn’t work worth beans when the file name has several periods scattered through it.

    The other part is at the bottom, where various alternate lines for the plot command must live after the last valid parameter line: the octothorpe comment header doesn’t work inside a command!

  • Peltier PWM Temperature Control: Better PI Loop

    As I feared, P control can’t push the platform into the deadband all by itself at high temperatures, so I rewrote the loop the way it should have been all along:

    • PWM=1 beyond a limit well beyond the deadband, set integral=0 to avoid windup
    • Proportional + integral control inside that limit
    • Not worrying about relay chatter

    Holding PWM=1 until the PI loop kicks in ensures that the P control won’t lose traction along the way, but full throttle must give way to PI control outside the deadband to avoid a massive overshoot. Relay chatter could be a problem around room temperature where the heating/cooling threshold falls within the deadband, but that won’t shouldn’t be a problem in this application.

    Without much tuning, the results looked like this:

    PI-Loop-Temps
    PI-Loop-Temps

    Each temperature plateau lasts 3 minutes, the steps are 10 °C, starting at 30 °C and going upward to 50 °C, then downward to 0 °C, and upward to 20 °C. These are screenshots from OpenOffice Calc, so the resolution isn’t all that great.

    Two internal variables show what’s going on:

    PI-Loop-ErrDrive
    PI-Loop-ErrDrive

    The blue trace is the temperature error (actual – setpoint: negative = too cold = more heat needed), the purple trace is the signed PWM drive (-1.0 = full heat, +1.0 = full cool) summed from the P and I terms.

    Overlaying all the plateaus with their starting edges aligned on the left, then zooming in on the interesting part, shows the detailed timing:

    PI-Loop-ErrDrive-Overlay
    PI-Loop-ErrDrive-Overlay

    These X axis units are in samples = calls to the PI function, which happened about every 100 ms, which is roughly what the main loop will require for the MOSFET measurements.

    The Peltier module just barely reaches 0 °C with a 14 °C ambient: the drive exceeds +1.0 (output PWM = 255) as the temperature gradually stabilized at 0 °C with the module at full throttle; it’s dissipating 15 W to pump the temperature down. The heatsink reached 20 °C, with a simple foam hat surrounding the Peltier module and aluminum MOSFET mount. Any power dissipation from a MOSFET would add heat inside the insulation, but a bit more attention to detail should make 0 °C workable.

    On the high end, it looks like the module might barely reach 60 °C.

    Increasing the power supply voltage to increase the Peltier current would extend the temperature range, although a concerted stack probe didn’t produce anything like an 8 V 5A supply in the Basement Laboratory Parts Warehouse. If one turns up I’ll give it a go.

    There’s a bit of overshoot that might get tuned away by fiddling with the P gain or squelching the integral windup beyond the deadband. The temperature changes will be the most time-consuming part of the MOSFET measurement routine no matter what, so it probably doesn’t make much difference: just stall 45 s to get past most of the transient overshoot, then sample the temperature until it enters the deadband if it hasn’t already gotten there. Reducing the initial overshoot wouldn’t improve the overall time by much, anyway, as it’d just increase the time to enter the deadband. Given that the initial change takes maybe 30 seconds at full throttle, what’s the point?

    The PI loop Arduino source code, with some cruft left over from the last attempt, and some tweaks left to do:

    #define T_LIMIT         3.0                 // delta for full PWM=1 action
    #define T_ACCEPT        1.5                 // delta for good data (must be &gt; deadband)
    #define T_DEADBAND      1.0                 // delta for integral-only control
    #define T_PGAIN         (1.0 / T_LIMIT)     // proportional control gain: PWM/degree
    #define T_IGAIN         0.001               // integral control gain: PWM/degree*sample
    
    #define sign(x) ((x>0.0)-(x<0.0))           // adapted from old Utility.h library
    
    //-- Temperature control
    //      returns true for temperature within deadband
    
    int SetPeltier(float TNow, float TSet) {
    
    float TErr, TErrMag;
    int TSign;
    float PelDrive;
    
    int EnableHeat,OldEnableHeat;
    static float Integral;
    int TZone;
    int PWM;
    int PWMSigned;
    
        TErr = TNow - TSet;                  // what is the temperature error
        TErrMag = abs(TErr);                 //  ... magnitude
        TSign = sign(TErr);                  //  ... direction
    
        if (TErrMag >= T_LIMIT)                 // beyond outer limit
          TZone = 3;
        else if (TErrMag >= T_DEADBAND)         // beyond deadband
          TZone = 2;
        else if (TErrMag >= T_DEADBAND/2)       // within deadband
          TZone = 1;
        else                                    // pretty close to spot on
          TZone = 0;
    
        switch (TZone) {
          case 3:                                   // beyond outer limit
            PelDrive = TSign;                       //  drive hard: -1 heat +1 cool
            Integral = 0.0;                         //  no integration this far out
            break;
          case 2:                                   // beyond deadband
          case 1:                                   // within deadband
          case 0:                                   // inner deadband
            PelDrive = T_PGAIN*TErr + T_IGAIN*Integral;             // use PI control
            Integral += TErr;                                       // integrate the offset
           break;
          default:                                  // huh? should not happen...
            PelDrive = 0.0;
            break;
        }
    
        EnableHeat = (PelDrive > 0.0) ? LOW : HIGH;             // need cooling or heating?
        OldEnableHeat = digitalRead(PIN_ENABLE_HEAT);           // where is the relay now?
    
        if (OldEnableHeat != EnableHeat) {          // change from heating to cooling?
          analogWrite(PIN_SET_IPELTIER,0);          // disable PWM to flip relay
          digitalWrite(PIN_ENABLE_HEAT,EnableHeat);
          delay(15);                                // relay operation + bounce
        }
    
        PWM = constrain(((abs(PelDrive) * AO_PEL_SCALE) + AO_PEL_OFFSET),0.0,255.0);
        analogWrite(PIN_SET_IPELTIER,PWM);
    
        if (true) {
          PWMSigned = (EnableHeat == HIGH) ? -PWM : PWM;
          Serial.print(TSet,1);
          Serial.print("\t");
          Serial.print(TNow,1);
          Serial.print("\t");
          Serial.print(TZone,DEC);
          Serial.print("\t");
          Serial.print(TErr);
    
          Serial.print("\t");
          Serial.print(Integral,3);
          Serial.print("\t");
          Serial.print(PelDrive,3);
          Serial.print("\t");
          Serial.print(PWMSigned,DEC);
          Serial.print("\t");
          Serial.print(NowTime - StartTime);
          Serial.println();
        }
    
        return (TZone <= 1);
    
    
  • EAGLE Library: 10 W Aluminum Power Resistor

    It appears there are at least two different 10 W aluminum resistor sizes: the one used by Dale and the one used by everybody else. It’s either that or the EAGLE HS10 symbol is wrong…

    Using those dimensions, here’s a part that more closely fits the resistors in my heap. EAGLE 6 uses an XML file format, so you can stuff some ASCII text into the appropriate sections of your custom.lbr file (or whatever).

    The EAGLE package, which remains HS10 as in the resistor-power library, should produce something that looks like this:

    EAGLE 10 W Resistor package
    EAGLE 10 W Resistor package

    The XML code includes top-keepout rectangles under the body footprint:

    <package name="HS10">
    <description>DALE Power Resistor 10W</description>
    <wire x1="9.525" y1="5.461" x2="9.525" y2="10.3378" width="0.2032" layer="21"/>
    <wire x1="9.525" y1="10.3378" x2="4.6482" y2="10.3378" width="0.2032" layer="21"/>
    <wire x1="-9.525" y1="-5.461" x2="-4.6482" y2="-5.461" width="0.2032" layer="21"/>
    <wire x1="-4.6482" y1="-5.461" x2="9.525" y2="-5.461" width="0.2032" layer="21"/>
    <wire x1="9.525" y1="-5.461" x2="9.525" y2="5.461" width="0.2032" layer="21"/>
    <wire x1="9.525" y1="5.461" x2="4.6482" y2="5.461" width="0.2032" layer="21"/>
    <wire x1="4.6482" y1="5.461" x2="-9.525" y2="5.461" width="0.2032" layer="21"/>
    <wire x1="-9.525" y1="5.461" x2="-9.525" y2="-5.461" width="0.2032" layer="21"/>
    <wire x1="4.6482" y1="5.461" x2="4.6482" y2="10.3378" width="0.2032" layer="21"/>
    <wire x1="-9.525" y1="-5.461" x2="-9.525" y2="-10.3378" width="0.2032" layer="21"/>
    <wire x1="-9.525" y1="-10.3378" x2="-4.6482" y2="-10.3378" width="0.2032" layer="21"/>
    <wire x1="-4.6482" y1="-5.461" x2="-4.6482" y2="-10.3378" width="0.2032" layer="21"/>
    <wire x1="-9.47" y1="0.5" x2="-17.78" y2="0.5" width="0.2032" layer="51"/>
    <wire x1="-17.78" y1="0.5" x2="-17.78" y2="-0.5" width="0.2032" layer="51"/>
    <wire x1="-17.78" y1="-0.5" x2="-9.47" y2="-0.5" width="0.2032" layer="51"/>
    <wire x1="9.47" y1="-0.5" x2="17.78" y2="-0.5" width="0.2032" layer="51"/>
    <wire x1="17.78" y1="-0.5" x2="17.78" y2="0.5" width="0.2032" layer="51"/>
    <wire x1="17.78" y1="0.5" x2="9.47" y2="0.5" width="0.2032" layer="51"/>
    <pad name="1" x="-15.24" y="0" drill="1.3" shape="octagon"/>
    <pad name="2" x="15.24" y="0" drill="1.3" shape="octagon"/>
    <text x="-6.35" y="1.27" size="1.27" layer="25">&gt;NAME</text>
    <text x="-6.35" y="-2.54" size="1.27" layer="27">&gt;VALUE</text>
    <rectangle x1="-9.779" y1="-5.715" x2="9.779" y2="5.715" layer="43"/>
    <rectangle x1="4.318" y1="5.715" x2="9.779" y2="10.668" layer="43"/>
    <rectangle x1="-9.779" y1="-10.668" x2="-4.318" y2="-5.715" layer="43"/>
    <hole x="-7.1374" y="-7.9375" drill="2.3876"/>
    <hole x="7.1374" y="7.9375" drill="2.3876"/>
    </package>
    

    The EAGLE symbol looks just an ordinary schematic resistor:

    <symbol name="RESISTOR">
    <wire x1="-2.54" y1="0" x2="-2.159" y2="1.016" width="0.2032" layer="94"/>
    <wire x1="-2.159" y1="1.016" x2="-1.524" y2="-1.016" width="0.2032" layer="94"/>
    <wire x1="-1.524" y1="-1.016" x2="-0.889" y2="1.016" width="0.2032" layer="94"/>
    <wire x1="-0.889" y1="1.016" x2="-0.254" y2="-1.016" width="0.2032" layer="94"/>
    <wire x1="-0.254" y1="-1.016" x2="0.381" y2="1.016" width="0.2032" layer="94"/>
    <wire x1="0.381" y1="1.016" x2="1.016" y2="-1.016" width="0.2032" layer="94"/>
    <wire x1="1.016" y1="-1.016" x2="1.651" y2="1.016" width="0.2032" layer="94"/>
    <wire x1="1.651" y1="1.016" x2="2.286" y2="-1.016" width="0.2032" layer="94"/>
    <wire x1="2.286" y1="-1.016" x2="2.54" y2="0" width="0.2032" layer="94"/>
    <text x="-3.81" y="1.4986" size="1.778" layer="95">&gt;NAME</text>
    <text x="-3.81" y="-3.302" size="1.778" layer="96">&gt;VALUE</text>
    <pin name="2" x="5.08" y="0" visible="off" length="short" direction="pas" swaplevel="1" rot="R180"/>
    <pin name="1" x="-5.08" y="0" visible="off" length="short" direction="pas" swaplevel="1"/>
    </symbol>
    

    And then the EAGLE resistor device lashes everything together:

    <deviceset name="R" prefix="R" uservalue="yes">
    <description>Resistors</description>
    <gates>
    <gate name="R" symbol="RESISTOR" x="0" y="0"/>
    </gates>
    <devices>
    ... many more devices...
    <device name="ALUM-10W" package="HS10">
    <connects>
    <connect gate="R" pin="1" pad="1"/>
    <connect gate="R" pin="2" pad="2"/>
    </connects>
    <technologies>
    <technology name=""/>
    </technologies>
    </device>
    ... many more devices ...
    </devices>
    </deviceset>
    

    Update the libraries and then it should Just Work.

    It would have been much better had I discovered this before drilling & etching the board with one of those resistors…

  • EAGLE 6.x Invalid Device Names: Repair Thereof

    It seems that a much older version of Eagle allowed device names along the lines of ELECTRET MIC that contained blanks and worked perfectly at the time. Since then, the rules changed to prohibit blanks, but the EAGLE 5.x series evidently allowed those names to exist as long as they weren’t used in the schematic or touched in the library editor. In 6.x, however, you can’t even load the library without triggering an error message.

    Because 6.x won’t load the library, you can’t use the library editor to remove the blank.

    Because the most recent version of 5.x kvetches about the blank, you can’t use the library editor to remove the blank.

    Having only two offending device names, I figured I could use a hex editor to jam a hyphen in place of the blanks and be done with it. Come to find out that EAGLE (wisely) wraps a checksum around the binary library file to detect such changes and prevent the files from loading. I think that’s an excellent idea, even if it was inconvenient in this situation.

    Fortunately, 6.x both complains about the problem and offers up a “text editor” window with the complete XML source code for the library that it converted from the 5.x binary format.

    So:

    • Copy-and-paste the text into an editor that supports highlighted XML editing
    • Find the offending device names
    • Change the blanks to hyphens
    • Rename the original custom.lbr to custom.lbr.bin
    • Save the modified XML as custom.lbr

    Done!

  • Capacitor Self-resonance Calculator

    A Circuit Cellar reader recommended the KEMET Spice calculator that lets you explore the Z / ESR / capacitance / inductance of their various capacitors:

    KEMET Spice Simulation - 100 nF C0G SMD
    KEMET Spice Simulation – 100 nF C0G SMD

    As nearly as I can tell, my measurements on all those random capacitors fell into the right general neighborhood…

  • Website Pwnage

    Despite the fact that nobody bothers to crack your web passwords, as it’s easier for them to crack the entire server and scoop out everyone’s personally sensitive bits like so much caviar, all websites remind / require you to pick strong passwords. So, when I registered myself on a high-value website, I did what I always do: ask my password-generation program for a dollop of entropy.

    It came up with something along the lines of:

    Gmaz78fb'd]

    You can see where this is going, right?

    Pressing Submit (which always makes me whisper Inshallah with a bad accent) produced:

    The mumble.com website is temporarily unavailable. Please try again later.

    Little Bobby Tables rides again!

    [Yes, pwnage.]

  • Computer Amusements

    A friend asked me to scrub and rebuild an ancient IBM Thinkpad 760XD (there were good reasons for this task that aren’t relevant here), which led to a blast from the past:

    Windows 98 Welcome
    Windows 98 Welcome

    After Windows settled down from its obligatory reboots, installing the exceedingly complex MWave DSP drivers from three diskettes (!) produced this classic result:

    Windows 98 - BSOD
    Windows 98 – BSOD

    Ordinarily, I’d suggest installing some flavor of Linux, but the 760XD’s BIOS can’t boot from either CD or USB, so you’d be forced to sneak the install files onto the hard drive, hand-craft a suitable boot diskette (!), and then perpetrate some serious fiddling around. That made even less sense than (re-)installing Windows 98.

    However, given that exposing a fresh Windows 98 installation to the 2012 Internet would resemble tossing a duckling into a brush chipper, we agreed that this laptop’s next experience should be at an upcoming e-waste recycling event.

    The next morning confronted me with this delightful reminder that nobody knows how to handle boot-time errors, not even on a 2011 PC:

    Lenovo - USB Keyboard not found
    Lenovo – USB Keyboard not found

    The keyboard cable had gotten dislodged when the USB hub fell from its perch along the back edge of the desk. It’s fine now…