My current place of employment makes a lot of large, dumb, orange LED boards. On occasion, old ones will come back from the field with bad pixels. Most of the time there is enough wrong with them that it would take too long to fix, and they get tossed.
Fortunately, I know where the trash bin is!
After filling my apartment with far more boards than I should have, I decided to finally do something with them.
What's wrong with them
Ninety-nine percent of the time the problems are constrained to the pixels. The LEDs are all through-hole, and automated through-hole assembly never works that well. Damaged dies and cold solder joints are common. Now stick the board in a enclosure outside for ten years, and you push these poor LEDs to their breaking point.
Fixing them up
To fix the boards, first I light them up. For the sake of professional responsibility, I wont explain that protocol here (sorry open-source hardware guys). But it's a digital protocol that my arduino can spit out.
I mark all of the pixels that are out, and shut off the board. Next, I go over the solder joints of all the offending LEDs with a soldering iron. Sometimes a cold solder joint finally gives up after a few years, and it's easily fixed by reflowing the joint.
Next I light it up again and remove the marks from any pixels that start working. The remaining pixels probably have a bad LED, so I shut it off again. The pixels are all quadruple-series strings, so a dead pixel could be caused by any one of four LEDs. I check each one with a multimeter in diode mode to find the culprit.
When I find a dead LED, I pull it out and replace it with a good LED from a sacrificial board.
Now the board should be fixed... for now. LEDs will continuously go out at random even still. They sometimes turn back on too; thermal expansion is weird. Every now and then I pull the boards down and go through the process again.
Now what?
I have several, large, bright, orange LED dot matrices that I can talk to with an arduino. So the obvious application is a giant audio spectrum analyzer. Similar projects been done a few times before, though often using a hardware fft such as the MSGEQ7. I decide to try a software implementation based around open music labs's Fourier library.
The design process
The open music labs library can run arbitrarily-sized FFT's (with proportionally massive RAM usage). It can also run the FFT's faster cousin, the Fast Hartley Transform (FHT). FHT's are faster because they only compute with real numbers. Real numbers are all I need, so I use the FHT library.
To read my desktop's audio stream, I first had to get windows to split my audio to my speakers and stereo output at the same time. This turned out to be, without exaggeration, the hardest part of this project. The solution deserves its own post, so I wont get into it here.
After I got the audio stream coming out on a 2.5mm jack, I made what was likely my last ever radioshack purchase for a receptacle. I biased the signal up 2.5V using a capacitor coupled to a voltage divider, then fed it into my arduino's audio pin. I also fed in an old rotary pot for dimming control (did I mention these boards are bright?).
For the analog sampling, the classic AnalogRead() was not going to cut it. "Arduino" and "Digital Signal Processing" don't often go together, so I had to splice in some bare-metal programming. Even so, back-of-the-napkin calculations showed my Nyquist was going to be in the range of 13kHz. That's pretty bad, even for audio, but I decided to try it anyways. I can try a faster processor later.
Code structure
The code follows a three-step process
Step 1: Sample the audio
I turn off interrupts, and run balls-to-the-wall analog sampling for 128 samples. This makes sure there is a consistent timing between each sample, which should hopefully improve my fidelity
Step 2: (every 10 iterations) read the brightness
I manually set the mux back to the pot, let is settle, and take a reading
Step 3: Send the data out
<Data transmission process redacted>
This process extracts every kHz of sample rate I can out of that poor Pro Mini, but it does mean I miss events that occur during the data shift process. Occasionally you notice a missed beat as a result. If only there were some kind of processor that could do both things in parallel...
Testing results
Having already worked with shifting data out to the sign, getting the FFT -> LED stream working was a breeze (the FHT library is pretty easy to use). Here's a 600Hz tone:
Ugh, talk about sideband leakage. My Nyquist is killing me here. Unfortunately there's nothing we can do about it now, so I test a few songs
Hmm, it's working, but It doesn't look quite right. After trying a few ideas, I realize that it needed a fade affect. I allow bins to rise in value instantaneously, but only allow them to decay slowly. Here's what is looks like now:
That's much better! Now I just have to add dimming and I can wra-
Adding dimming makes this show up, even with the sound off. At first I was annoyed, but then I realized it was one of the small things that make me love electronics. The dimming is achieved with a simple PWM signal. The default PWM frequency on Arduino is... 490Hz. And that line is right where I'd expect a 490Hz tone to go.
These panels all have synchronized dimming. So all of the LEDs turn on and off at the same time. Given that they are positively massive, it's no wonder this radiates enough energy for my audio cable to pick it up. I've dealt with interference at work, but this is definitely the first time I've seen an interference problem diagnose itself.
To "fix" the issue, I changed the PWM frequency to something well out of Audio frequencies. I'm sure I'll end up dealing with further interference problems at work. At least at home, I can chill to some audio-visual in peace.
Future work
I couldn't leave it there of course. It's pretty apparent that a cheap FPGA could solve the Nyquist and parallelization issues I had here. I've started replicating the project on a Mojo V3, so stay tuned for my FPGA-based solution!
Appendix
Normally I'd post the full code on my github, but with a full section edited out and people unable to buy the hardware, I'm just going to post some sections here.
Setup:
void setup() {
TIMSK0 = 0; // turn off timer0 for lower jitter
ADCSRA = 0xe7; // set the adc to free running mode
ADMUX = 0x45; // use adc5
DIDR0 = 0x20; // turn off the digital input for adc5
//move the PWM frequency out of the FHT's band, or the induced noise will actually appear on the display
setPwmFrequency(10, 1);
Main Loop:
void loop() {
//run the FHT
sampleInput();
//apply the FHT results to the existing bin data
for(int i=0;i<displayWidth;i++) {
//pull a bin from the fht results
int bin = min(1023,fht_lin_out[i]);
//compare the result to the current value
if(bin > displayedBinValue[i]) {//if the result is higher than the bin, set the bin to the result
displayedBinValue[i] = bin;
} else if(displayedBinValue[i] > DECAY_RATE) {//if the bin is higher, allow the bin to fade some
displayedBinValue[i] -= DECAY_RATE;
} else {
displayedBinValue[i] = 0;//only let the bin fall to 0
}
int value = ((uint16_t)displayedBinValue[i]*displayHeight)/1023;
for(int j=0;j<displayHeight;j++) {
if(j < value) {
pixelState[i][j] = HIGH;
} else {
pixelState[i][j] = LOW;
}
}
}
//read the dimming pot every 10 loops. only because this doesn't need to happen very frequently
iterationsSinceLastRead++;
if(iterationsSinceLastRead >= 10) {
iterationsSinceLastRead = 0;
//make a reading of the dimmer potentiometer
while(!(ADCSRA & 0x10));
ADCSRA = 0xf7;
ADMUX = 0x40;
delayMicroseconds(200);
byte m = ADCL; // fetch adc data
byte j = ADCH;
int k = (j << 8) | m; // form into an int
analogWrite(enablePin, k/4);
ADMUX = 0x45;
}
//send the bin data to the led's
sendLedData();
}
Sampling the ADC:
void sampleInput() {
for (int x=0; x<FHT_N; x++) {
while(!(ADCSRA & 0x10)); // wait for adc to be ready
ADCSRA = 0xf7; // restart adc
byte m = ADCL; // fetch adc data
byte j = ADCH;
int k = (j << 8) | m; // form into an int
k -= 0x0200; // form into a signed int
k <<= 6; // form into a 16b signed int
fht_input[x] = k; // put real data into bins
}
fht_window(); // window the data for better frequency response
fht_reorder(); // reorder the data before doing the fht
fht_run(); // process the data in the fht
fht_mag_lin();
}