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
The default Grub2 video mode for Ubuntu 12.04 is 640×480, which looks rather overwhelming on a 24 inch monitor that can do 1920×1600. I’m also a fan of Old Skool scrolling text, because when something goes wrong it’s handy to get an actual hint in real time.
Thus, some Grub tweakage was in order:
GRUB_DEFAULT=saved
GRUB_SAVEDEFAULT=true
#GRUB_HIDDEN_TIMEOUT=0
GRUB_HIDDEN_TIMEOUT_QUIET=true
GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
#GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
GRUB_CMDLINE_LINUX=""
... snippage ...
# Uncomment to disable graphical terminal (grub-pc only)
#GRUB_TERMINAL=console
# The resolution used on graphical terminal
# note that you can use only modes which your graphic card supports via VBE
# you can see them in real GRUB with the command `vbeinfo'
#GRUB_GFXMODE=640x480
#GRUB_GFXMODE=1600x1200
GRUB_GFXMODE=1280x1024
AFAICT, it’s impossible to:
Page(*) the output of the Grub vbeinfo command. A blind stab at 1600×1200 turned out to be too small and painfully slow at scrolling in graphics mode.
Boot a text-mode console in anything other than 80 characters x 24 rows, so un-commenting the GRUB_TERMINAL line isn’t helpful.
This being Grub2, you must do this special dance (anybody remember when one of Grub’s advantages over Lilo was that it didn’t require a special dance?) to make it work:
sudo nano /etc/default/grub
... make changes & save ...
sudo update-grub
sudo reboot ; exit
That’s from an SSH session across the room, of course…
Incidentally, shutting off the graphic drivel immediately revealed that those NFS mounts weren’t happening because statd wasn’t running. Knowing that immediately would have saved some diagnostic time, yes, it would.
(*) The Official Grub2 doc suggests set pager=1, but there’s no way to discover that using help set at the Grub2 command line. Now we both know and maybe we’ll remember it for the next time.
Back in the old days, the Unix startup sequence was rigidly fixed. For a variety of reasons, that’s no longer the case; Ubuntu (and, presumably, other distros) now use upstart, which turns the startup sequence into a lightly documentedPachinko machine. This parallel processing presumably works great for most of Ubuntu’s use cases and falls flat on its face for me: I’m apparently the only person who expects NFS mounts to be in place before signing in.
Well, maybe otherfolks expect that, but the entire startup mechanism is apparently broken as designed.
The only solution seems to be stalling the user sign-on screen by jamming the display manager until the NFS client hauls itself to its feet. This takes up to a minute, for reasons I do not understand, but it’s better to let it run to completion rather than signing on and expecting one’s files to be in the right places. Email clients, in particular, have difficulty coping with missing files.
The fix involves adding a line to /etc/init/lightdm.conf, as mentioned there (albeit with incorrect syntax):
start on ((filesystem
and runlevel [!06]
and started dbus
and (drm-device-added card0 PRIMARY_DEVICE_FOR_DISPLAY=1
or stopped udev-fallback-graphics)
and mounted MOUNTPOINT=/mnt/bulkdata)
or runlevel PREVLEVEL=S)
I tried to check for another filesystem that should also be mounted, but, as I understand neither the syntax nor the semantics of the language, what you see is what finally worked. As it turns out, upstart's syntax error messages aren’t particularly helpful; a single line (helpfully relating, perhaps, that the parser expected a token on line 16) appears on VT 7, but if you don’t know to switch from VT 1, you’ll never get even that minimal assistance. No, such errors don’t appear in the /var/log/upstart/* logs.
For unknown reasons, waiting for the remote-filesystems event didn’t delay the startup at all. Evidently, mountall emits that event almost immediately, long before the NFS mounts happen. Perhaps the event occurs even when the mount fails, contrary to what the doc suggests?
Most of the debugging occurred through an ssh session across the room. Edit the file, try a new version, reboot, watch for the filesystems to come up, watch for the sign-in screen to appear. Or not, as the case may be.
Grumpy though I may seem, the great thing about Open Source / Free Software is that when it breaks, you have access to all the pieces and can actually fix the problem. That makes up for nearly everything, I’d say.
No, I didn’t update any of those bug reports or start another one. It’s obvious this isn’t getting any attention, so what’s the point? If you’re also having the problem, you’ll eventually wind up here…
FWIW, I knew the NFS mounts weren’t working because I always set the screen background to an image on the file server: no mount = no picture = fix-the-problem-now. This image seemed appropriate:
So an email made its way through all the spam filtering:
From: USPS Service <us@usps.com>
Reply-To: USPS Service <us@usps.com>
To: (me)
Subject: Failure to deliver
Notification,
Your parcel can’t be delivered by courier service.
Status:The size of parcel is exceeded.
LOCATION OF YOUR ITEM:Riverside
STATUS OF YOUR ITEM: not delivered
SERVICE: One-day Shipping
:U954571533NU
INSURANCE: Yes
Label is enclosed to the letter.
Print a label and show it at your post office.
Information in brief:
If the parcel isn’t received within 30 working days our company will have the right to claim compensation from you for it’s keeping in the amount of $12.70 for each day of keeping of it.
You can find the information about the procedure and conditions of parcels keeping in the nearest office.
Thank you for your attention.
USPS Customer.
It had, of course, an attachment: Zip archive attachment (Label_Parcel_USPS_ID.45-123-14.zip)
Not having sent a package using “one-day shipping” (which the USPS would call Express Mail), this seemed odd, as did the somewhat stilted phrasing.
We all know how this is going to work out, but let’s do the exercise anyway.
Save the ZIP attachment in /tmp, then …
Apply ClamAV: run freshclam to update the virus signatures and fire clamscan at the ZIP file:
/tmp/Label_Parcel_USPS_ID.45-123-14.zip: OK
----------- SCAN SUMMARY -----------
Known viruses: 1201128
Engine version: 0.97.3
Scanned directories: 0
Scanned files: 1
Infected files: 0
Data scanned: 0.04 MB
Data read: 0.02 MB (ratio 2.00:1)
Time: 7.549 sec (0 m 7 s)
Huh. Well, then, it must be safe, right? (The alert reader will note that my version of clamav is one click back from the latest & greatest. Maybe that would make a difference. Probably not.)
Obviously, this blob of slime arrived still warm from the oven: even though the Big Name AV checkers have up-to-date signatures, they detect nothing wrong and would happily let me run a Trojan installer. That’s what malware protection buys you these days.
To a good first approximation, whatever virus scanner you’re using won’t save your bacon, either; the advice to keep the signatures up-to-date is necessary, but not sufficient. Of course, you know enough to not autorun random files on your Windows box, but this attack works often enough to justify sending messages to everybody in the world. Repeatedly.
I recently had a discussion with someone who wanted a system secured against email and web malware. She also insisted that it had to run Windows and share files with other Windows machines. I declined to bid on the job…
The MOSFET tester spits out datasets using this tedious Arduino code:
void PrintHeader(void) {
Serial.println(); // Gnuplot group break
Serial.println("#-----------------------------");
Serial.print("# VGate: ");
Serial.print(VGateSet,3);
Serial.println();
Serial.print("# TSetpoint: ");
Serial.print(TSetpoint,1);
Serial.println(" C");
Serial.println("# VGS \tVDS \tID \tRDS \tC \tTime");
}
void PrintTempHeader() {
Serial.println(); // Gnuplot index break
Serial.println();
Serial.print("#T="); // ... index name
Serial.println(TSetpoint,1);
Serial.println("#=============================");
Serial.print("# Setting temperature to: "); // human-readable annotation
Serial.print(TSetpoint,1);
Serial.println(" C ...");
}
... later, deep inside the main loop ...
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();
All that produces a text file formatted to work with Gnuplot, including a blank line between successive gate voltage groups to produce separate plot traces:
#T=0.0
#=============================
# Setting temperature to: 0.0 C ...
#-----------------------------
# VGate: 4.250
# TSetpoint: 0.0 C
# VGS VDS ID RDS C Time
4.250 1.200 0.000 0.000 1.0 1757
4.250 1.665 0.044 37.851 1.0 1861
#-----------------------------
# VGate: 4.500
# TSetpoint: 0.0 C
# VGS VDS ID RDS C Time
4.500 0.003 0.000 0.000 1.0 2038
4.500 0.016 0.044 0.370 1.0 2143
... snippage ...
4.500 0.212 1.953 0.108 0.9 6105
4.500 0.216 2.001 0.108 0.9 6210
Which produces a plot like this:
IRFZ44
It’d be handy to automatically generate labels for the gate voltages, but I haven’t been able to figure out how to read values from the dataset and plunk them into the label strings. You can, however, select blocks of gate voltage and superblocks of temperature with a bit of effort.
The Bash script that feeds Gnuplot looks something like this:
#!/bin/sh
#-- set plot limits
tx=3
vgs_min="4.0"
vds_max="0.2"
rds_max=100
rds_tics=$((${rds_max} / 4))
id_max="2.0"
#-- overhead
export GDFONTPATH="/usr/share/fonts/truetype/"
base="${1%.*}"
echo Base name: ${base}
ofile=${base}.png
echo Output file: ${ofile}
#-- do it
gnuplot << EOF
#set term x11
set term png font "arialbd.ttf" 18 size 950,600
set output "${ofile}"
set title "${base}"
set key noautotitles
unset mouse
set bmargin 4
set grid xtics ytics
set xlabel "Drain-Source Voltage - VDS - V"
set format x "%4.2f"
set xrange [0:${vds_max}]
#set xtics 0,5
set mxtics 2
set ytics nomirror autofreq
set ylabel "Drain Current - ID - A"
set format y "%4.1f"
set yrange [0:${id_max}]
#set mytics 2
set y2label "Drain Resistance - RDS - mohm"
set y2tics nomirror autofreq ${rds_tics}
set format y2 "%3.0f"
set y2range [0:${rds_max}]
#set y2tics 32
#set rmargin 9
set datafile separator "\t"
#set label 1 "Temp index = ${tx}" at 0.81,0.55 font "arialbd,18"
set label 2 "VGS >= ${vgs_min} V" at 0.11,0.55 font "arialbd,18"
plot \
"$1" using 2:((\$1 >= ${vgs_min})?\$3:NaN) index $tx:$tx with lines lt 3 lw 2 title "ID" ,\
"" using 2:((\$1 >= ${vgs_min})?(\$4*1000):NaN) index $tx:$tx axes x1y2 with lines lt 4 lw 2 title "RDS"
EOF
The variables up near the top control the plot limits; it’d be nice to have a complex Bash script that prompted for values, had useful defaults, and fed all that into Gnuplot. Given what I’m doing, it’s easier to just keep the Bash script open in the portrait monitor, watch the results on the landscape monitor, and twiddle until it looks right.
This script produces a plot for a single temperature range based on the superblock index tx; you can select a single block using index name (along the lines of “T=0.0”), but you can’t select multiple such blocks in a single plot statement.
Selecting gate voltages requires testing the first column for a match with the trinary operator and assigning the data value for lines that don’t match to the not-a-number value NaN to prevent it from appearing in the plot:
((\$1 >= ${vgs_min})?\$3:NaN)
All in all, the whole apparat makes for a fairly brittle set of code, but the plots come out ready for printing and that makes up for a lot.
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
Rescaling the graph to show just the interesting part down near the origin:
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
The key part of the schematic:
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.
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]
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
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;
}
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!