last updated: 2025-04-05
Work in progress
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.
The original Pi PICO (RP2040) contains 2 identical PIO blocks with 4 state machines per block. The 4 state machines share the same memory!
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:
Jean-Claude created the following block diagram, helping to understand the different flows:
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 1MHz.
""" 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.
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
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.6s for 100000 passes. So one pass needs 16µs (62.5kHz). 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 1MHz :). 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 2MHz.
We could also use a scratch register to set the OSR, but I think this approach is not elegant and there are better solutions.
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