Exploiting Serial Peripheral Interface to Speak RGB LED with Rust

Dominick Caponi
7 min readApr 16, 2023

Trust me it isn’t too bad 😬

WS2812B RGB LED

It seems like every cool kid on the block has one of these light strips in their room. They’re a quick and easy way to add style to a room and there are many readily available off-shelf modules to buy that add cool effects to the simple LED array. Typically when we want to light up a LED, we apply some voltage (don’t forget the current limiting resistor) to a LED and it blinks… and then we put the arduino in a box and forget about it.

However there is much more to that story. Some LEDs like the 4-prong RGB LED have the ability to change colors like the one shown above. Other RGB LEDs come with specialized microcontrollers that enable them to be strung in a row and addressed individually like those in the WS2812B light strips everyone knows and loves.

How does the WS2812 work? There are 3 wires for most basic installations. Power connected to a 5 volt supply, ground to complete the power circuit and the data line in the middle. The data line is what tells each LED what color to be and how bright.

Each LED is actually 3 tiny LEDs, one for each primary color, red, blue, and green. The intensity of each primary color dictates the final color. We’ll send a signal in the format of Green, Red, Blue (GRB) to each light on the strip. We do that by stringing together as many groups of GRB inputs as there are LEDs on the strip. If we have 3 LEDs we send 3 groupings of GRB (so GRB|GRB|GRB).

How do we send GRB? Well each color has a possible value from 0 to 255. We use 255 because that’s the biggest number we can represent with 8 bits and we need to work with bits because digital circuits and voltage signals explained in the next section. When you send these packets of GRB you’re actually sending 3 numbers representing the combination of colors you want the LED to exhibit. If you sent blue, you’d send (0,0,255) and to send purple you’d send (0, 255, 255) or gray (170,170,170).

Well up to now its not too bad to understand that what we want to communicate is the different intensities of the primary colors and those combinations. Microprocessors however don’t count higher than 1 so we need to represent these digits in binary. 255 is a very specific number because thats the highest you can count with 8 bits. That is, 1111_1111 in binary. 8 bits being the number of digits where each digit can be 0 or 1. Similar to how we count to 10 before rolling over to the teens, then the twenties, computers count to 1 before rolling over to twos, then fours, then eights and so on. Using the purple example (0,255,255) we’re actually sending (0000_0000,1111_1111,1111_1111). How does the strip understand a 1 you ask? Well we send it pulses of high (usually 3.3V) voltages kinda like morse code.

Clocks (Not the Coldplay Song)

To send a 1 as a contrived example, there’s usually a signal from a line with some variable voltage. For this example, let’s say this line can be 0V or 5V. So to send a 1 we flip a switch and now the line is 5V. But how long do we hold the switch on to get our point across? That’s where clock pulses come in. A clock synchronizes communication speed between microcontrollers. It establishes how long a line is held high to count as a 1 vs a 0. The clock signal is a simple oscillation from high to low at a pre-configured frequency.

The WS2812b Datasheet tells us that it expects the signal line to be high for a specific amount of time to count as a 1 or a zero. This was confusing to me at first because sending a 0 still required pulling the signal line high. The difference was for how long. In the picture, the total time is 1250 nanoseconds. To get the LED to interpret a 0, the line must be pulled high for 400 nanoseconds and low for the remaining 800 nano (within + or — 150 nanoseconds). For the LED to read a 1, the line is pulled high for 800 nanoseconds and low for the remaining 400 nanoseconds.

We can use serial peripheral interface (SPI) to accomplish this waveform but there’s still some voodoo we have to do to get this done. The driver microchip (I use an ESP32 C3) pulses signals at a certain clock speed and doesn’t vary the period of its pulses as required by the WS2812B chip. I set the clock speed of the ESP32 at 3,333,333 Hz and my period is 300 nanoseconds and that’s it, no varying that period.

1 + 1 = 1

So great, how do we replicate this waveform with the ESP32 which can’t change its period? Well there’s 2 subtle things. First, a 300 nanosecond period is within our 150 nanosecond tolerance for the WS2812B. That means we can send a single ESP32 pulse and that counts as a 0. What about the 1? Well we can send 2 pulses from the ESP32 in a row for what is effectively a 900 nanosecond period (300 high, 300 low which the WS chip won’t see or care about, and another 300 high) which is within our tolerance for how long the line should be high to count as a 1. The reason the WS2812 chip doesn’t care about the 300 nanosecond interlude is because it’s reset period is 50,000 nanoseconds — meaning a 300 nanosecond drop is a blip and is ignored.

Blue 255 Set Hike!

Working backwards from the datasheet requirements we know we need for a 3 LED strip to be blue we need [(0,0,255),(0,0,255),(0,0,255)] which is [(0000_0000,0000_0000,1111_1111),(0000_0000,0000_0000,1111_1111),(0000_0000,0000_0000,1111_1111)]. That’s how the WS2812B needs to see the data. To make that possible, the ESP32 needs to run at 3.33MHz and because it needs to send 2 pulses to qualify as a 1, we need 4 clock cycles to communicate a 1 or 0 from the ESP32 to the WS2812B.

Effectively in order for the ESP32 to send a “0” it sends 1000 in binary (one pulse followed by 3 periods of nothing) and to send “1” is 1100 (two pulses followed by 2 periods of quiet). When I do this on the ESP32 and look at the resulting waveform from the pulses sent, we see this output. The 8 pairs of spikes on the left represent the “1000”s sent (or “0” if speaking WS2812) that represent the green and red values. The slightly wider 4 pairs of pulses on the right represent the “1100”s sent (“1s” in WS2812) to set the blue value to 255. The thin spikes effectively represent 1 pulse while the wider spikes represent 2 pulses together. Each individual bump represents a bit as seen by the WS2812 — there are 8 bits/pulses per color in G, R, and B.

Don’t Believe Me? Try It!

I didn’t put “Rust” in the title for clickbait but this isn’t exactly a tutorial either. I wanted to make some notes about the core concepts behind how we can bend the SPI protocol and apply some physics to make some cool things happen. The actual wiring and coding is pretty straightforward and the Arduino hookup instructions are a dime a dozen. This explainer goes a little deeper into the theory and was meant as a way to keep myself motivated as I pivot into a career in embedded systems and hardware engineering.

That said, Here’s a picture of the wiring and a link to the code. TLDR on the code is you grab the ESP32 hardware abstraction crate, set the clock speed on the data pin and configure it as a master out (meaning this will be the pin giving the instructions to any devices that are listening). The hardware abstraction crate takes care of writing bits to the appropriate registers to set up the data pin for squaking data at the appropriate frequency. I’ll do an explainer on that once I’ve had a chance to deep dive into that myself 🙂

I used a ESP32 C3 WROOM 02 and connected power to 5V (red) Ground to… ground (yellow) and data to GPIO 7 (green). Then I flashed the rust code using the cargo espflash available through cargo. You can get most of the setup done pretty easily using a Rust project template I built for the ESP32.

See I told you it wasn’t too bad

Full disclosure, I’m no expert in embedded systems but it has fascinated me since I was a kid. Hopefully this explainer piques your interest and enlightens you to the point where you feel like you understand what’s going on on the physical level and less like you’re just copy/pasting some code from Instructables and sticking Legos together just to lose interest a week later. I enjoy feedback and ideas for more explainers as I continue on my journey to being an embedded systems engineer so if you have comments or suggestions drop me a line.

--

--