Raspberry Pi Pico PIO Basics: Programmable I/O State Machines
Most microcontrollers have fixed peripherals: so many UARTs, so many SPI ports, and if you need a protocol they do not support, you bit-bang it in software, tying up the CPU and fighting interrupt jitter. The RP2040 and RP2350 add PIO, programmable I/O: small, independent state machines that run their own tiny programs and toggle pins with single-clock-cycle precision while the CPU does something else. PIO is how the Pico drives WS2812 LED strips, VGA and DVI video, extra UARTs, quadrature encoders and stepper pulses. This tutorial explains the architecture and walks through two complete programs with exact timing calculations.
PIO Architecture
The RP2040 has two PIO blocks (PIO0 and PIO1); the RP2350 has three. Each block contains four state machines and a shared instruction memory of 32 instructions. In MicroPython, state machines are numbered 0 to 7 on the RP2040 (0 to 3 in PIO0, 4 to 7 in PIO1).
| Resource (per state machine) | Purpose |
|---|---|
| TX FIFO and RX FIFO, 4 words each | Queues for 32-bit data from and to the CPU; can be joined into one 8-word FIFO |
| OSR (output shift register) | Holds data pulled from the TX FIFO, shifted out to pins |
| ISR (input shift register) | Collects bits shifted in from pins, pushed to the RX FIFO |
| X and Y scratch registers | 32-bit counters and temporary values |
| Clock divider | Sets how fast the state machine executes, from the system clock down |
| Pin mappings | Separate base pins for SET, OUT, IN and side-set operations |
Every PIO instruction takes exactly one clock cycle, and any instruction can carry a delay of up to 31 extra cycles, written in square brackets. This determinism is the point: if you know the state machine frequency, you know the exact timing of every pin edge.
The Nine Instructions
| Instruction | What it does |
|---|---|
| JMP | Jump, optionally on a condition such as X non-zero (with decrement) or a pin level |
| WAIT | Stall until a GPIO, pin or IRQ flag reaches a level |
| IN | Shift bits from pins or a register into the ISR |
| OUT | Shift bits from the OSR to pins or a register |
| PUSH | Move the ISR into the RX FIFO |
| PULL | Load the OSR from the TX FIFO |
| MOV | Copy between registers, optionally inverted or bit-reversed |
| IRQ | Set or clear an interrupt flag |
| SET | Write an immediate value (0 to 31) to pins, pin directions, X or Y |
Choosing the State Machine Frequency
The clock divider has a 16-bit integer part, so the lowest frequency a state machine can run at is the system clock divided by about 65536. On an RP2040 at MicroPython's default 125 MHz that is roughly 1907 Hz; on an RP2350 at 150 MHz it is roughly 2289 Hz. Asking for less raises a ValueError. To make slow events such as a 1 Hz blink, combine a frequency above that limit with delay loops inside the program.
Example 1: An Exact 1 Hz Blink with No CPU
Connect an LED with a 330 ohm resistor to GP15. The onboard LED on GP25 also works on the Pico and Pico 2, but not on the W boards, where the LED is behind the wireless chip and PIO cannot reach it.
State machine clock: 4000 Hz -> 0.25 ms per cycle
High half:
set(pins, 1) [7] 8 cycles
set(x, 31) [7] 8 cycles
loop body x 32 passes:
nop() [31] 32
nop() [28] 29
jmp(x_dec, ...) 1
62 x 32 = 1984 cycles
total = 8 + 8 + 1984 = 2000 cycles = 0.5 s
Low half is identical -> period 1.0 s, exactly 1 Hz# pio_blink.py - 1 Hz blink generated by PIO, CPU stays free
import rp2
from machine import Pin
import time
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def blink_1hz():
# ---- high half: 2000 cycles ----
set(pins, 1) [7]
set(x, 31) [7] # loop runs 32 times (31 down to 0)
label('high')
nop() [31]
nop() [28]
jmp(x_dec, 'high')
# ---- low half: 2000 cycles ----
set(pins, 0) [7]
set(x, 31) [7]
label('low')
nop() [31]
nop() [28]
jmp(x_dec, 'low')
# program wraps back to the top automatically
# 4000 Hz is above the divider limit on both RP2040 and RP2350
sm = rp2.StateMachine(0, blink_1hz, freq=4000, set_base=Pin(15))
sm.active(1)
# The CPU is completely free; prove it by counting
n = 0
try:
while True:
n += 1
if n % 100000 == 0:
print('CPU still counting:', n)
except KeyboardInterrupt:
sm.active(0)
Pin(15, Pin.OUT).off()
Example 2: Exact Pulse Trains Sent from Python
The real power of PIO appears when the CPU sends data through the FIFO. This program waits for a number, then produces exactly that many 20 microsecond pulses. Stepper motor drivers such as the A4988 and DRV8825 take one step per pulse on their STEP pin, so this is a precise, CPU-free stepper pulse generator. Watch GP16 with a logic analyser, or connect it to a driver's STEP input.
# pio_pulses.py - send N pulses (25 kHz, 50% duty) on GP16 via PIO
import rp2
from machine import Pin
import time
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def pulse_train():
pull(block) # wait for a count from the CPU
mov(x, osr) # X = count - 1 (see below)
label('loop')
set(pins, 1) [19] # high for 20 cycles
set(pins, 0) [18] # low for 19 cycles ...
jmp(x_dec, 'loop') # ... + 1 for the jump = 20 cycles
# 1 MHz clock -> 1 us per cycle -> 40 us per pulse
sm = rp2.StateMachine(1, pulse_train, freq=1_000_000, set_base=Pin(16))
sm.active(1)
def send_pulses(n):
# jmp(x_dec) loops while X was non-zero, so X = n - 1 gives n pulses
if n > 0:
sm.put(n - 1)
for count in (10, 200, 1000):
print('Sending', count, 'pulses, expected', count * 40, 'us')
send_pulses(count)
time.sleep_ms(count * 40 // 1000 + 50) # wait until they finish
sm.active(0)
Key Points in the Code
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)assembles the Python-looking function into PIO machine code and sets the SET pin as an output, initially low.set_base=Pin(15)maps the first SET pin. Withset(pins, value), bit 0 goes to the base pin, bit 1 to the next GPIO and so on, up to five pins.pull(block)stalls the state machine until the TX FIFO has data, so the program simply waits for work and the CPU never has to poll it.jmp(x_dec, label)tests X before decrementing: it jumps while X is non-zero, so a starting value of N - 1 gives N passes.- When execution reaches the end of the program it wraps to the beginning at no cost, which is why neither example needs a final jump.
sm.put()writes a 32-bit word into the TX FIFO. If the FIFO is full,putblocks until space is free.
Common Mistakes
ValueErrorcreating the StateMachine: the frequency is below the divider limit, or the program does not fit in the remaining instruction memory.- Nothing on the pin:
set_basewas not given, orset_initwas omitted so the pin was never made an output. - Pulse count is one too many: you put N instead of N - 1.
- Delay too large error: the maximum delay is 31 cycles, and fewer when side-set bits are used.
Summary
PIO state machines execute one instruction per cycle with programmable delays, giving cycle-exact pin timing that runs independently of the CPU. You calculated a 1 Hz blink down to the cycle, then used the TX FIFO to command exact pulse counts suitable for a stepper driver. The same building blocks of SET, OUT, IN, JMP and the FIFOs are how the official examples implement WS2812 LEDs, UARTs and encoders.