Driving the WS2812 RGB LED Array

WS2812 60-LED strip

 

The WS2812, sometimes known as a NeoPixel is an RGB LED with a built in controler IC. They can be purchased individually, or connected together into various shapes, such as the 60 LED, 1 metre strip shown above.

Each LED has pins for +5Volt Power, Ground, Serial Data In and Serial Data Out.  By connecting the data output pin of one LED to the data input of the next, it is possible to daisy chain them to almost any length.

Earlier PIC chips, such as the PIC16F627, could not drive the WS2812 due to their slower speed and limited memory, but – as you will see below – its easy with the LPLC.

 

Serial Interface

A single pin is all that is needed to send data to the entire LED array. 3 bytes of data (0ne byte for Green, Red and Blue) must be output serially for each LED, so a chain of 60 LEDs needs a total of 180 bytes of data (1440 bits).

 

ws2812-waveform

A zero bit is output by setting the pin high for 350 nano seconds and low for 900.

A one bit is output by setting the pin high for approximately 900 nano seconds and low for 350.

The time which the pin is high is critical and must be accurate to +/- 150 ns. The low time does not appear to matter quite so much, but should be less than a few micro seconds, or the controller chips will treat it as a reset signal.

 

Electrical Connections

The LED strip has three wires:

  • +5 Volt Power
  • Ground
  • Data

Each LED may consume up to 60mA (20mA for Red, Green and Blue), so with every LED showing maximum white, a 60 LED array may use up to 3.6Amps. If you don’t need full brightness (And these things are BRIGHT!), 2Amps will probably suffice.

The power supply must not exceed 6 volts, or the driver chips may be permanently damaged.

The LPLC requires a +3.3V DC supply, so unless you are supplying it from a seperate power supply you will need a low-dropout voltage regulator. I used one of these from Ebay, but you could of course build your own using something like the MCP1702-3302E/TO voltage regulator chip.

Ideally there should be a buffer in the data line to boost the 3.3v signal from the LPLC up to the 5v CMOS levels used by the LED strip. In practice it seems to work fine without it.

 

Programming

Because of it’s time-critical nature, writing the serial data to the LEDs is done in PIC assembler language. Everything else is in fairly simple C code.  The code can be broken down into sections most of which could be re-used in other projects.

 

1. Configuration

This bit of gobbledygook is copied from Mike Hibbert’s templates. It sets the PIC to use the internal 48MHz clock. Most PIC instructions need 4 clock cycles, so it will process 12 instructions per microsecond – worth knowing when we come to timing the serial data.

#pragma config WDTEN = OFF
#pragma config XINST = OFF
#pragma config IESO = OFF
#pragma config FCMEN = OFF
#pragma config DSWDTEN = OFF
#pragma config OSC = INTOSCPLL
#pragma config CFGPLLEN = ON
#pragma config PLLDIV = 2
#pragma config PLLSEL = PLL96
#pragma config CP0 = OFF
#pragma config WDTPS = 1

 

2. Initialise the I/O Pins

We are only using a single pin to drive the LED array, but it’s always a good idea to set any unused PIC pins to outputs to reduce their susceptibility to static. We also disable the Analogue to Digital Converters so they don’t interfere with the Digital IO. I always include these lines at the start of every program.

// A-D Configuration : Disable and set all pins as digital I/O

ANCON0 = 0b11111111;
ANCON1 = 0b00011111;

// Set all outputs low

LATA  = 0b00000000;
LATB  = 0b00000000;
LATC  = 0b00000000;

// Enable all pins as outputs

TRISA  = 0b00000000;
TRISB  = 0b00000000;
TRISC  = 0b00000000;

 

3. Writing to the LEDs

A new template file, WS2812.c has been produced to write to the LED strip.

This uses a mix of C code and assembly language. C code is perfectly adequate for most applications (and an awful lot easier to write), but doesn’t allow us to fine-tune the timing of the LED serial data.

Variables are shared between the C code and assembly language by storing them at a fixed address:

unsigned char ledData[LED_ARRAY_SIZE] @0x0c03;

The C Compiler stores Unsigned char variables as a single byte. The LED data is stored as an array of 180 bytes (3 bytes of Green, Red and Blue values per LED).

Because the assembly code uses byte variables, we are limited to a maximum array size of 255 bytes, or 85 LEDs.

 

4. The main program

This is where you can get creative!

For example the following code lights all LEDs bright red:

SetAllRGB(255,0,0);
WS2812Write();

And this displays a rainbow:

unsigned char color = 0;
for (unsigned char led = 0; led < 60; led++)
{
SetColor(led, color);
color += 4;
}
WS2812Write();

 

The final results

There is a short video of it in action on YouTube : Click Here

Here is an oscilloscope screenshot showing the actual timings.

Part of the waveform being generated by the LPLC

Part of the waveform being generated by the LPLC

 

A “One” is high for 918 ns and low for 328 ns. A “Zero” is high for 320 ns and low for 915 ns – pretty close to the ideal timings.

There is an additional delay of approximately 500 ns between bytes.

 

The full source code for this project can be downloaded from here : WS2812.X

Gary Bleads G0HJQ

 

 

 

 

 

2 Comments

  1. Hey Gary,

    Your code helped in a great deal. Thanks a tonne.
    Btw i converted your ASM code to C code which is as follows:
    void WS2812WriteC(void)
    {
    // The data pin must be low for at least 50uS to reset the LED drivers
    PIN_WS2812_LATCH = 0;
    __delay_us(50);

    tmpData = 0;
    ledCount = LED_ARRAY_SIZE;
    unsigned char i = 0;

    do
    {
    tmpData = ledData[i++];
    bitCount = 8;

    do
    {
    LATBbits.LATB0 = 1;

    if(tmpData & 0x80)
    {
    tmpData = tmpData << 1;
    Nop();
    Nop();
    Nop();
    Nop();
    Nop();
    Nop();
    LATBbits.LATB0 = 0;
    }
    else
    {
    LATBbits.LATB0 = 0;
    tmpData = tmpData << 1;
    Nop();
    Nop();
    Nop();
    Nop();
    Nop();
    Nop();

    }
    }while(–bitCount!=0);
    }while(–ledCount!=0);

    }

    Please correct me if i am wrong.

    Thanks once again.

    Regards,
    Nelson

    Reply
    • Hi Nelson,

      That’s great, glad my code was of use to you.

      Thanks for the c++ translation. Have you tried it?

      I had not thought the standard c compiler would have produced good enough code to give predictable timings – unless you purchase the professional version, it inserts random instructions to slow things down but if you have tried it, that’s great. It will save me a lot of work.

      I’m on holiday at the moment, but will try it out when I get home in a couple of weeks and check the timings on my oscilloscope.

      Reply

Submit a Comment

Your email address will not be published. Required fields are marked *