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.
This code is tested with the RGB NeoPixel linked below. The topic of neopixel timing is actually quite complex, and this program may not work with all kinds of neopixels.
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.



# 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. We are choosing zeros as <312ns hi, 936 lo> # and ones as <700 ns hi, 556 ns lo>. # The first two instructions always run while only one of the two final # instructions run per bit. We start with the low period because it can be # longer while waiting for more data. program = """ .program ws2812 .side_set 1 .wrap_target bitloop: out x 1 side 0 [6]; Drive low. Side-set still takes place before instruction stalls. jmp !x do_zero side 1 [3]; Branch on the bit we shifted out previous delay. Drive high. do_one: jmp bitloop side 1 [4]; Continue driving high, for a one (long pulse) do_zero: nop side 0 [4]; Or drive low, for a zero (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=12_800_000, # to get appropriate sub-bit times in PIO program 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)
Exactly how this program transmit data? Each original bit is sent in approximately 1250 nanoseconds. In the transmission program, 1250 nanoseconds are divided into three unequal portions.
To transmit a "0", the pin is set HIGH during the first portion, and then LOW during the other portions.
To transmit a "1", the pin is set HIGH during the first two porttions and low during the last portion.
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.
This is the most compatible timing for NeoPixels that we could find, and it is believed to work with all pixels and strips sold by Adafruit. It is more compatible than the "equal thirds" timing that many sources (including older versions of this page!) describe.
A medium delay (approximately 300 microseconds) 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 [6]; Drive low. Side-set still takes place before instruction stalls. jmp !x do_zero side 1 [3]; Branch on the bit we shifted out previous delay. Drive high. do_one: jmp bitloop side 1 [4]; Continue driving high, for a one (long pulse) do_zero: nop side 0 [4]; Or drive low, for a zero (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 16 clocks before the execution returns to bitloop
.
sm = rp2pio.StateMachine( assembled, frequency=12_800_000, # to get appropriate sub-bit times in PIO program 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 (16).
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 "most 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 12.8MHz a slightly different value 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.
Page last edited January 22, 2025
Text editor powered by tinymce.