Controlling WS2812 LEDs from Linux

This weekend hack is nothing exciting, but it has been the first time since years that I picked up a soldering iron so I found it worthwile to document here. The project is a simple ATMega328-based circuit and firmware that allows you to control an array of RGB LEDs from your computer.

The WS2812 driver circuit is controlled via UART from a PyQT GUI.

The WS2812-type is a family of RGBs LEDs that contain a small built-in IC which is running the PWM for the three (red/green/blue) internal diodes. You can set the LEDs color by talking to the built-in IC using a single-wire interface with a fixed 800kHz clock.

Generating this data signal requires precise timing on the order of a few nanoseconds, which is not generally something you can do from a linux system — not even from kernel land. So it makes sense to offload this task to a separate real-time co-processor and then send display state updates from linux to the co-processor using an already supported communications channel like a serial console.

Of course there are already tons of existing documentation and implementations of the WS2812 protocol for all kinds of low-level computing platforms on the web. However, you don't learn anything from using somebody else's code and I figured it would be nice to do something that doesn't run on linux for once so I decided to build my own little driver board from scratch.

The driver board is based on an ATMega328p microcontroller. The chip runs a small C program that listens for display updates on the UART/serial port and generates the WS2812 output signal on one of the I/O pins. It can be connected to any computer using a UART-to-USB cable, which will register as a character device on the computer. So sending display updates to the driver board is as simple as opening a file and writing data into it.

The circuitry on the board is pretty much the minial schematic required to get the ATMega chip running at 16MHz, taken more or less directly from the data sheet. I built it up on a small (3x7cm) perfboard instead of buying and waiting for a professional PCB to be manufactured. All components are standard parts and are easily available for a cost of less than 5€.

The firmware also turned out to be pretty small and simple. My first plan was to use a hardware interrupt for receiving data from the UART and a timer interrupt to generate the LED data signal. Alas, a single symbol of the WS2812 data signal takes less than 10 cycles on the 16MHz MCU, which doesn't leave any time for handling interrupts while data is being written to the LEDs.

So without the ability to drive everything from interrupts, the firmware instead consists of a main loop that repeatedly calls two routines: One routine reads the next control packet from the serial port using busy polling. Once a full control packet is read, a second routine is invoked that sends the color information out to the LEDs using another busy loop timed with NOP instructions.

With this design, the firmware can't react to new information on the UART while the LEDs are being refreshed, which puts a limit on the maximum allowable baud rate on the UART (since we don't want to miss any bits). The baud rate limit depends on the number of LEDs that are being driven driving, since more LEDs will result in a longer pause while the LEDs are being refreshed. I currently use 38.4kb/s, which allows controlling 24 daisy-chained LEDs at roughly 500FPS with 24 bits color depth.

A closeup of the circuit on a protoype board.

Just in case you want to build one of these too and want it to be exactly the same as mine, here is another closeup of top and bottom views. The schematic, list of parts I used and the firmware with a bunch of example python scripts are available on Github.