With a Rainbow Wave Pattern as an Example
In this article, I discuss a technique to help realize the beautiful LED pattern idea in your head. I will not use any particular microcontroller (MCU) or driver, but instead, I’ll cover my favored methodology—created by yours truly—which you can use with the MCU platform of your choice. This technique may or may not be standard practice for animating RGB LED patterns. Regardless, my example project is a great introduction to RGB animation.
In Circuit Cellar’s issue #386, I wrote my first article (“Addressable RGB LEDs: A Fast and Easy Way to Create Flexible RGB LED Lighting,” Circuit Cellar #386, September 2022) . As its title suggests, it covered how to control addressable RGB LEDs, as well as how to roll your own driver with the ESP32-S2. I described how an addressable LED works and explained its communication protocol in detail. The example I provided was enough to get started lighting a single RGB LED that was present on the development board, and it suggested a path to expand the code to control more LEDs in a string.
However, I (unintentionally) left out the fun part: animating cool patterns that dazzle an audience and keep viewers entertained. Since writing that piece, I’ve played with these types of LEDs for a few months and have learned a few tricks. I’ll admit that I thought implementing the animations I envisioned into code would be easy. I mean they’re just LEDs—how hard could it be? Only, when I actually tried to code it, I found that animating sophisticated patterns is a challenging and frustrating task without a solid plan. Of course, that’s a golden rule for any project, big or small: Always have a plan, because a new project is likely harder than you think, especially if you haven’t done it before.
But fortunately for you, in this case, you don’t have to find that out the hard way, as I did. Just sit back, relax, and learn from my experiments (and mistakes).
Before we can go into the animation details, we need to understand a little bit about color perception. The human eye is typically capable of seeing 380nm to 700nm of the electromagnetic radiation spectrum. This range of the spectrum is called visible light. If you have ever seen a prism break white light into an array of visible colors, that is what the visible spectrum looks like. Pick a wavelength in this spectrum and it will correspond to a color of light we can perceive. All of the colors we can perceive are within the visible light range of the electromagnetic spectrum.
RGB LEDs do not encompass the entire visible spectrum, but they do a good job of tricking us into thinking they do. They have three peak wavelengths that are dominant in each color. As an example, the red LED wavelength is between 620nm and 625nm, the green LED between 522nm and 525nm, and the blue LED between 465nm and 467nm. That means if you took this addressable LED and had all three LEDs fully lit to create a white light, a prism would break it up into these wavelengths. What we can understand is that the perception of “pure white light” from the entire visible spectrum is nearly the same as the perception of “RGB white light.” RGB white light approximates what many people perceive as and would call white light.
Alter the brightness of each LED, and we perceive different colors of light. But what you perceive in an RGB as a yellow light (as an example) is not the same as an electromagnetic wave with a wavelength from 570nm to 580nm. It is your eye and brain perceiving it as such. This is important to understand, as sometimes your project may need a “pure” colored light, and sometimes an approximation is sufficient.
For what we’re doing in this article—creating animations for entertainment—an RGB LED approximating colors is fine. But if you’re building a project that requires a “black” light, you can’t use an RGB, as it won’t produce light with a wavelength of 395nm. Another example is photochemistry. If a scientist is experimenting with yellow light to investigate its effects on a particular chemical, an RGB LED imitating yellow light will not work. The chemical is not like the human eye and will not perceive approximated yellow light—it requires actual yellow light for the photoreaction to occur. So, consider your project requirements to determine if the exact wavelength of a given color of light is needed, or if color approximation is enough.
Anyway, having established that RGB LEDs can approximate a color as perceived by the human eye, let’s get into color mixing. To recap my last article on this topic , addressable RGB LEDs allow a value from 0-255 to be written to their internal circuitry. A value of 0 represents completely off and a value of 255 represents completely on. Look at Figure 1 and Figure 2 to see what some of the value combinations represent in color.
Theoretically, if you write the RGB value of (128, 0, 255) to an addressable LED, we should get violet. But what you’ll perceive is actually more like magenta. Why is this? Well, there could be a few reasons. It could be that the “ideal” RGB wavelengths are not the same as the addressable RGB LED values and that we really need a different color wheel to correct for this. It could be that the brightness levels for our RGB LED are not well adjusted and are not exactly compatible with our color wheel values. Your guess is as good as mine! But in the end, it doesn’t matter—it’s not a characteristic of the LED we can change. We just need to compensate for it.
Why bring this up? I used visual aids like Figure 1 and Figure 2 to help determine the color I wanted to display in an animation. When I was animating color transitions, I thought I could easily calculate them, with nice, neat boundaries, like those shown in these figures. But this approach returned poor results for certain areas in the color space. For example, if I wanted to fade from cyan to violet with a for loop using the color values shown in these figures, I would have ended on magenta instead of violet. I found that I needed to play with the RGB values, and make my own color wheel based on these values, to produce a “proper” violet.
Brightness perception is exactly what you think it is: how bright we perceive something to be. There are two components to this perception. The first is how the human eye responds to brightness based on the wavelength of light. In technical terms, this is called “spectral sensitivity.” Figure 3 shows this for a typical human eye.
How exactly spectral sensitivity works is way beyond the scope of this article, but to put it simply: The cells in our eyes, called rods and cones, are sensitive to red, green, and blue bands of light wavelengths. The responses of these cells to light, and the intensity of the light itself, together affect the color sensation and the perception of that color’s brightness. Looking at Figure 3, the human eye is most sensitive to shades of green. Again, this is too complicated to go into much greater depth here. But the takeaway is that the brightness or intensity of the color is going to be perceived differently for each color. This matters to us when working on RGB animation, because if you are, for example, trying to produce a bright purple light, it might appear dimmer than you anticipated.
The second component of brightness perception is the brightness of a single color. The human visual system’s response to brightness is logarithmic, not linear. This is best understood by an example. Say you had a red-colored light that is very dim. Let’s say you decided to increase the brightness by a factor of two. You would expect that the light would be twice as bright. And technically speaking, yes, twice as many photons will be flung at your eye. And yes, at first, your eyes will agree. But now what if we try that again? This time it still seems to be twice as bright, but maybe it’s less apparent. Keep repeating this, and you will start to have a hard time recognizing the doubling of photons coming your way. In other words, as the light continues to double in brightness, it’s harder and harder for your eye to recognize that this is the case. For RGB LED animation, this matters for how bright you decide to drive each LED color component. You might find that you need to tone down the green LED and drive up the red LED for the color you’d like. It also affects animation speed (explained further in the next section). But let’s say you want to have a simple dimming/brightening pattern, in which you start with no light and then linearly turn up the light intensity at a steady rate. At a certain point, the animation will appear to have stopped due to how the eye perceives differences in brightness. So, you’ll need to either implement code that increases the light intensity logarithmically and uses the entire intensity range, or decide to stop the animation at the point where it becomes difficult for the viewer to discern the increase in brightness. I find the latter easiest, and use that method in an upcoming example.
ANIMATION TIMING & RESOLUTION
Here’s the last thing to understand before you attempt to code visually appealing patterns. Think about a stack of papers, each with a drawing on each sheet. Each sheet has a cartoon character walking in place. To create the appearance of movement, of course, each subsequent page must depict the next frame of the walk sequence. Now when you flip through the paper stack—voila!—you perceive the character moving in place. But for the animation to look just right, you can’t flip through the stack at inconsistent speeds. It must be a steady flip. This is the first requirement of a well-executed animation: it needs to advance to the next frame at a constant rate. Hence, your code needs to advance all LEDs in the chain to their next color frame at the same time. How often? For me, I found that 1ms advances worked best at producing smooth and crisp animations. This can be done in a variety of ways in code. You can have a timer call a function to move onto the next frame in the middle of application code. Or, if you are using an RTOS, dedicate a task and have it called every 1ms (this is what I wound up doing with FreeRTOS).
The second requirement of a well-executed animation is to not skip frames, or to not skip too many at once. Skipping from frame 1 to frame 10 may look choppy and unappealing for some patterns but okay for others. But skipping from frame 1 to frame 4 could look acceptably smooth. Frame 1 to frame 2 is the smoothest. Another way of thinking about this is to not have too large of a jump from one color set in a frame into another. For example, say that I want to smoothly brighten only the red LED from 0% to 100%. If I jumped from a value of 0 to 50, then 51 to 100, and so on all the way to 255, it would look very discontinuous. But if I instead performed this animation it in increments of 5, the result would appear smoother and more controlled. The steps need to be small enough that no choppiness or jerkiness is perceived. The obvious exception would be if the pattern is intended to have quick shots or bursts of color, in which case jerkiness works in your favor.
THE IDEA OF A MOVIE REEL
At this point, we now have enough information to discuss the technique I employed to ease the pain of coding a beautiful RGB LED pattern. I borrowed the idea of playing a movie. Whether it’s on a film reel, DVD, or video file on your computer, the concept of a “record” became useful to me. Here I use “record” to mean an entity that holds the information of each frame. For our RGB LEDs, the information in each frame is composed of three color intensity values—one each for red, green, and blue. However these values are saved, the goal is to traverse or “play” this record at a steady rate. We grab the values on one frame of the record, apply that frame’s color data to the RGB LED, and then wait 1ms until we do it again with the next frame’s color data. The way you hold this record is completely up to you. I used three arrays of the same size, where index 0 represented the first frame of color data for each color component. The three arrays are analogous to a DVD, the code that traverses through these arrays is analogous to a DVD player, and the LED displaying this color frame is the screen on which the DVD is being played. As simple and intuitive as this idea might seem, it’s tremendously helpful in organizing your pattern and enabling your animation.
EXAMPLE: THE RAINBOW WAVE
Let’s say we’re tasked with creating a rainbow wave pattern with our addressable LEDs. In this animation, a rainbow pattern smoothly traverses along a string of LEDs. See Circuit Cellar’s article materials page for a link to a short YouTube video that displays this pattern .
If you attempt this with no plan (like I did), you’ll either fail, or take an extremely long time to make it (along with unsustainable and messy code). So, let’s make a plan. Let’s start by figuring out how we can create each frame. My approach was to first get a “feel” for how my eyes experienced each intensity value. For example, does the intensity value of 100 to 200 differ that much? Is it bright enough for my needs? I found that an intensity value of 100 was plenty bright for me, and that exceeding that value wasn’t necessary to achieve the rainbow wave effect. When I did a test intensity sweep from values 0 to 255, I discovered that the effect was quite small, and the animation rate over values 150 to 255 appeared to slow down even though it was technically growing consistently brighter. Remember that this is due to the logarithmic nature of our eyes’ perception of brightness. The range will differ for each color, but I found that limiting the upper bounds of the range to somewhere between 80 and 120 worked well for my purposes. This is fortunate, as this also helped lower my overall electrical current draw, a common concern for RGB LED projects. For the sake of simplicity, let’s choose to limit the maximum value to 100 for each color component.
Great! We have a range of 0 to 100 for our intensity values that can be incremented linearly. Now we need to start constructing our frames. For me, ordering the frames’ colors from red to violet made the most sense. This order is depicted, left to right, in Figure 4.
We now need to translate this into an array of frames. The color wheel in Figure 1 will help us determine these frames, but replace all of the values of 255 with 100 and the values of 128 with 50 to remap the full range to the one we chose (0 to 100). Starting at red, the RGB value will be (100, 0, 0). To transition to orange, we need to increment the green value for each frame, leaving the red and blue values as they are. Table 1 captures these value changes in the sweep from red to orange. We reapply this method for the sweep from orange to yellow (Table 2). And to further beat this logic into our heads, we can show the transition from yellow to green with much the same technique. But this time we need to decrement the red values down to zero (this will transition through the chartreuse color space) while leaving the green and blue values alone (Table 3).
Next, increment the blue values while keeping red and green the same, and finally decrement the green values while keeping red and blue the same. Continue to do this until you reach the right side of the spectrum pictured in Figure 4. Once finished, we’ll have three arrays that together make up our record to play through, frame by frame (or, array element by array element). Personally, I found Excel to be the perfect tool to help generate these values. Once they are created in a column, it’s simple enough to cut and apply a transposed paste, export to a CSV format, and copy the values straight into code within an array initialization.
Finally, we can go ahead and “play” this record on our RGB LED chain. Assuming we have a dedicated RTOS task, we can write the pseudocode shown in Listing 1.
This is the pseudocode to play our “record” on an RGB LED chain. By assigning each RGB LED a different starting point in the “record,” we can achieve the wave effect.
// Record playing task for RGB LED chain
/* Calculate new index values that places
each RGB LED in its own starting point.
If out of bounds, reset the value to beginning. */
For i = 0 To (NUM_OF_RGB_LEDS_IN_CHAIN – 1)
indexArray[i] = indexArray[i] + 1
If indexArray[i] > (SIZE_OF_ANY_RGB_VALUE_ARRAY – 1)
Then indexArray[i] = 0
/* Apply color frame data to all RGB LEDs then render. */
For i = 0 To (NUM_OF_RGB_LEDS_IN_CHAIN – 1)
j = indexArray[i]
setLedValue(ledNumberInChain[i], rArray[j], gArray[j], bArray[j])
Let me break this code down into its steps to make some sense of what’s going on. When a context switch occurs for the Rainbow_Wave task, it will begin by starting at the first element for each color array within a for loop. Let’s assume that the array indexArray holds values from a range of 0 to the largest index value for our RGB frame arrays, rArray, gArray, and bArray. By doing so, we evenly spread each starting point of each RGB LED. So, the first LED will begin on red, and the next will begin on perhaps orange or yellow (depending on how many LEDs we have in the chain). The last LED will start near violet. To advance to the next piece of frame data, we’ll first increment the starting point by one. If the calculated value exceeds the size of our “record,” we reset it, bringing it back to red.
The next step is to apply the next set of frame data extracted from the “record.” We start by applying this to the first RGB LED in the chain. Since we have its starting point (plus one) figured out, we can apply the color data to it. We do this for each subsequent RGB LED until we’ve applied it to all of them. Once we set the color data for each RGB LED in our library’s data, we can render the whole RGB LED chain at once. We then perform an RTOS delay and context switch out a minimum of the amount (as mentioned, 1ms worked well for me). And this process repeats over and over for each RGB LED, traversing the “record” forever, producing the mesmerizing rainbow wave effect. The beauty of this code is that rArray, gArray, and bArray do not need to hold the values for just a rainbow wave effect—they can be used for any pattern. Remember the function of this bit of code is to act like a record player to play our chosen pattern, such that the only thing for us to change are the values in the RGB color arrays themselves.
Hopefully, the technique shown here can make your next animation project a bit easier to tackle. There are other methods that are better suited for other patterns. For example, to simulate a crackling fire, the record concept may not be useful or easy to implement. In that pattern’s case, it can be solved mathematically. As another example, it might be easier to apply conditional logic directly for a breathing pattern. Generally, the pattern type and complexity will dictate which method to employ. But the record technique I’ve outlined is, in my opinion, one of the most useful.
If you are interested in a more detailed, video-friendly explanation and demonstration of the code implementation on an ESP32-S3, I’m working on a course to deliver just that. It is still a work in progress, but I expect to release it sometime in Summer 2023. You can check on my progress at magicsmokefactory.com. Thanks for reading!
 Corleto, Joseph: Addressable RGB LEDs. A Fast and Easy Way to Create Flexible RGB LED Lighting. Circuit Cellar, Issue 386, September 2022, p. 6-11.
 Rainbow wave pattern example, YouTube short: https://www.youtube.com/shorts/R1dD1tV-kLA
 Sensitivity of human eye, Pixelsham: https://www.pixelsham.com/2016/03/10/sensitivity-of-human-eye/
Espressif Systems | www.espressif.com
PUBLISHED IN CIRCUIT CELLAR MAGAZINE • JUNE 2023 #395 – Get a PDF of the issueSponsor this Article
Joseph Corleto holds a Master’s Degree in Electrical Engineering. Aside from working as a full-time Electrical Engineer, he has a small business (Bit Bang Gaming LLC), which creates video game electronics hardware, and is actively pursuing the creation of a project-based video course using the ESP32. He describes himself as an Electrical Engineering handyman since he has skills in firmware, R&D, PCB design, PCB assembly, and automated testing. You may reach him anytime via his email: Corleto.email@example.com.