Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
Quite some time ago, I picked up a nice monitor that turned out to be a debranded (all OEM labels removed or covered) HP w2408. It eventually became erratic, refusing to turn on or return from power-save mode, so I took it apart. All the caps looked good and seemed to have low ESR, except for the big one in the middle of the power supply board:
HP 2408 monitor power supply – HV cap
It’s 30 mm in diameter, with 10 mm lead spacing, and stands a squat 26 mm tall, ignoring a millimeter or two of bulge in its should-be-flat black cap:
HP 2408 monitor power supply – HV cap bulge
Having never seen one of that size before, I sent a note and picture to folks who sell re-capping kits for monitors, in the hope that they’ll have a lead.
// Random LED Dots - from noise source
// Ed Nisley - KE4ANU - September 2015
//----------
// Pin assignments
const byte PIN_HEARTBEAT = 8; // DO - heartbeat LED
const byte PIN_SYNC = A3; // DO - scope sync
const byte PIN_LATCH = 4; // DO - shift register latch clock
const byte PIN_DIMMING = 9; // AO - LED dimming control
// These are *hardware* SPI pins
const byte PIN_MOSI = 11; // DO - data to shift reg
const byte PIN_MISO = 12; // DI - data from shift reg - sampled noise input
const byte PIN_SCK = 13; // DO - shift clock to shift reg (also Arduino LED)
const byte PIN_SS = 10; // DO - -slave select (must be positive for SPI output)
//----------
// Constants
#define DISPLAY_MS 10000ul
//----------
// Globals
// Input noise bits can produce one of four possible conditions
// Use the von Neumann extractor, discarding 00 and 11 sequences
// https://en.wikipedia.org/wiki/Randomness_extractor#Von_Neumann_extractor
// Sampling interval depends on SPI data rate
// LSB arrives first, so it's the earliest sample
#define VNMASK_A 0x00000001
#define VNMASK_B 0x01000000
enum sample_t {VN_00,VN_01,VN_10,VN_11};
typedef struct {
byte BitCount; // number of bits accumulated so far
unsigned Bits; // random bits filled from low order upward
int Bias; // tallies 00 and 11 sequences to measure analog offset
unsigned SampleCount[4]; // number of samples in each bin
} random_t;
random_t RandomData;
// LED selects are high-active bits and low-active signals: flipped in UpdateLEDs()
// *exactly* one row select must be active in each element
typedef struct {
const byte Row;
byte ColR;
byte ColG;
byte ColB;
} leds_t;
// altering the number of rows & columns will require substantial code changes...
#define NUMROWS 8
#define NUMCOLS 8
leds_t LEDs[NUMROWS] = {
{0x80,0,0,0},
{0x40,0,0,0},
{0x20,0,0,0},
{0x10,0,0,0},
{0x08,0,0,0},
{0x04,0,0,0},
{0x02,0,0,0},
{0x01,0,0,0},
};
byte RowIndex;
#define LEDS_ON 0
#define LEDS_OFF 255
unsigned long MillisNow;
unsigned long DisplayBase;
//-- Helper routine for printf()
int s_putc(char c, FILE *t) {
Serial.write(c);
}
//-- Useful stuff
// Free RAM space monitor
// From http://playground.arduino.cc/Code/AvailableMemory
uint8_t * heapptr, * stackptr;
void check_mem() {
stackptr = (uint8_t *)malloc(4); // use stackptr temporarily
heapptr = stackptr; // save value of heap pointer
free(stackptr); // free up the memory again (sets stackptr to 0)
stackptr = (uint8_t *)(SP); // save value of stack pointer
}
void TogglePin(char bitpin) {
digitalWrite(bitpin,!digitalRead(bitpin)); // toggle the bit based on previous output
}
void PulsePin(char bitpin) {
TogglePin(bitpin);
TogglePin(bitpin);
}
//---------
//-- SPI utilities
void EnableSPI(void) {
digitalWrite(PIN_SS,HIGH); // make sure this is high!
SPCR |= 1 << SPE;
}
void DisableSPI(void) {
SPCR &= ~(1 << SPE);
}
void WaitSPIF(void) {
while (! (SPSR & (1 << SPIF))) {
// TogglePin(PIN_HEARTBEAT);
continue;
}
}
byte SendRecSPI(byte DataByte) { // send one byte, get another in exchange
SPDR = DataByte;
WaitSPIF();
return SPDR; // SPIF will be cleared
}
//---------------
// Update LED shift registers with new data
// Returns noise data shifted in through MISO bit
unsigned long UpdateLEDs(byte i) {
unsigned long NoiseData = 0ul;
NoiseData |= (unsigned long) SendRecSPI(~LEDs[i].ColB); // correct for low-active outputs
NoiseData |= ((unsigned long) SendRecSPI(~LEDs[i].ColG)) << 8;
NoiseData |= ((unsigned long) SendRecSPI(~LEDs[i].ColR)) << 16;
NoiseData |= ((unsigned long) SendRecSPI(~LEDs[i].Row)) << 24;
analogWrite(PIN_DIMMING,LEDS_OFF); // turn off LED to quench current
PulsePin(PIN_LATCH); // make new shift reg contents visible
analogWrite(PIN_DIMMING,LEDS_ON);
return NoiseData;
}
//---------------
// Extract random data from sampled noise input
// ... tuck it into the global bit structure
// Returns von Neumann status of the sample
byte ExtractRandomBit(unsigned long RawSample) {
byte RetVal;
switch (RawSample & (VNMASK_A | VNMASK_B)) {
case 0: // 00 - discard
RetVal = VN_00;
RandomData.Bias--;
break;
case VNMASK_A: // 10 - true
RetVal = VN_10;
RandomData.BitCount++;
RandomData.Bits = (RandomData.Bits << 1) | 1;
break;
case VNMASK_B: // 01 - false
RetVal = VN_01;
RandomData.BitCount++;
RandomData.Bits = RandomData.Bits << 1;
break;
case (VNMASK_A | VNMASK_B): // 11 - discard
RetVal = VN_11;
RandomData.Bias++;
break;
}
RandomData.Bias = constrain(RandomData.Bias,-9999,9999);
RandomData.SampleCount[RetVal]++;
RandomData.SampleCount[RetVal] = constrain(RandomData.SampleCount[RetVal],0,63999);
return RetVal;
}
//---------------
// Set LED from random bits
// Assumes the Value contains at least nine low-order random bits
// On average, this leaves the LED unchanged for 1/8 of the calls...
void SetLED(unsigned Value) {
byte Row = Value & 0x07;
byte Col = (Value >> 3) & 0x07;
byte Color = (Value >> 6) & 0x07;
byte BitMask = (0x80 >> Col);
// printf("%u %u %u %u\r\n",Row,Col,Color,BitMask);
LEDs[Row].ColR &= ~BitMask;
LEDs[Row].ColR |= (Color & 0x04) ? BitMask : 0;
LEDs[Row].ColG &= ~BitMask;
LEDs[Row].ColG |= (Color & 0x02) ? BitMask : 0;
LEDs[Row].ColB &= ~BitMask;
LEDs[Row].ColB |= (Color & 0x01) ? BitMask : 0;
}
//------------------
// Set things up
void setup() {
pinMode(PIN_HEARTBEAT,OUTPUT);
digitalWrite(PIN_HEARTBEAT,HIGH); // show we arrived
pinMode(PIN_SYNC,OUTPUT);
digitalWrite(PIN_SYNC,LOW);
pinMode(PIN_MOSI,OUTPUT); // SPI-as-output is not strictly necessary
digitalWrite(PIN_MOSI,LOW);
pinMode(PIN_SCK,OUTPUT);
digitalWrite(PIN_SCK,LOW);
pinMode(PIN_SS,OUTPUT);
digitalWrite(PIN_SS,HIGH); // OUTPUT + HIGH is required to make SPI output work
pinMode(PIN_LATCH,OUTPUT);
digitalWrite(PIN_LATCH,LOW);
Serial.begin(57600);
fdevopen(&s_putc,0); // set up serial output for printf()
printf("Noisy LED Dots\r\nEd Nisley - KE4ZNU - September 2015\r\n");
//-- Set up SPI hardware
// LSB of SPCR set bit clock speed:
// 00 = f/4
// 01 = f/16
// 10 = f/64
// 11 = f/128
SPCR = B01110011; // Auto SPI: no int, enable, LSB first, master, + edge, leading, speed
SPSR = B00000000; // not double data rate
EnableSPI(); // turn on the SPI hardware
SendRecSPI(0); // set valid data in shift registers: select Row 0, all LEDs off
//-- Dimming pin must use fast PWM to avoid beat flicker with LED refresh rate
// Timer 1: PWM 9 PWM 10
analogWrite(PIN_DIMMING,LEDS_OFF); // disable column drive (hardware pulled it low before startup)
TCCR1A = B10000001; // Mode 5 = fast 8-bit PWM with TOP=FF
TCCR1B = B00001001; // ... WGM, 1:1 clock scale -> 64 kHz
//-- lamp test: send a white flash through all LEDs
// collects noise data to get some randomness going
printf("Lamp test begins: white flash each LED...");
digitalWrite(PIN_HEARTBEAT,LOW); // turn off while panel blinks
analogWrite(PIN_DIMMING,LEDS_ON); // enable column drive
for (byte i=0; i<NUMROWS; i++) {
for (byte j=0; j<NUMCOLS; j++) {
LEDs[i].ColR = LEDs[i].ColG = LEDs[i].ColB = 0x80 >> j;
for (byte k=0; k<NUMROWS; k++) {
ExtractRandomBit(UpdateLEDs(k));
delay(25);
}
LEDs[i].ColR = LEDs[i].ColG = LEDs[i].ColB = 0;
}
}
UpdateLEDs(NUMROWS-1); // clear the last LED
printf(" done!\r\n");
//-- Preload LEDs with random values
// We take whatever number of random bits arrived in RandomData during lamp test
digitalWrite(PIN_HEARTBEAT,LOW);
printf("Preloading LED array\r\nRandom bits %04x\r\n",RandomData.Bits);
randomSeed(RandomData.Bits);
for (byte Row=0; Row<NUMROWS; Row++) {
for (byte Col=0; Col<NUMCOLS; Col++) { // Col runs backwards, but we don't care
LEDs[Row].ColR |= random(2) << Col;
LEDs[Row].ColG |= random(2) << Col;
LEDs[Row].ColB |= random(2) << Col;
}
UpdateLEDs(Row);
}
RandomData.BitCount = 0;
RandomData.Bits = 0;
RandomData.Bias = 0;
for (byte i=0; i<4; i++) {
RandomData.SampleCount[i] = 0;
}
check_mem();
printf("SP: %u HP: %u Free RAM: %u\r\n",stackptr,heapptr,stackptr - heapptr);
printf("Running...\r\n");
DisplayBase = millis();
}
//------------------
// Run the test loop
void loop() {
byte ThisBit;
MillisNow = millis();
if (RowIndex >= NUMROWS) { // set up LED row index for this pass
RowIndex = 0;
PulsePin(PIN_SYNC);
}
if ((MillisNow - DisplayBase) >= DISPLAY_MS) {
analogWrite(PIN_DIMMING,LEDS_OFF); // turn off LED to prevent bright glitch
printf("Bias: %5d of %5u - %5u %5u %5u %5u\r\n",
RandomData.Bias,
RandomData.SampleCount[VN_00] + RandomData.SampleCount[VN_11],
RandomData.SampleCount[0],
RandomData.SampleCount[1],
RandomData.SampleCount[2],
RandomData.SampleCount[3]
);
RandomData.Bias = 0;
for (byte i=0; i<4; i++) {
RandomData.SampleCount[i] = 0;
}
// check_mem();
// printf("SP: %u HP: %u Free RAM: %u\r\n",stackptr,heapptr,stackptr - heapptr);
DisplayBase = MillisNow;
}
// Update one LED row per pass, get at most one random bit
ThisBit = ExtractRandomBit(UpdateLEDs(RowIndex++));
// Update the heartbeat LED to show bit validity
switch (ThisBit) {
case VN_00:
case VN_11:
digitalWrite(PIN_HEARTBEAT,HIGH);
break;
case VN_01:
case VN_10:
digitalWrite(PIN_HEARTBEAT,LOW);
break;
}
// If we have enough random data, twiddle one LED
if (RandomData.BitCount >= 9) {
// analogWrite(PIN_DIMMING,LEDS_OFF); // turn off LED array to prevent bright glitch
SetLED(RandomData.Bits);
RandomData.BitCount = 0;
RandomData.Bits = 0;
}
digitalWrite(PIN_HEARTBEAT,LOW);
}
The relatively low bandwidth of the amplified noise means two successive samples (measured in Arduino time) will be highly correlated. Rather than putz around with variable delays between the samples, I stuffed the noise directly into the Arduino’s MISO pin and collected four bytes of data while displaying a single row:
unsigned long UpdateLEDs(byte i) {
unsigned long NoiseData = 0ul;
NoiseData |= (unsigned long) SendRecSPI(~LEDs[i].ColB); // correct for low-active outputs
NoiseData |= ((unsigned long) SendRecSPI(~LEDs[i].ColG)) << 8;
NoiseData |= ((unsigned long) SendRecSPI(~LEDs[i].ColR)) << 16;
NoiseData |= ((unsigned long) SendRecSPI(~LEDs[i].Row)) << 24;
analogWrite(PIN_DIMMING,LEDS_OFF); // turn off LED to quench current
PulsePin(PIN_LATCH); // make new shift reg contents visible
analogWrite(PIN_DIMMING,LEDS_ON);
return NoiseData;
}
The bit timing looks like this:
SPI Sample – noise data – 01
The vertical cursors mark the LSB position in the first and last bytes of the SPI clock. The horizontal cursors mark the minimum VIH and maximum VIL, so the sampled noise should produce 0 and 1 bits at the vertical cursors. Note that there’s no shift register on the input: MISO just samples the noise signal at each rising clock edge.
I picked those two bit positions because they produce more-or-less equally spaced samples during successive rows; you can obviously tune the second bit position for best picture as you see fit.
Given a pair of sequential samples, a von Neumann extractor whitens the noise and returns at most one random bit:
#define VNMASK_A 0x00000001
#define VNMASK_B 0x01000000
enum sample_t {VN_00,VN_01,VN_10,VN_11};
typedef struct {
byte BitCount; // number of bits accumulated so far
unsigned Bits; // random bits filled from low order upward
int Bias; // tallies 00 and 11 sequences to measure analog offset
unsigned SampleCount[4]; // number of samples in each bin
} random_t;
random_t RandomData;
... snippage ...
byte ExtractRandomBit(unsigned long RawSample) {
byte RetVal;
switch (RawSample & (VNMASK_A | VNMASK_B)) {
case 0: // 00 - discard
RetVal = VN_00;
RandomData.Bias--;
break;
case VNMASK_A: // 10 - true
RetVal = VN_10;
RandomData.BitCount++;
RandomData.Bits = (RandomData.Bits << 1) | 1;
break;
case VNMASK_B: // 01 - false
RetVal = VN_01;
RandomData.BitCount++;
RandomData.Bits = RandomData.Bits << 1;
break;
case (VNMASK_A | VNMASK_B): // 11 - discard
RetVal = VN_11;
RandomData.Bias++;
break;
}
RandomData.Bias = constrain(RandomData.Bias,-9999,9999);
RandomData.SampleCount[RetVal]++;
RandomData.SampleCount[RetVal] = constrain(RandomData.SampleCount[RetVal],0,63999);
return RetVal;
}
The counters at the bottom track some useful statistics.
You could certainly use something more complex, along the lines of a hash function or CRC equation, to whiten the noise, although that would eat more bits every time.
The main loop blips a pin to show when the extractor discards a pair of identical bits, as in this 11 sequence:
SPI Sample – noise flag – 11
That should happen about half the time, which it pretty much does:
SPI Sample – noise flag – 1 refresh
The cursors mark the same voltages and times; note the slower sweep. The middle trace blips at the start of the Row 0 refresh.
On the average, the main loop collects half a random bit during each row refresh and four random bits during each complete array refresh. Eventually, RandomData.Bits will contain nine random bits, whereupon the main loop updates the color of a single LED, which, as before, won’t change 1/8 of the time.
The Arduino trundles around the main loop every 330 µs and refreshes the entire display every 2.6 ms = 375 Hz. Collecting nine random bits requires 9 x 330 µs = 3 ms, so the array changes slightly less often than every refresh. It won’t completely change every 64 x 8/7 x 3 ms = 220 ms, because dupes, but it’s way sparkly.
The noise spectrum at the collector of the NPN transistor looks dead flat:
Noise spectrum – 2N3904 collector
In fact, it’s down 3 dB at 4 MHz, 10 dB at 10 MHz, and has some pizzazz out through 50 MHz.
The cursor marks the second harmonic of the 125 kHz SPI clock that’s shoving bits into the shift registers that drive the LED display. In principle, you can’t get 250 kHz from a 125 kHz square wave, so we’re looking at various & sundry logic glitches that do have some energy there.
The big peak at 0 Hz comes from the LO punching through the IF filters; I should have set the start frequency to 9 kHz (the HP 8591 spectrum analyzer’s lower cutoff frequency) to let the filter get some traction.
With that setup, the first LM324 with a gain of 10 produces this dismal result:
Op Amp 1 10x gain spectrum – 9-209 kHz
Past an op amp’s -3 dB cutoff, the response drops at 10 dB/decade for a while. Squinting at that curve, it’s down 10 dB at 40 kHz and the cutoff looks to be around 4 kHz… not the 100 kHz you’d expect from the GBW/gain number.
[Edit: You’d expect a 6 dB/octave = 20 dB/decade drop from a single-pole rolloff. That’s obviously not what’s happening here.]
The LM324 has a large-signal slew rate of about 0.5 V/µs that pretty well hobbles its ability to follow full-scale random noise components.
Both traces come from a kludged 10 dB AC coupled “attenuator” probe: a 430 Ω resistor in series with a 1 µF Mylar cap, jammed into a clip-lead splitter on the BNC cable to the analyzer. Probably not very flat, but certainly good enough for this purpose.
The SA is averaging 100 (which was excessive) and 10 (more practical) successive traces in those pix, which gives the average of the maximum value for each frequency bin. That reduces the usual hash you get from a full-frontal noise source to something more meaningful.
The LM317 tweaks the reverse bias voltage to ram about 10 µA of DC current through the 2N2907A base-emitter junction; I picked that transistor because it has a 5.0 V breakdown spec that translates into about +8 V in reality, plus I have a few hundred lying around.
The 4.7 kΩ collector resistor sets the operating point of the 2N3904 NPN transistor at a bit over 6 V with a collector current of 1.6 mA, around which there appears ±500 mV of pure noise:
Noise – NPN C inv – op amp
The 2N3904 spec says hFE > 100 and, indeed, I measure 200: 8 µA in, 1.6 mA out.
The bottom trace is the output of the first LM324. With a GBW of 1 MHz and a voltage gain of 10, it should have a cutoff at 100 kHz, but that’s obviously not true; the 0.5 V/µs large-signal slew rate just kills the response. The flat tops at 3.8 V show that it’s definitely not a rail-to-rail op amp.
The horizontal cursors mark the ATmega 328 minimum VIH and maximum VIL; the actual thresholds lie somewhere between those limits. What you don’t get is the crisp transition from a comparator.
The trimpot setting the logic level lets you tune for best picture; in this case, that means balancing the number of 0 and 1 bits. It’s a bit more complex, of course, about which, more later.
I picked the LM324 for pedagogic reasons; it’s a Circuit Cellar column and I get to explain how a nominally good analog circuit can unexpectedly ruin the results, unless you know what to look for…
I duct-taped a pair of D cells onto the case and returned it to the bedroom shelf. According to the date scrawled on the tape, that was five years ago: 26 November 2010.
Over the last few months, the LED gradually faded from a blink to a steady glow as the battery voltage dropped below 2 V and the WWVB receiver output no longer reached the MOSFET’s threshold.
We’ll see how long these last:
Alpha Geek Clock – new batteries
Yeah, I should probably do something involving 3D printing…