The next example program introduces new concepts: the pull instruction, which receives data from the CircuitPython program; and registers, which are similar to variables in that they can store values and some simple operations can be performed on those values. However, the uses of PIO registers are much more restricted than the use of variables in Python.

# SPDX-FileCopyrightText: 2021 Jeff Epler, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
#
# Adapted from the example https://github.com/raspberrypi/pico-examples/tree/master/pio/pio_blink

import array
import time
import board
import rp2pio
import adafruit_pioasm

blink = adafruit_pioasm.assemble(
    """
.program blink
    pull block    ; These two instructions take the blink duration
    out y, 32     ; and store it in y
forever:
    mov x, y
    set pins, 1   ; Turn LED on
lp1:
    jmp x-- lp1   ; Delay for (x + 1) cycles, x is a 32 bit number
    mov x, y
    set pins, 0   ; Turn LED off
lp2:
    jmp x-- lp2   ; Delay for the same number of cycles again
    jmp forever   ; Blink forever!
"""
)


while True:
    for freq in [5, 8, 30]:
        with rp2pio.StateMachine(
            blink,
            frequency=125_000_000,
            first_set_pin=board.LED,
            wait_for_txstall=False,
        ) as sm:
            data = array.array("I", [sm.frequency // freq])
            sm.write(data)
            time.sleep(3)
        time.sleep(0.5)

Code Walkthrough

pull block    ; These two instructions take the blink duration
    out y, 32     ; and store it in y

The instruction "pull block" says to wait until a value sent from CircuitPython is available ("block"), and then to pull that value into a holding area known as OSR, or Output Shift Register. Then, all 32 bits of OSR are stored in the register called Y. Later, the CircuitPython program will send in a number that represents how long the LED spends in an on or off state, so remember that this is what the register Y now holds.

forever:
    mov x, y
    set pins, 1   ; Turn LED on
lp1:
    jmp x-- lp1   ; Delay for (x + 1) cycles, x is a 32 bit number

The next few lines' purpose is to turn the LED on, then wait for the desired length of time.

Working up from the bottom of this section of code, the last two lines create a loop that delays for (x+1) cycles. lp1: is a label, and a jmp instruction can skip to it instead of continuing to the next instruction. In this case, the jump is conditional on x--, which means "if X is not zero, jump to lp1. In any case, decrease X by 1." Because the delay numbers are large and because the program will to change the delay value without changing the PIO program itself, it cannot use the [#] notation to delay as in the previous program.

The set instruction to turn on the LED should be familiar by now.

The mov instruction takes the value in Y and copies it to X. If not, and the loop used jmp y--, then it would lose the original delay value. But the value is needed each time the program has a delay. Happily, there are two register X and Y so the program can just take a copy of the original delay value each time.

Remember that there's a forever: label here, it will be used later.

mov x, y
    set pins, 0   ; Turn LED off
lp2:
    jmp x-- lp2   ; Delay for the same number of cycles again
    jmp forever   ; Blink forever!

The next block is very much like the previous block, except that set is used to turn the LED off before the delay.

The final line, jmp forever, sends us back to the first delay. If it instead relied on the automatic wrap back to the first instruction, there would only be a single on-off blink before the program went back to the pull block instruction and waited for a new blink duration to be sent in, which would not give the desired result.

while True:
    for freq in [5, 8, 30]:
        with rp2pio.StateMachine(
            blink,
            frequency=125_000_000,
            first_set_pin=board.LED,
            wait_for_txstall=False,
        ) as sm:
            data = array.array("I", [sm.frequency // freq])
            sm.write(data)
            time.sleep(3)
        time.sleep(0.5)

The Python forever-loop repeatedly cycles through the frequencies 5, 8, and 30.

For each frequency, it creates a state machine with our program, calculates and send the required delay value to it, and waits 3 seconds. Then, before continuing with the next blink pattern, it delays a half a second.

Of course, your CircuitPython program doesn't need to sleep while PIO is making the LED blink, it could be doing calculations, updating an LCD display, reading button presses. The PIO program keeps running independently of what CircuitPython is doing.

In many applications of PIO, such as sending data out to NeoPixels, a write call needs to wait until the PIO program has completed. Since PIO programs run endlessly, there needs to be some definition of "completed". The usual definition is "the PIO program (through a pull instruction) requested fresh data from CircuitPython, but none was available". This is called a "transmit stall" or "txstall". Thus, the line wait_for_txstall=False means that CircuitPython does not wait for this condition before the sm.write(data) returns.

This guide was first published on Mar 03, 2021. It was last updated on May 10, 2022.

This page (Using PIO to blink a LED quickly or slowly) was last updated on Sep 23, 2023.

Text editor powered by tinymce.