If you are using a board with a built-in NeoPixel, the example on this page will use it. If you have another board, such as a Raspberry Pi Pico, you'll need to connect it to an external NeoPixel:

  • Connect Pico VSYS to NeoPixel + or VCC
  • Connect Pico GND to NeoPixel GND
  • Connect Pico GP16 to NeoPixel In or DIN

You can also select whatever pin you like and change the CircuitPython code accordingly.

You can use any NeoPixel (including a NeoPixel strip) but this code example only drives a single NeoPixel at a time.

Parts

If you have the Raspberry Pi Pico or another RP2040 board which does not have a built-in NeoPixel, you'll need the parts below to follow this example. Adafruit's RP2040-based boards include a built-in NeoPixel.

This is the easiest way possible to add small, bright RGB pixels to your project. We took the same technology from our Flora NeoPixels and made them breadboard friendly, with two rows...
$7.95
In Stock
This cute 3.2″ × 2.1″ (82 × 53mm) solderless half-size breadboard has four bus lines and 30 rows of pins, our favorite size of solderless breadboard for...
Out of Stock
75 flexible stranded core wires with stiff ends molded on in red, orange, yellow, green, blue, brown, black and white. These are a major improvement over the "box of bent...
$4.95
In Stock

Full Code Listing

# SPDX-FileCopyrightText: 2021 Scott Shawcroft, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import time
import rp2pio
import board
import microcontroller
import adafruit_pioasm

# NeoPixels are 800khz bit streams. Zeroes are 1/3 duty cycle (~416ns) and ones
# are 2/3 duty cycle (~833ns).
program = """
.program ws2812
.side_set 1
.wrap_target
bitloop:
  out x 1        side 0 [1]; Side-set still takes place when instruction stalls
  jmp !x do_zero side 1 [1]; Branch on the bit we shifted out. Positive pulse
do_one:
  jmp  bitloop   side 1 [1]; Continue driving high, for a long pulse
do_zero:
  nop            side 0 [1]; Or drive low, for a short pulse
.wrap
"""

assembled = adafruit_pioasm.assemble(program)

# If the board has a designated neopixel, then use it. Otherwise use
# GPIO16 as an arbitrary choice.
if hasattr(board, "NEOPIXEL"):
    NEOPIXEL = board.NEOPIXEL
else:
    NEOPIXEL = microcontroller.pin.GPIO16

sm = rp2pio.StateMachine(
    assembled,
    frequency=800000 * 6,  # 800khz * 6 clocks per bit
    first_sideset_pin=NEOPIXEL,
    auto_pull=True,
    out_shift_right=False,
    pull_threshold=8,
)
print("real frequency", sm.frequency)

for i in range(30):
    sm.write(b"\x0a\x00\x00")
    time.sleep(0.1)
    sm.write(b"\x00\x0a\x00")
    time.sleep(0.1)
    sm.write(b"\x00\x00\x0a")
    time.sleep(0.1)
print("writes done")

time.sleep(2)

Code Walk-through

Exactly how does a NeoPixel expect to receive data? Here's how. Each original bit needs to be sent in approximately 1200 microseconds. In the transmission program, 1200 microseconds are divided into three equal portions.

To transmit a "0", the pin is set HIGH during the first third, and then LOW during the other two thirds.

To transmit a "1", the pin is set HIGH during the first two thirds and low during the last third.

This is repeated 24 times for each RGB NeoPixel, and a strip repeats these 24 cycles once for each pixel—once a pixel has received its own data, it sends any more data to the next pixel using its Data Out (DOUT) pin.

A short delay without any pulses makes the pixels ready to receive fresh values.

program = """
.program ws2812
.side_set 1
.wrap_target
bitloop:
  out x 1        side 0 [1]; Side-set still takes place when instruction stalls
  jmp !x do_zero side 1 [1]; Branch on the bit shifted out. Positive pulse
do_one:
  jmp  bitloop   side 1 [1]; Continue driving high, for a long pulse
do_zero:
  nop            side 0 [1]; Or drive low, for a short pulse
.wrap
"""

The first new concept is "side set". So far, PIO has changed a pin value using "out"; with "side set" PIO can change the value of a pin while also doing some other activity. This is from the English idiom "do something on the side", which means in addition to one's regular job or duties.

When an instruction has side 0 next to it, the corresponding output is set LOW, and when it has side 1 next to it, the corresponding output is set HIGH. There can be up to 5 side-set pins, in which case side N is interpreted as a binary number.

The first instruction, out x 1, transfers the next NeoPixel data bit into the x register. It also ensures the data out pin is LOW until the data bit is available. This creates the delay necessary between refreshes of the NeoPixel strip, as well as the LOW period at the end of each transmitted bit.

Because the state machine is created with auto_pull=True, there's no need for a pull instruction.

Next, jmp !x do_zero side 1 sets the output pin HIGH and then continues either at the next line (if X is nonzero) or at do_zero (if it is zero). !x means "if X is zero" and is taken from the syntax of C/C++/Arduino code.

Depending whether execution continues at do_one or do_zero, the middle portion of the signal is transmitted as HIGH or LOW. nop indicates that "no operation" (except the side-set operation) is performed by the instruction.

Either way, PIO continues from bitloop:, setting the output pin LOW and then getting the next pixel data into X. If there's more pixel data waiting to be transmitted, it will continue immediately on to the next lines; otherwise, it will wait until more pixel data is available.

If you consider each possible path through a single loop (X is zero; X is nonzero) and count the number of instructions plus the number of [N] delays, you will see that there are 6 clocks before the execution returns to bitloop.

sm = rp2pio.StateMachine(
    assembled,
    frequency=800000 * 6,  # 800khz * 6 clocks per bit
    first_sideset_pin=NEOPIXEL,
    auto_pull=True,
    out_shift_right=False,
    pull_threshold=8,
)
print("real frequency", sm.frequency)

Accordingly, the StateMachine is constructed with a requested frequency based on the desired bit rate (800kHz) times the number of cycles per bit (6).

As discussed above, since the out x instruction should wait until data is available and then automatically pull it in from CircuitPython, auto_pull=True is specified.

out_shift_right controls how multi-bit values are sent in. NeoPixels expect the "least significant bits" first, which is the order you get when out_shift_right=False.

first_sideset_pin controls the pin(s) which are set by side-set operations.

pull_threshold controls the minimum number of bits that have to be available for an auto-pull to complete. Since neopixels are a sequence of bytes, set the value to 8, the number of bits in a byte.

PIO can't exactly provide any requested frequency. In this case, instead of the exact value 4800000Hz a slightly different value of 4799760Hz is provided. This is well within the tolerance of NeoPixels. When a device has to be controlled at a very specific frequency, it's important to check that your program is running at a rate that is close enough to the required rate.

for i in range(30):
    sm.write(b"\x0a\x00\x00")
    time.sleep(0.1)
    sm.write(b"\x00\x0a\x00")
    time.sleep(0.1)
    sm.write(b"\x00\x00\x0a")
    time.sleep(0.1)

It's finally time to light up your NeoPixel by directly specifying the bytes to send to it. For most RGB NeoPixels, this program will send a sequence of green, red, and blue pixels, with 30 repetitions.

On the RP2040, the standard neopixel module works very much in the way shown here, but it's ready to work with the other CircuitPython libraries you may already know and love, like the LED animations library.

For a more sophisticated example of driving 8 NeoPixel strips from just 3 GPIO pins using PIO, there's a dedicated guide.

This guide was first published on Mar 03, 2021. It was last updated on 2021-03-03 10:39:28 -0500.

This page (Using PIO to drive a NeoPixel) was last updated on Apr 17, 2021.

Text editor powered by tinymce.