The following example implements a NeoPixel-compatible class that performs the actual writing of data to the LEDs "in the background", allowing your program to continue processing while LED data is being transmitted. Background PIO requires CircuitPython 7.3 or later.

It's designed as a library you can incorporate into your own program, or as a demo you can load on an Adafruit MacroPad. You can also modify the demo to work on other devices or with different numbers of NeoPixels.

The example is designed to be used as a library: Copy it to CIRCUITPY and use from pioasm_neopixel_bg import NeoPixelBackground to import it. If placed on a MacroPad as code.py, it shows a demo. Adapt the demo to another device by modifying the connection used (board.NEOPIXEL) and the number of LEDs (12) to match your setup.

# SPDX-FileCopyrightText: 2022 Jeff Epler, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

"""Demonstrate background writing with NeoPixels

The NeoPixelBackground class defined here is largely compatible with the
standard NeoPixel class, except that the ``show()`` method returns immediately,
writing data to the LEDs in the background, and setting `auto_write` to true
causes the data to be continuously sent to the LEDs all the time.

Writing the LED data in the background will allow more time for your
Python code to run, so it may be possible to slightly increase the refresh
rate of your LEDs or do more complicated processing.

Because the pixelbuf storage is also being written out 'live', it is possible
(even with auto-show 'false') to experience tearing, where the LEDs are a
combination of old and new values at the same time.

The demonstration code, under ``if __name__ == '__main__':`` is intended
for the Adafruit MacroPad, with 12 NeoPixel LEDs. It shows a cycling rainbow
pattern across all the LEDs.
"""

import struct
import adafruit_pixelbuf
from rp2pio import StateMachine
from adafruit_pioasm import Program

# Pixel color order constants
RGB = "RGB"
"""Red Green Blue"""
GRB = "GRB"
"""Green Red Blue"""
RGBW = "RGBW"
"""Red Green Blue White"""
GRBW = "GRBW"
"""Green Red Blue White"""

# NeoPixels are 800khz bit streams. We are choosing zeros as <312ns hi, 936 lo>
# and ones as <700 ns hi, 556 ns lo>.
_program = Program(
    """
.side_set 1 opt
.wrap_target
    pull block          side 0
    out y, 32           side 0      ; get count of NeoPixel bits

bitloop:
    pull ifempty        side 0      ; drive low
    out x 1             side 0 [5]
    jmp !x do_zero      side 1 [3]  ; drive high and branch depending on bit val
    jmp y--, bitloop    side 1 [4]  ; drive high for a one (long pulse)
    jmp end_sequence    side 0      ; sequence is over

do_zero:
    jmp y--, bitloop    side 0 [4]  ; drive low for a zero (short pulse)

end_sequence:
    pull block          side 0      ; get fresh delay value
    out y, 32           side 0      ; get delay count
wait_reset:
    jmp y--, wait_reset side 0      ; wait until delay elapses
.wrap
        """
)


class NeoPixelBackground(  # pylint: disable=too-few-public-methods
    adafruit_pixelbuf.PixelBuf
):
    def __init__(
        self, pin, n, *, bpp=3, brightness=1.0, auto_write=True, pixel_order=None
    ):
        if not pixel_order:
            pixel_order = GRB if bpp == 3 else GRBW
        elif isinstance(pixel_order, tuple):
            order_list = [RGBW[order] for order in pixel_order]
            pixel_order = "".join(order_list)

        byte_count = bpp * n
        bit_count = byte_count * 8
        padding_count = -byte_count % 4

        # backwards, so that dma byteswap corrects it!
        header = struct.pack(">L", bit_count - 1)
        trailer = b"\0" * padding_count + struct.pack(">L", 3840)

        self._sm = StateMachine(
            _program.assembled,
            auto_pull=False,
            first_sideset_pin=pin,
            out_shift_right=False,
            pull_threshold=32,
            frequency=12_800_000,
            **_program.pio_kwargs,
        )

        self._first = True
        super().__init__(
            n,
            brightness=brightness,
            byteorder=pixel_order,
            auto_write=False,
            header=header,
            trailer=trailer,
        )

        self._auto_write = False
        self._auto_writing = False
        self.auto_write = auto_write

    @property
    def auto_write(self):
        return self._auto_write

    @auto_write.setter
    def auto_write(self, value):
        self._auto_write = bool(value)
        if not value and self._auto_writing:
            self._sm.background_write()
            self._auto_writing = False
        elif value:
            self.show()

    def _transmit(self, buf):
        if self._auto_write:
            if not self._auto_writing:
                self._sm.background_write(loop=memoryview(buf).cast("L"), swap=True)
                self._auto_writing = True
        else:
            self._sm.background_write(memoryview(buf).cast("L"), swap=True)


if __name__ == "__main__":
    import board
    import rainbowio
    import supervisor

    NEOPIXEL = board.NEOPIXEL
    NUM_PIXELS = 12
    pixels = NeoPixelBackground(NEOPIXEL, NUM_PIXELS)
    while True:
        # Around 1 cycle per second
        pixels.fill(rainbowio.colorwheel(supervisor.ticks_ms() // 4))

Let's focus on some specific parts of the program. First, take a look at the PIO program itself. This program first fetches the number of bits of data to be sent to the neopixel. Then, while there are still bits left to send, it performs a bit transmission just like we already saw for NeoPixels in this guide. Finally, it performs a delay loop to ensure that multiple data transmissions have a gap between them, as required by the NeoPixel protocol.

_program = Program(
    """
.side_set 1 opt
.wrap_target
    pull block          side 0
    out y, 32           side 0      ; get count of NeoPixel bits

bitloop:
    pull ifempty        side 0      ; drive low
    out x 1             side 0 [5]
    jmp !x do_zero      side 1 [3]  ; drive high and branch depending on bit val
    jmp y--, bitloop    side 1 [4]  ; drive high for a one (long pulse)
    jmp end_sequence    side 0      ; sequence is over

do_zero:
    jmp y--, bitloop    side 0 [4]  ; drive low for a zero (short pulse)

end_sequence:
    pull block          side 0      ; get fresh delay value
    out y, 32           side 0      ; get delay count
wait_reset:
    jmp y--, wait_reset side 0      ; wait until delay elapses
.wrap
        """
)

Next, a NeoPixel-like class is implemented. It needs to send the bit count first, then the pixel data, and finally the delay amount; for this purpose, the header and trailer arguments for PixelBuf are used.

Because of how the pixel data is organized, it has to be "byte-swapped" before sending to the NeoPixels. Logic in _transmit also enables background writing if auto_write is requested, otherwise it writes just once.

def _transmit(self, buf):
    if self._auto_write:
        if not self._auto_writing:
            self._sm.background_write(loop=memoryview(buf).cast("L"), swap=True)
            self._auto_writing = True
    else:
        self._sm.background_write(memoryview(buf).cast("L"), swap=True)

When used as a main program, it simply shows a rainbow on the configured NeoPixel LEDs:

NEOPIXEL = board.NEOPIXEL
NUM_PIXELS = 12
pixels = NeoPixelBackground(NEOPIXEL, NUM_PIXELS)
while True:
    # Around 1 cycle per second
    pixels.fill(rainbowio.colorwheel(supervisor.ticks_ms() // 4))

Finish up our tour of the background_write feature with an example for controlling a large number of RC Servo motors with a single State Machine.

This guide was first published on Mar 03, 2021. It was last updated on Mar 29, 2024.

This page (Advanced: Using PIO to drive NeoPixels "in the background") was last updated on Mar 29, 2024.

Text editor powered by tinymce.