I’m building a project using non-addressable RGB strip, where all of the LEDs in the strip are connected in parallel and are therefore the same color. I thought it might be a bit useful to talk about the techniques that I’m using in the software, and some of the options & tradeoffs that exist.
The hardware and code are from the Arduino world, but they apply equally well in other ecosystems.
The LED strip is composed of RGB LEDs, which are in turn made up of separate Red, Green, and Blue LEDs. Simplistically, I can get 8 colors out of the combinations of these colors:
- Yellow (Red + Green)
- Purple (Red + Blue)
- Cyan (Blue + Green)
- White (Red + Green + Blue)
To get more colors, I need to be able to do something more than just turn a color on or off – I need to control the intensity of each color independently. While it is possible to control LED brightness directly by controlling the current through it, that is fairly complicated, so in most cases we take a shortcut. If we want half the light output, we flash the LED so that it is only on for 50% of the time. This would seem to produce a flashing LED, but because of the way our vision works, if the flashing is quick enough, we see not a flashing LED but a dim one. This technique is known as “Pulse Width Modulation”, or PWM. Doing it 60 times per second (ie 60 Hertz, or Hz) is a minimum bar, but I like to shoot for 100Hz. A lot of cheap holiday LED lights flicker at 60 Hz and if you move you head you can see it.
The number of colors we can get depends on how many levels of intensity we support. 64 levels gives us 64 * 64 * 64 = 262144 different colors, while 256 levels gives us about 16 million. We want enough to give us smooth fades between colors, and I think 64 levels is probably significant.
PWM is also used for controlling the speed of some motors and for driving servos.
PWM is such a useful technique that it is implemented in hardware on many microcontrollers. Using the hardware PWM has a lot of advantages:
- You don’t have to write/debug/maintain the PWM implementation.
- You don’t have to deal with interrupt-driven code, which is more complex and a bit mind-bending at first.
- The hardware PWM does not consume any execution resources, so your software has 100% of the microcontroller to use.
- It’s very simple
So, hardware PWM is great. However, it has some disadvantages, the biggest being a limited number of PWM channels per microcontroller. The Atmel ATMega386 in the Arduino supports 6 individual channels. I’m using a Trinket, which is built around the ATTiny85 microcontroller, and therefore supports only 3 channels. Hardware PWM is also limited to a specific number of discrete levels. If you need a lot of outputs and/or more discrete levels, there are boards that you can connect to your microcontroller that can do more channels at with more levels, such as this 16-channel one.
If the hardware PWM support is not enough, PWM can be implemented in software. We configure the microcontroller to call a bit of code at a specified interval, and in that code, we decide whether the output should be on or off.
This gives us a bunch of flexibility; we can, at least in theory, do PWM on every single output on a microcontroller. However, the code that is running is taking up system resources, and at some point we won’t have any resources to do any other work.
There are two ways of storing animation; we can express them in procedural code such as:
DoFade(redTarget, blueTarget, greenTarget, fadeTime)
and then create a program that has one DoFade() for each animation. Or, we can do a table-driven approach, where we express what we want in a table:
|Red Target||Blue Target||Green Target||Fade Time|
And then write a very simple program to loop through the entries in the table sequentially.
The table-driven approach has simplicity going for it, and works well in most cases. It doesn’t work well in cases where there are repeated patterns; if we wanted to fade from red to green and back to red 50 times quickly, that would take 100 entries in the table, but would only require a short loop to write procedurally. There are ways to get around this by making the table more complex.
There are a few different architectural options. In rough order of complexity:
- Use hardware PWM and code in the main thread (ie “loop()” in the arduino world) to drive the animation.
- Use hardware PWM and a separate timer interrupt to handle the animation. The timer interrupt code is called a fixed number of times a second, and that code drives the animation.
- A timer interrupt to implement the PWM (software PWM) and to handle the animation.
There is also a fourth option – using a timer interrupt for PWM and splitting the animation between the timer interrupt and the main thread. I built one system that does that, but I think it ends up being more clever than necessary.