Microcontroller projects

Using PIO state machines on the Raspberry Pi Pico

last updated: 2025-04-05

Work in progress

Quick links

Intro

This is all the fault of Jean-Claude Feltes.

I really like Python, but struggled with Micropython, especially because it is slow. But the Pico has PIO state machine that are fast. The Pico is manufactured in Europe an has now even a RISC-V processor, so definitely I have to use it in parallel to the ESPs and the Teensys. And there is always the possibility to use C or C++ with the Pico SDK.

It is not very intuitive to program the PIO state machines. But Jean-Claude has already done a big part of the work and documented it (in German). Also the videos from David are really good and helpful: https://www.youtube.com/@LifewithDavid1/videos

I will try to focus here on some practical examples.

Some images used on this page are sourced from the Raspberry Pi Pico datasheet, which is licensed under the Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license. © 2012-2024 Raspberry Pi Ltd. You can view the full datasheet and license details here: https://datasheets.raspberrypi.com/pico/pico-datasheet.pdf.

PIO blocks, state machines and instructions

The original Pi PICO (RP2040) contains 2 identical PIO blocks with 4 state machines per block. The 4 state machines share the same memory!

PIO block level diagram
Click for a better view!

You find all the relevant commands to use with micropython here: https://docs.micropython.org/en/latest/library/rp2.html. Here is an little overview from the datasheet:

PIO instructions encoding
Click for a better view!

Jean-Claude created the following block diagram, helping to understand the different flows:

circuit


Toggle one pin (clock 1MHz)

There are only 9 commands, but they are powerful! and there is often more than one way to do things. Let's start with a simple clock signal with 1⁥MHz.

Set

    """ This code toggles a pin at 2MHz using a state machine
        to get a clock signal of 1MHz with the set command. """

    from time import sleep
    from machine import Pin
    from rp2 import PIO, StateMachine, asm_pio

    CLOCK_PIN = 14

    @asm_pio(set_init = PIO.OUT_LOW)
    def toggle_clock():
        set(pins, 1) # sets the pin defined with set_base
        set(pins, 0) # sets the pin low

    # Initialize the state machine
    sm0 = StateMachine(0, toggle_clock, freq = 2000000, set_base = CLOCK_PIN)

    sm0.active(1) # Activate the state machine to generate the clock signal
    sleep(5)      # Run the clock signal for 5 seconds
    sm0.active(0) # Deactivate the state machine

Important are the set_init and the set_base command to define the pin. We use state machine number 0. After some time (sleep()) we stop the state machine.

Sideset

It is possible to set pins side to a command. This is very helpful e.g. to create a clock signal simultaneously with a parallel output. Here we will use the no operation commmand (nop()does nothing but uses one clock cycle) with a sideset pin to create our clock. Important are the sideset_init and the sideset_base command to define the pin.

    """ This code toggles a pin at 2MHz using a state machine
        to get a clock signal of 1MHz with the set command. """

    from time import sleep
    from machine import Pin
    from rp2 import PIO, StateMachine, asm_pio

    CLOCK_PIN = 14

    # State machine outputs clock with 1MHz (2 clock cycles)
    @asm_pio(sideset_init = PIO.OUT_LOW)
    def toggle_clock():
        nop() .side(1)  # sets the pin defined with set_side
        nop() .side(0)  # sets the pin low

    # Initialize the state machine
    sm0 = StateMachine(0, toggle_clock, freq = 2000000, sideset_base = CLOCK_PIN)

    sm0.active(1) # Activate the state machine to generate the clock signal
    sleep(5)      # Run the clock signal for 5 seconds
    sm0.active(0) # Deactivate the state machine

Out

The out() command is mostly used to out what is in the OSR register. The OSR register is often filled with pull() (or autopull) from the TX FIFO register that can be accessed from the Micropython main loop. But Micropython is not very fast. I measured the time of a for loop with 1.6⁥s for 100000 passes. So one pass needs 16⁥µs (62.5⁥kHz). This is not fast enough to fill the register if we use one bit. But with 32 bit we are just fast enough to reach our goal of 1⁥MHz :). So here is the code:

    """ This code toggles a pin at 1MHz using a state machine using
        the out command with 32 bit and autopull enabled. """

    from time import sleep, sleep_us, time_ns
    from machine import Pin
    from rp2 import PIO, StateMachine, asm_pio

    CLOCK_PIN = 14

    # State machine outputs clock with 1MHz (6 clock cycles)
    @asm_pio(out_init = PIO.OUT_LOW, out_shiftdir = PIO.SHIFT_RIGHT, autopull = True, pull_thresh = 32)
    def toggle_clock():
        out(pins, 1)       # Output 1 bit from the OSR to the pin (pin goes LOW)

    # Initialize the state machine
    sm0 = StateMachine(0, toggle_clock, freq = 2000000, out_base = CLOCK_PIN)

    sm0.active(1) # Activate the state machine to generate the clock signal
    for i in range(300000): # Load the TX FIFO register 300000 times (4.8s)
        sm0.put(0b01010101010101010101010101010101) # Put a value of 0 in the TX FIFO register
    sm0.active(0) # Deactivate the state machine

Important are out_init, out_shiftdir. With autopull = True we avoid the pull() command an so one clock cycle and pull_thresh = 32 tells to use all 32 bit from the register. set_base defines the pin. Loading the TX FIFO register takes one clock cycle. so the frequency must be 2⁥MHz.

We could also use a scratch register to set the OSR, but I think this approach is not elegant and there are better solutions.


Parallel output to more pins

RP2350 vs RP2040

RP2350 contains 3 identical PIO blocks with 4 state machines per block (RP2040 2 blocks with 8 state machines).

11.1.1. Changes from RP2040 RP2350 adds the following new registers and controls: • DBGCFGINFO.VERSION indicates the PIO version, to allow PIO feature detection at runtime. ◦ This 4-bit field was reserved-0 on RP2040 (indicating version 0), and reads as 1 on RP2350. • GPIOBASE adds support for more than 32 GPIOs per PIO block. ◦ Each PIO block is still limited to 32 GPIOs at a time, but GPIOBASE selects which 32. • CTRL.NEXTPIOMASK and CTRL.PREVPIOMASK apply some CTRL register operations to state machines in neighbouring PIO blocks simultaneously. ◦ CTRL.NEXTPREVSMDISABLE stops PIO state machines in multiple PIO blocks simultaneously. ◦ CTRL.NEXTPREVSMENABLE starts PIO state machines in multiple PIO blocks simultaneously. ◦ CTRL.NEXTPREVCLKDIVRESTART synchronises the clock dividers of PIO state machines in multiple PIO blocks • SM0SHIFTCTRL.INCOUNT masks unneeded IN-mapped pins to zero. ◦ This is useful for MOV x, PINS instructions, which previously always returned a full rotated 32-bit value. • IRQ0INTE and IRQ1INTE now expose all eight SM IRQ flags to system-level interrupts (not just the lower four). • Registers starting from RXF0PUTGET0 expose each RX FIFO’s internal storage registers for random read or write access from the system, ◦ The new FJOINRXPUT FIFO join mode enables random writes from the state machine, and random reads from the system (for implementing status registers). ◦ The new FJOINRXGET FIFO join mode enables random reads from the state machine, and random writes from the system (for implementing control registers). ◦ Setting both FJOINRXPUT and FJOINRXGET enables random read and write access from the state machine, but disables system access. RP2350 Datasheet 11.1. Overview 874 RP2350 adds the following new instruction features: • Adds PINCTRLJMPPIN as a source for the WAIT instruction, plus an offset in the range 0-3. ◦ This gives WAIT pin arguments a per-SM mapping that is independent of the IN-mapped pins. • Adds PINDIRS as a destination for MOV. ◦ This allows changing the direction of all OUT-mapped pins with a single instruction: MOV PINDIRS, NULL or MOV PINDIRS, ~NULL • Adds SM IRQ flags as a source for MOV x, STATUS ◦ This allows branching (as well as blocking) on the assertion of SM IRQ flags. • Extends IRQ instruction encoding to allow state machines to set, clear and observe IRQ flags from different PIO blocks. ◦ There is no delay penalty for cross-PIO IRQ flags: an IRQ on one state machine is observable to all state machines on the next cycle. • Adds the FJOINRXGET FIFO mode. ◦ A new MOV encoding reads any of the four RX FIFO storage registers into OSR. ◦ This instruction permits random reads of the four FIFO entries, indexed either by instruction bits or the Y scratch register. • Adds the FJOINRXPUT FIFO mode. ◦ A new MOV encoding writes the ISR into any of the four RX FIFO storage registers. ◦ The registers are indexed either by instruction bits or the Y scratch register. RP2350 adds the following security features: • Limits Non-secure PIOs (set to via ACCESSCTRL) to observation of only Non-secure GPIOs. Attempting to read a Secure GPIO returns a 0. • Disables cross-PIO functionality (IRQs, CTRL_NEXTPREV operations) between Non-secure PIO blocks (those which permit Non-secure access according to ACCESSCTRL) and Secure-only blocks (those which do not). RP2350 includes the following general improvements: • Increased the number of PIO blocks from two to three (8 → 12 state machines). • Improved GPIO input/output delay and skew. • Reduced DMA request (DREQ) latency by one cycle vs RP2040

[PIO block level diagram](png/PIOblocklevel_diagram.png)

Downloads

Interesting links