For a long time I thought it would be nice to have a customizable colour light, but I didn't get around to building it. This year, inspired by the MSP430 LaunchPad and my purchase of a TLC5940 with SparkFun Free Day funds I finally built it.
For the LED, I chose the 10W 500-Lumen Multi-Color RGB LED Emitter Metal Plate (140 degree) from DealExtreme. It's a bright LED at a good price. Similar LEDs are available from multiple Chinese sites. I chose DealExtreme because I like the way their site is organized, they have a good reputation and good prices.
Initially, I was disappointed with the TLC5940. Yes, it is a "16 channel PWM unit with 12 bit duty cycle control", as described on SparkFun. However, it requires an external clock and PWM cycle start signal, with some very specific timings when loading new PWM data and starting a new cycle. Generating all that precisely would require a lot of resources from the microcontroller.
My first circuit clocked the TLC5940 using a 555 timer, and restarted the PWM cycle using a simple R-C network. The PWM data was bit-banged from my computer, and not in any way synchronized with the PWM cycle. This worked, with the main disadvantage being that the method was slow and not suited to colour changing effects.
I spent quite a bit of time wondering how to satisfy the timings in the datasheet while having low CPU utilization and fast PWM speed. My first design used a 7474 dual D-type flip-flop. A flip-flop in toggle configuration generated SMCLK/2, and I used the CLR input to extend the GSCLK cycle at the end of the PWM cycle.
This worked well, but I ended up choosing a simpler design, connecting SMCLK to GSCLK without glue logic. BLANK was generated from TACCR0, with the timer in up mode, and output mode 3. This creates a 1 cycle pulse, using only one compare register. This does not guarantee the proper timing between the rising edges of GSCLK and BLANK, but it works perfectly. I feel it's okay because this is just a personal project, and because that GSCLK edge is after the 4096th edge which ends the PWM cycle.
For XLAT, the nicest solution would be to use the multiple TA0.0 outputs, and enable XLAT via P1SEL. However, I chose to use USI in SPI mode to load PWM data more quickly, and so the other TA0.0 output wasn't available. I instead connected another pin to XLAT, with a 1 kΩ resistor between BLANK and XLAT. When the XLAT pin outputs a low, XLAT is inhibited, and when it is an input, XLAT is pulsed when BLANK is pulsed. It's unfortunate that even the 20-pin MSP430 Value Line chips cram most special functions into the 14-pin footprint.
I wanted the light to have both computer control and a user interface. I chose a serial port for computer control. A capture/compare register can be used to build a nice software UART which is not affected by interrupt jitter. I based my code on msp430g2xx1_ta_uart9600.c from the TI sample code. I wasn't too happy with the DCO tolerances however. They are sufficient for serial communication if the other side is precise, but I wanted something that would use up well under half the error budget. (Maxim AN2141 (PDF) provides a nice explanation on the subject.) The MSP430 Value Line chips don't support high frequency crystals, and according to the datasheet, they can't even accept a high frequency external clock input. It is possible to provide a high frequency external clock, but I didn't want to rely on this undocumented feature, so I used a watch crystal. The crystal triggers the watchdog timer interrupt 4 times a second, and code calculates the length of one bit in SMCLK cycles, based on the crystal. To avoid PWM jitter, I don't actually change DCO settings like an FLL, and I set up the DCO without modulation. I chose a high frequency, just under 16 MHz, so the PWM rate is high and calculations are finished quickly. Serial communication is at 9600 baud, which allows colour changes up to about 192 times a second, with the three 12 bit values packed into 5 bytes.
With all the pins needed, it became difficult to use a 14-pin chip. Some tricks and compromises could have allowed it, but I didn't really like those ideas. I got an MSP430G2252 in a 20-pin package.
For the user interface, I used two switches and three potentiometers. One switch selects between off, serial control and local control, while the other selects between RGB, effects and HSV during local control. I had thoughts of using the comparator to measure pots, but I went for the easy solution, using the ADC10. I was disappointed at the noise, even with proper bypass capacitors. To mitigate the issue, code performs smoothing. Rotary encoders would be a better alternative, but I have plenty of pots, and quadrature encoders would need more pins. With a rotary encoder, it would be possible to avoid colour changes when changing between RGB and HSV input modes, and instead just allow further tweaking in the new mode.
The key software component of the local user interface is a multiplication routine, which multiplies two 16-bit values as fixed point numbers between 0 and 1. That same routine is used for gamma correction, HSV to RGB conversion and fading. For gamma correction, values are simply squared. A power of 2.2 might be more accurate, but squaring is close enough. HSV to RGB conversion is done via a highly optimized assembler routine, partly just because I had fun writing it. Fading via Bresenham's line algorithm would have been more efficient, but the multiplication based version was fast enough, and code size matters when only 2 KB of flash is available.
After all that was done, one pin and some flash space remained. I used the pin as a serial output, so the computer could read the current colour and potentiometer positions. Due to the special purpose pins being crammed into the 14 pin footprint, I wasn't able to directly output from TA0.2, and so the output is done from the interrupt handler. It's not ideal, but it works. I consider it to be a bonus feature.
The gamma correction in the local interface is necessary, but it created a problem. Fading requires linearly changing the value before gamma correction, but the original serial interface only allowed setting the raw PWM value, which is the result of gamma correction. For proper fading, the code would require the corresponding value before gamma correction. The serial output can help here, by allowing the current setting to be read and initial fading to be done on the computer. The serial input also allows input of values before gamma correction. For a proper fade when switching away from serial mode, code can either use that all the time or just use it once before quitting.
Originally, I had various ideas for colour changing effects. Due to the limited code size, I ended up only implementing hue spinning, with the ability to set brightness, saturation and speed. The speed selection allows a wide range, from rotations taking several minutes to such rapid rotation that the light seems steady but fast movement leaves coloured trails.
The circuit is relatively uninteresting. Mostly, it's a matter of point to point connections between chips. I used an LM317 to supply power to the MSP430 and TLC5940. At the inputs, single transistor inverters perform level shifting and provide some protection for the MSP430.
The biggest difficulty with the circuit was the TLC5940's power dissipation. With a 10W LED and 2456mW maximum power dissipation, I had to be careful to avoid overheating the chip. The light requires a 12V regulated wall wart, and I added resistors to limit the voltage drop at the chip to about 1.4V. I also mounted the chip on the underside of the circuit board and connected it to the metal bottom panel using thermally conductive putty.
According to calculations, the chip isn't close to its limit, but it's nice to have a big safety margin. The microcontroller also monitors the XERR pin via an interrupt and turns off the light if the TLC5940 overheats. I'm satisfied with the power losses due to the linear current regulation. However, considering the power losses and resultant heat dissipation, if driving a higher power LED or array I would choose three switch-mode LED drivers instead of the TLC5940.
In the local interface, I mostly use the HSV input mode. It's far more convenient than the RGB mode. HSV is kind of stupid, because it ignores many perceptual factors. (The three primaries have different apparent brightness. When multiple primaries produce a colour, there is an increase in brightness and decrease in saturation. Colour does not seem to vary at a fixed speed as H is changed.) However, more complex colour spaces such as CIE LCH have many colours which cannot be reproduced via the three primaries. If the three potentiometers set L, C and H, there would be settings that are out of range. I tried it via the serial interface, and it was quite confusing. HSV to RGB conversion is also a much simpler algorithm and more suited to small microcontrollers. I have no regrets about choosing HSV.
So far, my favourite computer-based effect is a Winamp plugin which divides the spectrum into 3 zones, and sets red, green and blue values based on an exponential moving average of sound intensity in the corresponding zone. I used red for the lowest band and blue for the highest band. It's very nice with some types of music.
I'm releasing the firmware under the GNU General Public License (GPL) version 3, because I like how the GPL encourages creation of free software. I developed and compiled the code using IAR Embedded Workbench KickStart, because it offers a nice IDE for developing and debugging. Pin assignments are listed in the header file. If you want to change them, consider that many of the port 1 connections depend on the special functionality available on certain pins. You can download MSPRGB source code from Dropbox.
I'm separately releasing some code which uses the RGB lamp via a serial connection. The zip you can download from Dropbox contains librgb, a library for interfacing to the lamp, and vis_rgb, the Winamp plugin I described. I just cleaned up librgb and improved portability. I'm not protecting librgb and vis_rgb via the GPL because I don't feel it is especially worthy and because I don't want to restrict its usage.