Skip to content

WS2812 PIO Driver

WS2812

PIO drive for WS2812-based LED strips.

Provides a class that uses the Raspberry Pico RP20204 PIO to implement a serial driver for WS2812-like LED strips. The LED pixel data is kept in an array that is passed to a function that copies the data to the PIO state machine where it is shifted out on a GPIO in the proper format.

Recommended usage::

# use state machine 0, and IO pin 16
ws2812 = WS2812(smid=0, pin=16)
...
# create 3 pixel array of red, green, blue
pixels = array.array("I", [0x00FF00, 0xFF0000, 0x0000FF])
# write the pixel data to the LED strip
ws2812.show(pixels)
...

Parameters:

Name Type Description Default
smid int

state machine number to use for PIO

required
pin int

GPIO pin number to use for WS2812 signal

required
Source code in ledstrip/ws2812_pio.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
class WS2812():
    """PIO drive for WS2812-based LED strips.

    Provides a class that uses the Raspberry Pico RP20204 PIO to implement a
    serial driver for WS2812-like LED strips. The LED pixel data is kept in an
    array that is passed to a function that copies the data to the PIO state
    machine where it is shifted out on a GPIO in the proper format.

    Recommended usage::

        # use state machine 0, and IO pin 16
        ws2812 = WS2812(smid=0, pin=16)
        ...
        # create 3 pixel array of red, green, blue
        pixels = array.array("I", [0x00FF00, 0xFF0000, 0x0000FF])
        # write the pixel data to the LED strip
        ws2812.show(pixels)
        ...

    :param smid: state machine number to use for PIO
    :param pin: GPIO pin number to use for WS2812 signal
    """

    def __init__(self, smid: int, pin: int) -> None:
        """Class constructor for WS2812."""

        # debug pin, if needed
        #self.debug_pin = Pin(23, Pin.OUT)
        #self.debug_pin.low()

        # create the state machine
        ws_pin = Pin(pin, Pin.OUT)
        # 64 ns, divider is 8
        self._sm = rp2.StateMachine(smid, ws2812_shifter, freq=15625000,
                          set_base=ws_pin, out_base=ws_pin)
        self._sm.active(1)

        # set up dma for state machine
        # code snippets from: https://docs.micropython.org/en/latest/library/rp2.DMA.html
        self._dma = rp2.DMA()
        pio_num = 0 if smid < 4 else 1
        dreq_idx = (pio_num << 3) + smid
        self.dmactrl = self._dma.pack_ctrl(size=2, inc_write=False, treq_sel=dreq_idx)

    def shutdown(self):
        """Halt the state machine.

        This will stop the state machine from running. Once this method is
        called, the object can no longer be used.
        """
        self._sm.active(0)

    def show(self, pixarray):
        """Send pixel data to ws2812 GPIO pin.

        Copies an array of pixel data to the WS2812 PIO driver. The pixel data
        is formatted as an iterable of 32-bit integers. The pixel data is 3
        colors, each 8-bits, in the lower 24 bits. The upper 8 bits are
        ignored. The ordering of RGB depends on the specific LEDs used so there
        is not a universal setting. However GRB is common.

        If the ordering is GRB:
        - ``0xFF0000`` is green
        - ``0x00FF00`` is red
        - ``0x0000FF`` is blue

        It is best to allocate an array or list for your pixel data at the
        beginning of the program and modify the contents, as opposed to
        creating a new list each time you want to write pixel data.

        You can use a regular python list, but micropython also provides an
        ``array`` type that is a C-like array and that may be more efficient.
        """
        #self.debug_pin.high()

        sm = self._sm
        xfer_count = len(pixarray)
        self._dma.config(read=pixarray, write=sm, count=xfer_count,
                         ctrl=self.dmactrl, trigger=True)
        while self._dma.active():
            pass

__init__(smid: int, pin: int) -> None

Class constructor for WS2812.

Source code in ledstrip/ws2812_pio.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def __init__(self, smid: int, pin: int) -> None:
    """Class constructor for WS2812."""

    # debug pin, if needed
    #self.debug_pin = Pin(23, Pin.OUT)
    #self.debug_pin.low()

    # create the state machine
    ws_pin = Pin(pin, Pin.OUT)
    # 64 ns, divider is 8
    self._sm = rp2.StateMachine(smid, ws2812_shifter, freq=15625000,
                      set_base=ws_pin, out_base=ws_pin)
    self._sm.active(1)

    # set up dma for state machine
    # code snippets from: https://docs.micropython.org/en/latest/library/rp2.DMA.html
    self._dma = rp2.DMA()
    pio_num = 0 if smid < 4 else 1
    dreq_idx = (pio_num << 3) + smid
    self.dmactrl = self._dma.pack_ctrl(size=2, inc_write=False, treq_sel=dreq_idx)

show(pixarray)

Send pixel data to ws2812 GPIO pin.

Copies an array of pixel data to the WS2812 PIO driver. The pixel data is formatted as an iterable of 32-bit integers. The pixel data is 3 colors, each 8-bits, in the lower 24 bits. The upper 8 bits are ignored. The ordering of RGB depends on the specific LEDs used so there is not a universal setting. However GRB is common.

If the ordering is GRB: - 0xFF0000 is green - 0x00FF00 is red - 0x0000FF is blue

It is best to allocate an array or list for your pixel data at the beginning of the program and modify the contents, as opposed to creating a new list each time you want to write pixel data.

You can use a regular python list, but micropython also provides an array type that is a C-like array and that may be more efficient.

Source code in ledstrip/ws2812_pio.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def show(self, pixarray):
    """Send pixel data to ws2812 GPIO pin.

    Copies an array of pixel data to the WS2812 PIO driver. The pixel data
    is formatted as an iterable of 32-bit integers. The pixel data is 3
    colors, each 8-bits, in the lower 24 bits. The upper 8 bits are
    ignored. The ordering of RGB depends on the specific LEDs used so there
    is not a universal setting. However GRB is common.

    If the ordering is GRB:
    - ``0xFF0000`` is green
    - ``0x00FF00`` is red
    - ``0x0000FF`` is blue

    It is best to allocate an array or list for your pixel data at the
    beginning of the program and modify the contents, as opposed to
    creating a new list each time you want to write pixel data.

    You can use a regular python list, but micropython also provides an
    ``array`` type that is a C-like array and that may be more efficient.
    """
    #self.debug_pin.high()

    sm = self._sm
    xfer_count = len(pixarray)
    self._dma.config(read=pixarray, write=sm, count=xfer_count,
                     ctrl=self.dmactrl, trigger=True)
    while self._dma.active():
        pass

shutdown()

Halt the state machine.

This will stop the state machine from running. Once this method is called, the object can no longer be used.

Source code in ledstrip/ws2812_pio.py
123
124
125
126
127
128
129
def shutdown(self):
    """Halt the state machine.

    This will stop the state machine from running. Once this method is
    called, the object can no longer be used.
    """
    self._sm.active(0)

ws2812_shifter()

Shift 24-bit LED values to a GPIO per the WS2812 protocol.

This is not a callable function.

The following describes module internals and is not part of the API.

This is an RP2040 PIO state machine program to shift 24-bit values per the WS2812 protocol. Pixel values are represented as 24-bit values, 8-bits each of RGB (order changes depending on the specific LED used). Bits are encoded with a varying pulse width - a wider pulse for a 1 and a shorter pulse for a 0. There is always a high period and a low period.

32-bits are pulled from the PIO TX FIFO. This is the pixel value that was written to the FIFO by the application. The lower 24-bits contain the pixel data. First the top 8 bits are discarded. Then the remaining 24-bits are considered one at a time.

First the output is driven high for a fixed period of time. This is the high time regardles whether the bit is 1 or 0. Then the output is driven low or high depending on the bit value, for a fixed amount of time. This is the variable section. Finally, the output is driven low for a fixed amount of time. This is the low time for both high and low bit values.

Once all 24-bits are shofted out, the cycle repeats and the next word is read from the FIFO. If there are no more data in the FIFO, then the state machine blocks with the output in a low state. This ensures the latching period occurs at the end of a set of pixels.

Source code in ledstrip/ws2812_pio.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW,
             fifo_join=rp2.PIO.JOIN_TX)
def ws2812_shifter():
    """Shift 24-bit LED values to a GPIO per the WS2812 protocol.

    *This is not a callable function.*

    **The following describes module internals and is not part of the API.**

    This is an RP2040 PIO state machine program to shift 24-bit values per the
    WS2812 protocol. Pixel values are represented as 24-bit values, 8-bits each
    of RGB (order changes depending on the specific LED used). Bits are encoded
    with a varying pulse width - a wider pulse for a 1 and a shorter pulse for
    a 0. There is always a high period and a low period.

    32-bits are pulled from the PIO TX FIFO. This is the pixel value that was
    written to the FIFO by the application. The lower 24-bits contain the pixel
    data. First the top 8 bits are discarded. Then the remaining 24-bits are
    considered one at a time.

    First the output is driven high for a fixed period of time. This is the
    high time regardles whether the bit is 1 or 0. Then the output is driven
    low or high depending on the bit value, for a fixed amount of time. This is
    the variable section. Finally, the output is driven low for a fixed amount
    of time. This is the low time for both high and low bit values.

    Once all 24-bits are shofted out, the cycle repeats and the next word is
    read from the FIFO. If there are no more data in the FIFO, then the state
    machine blocks with the output in a low state. This ensures the latching
    period occurs at the end of a set of pixels.
    """
    pull(block)                 # wait for next pixel value
    out(x, 8)                   # throw away upper 8 bits
    label("more_bits")
    set(pins, 1).delay(5)       # fixed high time
    out(pins, 1).delay(4)       # output set according to bit value
    set(pins, 0).delay(6)       # fixed low time
    jmp(not_osre, "more_bits")  # repeat until all 24 bits are shifted
    wrap()                      # back to top for next pixel value