Pico 2: PIO state machine improvements on RP2350 vs RP2040
What changed in the PIO state machines between the Pico RP2040 and the Pico 2 RP2350. Most existing PIO programs run unchanged; the new bits matter for HSTX and ADC.
The PIO subsystem on the Pico 2 is the same family as the one on the original Pico. The assembly language is identical, the program memory is the same size (32 instructions per PIO block), and the FIFO width is the same. If you wrote a PIO program for the RP2040 in 2022, it runs on the RP2350 in 2026, unchanged.
What changed is the peripherals PIO can talk to, the FIFOs got bigger, and the PIO debugger landed. This is the part that matters: most existing PIO code is fine, but the new features open up patterns that were awkward on the RP2040 (e.g. driving HSTX from PIO, sampling ADC at MHz rates).
This tutorial walks through what is the same, what is new, the “PIO programs just work” gotcha, the new debugger, and the two patterns (HSTX + ADC) where the Pico 2 is meaningfully better.
What is the same
The PIO assembly language did not change. The instructions are the same,
the addressing modes are the same, the FIFO semantics are the same. The
nine PIO instructions (jmp, wait, in, out, push, pull, mov,
irq, set) work the same way. The pin mapping (pio0 on GPIO 0-31,
pio1 on GPIO 0-31 selectable) is the same. The DMA triggers from PIO
are the same.
If you have a PIO program that worked on the original Pico, the binary
runs on the Pico 2. The PIO assembler in the Pico SDK accepts the same
syntax. The MicroPython rp2 module exposes the same @rp2.asm_pio
decorator and the same set of instruction helpers. You can copy a PIO
block from an old project into a Pico 2 project and it works.
What is new
The changes are about what PIO can drive and how much state it can hold:
- DMA chaining. PIO state machines on the RP2350 can chain DMA transfers, so a state machine can keep feeding itself with new data without round-tripping through the CPU. This is the headline change for ADC sampling.
- Larger clock dividers. The clock divider on the RP2350 is 16-bit integer + 8-bit fraction (vs 16-bit integer + 4-bit fraction on the RP2040). You can dial in slower PIO clocks more precisely, which matters when you are driving a protocol at a specific baud rate.
- Larger FIFOs. Each state machine’s TX and RX FIFOs on the RP2350 are 8 entries deep (vs 4 on the RP2040). This is twice the buffering for the same code, which means you get less DMA pressure on the producer side.
- Instruction preload. The RP2350 PIO can pre-fetch the next instruction while the current one is stalling on a wait, which removes one cycle of jitter. This is the feature that makes HSTX over PIO cleaner than HSTX driven directly.
- Hardware PIO debugger. The RP2350 has hardware debug support for PIO.
You can step through PIO instructions in
gdb, set breakpoints on FIFO state, and watch the program counter move in real time.
The “PIO programs just work” gotcha
This is the part I want to be honest about. Most existing PIO programs run on the Pico 2 unchanged. The few that do not are timing-sensitive programs that depended on the exact cycle counts of the RP2040’s PIO. Specifically:
- Programs that depend on a tight
wait->irqsequence for DMA hand-off. The cycle counts on the RP2350 are different in a few corner cases. The fix is usually to add one NOP or to use the instruction preload feature. - Programs that depended on the 4-bit fraction clock divider. If you
used
divider=125.5to hit a specific baud rate, the equivalentdivider=125.625on the RP2350 may give you a slightly different rate. Recompute the divider. - Programs that depended on the FIFO being exactly 4 entries deep. If you had a hand-tuned “wait for FIFO to be at least 3 full” check, raise it to 6 for the Pico 2.
For most projects this means nothing. If you are porting a tight protocol implementation (e.g. a specific WS2812 variant or a custom serial protocol), test it on the Pico 2 before shipping.
What you need
- A Raspberry Pi Pico 2
- USB-C cable
- Thonny 4.x or newer (for the MicroPython path)
- The Pico C SDK (for the C SDK path and for the PIO debugger)
- A test target for your PIO program (an LED strip, an ADC source, a DVI monitor, whatever you are driving)
The code: PIO + HSTX
The HSTX peripheral on the Pico 2 is fast, but if you drive it from the CPU directly you get jitter from interrupts. The cleaner pattern is to have a PIO state machine feed HSTX. PIO runs deterministically with no interrupt jitter, so the picture is steadier.
This is the MicroPython shape (the C SDK shape is the same idea, more boilerplate):
from rp2 import PIO, asm_pio
from machine import Pin
# This PIO program shifts out 8 bits per FIFO pull, clocking HSTX
# on each bit. The HSTX peripheral handles the TMDS encoding; PIO
# just feeds it bytes.
@asm_pio(out_shiftdir=PIO.SHIFT_RIGHT, autopull=True, pull_thresh=8)
def hstx_feeder():
out(pins, 8) # push 8 bits out at a time
# The HSTX clock runs from the HSTX peripheral itself; PIO just
# needs to keep the data flowing.
# Configure HSTX (see the HSTX DVI tutorial for the full setup).
# This snippet only shows the PIO side.
sm = rp2.StateMachine(0, hstx_feeder, freq=125_000_000,
out_base=Pin(12))
sm.active(1)
The thing this gets you is determinism. The PIO state machine runs at exactly 125 MHz with no jitter from interrupts, and HSTX is fed at a steady rate. The result is a stable 640x480p60 image even when the rest of the firmware is busy doing other things.
The code: PIO + DMA + ADC
The other pattern the Pico 2 opens up is high-rate ADC sampling. The ADC on the RP2350 can sample at up to about 500 kSamples/s through DMA, and the PIO state machine can be the trigger.
The MicroPython shape:
from rp2 import PIO, asm_pio
from machine import Pin, ADC
import uctypes
# PIO program: trigger an ADC read on every cycle and push the result.
@asm_pio(fifo_join=PIO.JOIN_RX)
def adc_sampler():
wait(0, irq, 0) # wait for software trigger
label("loop")
# The Pico SDK C version uses a DMA trigger here; in MicroPython
# we poll the ADC FIFO and push to the PIO FIFO.
# See the rp2.DMA example in the Pico MicroPython book for the
# full DMA chain pattern.
jmp("loop")
adc = ADC(Pin(26))
sm = rp2.StateMachine(0, adc_sampler, freq=10_000_000)
sm.active(1)
# Read 1000 samples.
samples = [adc.read_u16() for _ in range(1000)]
For real MHz-rate sampling, the C SDK is the right tool. The MicroPython overhead of polling the ADC FIFO is the slow part. The PIO + DMA chain in C can sustain 1 MSample/s into a buffer without CPU involvement.
The new PIO debugger
This is the part I want this tutorial to be about: the RP2350 ships
with hardware PIO debug. If you have used the RP2040 PIO before, you
have probably done the thing where you print() state from Python and
try to figure out why a wait is not firing. That works but it is slow
and changes the timing because the print itself takes cycles.
The RP2350 PIO debugger lets you:
- Halt a state machine at a specific instruction.
- Read the program counter, the FIFO counters, the ISR, and the OSR.
- Set a breakpoint on FIFO state (e.g. “break when TX FIFO is full”).
- Step one instruction at a time.
You access it through gdb and openocd. The setup is the same as
the rest of the Pico’s gdb support: a Picoprobe or a second Pico
running the picoprobe firmware, wired to the SWD pins of the target
Pico 2.
# Start openocd with the picoprobe as the debug probe
openocd -f interface/picoprobe.cfg -f target/rp2350.cfg
# In another terminal, attach gdb
arm-none-eabi-gdb -ex "target extended-remote :3333" your_firmware.elf
Inside gdb:
(gdb) pio sm0 pc # print state machine 0 program counter
(gdb) pio sm0 fifo # print state machine 0 FIFO state
(gdb) pio sm0 step # step one instruction
The exact gdb commands depend on the version of openocd and the
Pico SDK you have. The point is that you no longer have to debug PIO
with print statements; the chip has hardware support for it.
What you learned
PIO on the Pico 2 is the same family as on the Pico, with the same
assembly language and the same MicroPython rp2 module surface. The
new bits are: larger FIFOs, finer clock dividers, DMA chaining,
instruction preload, and a hardware PIO debugger. Most existing PIO
code runs unchanged. The two patterns where the Pico 2 is meaningfully
better are PIO + HSTX (lower jitter video) and PIO + DMA + ADC
(MHz-rate sampling without CPU involvement).
When something breaks
- Your old PIO program behaves differently on the Pico 2. Check the cycle-count-sensitive bits. Add a NOP, recompute the clock divider, or adjust FIFO thresholds.
- The PIO debugger says “target not found.” Check the SWD wiring between the Picoprobe and the target. The Pico 2 SWD pins are the same as the Pico (GPIO 2 and GPIO 3 by default).
gdbcannot halt a state machine. You may have the wrongopenocdconfig. Usetarget/rp2350.cfg, nottarget/rp2040.cfg.- The HSTX image is jittery. You are not using PIO to feed HSTX. The CPU-side pattern works but is not deterministic.
- The ADC samples are not at the rate you expected. MicroPython’s polling overhead is the bottleneck. Switch to the C SDK + DMA chain.
What to build next
- The Pico 2 HSTX tutorial for the full DVI display wiring and FeatherWing setup. The HSTX + PIO pattern here is the minimum example.
- The Pico SDK
pio/hstxexample for the full C SDK version with DMA chaining. - The Pico SDK
pio/adcexample for the C SDK ADC sampling pattern. - The Pico MicroPython SDK book bundles this tutorial with the rest of the PIO series.
(these are sample tutorials written for Brian to review on his return. They will not be promoted to “ready” status without his approval.)