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

  • MOSFET RDS Tester: First Light

    Well, truth be known, it took a bit of tweaking to get to this point, but this was the first dependable & repeatable measurement:

    BUZ71A-overview
    BUZ71A-overview

    Rescaling the graph to show just the interesting part down near the origin:

    BUZ71A-detail
    BUZ71A-detail

    The VGS output steps from 4.0 to 10.0 V by 0.25 V, which is too fine until I get the Gnuplot script sorted out. The ID output runs from 0.0 A to 2.0 A in steps of 50 mA, which makes for smooth curves. These are all at 30 °C.

    The drain resistance flattens out nicely for VGS beyond 7 V, which is well over the BUZ71A max threshold of 4.0 V. That means you really need more than the usual 5 V supply to control the thing; I’ll eventually try some “logic level” MOSFETs. Part of the trick will be to find a logic-level MOSFET with a relatively high drain resistance suitable for current sensing.

    The board looks like this, with the foam shako for the thermal block and some MOSFET victims off to the side:

    MOSFET RDS Tester - overview
    MOSFET RDS Tester – overview

    The key part of the schematic:

    Schematic - MOSFET path
    Schematic – MOSFET path

    Two Arduino PWM outputs set the gate voltage and maximum drain current. The three jumpers near the middle allow various feedback paths, although the only one that really makes sense is closing the current loop. The trimpot is unused and the analog output directly sets the drain current limit at 0.5 A/V: 4 V → 2 A. The PWM outputs must run at 32 kHz, not the Arduino-standard 500-ish Hz.

    The MAX4544 SPDT analog multiplexers switch between ground and the PWM voltages. That’s a simple way to turn the outputs off and on without waiting for the PWM values to ramp up and down. The LEDs on those control signals provide an indication that the firmware hasn’t fallen off the rails.

    Three Arduino analog inputs report the drain voltage, actual drain current, and temperature input. The LM324 op amps run from ±12 V, so a pair of BAT54S dual diodes clamp the analog inputs at one Schottky diode drop below ground and above 5 V. That should be close enough to prevent any damage without rounding off the values near the extremes, given the fairly high op-amp output resistors; the analog inputs present a reasonably high impedance and it seems to not matter much.

    The measuring sequence amounts to a pair of nested loops:

    • Step the gate voltage
    • Step the drain current limit

    The inner loop ends when the current limit, the actual current, or the drain voltage exceeds the corresponding maximum value. The outer loop ends when the gate voltage exceeds its limit.

    A 100 ms delay after changing any analog output allows time for the voltages to settle before taking the next set of inputs.

    Each pass of the loop updates the PI loop controlling the thermal block temperature. That’s certainly sub-optimal, but works well enough for my simple needs.

    The Arduino source code for the measurement loop:

    void loop() {
    
        digitalWrite(PIN_HEARTBEAT,HIGH);           // show that we've arrived
    
    //--- Stabilize temperature
    
        Temperature = ReadTemperature();
        SetPeltier(Temperature,TSetpoint);
    
        if (abs(Temperature - TSetpoint) > T_ACCEPT) {
    
          Serial.print("# Exceed T limit: ");
          Serial.print(Temperature,1);
          Serial.print(" C ");
    
          while (abs(Temperature - TSetpoint) > T_DEADBAND) {
            Temperature = ReadTemperature();
            SetPeltier(Temperature,TSetpoint);
            TogglePin(PIN_HEARTBEAT);
            delay(SETTLING_TIME);
            Serial.print('.');
          }
          Serial.print(" Now at: ");
          Serial.print(Temperature,1);
          Serial.println(" C");
        }
    
    //--- Record current data point
    
        IDrainSense = GetIDrain();
        VDrainSense = GetVDrain();
    
        Serial.print(VGateSet,3);
        Serial.print('\t');
        Serial.print(VDrainSense,3);
        Serial.print('\t');
        Serial.print(IDrainSense,3);
        Serial.print('\t');
        Serial.print((IDrainSense == 0.0) ? 0.0 : (VDrainSense / IDrainSense),3);
        Serial.print('\t');
        Serial.print(Temperature,1);
        Serial.print('\t');
        Serial.print(millis() - StartTime);
        Serial.println();
    
    //--- Step to next point
    
        if ((IDrainLimit > MAX_DRAIN_CURRENT) ||        // beyond last current increment
            (IDrainSense > MAX_DRAIN_CURRENT) ||        // power supply current limit
            (VDrainSense > MAX_DRAIN_VOLTAGE)) {        // beyond linear voltage measurement
          IDrainLimit = 0.0;
          VGateSet += VGATE_STEP;
          if (VGateSet <= MAX_GATE_VOLTAGE) {
            PrintHeader();
          }
        }
        else {
          IDrainLimit += IDRAIN_STEP;
        }
    
        SetIDrain(IDrainLimit);
        SetVGate(VGateSet);
    
        TogglePin(PIN_HEARTBEAT);
        delay(SETTLING_TIME);                           // wait for settling
    
        if (VGateSet > MAX_GATE_VOLTAGE) {
          Serial.print("# Done! Elapsed: ");
          Serial.print((millis() - StartTime)/1000);
          Serial.println(" sec");
    
          SetIDrain(0.0);
          SetVGate(0.0);
          digitalWrite(PIN_DISABLE_IDRAIN,HIGH);
          digitalWrite(PIN_DISABLE_VGATE,HIGH);
          digitalWrite(PIN_ENABLE_HEAT,LOW);
          analogWrite(PIN_SET_IPELTIER,0);
    
          while (true) {
            TogglePin(PIN_HEARTBEAT);
            delay(25);
          }
        }
    
    }
    

    Everything is a compile-time option, which is certainly user-hostile. On the other paw, that allows me to get on with writing column instead of putzing around with the user interface… [grin]

  • Peltier PWM Temperature Control: Noise Blanking

    The MOSFET tester I’m building controls the MOSFET’s gate voltage and drain current, while measuring the drain voltage. That, however, puts the drain terminal at a relatively high-impedance node between two current sources: the limiter and the MOSFET-under-test. When they’re both set to nearly the same value, the drain terminal picks up a generous helping of 32 kHz noise from the 3 A PWM Peltier module current. When either current source is set much larger than the other, the higher one serves as a relatively low impedance path that reduces the pickup.

    I thought about grounding the thermal block, but that means adding an insulating washer under every MOSFET-under-test, which means an even greater thermal control problem. So the easiest solution is to just turn off the PWM during measurements:

    Peltier Noise - VDS - PWM Shutdown
    Peltier Noise – VDS – PWM Shutdown

    The lower trace (at 5 V/div, not 500 mV as shown) is a digital output marking the duration of the three analog reads: temperature, drain voltage, and drain current. The upper trace shows the absolute worst case for the noise, which looks rather awful.

    The Peltier PWM comes from Arduino digital output 10, which is lashed to hardware Timer 1. Turning off the PWM requires setting the corresponding clock prescaler to “no input”, then setting it back to select the appropriate clock input after the measurement.

    Just on general principles, I average three successive analog inputs, so the Arduino source code for the analog reads looks like this:

    #define TCCRxB                  0x01        // set prescaler to 1:1 for 32 kHz PWM
    #define NUM_T_SAMPLES    3
    
    float ReadAI(byte PinNum) {
    word RawAverage;
    
        digitalWrite(PIN_SYNC,HIGH);                // scope sync
        TCCR1B = 0x00;                              // turn off Peltier module PWM
    
        RawAverage = analogRead(PinNum);            // prime the averaging pump
    
        for (int i=2; i <= NUM_T_SAMPLES; i++) {
            RawAverage += (word)analogRead(PinNum);
        }
    
        TCCR1B = TCCRxB;                            // restart Peltier PWM
        digitalWrite(PIN_SYNC,LOW);
    
        RawAverage /= NUM_T_SAMPLES;
    
        return (float)RawAverage;
    }
    
  • 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…