I'm not going to go into an extensive discussion of the microcontroller's firmware as that goes way beyond the scope of this project (especially on the USB side). However there are some aspects of the firmware which are relevant if someone were to try and duplicate this effort on a different type of micro.
The first topic concerns the USB communication. For every LED I wanted to have 16 brightness settings (achieved via the PWM signal discussed below). Since there are 106 LEDs and each one needs a number 0 to 15, that implies a 4-bit piece of information per LED. By bit packing the information into bytes, I can get two LEDs into one byte. Now even though there are 106 LEDs, a total of 14 8-bit shift registers are used to control them, which is 112 signals total. To keep things simple I decided to send all 112 even though only 106 are used. So, 112 at 2 per byte means 56 bytes of information need to be sent.
How does this relate to the USB? Well it turns out many micros, including the JB8, only support the lowest speed of USB communication. The JB8 is setup to run in what's called "interrupt" mode. This means the part is polled at given intervals to communicate back and forth. Unfortunately it cannot transfer the whole update at once, in this mode it can only move 8-bytes per interval. That means to move the 56-bytes plus a 1-byte header it's going to take 8 communication intervals.
The rate of these intervals can be controlled, and the fastest it can be set to is a 1ms period. This means at minimum the communication will take 8-10ms. Including host-side and device-side latencies that may stretch into a longer period. When these USB interrupts occur on the device-side the PWM sequence gets interrupted, possibly causing the LEDs to visibly blink. By keeping the transfer as short as possible, the hope is that this effect will be minimized (this is where a faster micro would be nice).
The next topic concerns the PWM (pulse-width-modulation) scheme used to control LED brightness. This part of the design wasn't actually in my original concept. At first my plan was to simply turn LEDs either on or off. However as I worked on the firmware it occurred to me that if I only loaded static values into the shift registers the micro would be spending most of its time idling. So instead I decided to go the other way and see if I could run continuous updates on the shift register chain and put out a PWM signal to the LEDs.
Using PWM signals to drive LEDs is a pretty common technique. The idea is simple, instead of just turning something on or off you turn it on and off at a high rate. In essence its blinking at a very high speed. The brightness of the LED will be proportional to the duty cycle of the signal. So for instance the if the LED is on for 25% of the time and off for 75% it will appear dimmer than one which is on for 50% and off for 50%.
It turns out the JB8 has some interesting characteristics on the programming side. It has a very limited amount of RAM, and relative to the RAM an abundant amount of flash ROM (where the program is stored). As such it makes sense to use things such as lookup tables because they consume the flash and not the RAM. In addition, precalculated lookup tables increase the running speed as the micro needs to do less real-time calculations. Now looking at the circuit below one thing to notice is that the signals to drive the top-side and bottom-side LEDs will be inverted. For instance to turn off a top-side LED the shift register needs to pull its signal high, whereas a bottom-side LED would need to have its signal pulled low.
To keep things simple and improve the LED array refresh rate, the drive signals from the micro were split. One output pin is used to drive the top-side shift registers while a different pin is used to drive the bottom-side registers. In this way it is a simple matter to know if turning on a LED requires a high or low signal. It helps the refresh rate because the shift registers are loaded twice as fast. Dividing the array into 4 parallel paths (two top, two bottom) may even be faster but I didn't try that.
What does any of this have to do with the PWM? To get the PWM working I setup a counter which gave me a cycling index. The index counts from 0 up to 15 and then resets back to zero. After every time the register chain is updated the index is incremented. The index along with the brightness setting of an individual LED is used to look into a 2-dimensional table which indicates if the LED should be turned on or off. This could be calculated in real-time, but it's much faster, and for the JB8 more efficient, if lookup tables are used. There are two such tables, one for the top-side registers and one for the bottom-side registers. These tables accomplish the dual task of handling the proper logic levels and the PWM timing. The table layout is like this, where ON represents whatever logic level (low/high for the top/bottom respectively) would be needed, and blank spots represent OFF:
| Brightness | PWM Index | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | |
| 0 | ||||||||||||||||
| 1 | ON | ON | ||||||||||||||
| 2 | ON | ON | ON | |||||||||||||
| 3 | ON | ON | ON | ON | ||||||||||||
| 4 | ON | ON | ON | ON | ON | |||||||||||
| 5 | ON | ON | ON | ON | ON | ON | ||||||||||
| 6 | ON | ON | ON | ON | ON | ON | ON | |||||||||
| 7 | ON | ON | ON | ON | ON | ON | ON | ON | ||||||||
| 8 | ON | ON | ON | ON | ON | ON | ON | ON | ON | |||||||
| 9 | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ||||||
| 10 | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | |||||
| 11 | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ||||
| 12 | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | |||
| 13 | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ||
| 14 | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | |
| 15 | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON | ON |
As shown by the table, as the PWM index cycles the LEDs get turned on/off. Depending on the brightness setting it could be anywhere from always ON to always OFF. Originally I setup the cycling on a fixed timer interrupt running at 60Hz, but that was too slow and it produced very visible blinking. I didn't want to use an interrupt driven timer at too high a rate as it would conflict with the USB interrupt, so instead of that I decided to simply run the update in the main loop, where the LEDs would refresh at the max rate possible. Naturally the USB communication will interrupt this cycling, and cause certain pulses to get stretched for the duration of the interrupt, but keeping the duration short as explained above it's not noticeable. That seemed to work pretty well. On occasion when using very dim brightness levels if you look at the keys from about an inch away you can see some flickering as the USB interrupts hit, but for the most part it can't be seen. Out of curiosity I used an oscilloscope to check the refresh rate of the array (the rate at which all the LEDs get updated) and it was somewhere in the range of 450Hz. So the micro is putting out 112 PWM signals at 450Hz all while handling the USB interface, not too bad IMO.
Enough theory, next up is the hardware, starting with the PCB build.