Projects Research & Design Hub

Addressable RGB LEDs

Figure 5 Example of the data transmission method for a chain of three LEDs [1]. Each LED in the chain applies the payload of 24-bits it first sees, and then passes along the rest of the data to the next LED.
Written by Joseph Corleto

A Fast and Easy Way to Create Flexible RGB LED Lighting

Addressable RGB LEDs are popular because they can produce dramatic, dynamic displays in every imaginable color. But few people understand how to use them without an external library. In this article, Joseph explains the protocol for one type of addressable RGB LED, gives an example of simple implementation and code, using an ESP32 microcontroller, and provides useful tips for using them in your design.

  • How do you use an addressable RGB LED?

  • Do addressable RGB LEDs need an external library?

  • How do I set up RGB LEDs in a grid?

  • ESP32-S2

  • WS2812B by WorldSemi

  • ESP32-S2-DEVKITC

The LED has come quite a long way since it was invented as a practical component in 1962. Today, many different sizes are available in a variety of packages, power outputs, and colors. A personal favorite of mine is the RGB LED. With a red, green, and blue LED integrated in one package, it becomes possible to have whatever color you want. There is no waiting for a particular wavelength to be released by a manufacturer; you simply hook up the appropriate drive circuitry. Pulse width modulation (PWM) control with a microcontroller is a popular choice.

What if you want to have many RGB LEDs in a grid or in a strip? There are tried and true methods to achieve this, but the complexity of wiring, coding, and driving circuits starts to get complicated, and fast! This is where “addressable” LEDs come to the rescue. With addressable RGB LEDs, the color and brightness of individual LEDs or groups of LEDs can be controlled, because each module or package contains an integrated circuit (IC) chip. They are a little tricky to understand, however, if you have never used them before, because they use a unique communication protocol. In this article, I want to demystify this protocol and give some sample code for controlling them easily with an ESP32-S2 microcontroller.

CONNECTION METHOD

Before diving straight into the protocol, it helps to see how the LEDs are supposed to be connected. Addressable LEDs are available from several manufacturers, but for this article I am going to be referring to the WS2812B intelligent control LED integrated light source by WorldSemi. The example connection method from its datasheet [1] is shown in Figure 1.

Figure 1
Example connection method for the WS2812B LEDs [1]. Only 4 pins are required to connect them for use in a chain.
Figure 1
Example connection method for the WS2812B LEDs [1]. Only 4 pins are required to connect them for use in a chain.

Note that each LED has four pins: VDD, VSS, DIN, and DOUT. The DIN from one LED is connected to DOUT of another, going down the chain. The first LED in the chain receives a command that propagates all the way down the chain, until all LEDs are illuminated as commanded. As far as hardware goes, that is all that is required at its simplest form. It’s a heck of a lot simpler than traditional RGB LEDs, right? The reason behind this simplicity is that each LED has a tiny control chip that decodes commands sent to it, and then drives each red, green, and blue LED. There’s no need to worry about LED forward voltage tolerances, picking the correct resistor, or fiddling with PWM settings. Just send the command for the intensity of each LED, to create the color you wish to illuminate.

WS2812B PROTOCOL

This hardware simplicity comes with a little bit of complexity on the firmware end. The WS2812B protocol defines a “0” or “1” in a way with which you might not be familiar. A “0” or “1” is not sent simply by having the DIN pin driven 0V or 5V, as is done in other electrical communication media. Instead, a combination of the two logic levels is required. The LEDs also need to operate at a very particular duration. Figure 2 shows the data transfer time, and Figure 3 shows the sequence chart [1].

Figure 2
Details of the data transfer time required to send data [1]. The timing is strict and has a small tolerance window.
Figure 2
Details of the data transfer time required to send data [1]. The timing is strict and has a small tolerance window.
Figure 3
Sequence chart showing how a "0" and "1" are created [1]. Both bit values are composed of the timing of two voltage levels.
Figure 3
Sequence chart showing how a “0” and “1” are created [1]. Both bit values are composed of the timing of two voltage levels.

In other words, to send a “0,” the DIN line must be held high for 0.4µs and then low for 0.85µs. Likewise, to send a “1,” the DIN line must be held high for 0.85µs and then low for 0.4µs. To send a reset code, keep the DIN line low for over 50µs. It is strange when you first see this, but it’s not entirely uncommon for serial communication. The 1-wire protocol uses a similar setup.

— ADVERTISMENT—

Advertise Here

Now that we understand what a “0” and “1” mean for the WS2812B protocol, we can look at the structure of how these bits are sent. The WS2812B allows for the control of each LED with a resolution of 8 bits. Since there are three LEDs, this means the payload to one LED is 24 bits. The payload must be sent with the most significant bit (MSB) sent first. That is a lot of control, because technically speaking, our RGB LED can display 16,777,216 different combinations of colors (224 or 2563)! However, practically speaking, you may not be able to perceive the difference with payload values similar to each other, so the exact number of different combinations varies with the interpretation of the viewer. Did you ever get into a debate with a friend that the color you see is green, but they think it’s blue? That is precisely what I am describing here. To see the entire structure, refer to Figure 4.

Figure 4
Each RGB LED payload is composed of 24 bits. Each color has a resolution of 8 bits [1].
Figure 4
Each RGB LED payload is composed of 24 bits. Each color has a resolution of 8 bits [1].

This covers the protocol for a single LED. But how do we light the next ones down the row? The answer is to send another 24-bit payload immediately after the first payload. The number of bits you’ll send for any number (n) of LEDs will be 24n. For example, a chain of 100 LEDs will be 24 x 100 = 2,400 bits.

The interesting part is what comes out of the DOUT pin of the first LED. Let’s imagine a chain of three RGB LEDs, and we send the 72-bit data stream to refresh them all. The first LED will see this 72-bit sequence, but will only look at the first 24-bits and apply the color settings to itself. It will then reamplify the next 48 bits and send them to the second LED, where a similar process occurs. The second LED will look at the first 24 bits, apply its color settings to itself, and then send the last 24 bits out of its DOUT pin and into the DIN pin of the third LED. The third LED will see 24 bits, apply the color settings to itself, and will have nothing to send out of it’s DOUT pin. This process of grabbing the first 24 bits and sending them off is the trick these addressable LEDs use to simplify the hardware setup. This data transmission method, from the datasheet [1], is shown in Figure 5.

Figure 5
 Example of the data transmission method for a chain of three LEDs [1]. Each LED in the chain applies the payload of 24-bits it first sees, and then passes along the rest of the data to the next LED.
Figure 5
Example of the data transmission method for a chain of three LEDs [1]. Each LED in the chain applies the payload of 24-bits it first sees, and then passes along the rest of the data to the next LED.

It is important to note that this is not the only way addressable LEDs can function. Other LEDs on the market communicate in the more traditional sense, in that they use a clock line and a data line. This type of setup resembles how a cascaded array of shift registers would be controlled. A good example of this type of LED is the Adafruit SK9822 [2]. But apparently they are not that popular. I imagine it is because controlling the color settings now requires a data line and clock line. It does make it simpler to understand and control, but at the cost of an extra pin from a microcontroller-controlled implementation.

HARDWARE USED

Many different kinds of products and modules use the WS2812B. But to demonstrate the concept as simply as possible, I will be using an Espressif ESP32-S2-DEVKITC, which has one addressable LED already soldered on and connected to one of the GPIO pins. Any other ESP32 can also be used, as long as the RMT module (discussed later in the article) is included in its peripheral list, and it has an on-board addressable RGB LED. Other than the development board, a USB also is required to power the board.

IMPLEMENTATION FROM SCRATCH

The protocol timings are really fast, especially if you were to try to “bit bang” this. As someone who has tried to do this for a similar-styled protocol, getting sub-microsecond resolution is no fun at all to get working. If you do decide to give it a try, you can use embedded techniques, such as interrupts or timers. In my experience, though, interrupts never were fast enough, and the timers for the microcontroller I was using did not have the correct tick resolution. In the end, I used NOPs to burn CPU cycles with mild success. If you want to avoid banging your head bit-by-bit against your keyboard, I strongly encourage the use of a peripheral to do the heavy lifting for the bit timings. Peripherals such as UART and SPI are common in most microcontrollers, and will lead you to a workable solution. But even with these at your disposal, you will need to do some clever bit manipulation to properly adhere to the WS2812B bit structure.

Fortunately, the ESP32 series of microcontrollers has a handy peripheral called the RMT (remote control transceiver) peripheral [3] that lends itself well to manipulating addressable LEDs. It was designed to act as an infrared transceiver, but the data format can be configured flexibly. The timings it can produce are consistent, accurate, and precise. This is exactly the kind of peripheral that can be used effectively for the WS2812B protocol!

The code in our listings was written in C and requires some knowledge of the ESP-IDF. If you use the Arduino platform, this code should also work. You can mix the ESP-IDF along with Arduino code, but it has not been tested, so some tweaks might be necessary.

To begin let’s create a header file to create a small library for our demo [Listing 1].

— ADVERTISMENT—

Advertise Here

To use the RMT peripheral, we only need to include one file for the driver. As a standard practice, I always include an initialization function to set up the library. To make it easier to use this library, the AddrRgbLedColor_t enumeration was created. It contains a list of all the colors to choose from, so there’s no need to memorize what integer is mapped to what color. It also makes the function AddrRgbLed_SetColor easier to use.

Next, we create the implementation details for this library, starting with the initialization function (Listing 2).

The RMT peripheral has a lot of features, and to go over every detail in it would require a separate article. But it is important to know, in general, what this snippet of code is doing for us. It configures the RMT to be in transmission mode, within the first channel, and on GPIO18, which is the same pin the RGB LED is connected to. The carrier wave typically used in IR communication (which was the original purpose of RMT) has been disabled, and we wish to have our bit stream sent only once. The output should also be idling low, for compliance with the WS2812B protocol.

Finally, before enabling the RMT driver, the clock divisor is configured such that the smallest pulse resolution is at 400ns. This number varies with your clock speed, so if you are running faster or slower than 160MHz, you will need to adjust this value. Next, we need to set up an array of RMT items to encode the 24-bit payload to our LED (Listing 3).

Each item for the RMT consists of an array of four elements. They are defined as “L” and “H,” and are the “0” and “1” descriptions discussed earlier. The first element describes how long this pulse should last, the second element describes at what voltage level (5V or 0V), and the third and fourth elements are the same idea as the first two. In other words, the “L” item describes a 400ns pulse at 3.3V, followed by an 800ns pulse at 0V. Following this logic, the “H” item describes an 800ns pulse at 3.3V, followed by a 400ns pulse at 0V. We can use these item definitions to create a 24-bit payload in an array. These can be created for four different colors: red, green, blue, and none. These arrays will be used later to feed an RMT driver function to send out the bit stream. Although this may not be the most elegant approach, it does illustrate the protocol’s concept cleanly.

To finish our library, we need to complete the implementation of the AddrRgbLed_SetColor function (Listing 4).

LISTING 1
This is the header file for our addressable LED library. It defines enumerations to make it easy to pick a color and two methods to set up and use our library.

#ifndef _ADDRESSABLE_RGB_LED_H
#define _ADDRESSABLE_RGB_LED_H

/****** ENUMS ******/
typedef enum
{
    ADDR_RGB_RED = 0,
    ADDR_RGB_GREEN = 3,
    ADDR_RGB_BLUE = 4,
    ADDR_RGB_VIOLET = 5,
    ADDR_RGB_NONE = 255
} AddrRgbLedColor_t;

/****** INCLUDES ******/
#include “driver/rmt.h”

/****** PUBLIC FUNCTIONS ******/ 
/** INIT **/
void AddrRgbLed_Init(void);

/** SETTERS **/
void AddrRgbLed_SetColor(AddrRgbLedColor_t);

#endif
LISTING 2

Implementation details for initializing our library. It mostly consists of configuring the RMT module on the ESP32-S2.

/** VARIABLES **/
rmt_config_t hrmt;

/** INIT **/
void AddrRgbLed_Init()
{
    hrmt.rmt_mode = RMT_MODE_TX;
    hrmt.channel = RMT_CHANNEL_0;
    hrmt.gpio_num = GPIO_NUM_18;
    hrmt.mem_block_num = 1;
    hrmt.tx_config.loop_en = 0;
    hrmt.tx_config.carrier_en = 0;
    hrmt.tx_config.idle_output_en = 1;
    hrmt.tx_config.idle_level = RMT_IDLE_LEVEL_LOW;
    hrmt.tx_config.carrier_level = RMT_CARRIER_LEVEL_HIGH;
    hrmt.clk_div = 32;
    rmt_config(&hrmt);
    rmt_driver_install(hrmt.channel, 0, 0);
}
LISTING 3

Definitions for the RMT module to easily interpret along with arrays containing static 24-bit payloads. This simplifies the control of the RMT module.

/****** MACROS  ******/
#define L     {{{1, 1, 2, 0}}}
#define H     {{{2, 1, 1, 0}}}

/** CONSTANTS **/
const rmt_item32_t red[] = {L,L,L,L,L,L,L,L,H,H,H,H,H,H,H,H,L,L,L,L,L,L,L,L};
const rmt_item32_t grn[] = {H,H,H,H,H,H,H,H,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L};
const rmt_item32_t blu[] = {L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,H,H,H,H,H,H,H,H};
const rmt_item32_t non[] = {L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L,L};

LISTING 4

Implementation details for controlling the LED color. Our past definitions and enumerations make this much clearer to write and read in our code editor.

/** SETTERS **/
void AddrRgbLed_SetColor(AddrRgbLedColor_t color)
{
    if(color == ADDR_RGB_RED){
        rmt_write_items(hrmt.channel, red, 
                        sizeof(red) / sizeof(red[0]), 1);}
    else if(color == ADDR_RGB_GREEN){
        rmt_write_items(hrmt.channel, grn, 
                        sizeof(grn) / sizeof(grn[0]), 1);}
    else if(color == ADDR_RGB_BLUE){
        rmt_write_items(hrmt.channel, blu, 
                        sizeof(blu) / sizeof(blu[0]), 1);}
    else{
        rmt_write_items(hrmt.channel, non, 
                        sizeof(non) / sizeof(non[0]), 1);
    }
}

This function takes one of our predefined enumerations and decides what array to send out of the RMT using the rmt_write_items function.

TRYING IT OUT

We can finally give our code a try, and observe our results. All we need to do is include our library in our main file, initialize it, and choose a color to set—in this example, green.

#include “addressable_rgb_led.h”

void app_main(void)

{

AddrRgbLed_Init();

AddrRgbLed_SetColor(ADDR_RGB_GREEN);

}

As shown in Figure 6, it worked!

— ADVERTISMENT—

Advertise Here

Figure 5
 Example of the data transmission method for a chain of three LEDs [1]. Each LED in the chain applies the payload of 24-bits it first sees, and then passes along the rest of the data to the next LED.
Figure 6
Example of the data transmission method for a chain of three LEDs [1]. Each LED in the chain applies the payload of 24-bits it first sees, and then passes along the rest of the data to the next LED.

Let’s take a peek at our payload data to see if it is what we expected. Figure 7 shows what has been captured with a logic analyzer placed on GPIO18.

Figure 7
Payload observed on the DIN pin (also GPIO18) on our development board. The first 8 bits, which represent green, all have a maximum value of 255, whereas the red and blue bits are set to 0.
Figure 7
Payload observed on the DIN pin (also GPIO18) on our development board. The first 8 bits, which represent green, all have a maximum value of 255, whereas the red and blue bits are set to 0.

Since we chose to illuminate the green LED at maximum brightness with the red and blue turned off, what we capture agrees with what we observe. The data align just as was shown earlier in Figure 4. I encourage readers to experiment with editing the code here to include other colors.

ADDITIONAL CONSIDERATIONS

There are some things that you should be aware of before constructing your addressable LED circuit or buying an off-the-shelf solution. Not all addressable LEDs are created equally. Having the datasheet for your particular setup will save you a lot of time and avoid frustration.

First, timings could be slightly different for the “0” and “1” definitions. Having the timings on the edge of the tolerance may work adequately, but you will likely get erratic behavior if you are not in a comfortable timing region.

Second, different versions of the same addressable LED from the same manufacturer can have different input voltage levels. For example, the WS2812B datasheets requires higher than 3.3V at 5V for the DIN pin. However, this might still work well if the microcontroller is close enough, and if the tolerance of your LED is okay with the slightly undervoltage signal. Actually, the ESP32-S2 dev kit does this intentionally, and it seems to work for every board I have tried so far. What is interesting is that this is only an issue for the first LED. The next ones down the chain have the correct signal, because the first LED amplifies the rest of the bit stream. The general recommendation is to level shift to what is described in the datasheet.

Third, the bypass capacitor is not optional! Without the proper decoupling capacitance, erratic behavior might result, depending on how fast the LEDs get refreshed and how many are in the chain. Some RGB LEDs have the capacitor built in, and the only way to know is by looking at the die under a microscope—or better yet, have a look at the datasheet.

Last is the propagation time to update the entire chain of LEDs. The theoretical timing for a single LED payload is about 30µs. Had the chain been two LEDs long, it would have been 60µs. The general equation for the total refresh rate is:

Total Refresh Rate = (Payload Time x Number of LEDs) + Reset Pulse

For example, using 50 LEDs results in about 1,550µs to update the entire chain. Depending on how fast you’d like to animate colors, this may or may not be a problem. This is also a theoretical number; actual results may vary, so experimentation and testing are strongly encouraged.

CONCLUSION

I hope this introduction to addressable RGB LEDs helped you better understand how they achieve their amazing hardware simplicity. With this knowledge at hand, you should now be able to roll your own solution or, if using a predefined library, troubleshoot bit timings with a logic analyzer. Now go out there and illuminate the world! 

REFERENCES
[1] WorldSemi WS28212B Datasheet:
https://www.digikey.com/en/datasheets/parallaxinc/parallax-inc-28085-ws2812b-rgb-led-datasheet
[2] Adafruit SK9822 Datasheet:
https://cdn-shop.adafruit.com/product-files/2351/SK9822_datasheet_SHIJI.pdf
[3] Remote Control Transceiver (RMT):
https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-reference/peripherals/rmt.html

Code and Supporting Files

PUBLISHED IN CIRCUIT CELLAR MAGAZINE • SEPTEMBER 2022 #386 – Get a PDF of the issue

Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.


Note: We’ve made the May 2020 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
+ posts

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.joseph@gmail.com.

Supporting Companies

Upcoming Events


Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2023 KCK Media Corp.

Addressable RGB LEDs

by Joseph Corleto time to read: 13 min