Let's review how data is transmitted to a NeoPixel: A "1"-bit is sent as a LONGER period of HIGH voltage followed by a SHORTER period of LOW voltage. A "0"-bit is sent as a SHORTER period of HIGH voltage followed by a LONGER period of LOW voltage.

The code on this page uses ratios of 1:2 and 2:1. This isn't the most compatible waveform possible, but it works on most strips that identify themselves as NeoPixel-compatible.

Since each pixel is made up of 24 bits of red, green, and blue data, this cycle repeats 24 times for each pixel, and 720 times for a strand of 30 pixels. All together, that takes just under 1 millisecond.

When running CircuitPython, the neopixel_write module has optimized code (often in assembler) to produce exactly the required waveform. On the Raspberry Pi Pico with RP2040 microcontroller, this is actually done with the PIO programmable I/O block.

However, the PIO is so powerful that it laughs at the simplicity of running a single LED strip and boasts that (with the addition of the lowly 74HC595 shift register) it can run 8 LED strips from just 3 I/O pins.

First, let's look at the code that works on the PIO programmable I/O block:

.program neopio
.side_set 2

.wrap_target
    set x, 7            side 2
    pull

bitloop0:
    set pins, 1         side 0
    jmp x--, bitloop0   side 1
    set x, 7            side 2
bitloop1:
    out pins, 1         side 0
    jmp x--, bitloop1   side 1
    set x, 7            side 2
bitloop2:
    set pins, 0         side 0
    jmp x--, bitloop2   side 1
.wrap

The code is divided into 3 loops (bitloop0, bitloop1, and bitloop2), plus a little housekeeping code. The full loop will run in about 1.25 microseconds, and transfers 1 bit for each of the 8 NeoPixel strips.

The register x is used as a loop variable. The loop instruction jmp x-- checks if x is zero, then updates x by subtracting 1, and depending on the previous value of x jumps to the given label (if nonzero) or continues to the next line (if zero). By storing 7 in x to begin with, each loop executes 8 times (this may make more sense to you if you're a robot)

In the first loop, the first instruction is "set pins, 1" which turns the first 'set pin' to HIGH.

In the second loop, the first instruction is "out pins, 1" which turns the first 'out pin' to either HIGH or LOW depending on the next bit of data sent to it from the data buffer in Python. Note that the 'set pin' and 'out pin' are actually the same pin in this case.

In the third loop, the first instruction is "set pins, 0" which turns the 'set pin' to LOW.

See how the structure of the three loops reflect the structure of a NeoPixel bit transmission? The first part is always HIGH, the second part depends on the data to be transmitted, and the third part is always LOW.

Next: to understand how each bit gets received by the shift register with concentration on the middle bitloop:

bitloop1:
    out pins, 1         side 0
    jmp x--, bitloop1   side 1
    set x, 7            side 2

This time, concentrate on the part of each line that says side N. This instruction updates the value of the side-set pins. There are two side-set pins, and the possible side values are:

  • 0: turn both pins LOW
  • 1: turn first pin HIGH and second pin LOW
  • 2: turn first pin LOW and second pin HIGH
  • 3: turn both pins HIGH (not used in this program)

Side-set pin 1 is connected to the shift register's clock pin, and side-set pin 2 is connected to the shift register's parallel load pin. Each of those functions is activated by a rising edge (from LOW to HIGH). This means that in the loop the data pin is updated first with its new value (out pins) with clock low (side 0) and then the clock pin is activated (side 1).

When the loop finishes, the parallel load pin is activated (side 2) and the 8 values just sent out appear on the 8 output pins of the '595.

This guide was first published on Feb 23, 2021. It was last updated on Mar 27, 2024.

This page (Code Walkthrough: PIO) was last updated on Mar 08, 2024.

Text editor powered by tinymce.