Last month I bought an interesting RGB LED ring from Aliexpress and finally, now, it is here. So that, I’m about to present how I interfaced an STM32 MCU to WS2812B LEDs. This demo uses also FreeRTOS and HAL libraries. I wrote a simple code to make an STM32 interface WS2812B LEDs.
“Intelligent control LED light source” in the family of the WS2812B are amazing devices (like the WS2812 and the WS2811). In particular, they are intelligent control LED light sources with control circuit and RGB driving circuitry both integrated in a single 5050 component package. They include internally an intelligent digital port data latch and signal reshaping and amplification circuitries.
They are also equipped with a precision internal oscillator and a programmable constant current drive, effectively ensuring that each pixel outputs an amount of light (for each color) consistent with the one programmed. The data transfer protocol uses a single-line NZR communication mode. After the device is powered-on after reset, the DIN port receives data from a micro-controller.
- Reverse power connection protection, it does not damage the IC
- Only three wire required
- All circuitry and LEDs integrated in a single 5050 package
- Built-in signal reshaping circuit, waveform distortion not accumulate
- Built-in electric reset circuit and power lost reset circuit
- Each LED of the three primary color can achieve 256 brightness degrees, supporting 16 Million of colors
- Cascading port transmission signal by single line
- Between two device the distance can be more than 5m without any additional component
- When the refresh rate is 30Hz, more than 1024 LEDs can be connected with a single line
- Data line operates at speeds of 800Kbps
- The color of the light were highly consistent, thanks to the integrated constant current drives
One of the most useful and peculiar WS2812B’s characteristic is that more than one of them can be drove with a single MCU’s line. That’s because more WS281x can be connected in a cascade fashion, as depicted in the figure below.
The first device (upper in the figure) receives all the data from the MCU but removes the first 24bit (these bits codify its own RGB code) and forwards the others to the next cascade-connected device. To have a clearer idea, see this figure.
D1 is the first device, it received all the data and forwards all but the first. D2 (in order, the second device) receives all the data but the first and forwards, again, all the data but the first two. And so on..
This mechanism can be replicated indefinitely since each device performs a signal reshaping and amplification. This allows to reach greater distances since the signal that we output from the MCU is regenerated on each WS2812B during the way to the fartest LED.
However, no free lunch. The communication follows a high frequency NRZ protocol at 800kHz. Moreover, each bit must be encoded based on HIGH and LOW times.
From the figure above, we have 5 different interval, of which 4 must be followed accurately: T0H, T0L, T1H, T1L. Timings are reported on the datasheet as not strict (we have a margine of +-150ns) so that we can resume these intervals as:
- Bit 0 1/3 of the period HIGH, 2/3 of the period LOW
- Bit 1 2/3 of the period HIGH, 1/3 of the period LOW
- Reset Stay LOW for at least 40 cycles (50us are about 40 periods of 1.25us)
Each WS2812B requires 24bits of data to reproduce a color. Each color is, in fact, composed of 3 groups of 8bits each that represent its RGB coding. This data must be sent following this order.
We need to create a flow of bits with NRZ encoding at high frequency. How can we solve this problem? We have basically two ways:
- Directly control a GPIO taking into account the constraint about the time
- Use one or more peripherals to unload the CPU
The first approch is, for some ways, simpler. We hard code four different delays for T0H, T0L, T1H, T1L and three functions: one for sending the bit 0, one for sending the bit 1, one for reset and 0 that takes a color and generates the corresponding sequence of 24 functions. It is, obviously, not efficient and occupies our CPU for long times (especially if we connect a long strip of LEDs).
The second approach, instead, aims to unload the CPU as much as possible, taking advantage of two peripherals (a TIMER and a DMA, in my case) and some of the MCU’s RAM. How to glue together all this stuff?
From the datasheet, we know that every bits is characterized by a fixed period of 1.25us. This interval corresponds to a frequency of 800kHz. Moreover, looking carefully to the protocol we can notice that the only thing that changes is the high part of each period. It resembles a PWM signal. Most timers available on STM32 MCUs can dinamically generate different PWM signals. So that, changing at the right time the PWM signal generated by the TIMER, is possible to output an arbitrary series of ones and zeros following the WS2812B family specific protocol.
It is not enough. If we use only the TIMER, we also need the CPU to constantly change the TIMER PWM’s high interval, thus we still load the CPU. Another peripheral comes in our aid, the DMA. It is a peripheral that basically moves streams of data from one register to another (or from a portion of memory to another, or a mix of these). Since the DMA can also made these transfers in sync with a request or an interrupt, we can use it to update the PWM generated from the TIMER.
Here is one catch. The DMA is not a CPU, so it needs to move a bunch of data that is already available (and calculated). This requires that a location of memory must contain the sequence of PWMs that the TIMER should produce. The amount of memory required, is proportional to the amount of WS281x devices that we need to control.
Each WS2812B requires 24 different PWMs for the color. Moreover, the RESET signal must be a PWM with no high time of about 40 (but I used 50 to be sure) periods. I am talking about number of periods because the DMA is configured to update the TIMER’s PWM value at the end of each PWM signal.
So that the amount of bytes required is: 50 + 24 * #WS281x, one for each PWM period.
The last step is configuring the (only) one GPIO so that it can output the PWM given by the TIMER.
I used a ST‘s NUCLEO-L476, equipped with the ultra-low power STM32L476RGT6. This amazing MCU implements a Cortex-M4 processor with an astonishing low power consumption. Its most noticeable characteristics are:
- Ultra-low-power with FlexPowerControl
- Core: ARM® 32-bit Cortex®-M4 CPU with FPU, Adaptive real-time accelerator (ART Accelerator™) with frequency up to 80 MHz
- Internal 16 MHz factory-trimmed RC (±1%)
- Internal low-power 32 kHz RC (±5%)
- 3 PLLs for system clock, USB, audio, ADC
- RTC with HW calendar, alarms and calibration
- 16x timers
- Up to 114 fast I/Os
- Up to 1 MB Flash
- Up to 128 KB of SRAM including 32 KB with hardware parity check
- Rich analog peripherals (independent supply)
- 3× 12-bit ADC 5 Msps
- 2x 12-bit DAC
- 2x operational amplifiers with built-in PGA
- 2x ultra-low-power comparators
- 18x communication interfaces
- USB OTG 2.0 full-speed, LPM and BCD
- 3x I2C FM+(1 Mbit/s), SMBus/PMBus
- 6x USARTs (ISO 7816, LIN, IrDA, modem)
- 3x SPIs (4x SPIs with the Quad SPI)
- CAN (2.0B Active) and SDMMC interface
- 14-channel DMA controller
- True random number generator
- CRC calculation unit, 96-bit unique ID
Briefly, it is a compact, low-power, full-flegged MCU. It is more than enough to control WS2812B family devices and much more! I modified my NUCLEO board adding an USB battery charger and also a LiPo battery from a broken smartphone. Now it can run without the USB cable on its own. I attached the RGB LEDs ring DATA-IN directly to:
PORT: A – GPIO: 5
My ring is composed by 16x WS2812B smart LEDs, however every strip/matrix/circle of smart LEDs in the WS281x family should do the job.
I used the wonderful HAL library given by the STM32CubeMX software for ST Microelectronics. It offers a really comprehensive set of instructions and configurations that allows a great speed-up in prototyping. Moreover the STM32CubeMX offers an useful functionality to graphically set-up pretty all the low-level peripherals as well as the main clock.
On a higher level, I used FreeRTOS as real-time operative system. The millions of users worldwide are a guarantee of dependability and smooth operation.
The following video demostrates some capabilities of the driver, drawing patterns on the RGB LED ring.
All the code that I used for the demo is available HERE but the HAL libraries and FreeRTOS that can be downloaded for free on the web.
Excellent write up! Thanks for sharing all your findings. I’m relatively new to ARM, do you think it should be straightforward to make this work on an STM32F411?
Well, the WS2812B requires some peripherals to work together (TIMER and DMA), it is not the most simple project to port. However I tried to make the library as portable as possible (at least within the libraries provided by ST), so you could try. I’m pretty sure that with the datasheet on hand it can be done. Please let me know
I have a question wrt the code.
Specifically wrt DMA transfer from the memory byte array (LEDBuffer) and the PWM OC compare register. The PWM OC compare register is 32bits wide. The LEDBuffer is 8bits wide. Your DMA configuration has correctly configured the memory width as a byte and the peripheral width as a word:
hdma_tim.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
hdma_tim.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
According to how I understand the datasheet is that:
The DMA controller will only pack/unpack the size differences, it will not “expand” it. Which means that each “bit” you want to PWM out will need to consume 32 bits in your memory buffer (LEDBuffer) and not 8 bits.
Does this make sense?
I don’t remember how I figured out these values. It is possible that I simply wrote them and since I have not encountered any problem, I have not investigated further. Which datasheet are you referring to?
I’m trying to get your code running but I keep getting errors about missing hal libraries and FreeRtos problems. I’m using the same board as you used. Could you please tell me or send me your cubeMX file so I can create a project with the same setting as you did?
I don’t use CubeMX, I start with the Keil template in the HAL library and them I modify it. I’m planning a mayor migration to GitHub. Thus, all this code will be available with the samples.
Did you manage to migrate to GITHUB.
If so, please tell us the path.
Not for the moment, however I started. Check here, https://github.com/FabioRM/