How to Speak NeoPixel

Adafruit calls them NeoPixels, but their real name is WS2812 or SK6812. They’re handy little RGB LEDs with built-in controllers.

They use a single-wire anisochronous self-clocking signal, so controlling them requires some precise timing. There’s an easy-to-use Arduino library available, but I wanted to use a PIC16F1455, so I had to get into the protocol.

Each device expects 24 bits: 8 bits’ green, then red, then blue, all most-significant-bit-first. Each device has a data input and a data output, so multiple devices can be connected in series and the entire chain controlled with one signal. Each device consumes its 24 bits and then any additional data is forwarded to the next in line.

The signal consists of one high voltage pulse per bit, then an extended low voltage to latch and update the display. According to the WS2812 datasheet, the signal format is:

The datasheets are kinda crummy. Notice that the waveforms aren’t to scale and the two cycles aren’t even the same length. This guy tipped me off that the datasheets can’t be trusted, and he found that all that really matters is the duration of the high voltage. A short pulse is “0” and a longer pulse is “1”. The parts I have count 0.5uS or less as a short pulse, and they accept anything down to 0.1uS (the fastest pulse I can get from my PIC). For the long pulse my parts work well with 0.6uS or longer.

The low voltage time doesn’t really matter as long as the frequency is correct. The datasheets suggest an 800KHz signal. My parts work properly in the range 170KHz up to around 1MHz.

The devices themselves offer a second opinion on the true signal format; when they forward additional data they do in fact re-shape the signal. Feeding the WS2812 with pulses that are very short and very long produces this:

According to the WS2812 itself, a short pulse is 0.3uS and a long pulse is 0.7uS. There’s no way to get a second opinion on the frequency; the output just matches the input pulse-for-pulse with a delay of about 0.2uS.

For a PIC16 running at 48MHz, this code works well for me:

void neopix_byte_out(uint8_t x) {
    for(uint8_t i = 1; i != 0; i <<= 1) {
        if(x & 0x80) {
            NEOPIX_OUT_PIN = 1;
            NOP();
            NOP();
            NOP();
            NOP();
            NOP();
            NOP();
            NEOPIX_OUT_PIN = 0;
        } else {
            NEOPIX_OUT_PIN = 1;
            NEOPIX_OUT_PIN = 0;
        }
        x <<= 1;
    }
}

void neopix_out(rgb_t * rgb) {
    neopix_byte_out(rgb->g);
    neopix_byte_out(rgb->r);
    neopix_byte_out(rgb->b);
}

The goofy loop produces the smallest and fastest code I can get out of the free XC8 compiler (optimizations crippled). This puts out up to 600Kbps.

This entry was posted in Uncategorized and tagged . Bookmark the permalink.