If you like blinky LEDs – especially the full color RGB ones – you might have already come across the WS2812 LEDs.
These little suckers are extremely bright and can be purchased rather cheaply from eBay, 50 or a 100 pcs at a time or strips with 60 LEDs per meter.
You can also get them as bare LEDS, as strips or even as breadboard friendly breakout boards from Adafruit (NeoPixel) and SparkFun.
These devices can be chained together to endless strings (given you can supply enough power) and therefore are perfect for making big RGB screens/matrices.
As it turns out all the awesomeness comes with a downside: the digital interface used is not a standard interface. It’s an 800kHz data stream (1.25us per bit) with different duty cycles (high/low times) to represent a ‘0’ or a ‘1’ bit.
WS2812 timing diagram
WS2812 data packet format
In between each data sequence of 24 bits there is a 50us dead time which the LED uses to update it’s PWM output for each emitter.
Because the WS2812 protocol is not standard interface there are no hardware peripherals on most microcontrollers to support this interface.
This leaves only one possible solution on most microcontrollers: bit banging.
This means that the micro has to fetch the next bit, figure out if it’s high or low and then set the GPIO pin low at the correct time, all within 1.25us.
The timing requirements of the protocol will actually be rather tough to come by for most 8 bit micros clocked at 16MHz (depending on architecture).
It’s almost impossible to bit bang the protocol without coding directly in assembly and optimizing the code for execution time and paying close attention to the number of instructions (which is what the Adafruit NeoPixel library is doing).
A different approach
Therefore I decided to try my luck using an STM32VL Discovery board to drive the LED. As I have mentioned earlier the protocol used is non standard. As we can see from the timing diagram above, it basically is a crude form of PWM (pulse width modulation) and I’m actually leveraging the PWM and DMA capabilities of the STM32 to generate the needed PWM signals.
The trick with DMA (direct memory access) is that a number of data bytes (in this case a buffer) can be transferred from a memory location to the compare register of a timer without CPU intervention. The DMA controller will listen for an event, in this case the counter register reaching the compare value (i.e. the PWM pin going low) and then send the next byte to the timer’s compare register.
It might seem a bit overkill at first but the cool thing about DMA is that the compare register gets updated with a new value before the current PWM cycle is over, therefore the next PWM cycle will already use an update compare value and there will be no duplication of bits due to the CPU not being able to keep up with the timer.
This is an abstract of what my code is doing:
- Bring the 8 bit R, G and B values into the correct order (LED wants G-R-B)
- Figure out the bit sequence from the ordered bytes and create a buffer with 24 bytes of compare values to give the correct pulse width
- Append a number of 0 bytes to the buffer in order to create 50us dead time between data packets (pulse width = 0)
- Configure the DMA buffer size, memory location of the buffer and enable DMA channel for the timer
- Start the timer configured to create an 800kHz PWM signal and wait until the DMA buffer is empty
- Stop the timer and return to main program
The code can be found on GitHub as always.
Thanks for reading!