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-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
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
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.