Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

MicroPython for Microcontrollers  Module 13 – Ports and boards: RP2040/RP2350, ESP32 and the rest

RP2040 and RP2350 specifics: PIO, DMA and flash

Program the RP2040 and RP2350 from MicroPython 1.29: PIO state machines timed to the cycle, DMA copies and paced transfers, rp2.Flash and BOOTSEL.

  • Advanced
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Count the cycles of a PIO program and predict the pulse lengths it makes
  • Choose a StateMachine freq from the clock divider it becomes, and say what a fraction costs
  • Configure an rp2.DMA transfer with pack_ctrl(), a count in transfers and a pace
  • Use rp2.Flash(start, len) and rp2.bootsel_button() without corrupting files or stalling the board
  • Name what the RP2350 adds to the rp2 port and what stays the same

Before you start

What you’ll learn · Before you start

What you will learn

  • Count the cycles of a PIO program and predict the pulse lengths it makes
  • Choose a StateMachine freq from the clock divider it becomes, and say what a fraction costs
  • Configure an rp2.DMA transfer with pack_ctrl(), a count in transfers and a pace
  • Use rp2.Flash(start, len) and rp2.bootsel_button() without corrupting files or stalling the board
  • Name what the RP2350 adds to the rp2 port and what stays the same

Before you start

On this page

An infrared remote’s 38 kHz carrier flips a pin 76,000 times a second, once every 13.2 µs, for as long as a burst lasts. A Python loop on a Pico cannot keep that rhythm while it does anything else: the rp2 quick reference warns that even timer callbacks are prone to jitter from the garbage collector.

The RP2040 has hardware that keeps time on its own: eight PIO state machines that run one instruction per clock cycle, and twelve DMA channels that move data without the processor. This lesson programs both from MicroPython 1.29: a PIO program timed to the cycle, the clock divider behind freq, DMA transfers set up with pack_ctrl() and paced by a request or the new DMATimer, the flash behind the filesystem, the BOOTSEL button, and what the RP2350 adds. Every program runs here on a model of the rp2 module built from the port’s own source.

A state machine runs one instruction per cycle, so its timing can be counted

PIO stands for programmable I/O. The RP2040 has two PIO blocks of four PIO state machine: One of the small processors in the programmable I/O blocks of the RP2040 and RP2350. It runs a program of up to 32 instructions, one per cycle of its own clock, to drive and read pins with exact timing. each, numbered 0 to 7 by rp2.StateMachine, and each block holds 32 instructions that its four machines share. A state machine knows nine instructions, among them set to drive pins, pull to take a word that Python sent, mov to copy it and jmp to loop. The RP2040 datasheet makes the promise that matters: every instruction takes exactly one cycle unless it stalls, and up to 31 delay cycles may follow it, written in MicroPython as [n] after the instruction. A PIO program’s timing is counted, not measured.

The program below sends bursts of a 38 kHz carrier. Python puts the number of periods minus one into the state machine’s transmit FIFO: First in, first out: a small queue in hardware. Each PIO state machine has a 4-word FIFO for data from the processor and one for data back to it.; the program pulls it, copies it into the scratch register X and loops: a set with 12 delay cycles keeps the pin high for 13 cycles, a set with 11 keeps it low for 12, and the jump adds the thirteenth. Step through one burst:

Interactive · step by step One burst of the 38 kHz carrier, cycle by cycle Step through one burst of 24 periods and watch the pin, the registers and the cycle count.
One burst of the 38 kHz carrier, cycle by cycle, step 9 of 9. Showing Python: sm.put(23), TX FIFO (empty), Program counter (pull(), waiting), OSR and X (X = 0xFFFFFFFF), GP16 (low), Cycle count (from cycle 626). In focus: GP16, TX FIFO.Python: sm.put(23)TX FIFOemptyProgram counterpull(), waitingOSR and XX = 0xFFFFFFFFGP16lowCycle countfrom cycle 626

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. Python sends a count. The burst needs 24 periods, so Python puts 23 into the transmit FIFO with sm.put(24 - 1). The state machine is already waiting at pull().
  2. Cycle 0: pull(). pull() moves the word from the FIFO into the output shift register, OSR, in one cycle. With the FIFO empty it would stall here instead, cycle after cycle.
  3. Cycle 1: mov(x, osr). X becomes 23, the number of periods still to send after the first one. Two cycles have passed and the pin is still low.
  4. Cycles 2 to 14: high. set(pins, 1) drives GP16 high at cycle 2, and its delay of 12 holds the state machine for 12 more cycles. The pin is high for 13 cycles.
  5. Cycles 15 to 26: low. set(pins, 0) drives the pin low at cycle 15 and its delay of 11 adds 11 cycles. So far the pin has been low for 12 cycles.
  6. Cycle 27: the jump. jmp(x_dec) finds X = 23, which is not zero, so it jumps back to the label, and X becomes 22. Its cycle is the 13th low one, so a period is 26 cycles.
  7. Periods 2 to 24. The loop repeats. Rising edges come at cycles 28, 54 and so on, 26 cycles apart, and every jump leaves X one lower. At 988,020 Hz that is a 38,000.8 Hz carrier.
  8. Cycle 625: X is 0. The 24th jump finds X = 0, so it does not jump, and X still decrements, to 0xFFFFFFFF. The program wraps to pull(), which costs no cycle.
  9. Waiting for the next count. From cycle 626 pull() finds the FIFO empty and stalls. GP16 stays low, and the next sm.put() starts the next burst two cycles after it lands.

The model beside the program runs those instructions one cycle at a time at freq=988_000, which the next section explains; its bench_ functions report what a board keeps to itself, so they exist only in the model. Run it, then press Edit, change sm.put(24 - 1) to sm.put(24) and run it again:

A 38 kHz carrier burst from a PIO program

rp2/burst.py

"""A burst of a 38 kHz infrared carrier from a PIO program, run on the model of rp2 beside this file."""
import rp2
from machine import Pin


@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def burst():
    pull()  # wait for a count from the CPU
    mov(x, osr)  # x = periods - 1
    label("period")
    set(pins, 1)[12]  # 1 + 12 cycles high
    set(pins, 0)[11]  # 1 + 11 cycles low ...
    jmp(x_dec, "period")  # ... and 1 more for the jump


sm = rp2.StateMachine(0, burst, freq=988_000, set_base=Pin(16))
sm.active(1)
sm.put(24 - 1)  # 24 carrier periods

whole, frac = rp2.bench_divider(sm)
ns = rp2.bench_cycle_ns(sm)
print(sm, "divider", whole, "+", frac, "/ 256: one cycle is", round(ns, 1), "ns")

trace = rp2.bench_run(sm, 700)
levels = [level for _, level in trace]
rises = [c for c in range(1, len(levels)) if levels[c] and not levels[c - 1]]
high = [levels[r:].index(0) for r in rises]
period = rises[1] - rises[0]
print("first rising edge at cycle", rises[0], "after pull and mov")
print(len(rises), "periods of", period, "cycles:", high[0], "high and", period - high[0], "low")
print("carrier:", round(1e9 / (period * ns), 1), "Hz")
stall = [what for what, _ in trace].index("stall")
print("burst: cycles", rises[0], "to", stall - 1, "=", round((stall - rises[0]) * ns / 1000, 1), "us")
print("from cycle", stall, "pull() waits for the next count; the pin stays", levels[-1])

Output

StateMachine(0) divider 126 + 132 / 256: one cycle is 1012.1 ns
first rising edge at cycle 2 after pull and mov
24 periods of 26 cycles: 13 high and 13 low
carrier: 38000.8 Hz
burst: cycles 2 to 625 = 631.6 us
from cycle 626 pull() waits for the next count; the pin stays 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 burst.py

rp2/divider.py

"""The clock divider StateMachine(freq=...) sets at a 125 MHz system clock, on the model of rp2 beside this file."""
import rp2


@rp2.asm_pio()
def idle():
    nop()


print("freq asked   divider       one cycle      real freq  system clocks a cycle")
for freq in (125_000_000, 10_000_000, 1_000_000, 988_000, 2_000, 1_908, 1_907):
    try:
        sm = rp2.StateMachine(0, idle, freq=freq)
    except ValueError as e:
        print(f"{freq:>11,}  ValueError: {e}")
        continue
    whole, frac = rp2.bench_divider(sm)
    ns = rp2.bench_cycle_ns(sm)
    clocks = f"{whole}, every cycle" if frac == 0 else f"{whole} or {whole + 1}"
    print(f"{freq:>11,}  {whole:>5} + {frac:>3}/256 {ns:>9.1f} ns {1e9 / ns:>13,.2f} Hz  {clocks}")

Output

freq asked   divider       one cycle      real freq  system clocks a cycle
125,000,000      1 +   0/256       8.0 ns 125,000,000.00 Hz  1, every cycle
 10,000,000     12 + 128/256     100.0 ns 10,000,000.00 Hz  12 or 13
  1,000,000    125 +   0/256    1000.0 ns  1,000,000.00 Hz  125, every cycle
    988,000    126 + 132/256    1012.1 ns    988,020.25 Hz  126 or 127
      2,000  62500 +   0/256  500000.0 ns      2,000.00 Hz  62500, every cycle
      1,908  65513 + 160/256  524109.0 ns      1,908.00 Hz  65513 or 65514
      1,907  ValueError: freq out of range

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 divider.py

rp2.py (the model) · rp2/rp2.py

"""A model of MicroPython's rp2 module on a Raspberry Pi Pico (RP2040, 125 MHz), so rp2 code runs on a computer.

It follows the MicroPython 1.29.0 documentation and the rp2 port's source: a subset of the PIO assembler of
ports/rp2/modules/rp2.py (set, mov, pull, jmp, nop, label, wrap_target, wrap and [delay]), the clock divider of
StateMachine.init() in ports/rp2/rp2_pio.c, the DMA control fields and defaults of ports/rp2/rp2_dma.c with the
RP2040's bit positions, the DMATimer ratio search of the same file, and rp2.Flash(start, len) of
ports/rp2/rp2_flash.c. Functions whose names start with bench_ are not MicroPython: they run a state machine for a
number of cycles, or report what the hardware would have done. Never copy this file to a board.
"""

FAKE = True
SYS_CLK_HZ = 125_000_000  # machine.freq() after boot on a Pico (rp2 quick reference)
_MASK32 = 0xFFFFFFFF


class PIOASMError(Exception):
    pass


class BenchError(Exception):
    """What the hardware would do silently, said out loud by the model."""


class PIO:
    IN_LOW, IN_HIGH, OUT_LOW, OUT_HIGH = 0, 1, 2, 3
    SHIFT_LEFT, SHIFT_RIGHT = 0, 1
    JOIN_NONE, JOIN_TX, JOIN_RX = 0, 1, 2


class _Instr:
    def __init__(self, op, args):
        self.op, self.args, self.delay = op, args, 0

    def __getitem__(self, cycles):  # set(pins, 1) [12]
        if not 0 <= cycles <= 31:
            raise PIOASMError("delay too large")
        self.delay = cycles
        return self


class _Program:
    def __init__(self, set_init):
        self.set_init, self.instrs, self.labels = set_init, [], {}
        self.wrap_target, self.wrap = 0, None

    def _add(self, op, *args):
        ins = _Instr(op, args)
        self.instrs.append(ins)
        return ins

    def names(self):
        """What a PIO function sees as globals while asm_pio() assembles it."""
        def set_(dest, data):
            if not 0 <= data <= 31:
                raise PIOASMError("set takes a value from 0 to 31")
            return self._add("set", dest, data)

        def jmp(cond, label=None):
            return self._add("jmp", "always", cond) if label is None else self._add("jmp", cond, label)

        def label(name):
            self.labels[name] = len(self.instrs)

        def wrap_target():
            self.wrap_target = len(self.instrs)

        def wrap():
            self.wrap = len(self.instrs) - 1

        return {
            "set": set_, "jmp": jmp, "label": label, "wrap_target": wrap_target, "wrap": wrap,
            "nop": lambda: self._add("nop"), "pull": lambda: self._add("pull"),
            "mov": lambda dest, src: self._add("mov", dest, src),
            "pins": "pins", "x": "x", "y": "y", "osr": "osr",
            "x_dec": "x_dec", "y_dec": "y_dec", "not_x": "not_x", "not_y": "not_y",
        }


def asm_pio(*, set_init=None, **settings):
    """Assemble the decorated function's instructions (as @rp2.asm_pio does, for the subset above)."""
    def dec(f):
        prog = _Program(set_init)
        names = prog.names()
        gl = f.__globals__
        saved = {k: gl[k] for k in names if k in gl}
        gl.update(names)
        try:
            f()
        finally:
            for k in names:
                if k in saved:
                    gl[k] = saved[k]
                else:
                    gl.pop(k, None)
        if prog.wrap is None:
            prog.wrap = len(prog.instrs) - 1
        return prog
    return dec


class StateMachine:
    """One of the RP2040's 8 state machines (2 PIO blocks of 4), running at a divided system clock."""

    def __init__(self, id, program=None, freq=-1, *, set_base=None):
        if not 0 <= id <= 7:
            raise ValueError("invalid StateMachine")
        self.id = id
        if program is not None:
            self.init(program, freq, set_base=set_base)

    def init(self, program, freq=-1, *, set_base=None):
        if len(program.instrs) > 32:
            raise OSError(12, "ENOMEM")  # a PIO block holds 32 instructions
        if freq < 0:
            div = 256  # the default: run at the system clock
        elif freq == 0:
            div = 65536 * 256  # a divider of 0 means 65536, the slowest
        else:
            div = SYS_CLK_HZ * 256 // freq  # ports/rp2/rp2_pio.c: clkdiv in 1/256 steps
            if not 256 <= div <= 65536 * 256:
                raise ValueError("freq out of range")
        self._div, self._prog = div, program
        self._pc, self._delay, self._x, self._y, self._osr = 0, 0, 0, 0, 0
        self._fifo, self._running = [], False
        self.level = 1 if program.set_init == PIO.OUT_HIGH else 0

    def active(self, value=None):
        if value is not None:
            self._running = bool(value)
        return self._running

    def put(self, value):
        if len(self._fifo) == 4 and not self._running:
            raise BenchError("put() would wait for ever: the 4-word TX FIFO is full and nothing pulls")
        self._fifo.append(value & _MASK32)

    def tx_fifo(self):
        return len(self._fifo)

    def __repr__(self):
        return "StateMachine(%d)" % self.id

    def _cycle(self):
        """One state machine cycle: an instruction, a delay cycle or a stall."""
        if self._delay:
            self._delay -= 1
            return "delay"
        ins = self._prog.instrs[self._pc]
        nxt = self._pc + 1
        if ins.op == "pull":
            if not self._fifo:
                return "stall"  # a blocking pull waits; its delay starts only after it completes
            self._osr = self._fifo.pop(0)
        elif ins.op == "mov":
            dest, src = ins.args
            setattr(self, "_" + dest, getattr(self, "_" + src))
        elif ins.op == "set":
            dest, data = ins.args
            if dest == "pins":
                self.level = data & 1
            else:
                setattr(self, "_" + dest, data)
        elif ins.op == "jmp":
            cond, target = ins.args
            take = True
            if cond in ("x_dec", "y_dec"):
                reg = "_" + cond[0]
                take = getattr(self, reg) != 0  # tested before the decrement, which always happens
                setattr(self, reg, (getattr(self, reg) - 1) & _MASK32)
            elif cond in ("not_x", "not_y"):
                take = getattr(self, "_" + cond[-1]) == 0
            if take:
                nxt = self._prog.labels[target]
        if self._pc == self._prog.wrap and nxt == self._pc + 1:
            nxt = self._prog.wrap_target  # wrapping costs no cycle
        self._pc, self._delay = nxt, ins.delay
        return ins.op


def bench_divider(sm):
    """The (whole, frac) clock divider the port set: one cycle lasts whole + frac/256 system clocks."""
    return sm._div // 256, sm._div % 256


def bench_cycle_ns(sm):
    """The average length of one state machine cycle in nanoseconds."""
    return sm._div / 256 * 1e9 / SYS_CLK_HZ


def bench_run(sm, cycles):
    """Run sm for a number of cycles; return what each cycle did and the pin level during it."""
    if not sm._running:
        raise BenchError("the state machine is not active")
    return [(sm._cycle(), sm.level) for _ in range(cycles)]


# ---- DMA ------------------------------------------------------------------------------------------------------
# (name, lowest bit, width) of the RP2040's CH0_CTRL_TRIG register, in the order rp2_dma.c lists them.
_CTRL_FIELDS = (
    ("enable", 0, 1), ("high_pri", 1, 1), ("size", 2, 2), ("inc_read", 4, 1), ("inc_write", 5, 1),
    ("ring_size", 6, 4), ("ring_sel", 10, 1), ("chain_to", 11, 4), ("treq_sel", 15, 6), ("irq_quiet", 21, 1),
    ("bswap", 22, 1), ("sniff_en", 23, 1), ("busy", 24, 1), ("write_err", 29, 1), ("read_err", 30, 1),
    ("ahb_err", 31, 1),
)
_READ_ONLY = ("busy", "ahb_err")
_PERMANENT = 0x3F  # treq_sel: no pacing, run at full speed
_claimed_channels, _claimed_timers = set(), set()


class DMA:
    """One of the RP2040's 12 DMA channels, claimed for this object."""

    def __init__(self):
        free = [c for c in range(12) if c not in _claimed_channels]
        if not free:
            raise OSError(16, "EBUSY")
        self.channel = free[0]
        _claimed_channels.add(self.channel)
        self._busy = False

    def __repr__(self):
        return "DMA(%d)" % self.channel

    def pack_ctrl(self, default=None, **fields):
        value = default
        if value is None:  # quiet, unpaced, both addresses incrementing, 4-byte words, enabled, chained to itself
            value = 1 << 21 | _PERMANENT << 15 | 1 << 5 | 1 << 4 | 2 << 2 | 1 | (self.channel & 0xF) << 11
        known = {name: (shift, width) for name, shift, width in _CTRL_FIELDS}
        for name, v in fields.items():
            if name not in known:
                raise TypeError("unexpected keyword argument '%s'" % name)
            if name in _READ_ONLY:
                continue
            shift, width = known[name]
            mask = (1 << width) - 1
            v = int(v)
            if v & mask != v:
                raise ValueError("bad field value")
            value = value & ~(mask << shift) | v << shift
        return value

    @staticmethod
    def unpack_ctrl(value):
        return {name: value >> shift & (1 << width) - 1 for name, shift, width in _CTRL_FIELDS}

    def config(self, read=None, write=None, count=None, ctrl=None, trigger=False):
        self._read, self._write, self._count, self._ctrl = read, write, count, ctrl
        if trigger:
            self._run()

    def _run(self):
        f = DMA.unpack_ctrl(self._ctrl)
        width = 1 << f["size"]
        total = self._count * width
        short = [(name, len(buf)) for name, buf, inc in (("read", self._read, f["inc_read"]),
                                                          ("write", self._write, f["inc_write"]))
                 if inc and total > len(buf)]
        if short:
            raise BenchError(
                "%d transfers of %d bytes move %d bytes, but %s: on a board the channel carries on into the memory "
                "after the buffer" % (self._count, width, total,
                                      " and ".join("the %s buffer holds %d" % s for s in short))
            )
        if f["inc_read"] and f["inc_write"]:
            self._write[:total] = self._read[:total]
        self._busy = False

    def active(self, value=None):
        return self._busy

    def close(self):
        _claimed_channels.discard(self.channel)


class DMATimer:
    """One of the 4 DMA pacing timers: it asks for a transfer X/Y times per system clock cycle."""

    def __init__(self, timer_id=None, *, freq=None, ratio=None):
        if timer_id is not None and (not 0 <= timer_id <= 3 or timer_id in _claimed_timers):
            raise ValueError("value out of range") if not 0 <= timer_id <= 3 else OSError(16, "EBUSY")
        if timer_id is None:
            free = [t for t in range(4) if t not in _claimed_timers]
            if not free:
                raise OSError(16, "EBUSY")
            timer_id = free[0]
        self.timer_id = timer_id
        _claimed_timers.add(timer_id)
        self._x, self._y = 0, 0
        if ratio is not None:
            self.ratio = ratio
        elif freq is not None:
            self.freq = freq

    @property
    def ratio(self):
        return (self._x, self._y)

    @ratio.setter
    def ratio(self, xy):
        x, y = xy
        if not (1 <= x <= 65535 and 1 <= y <= 65535 and x <= y):
            raise ValueError("value out of range")
        self._x, self._y = x, y

    @property
    def freq(self):
        return SYS_CLK_HZ * self._x // self._y  # read back as a whole number of hertz

    @freq.setter
    def freq(self, hz):
        if hz > SYS_CLK_HZ or SYS_CLK_HZ // 65535 > hz:
            raise ValueError("value out of range")
        self._x, self._y = _best_ratio(hz, SYS_CLK_HZ)

    def __int__(self):
        return 0x3B + self.timer_id  # the treq_sel value of this timer

    def __repr__(self):
        return "DMATimer(%d)" % self.timer_id

    def close(self):
        _claimed_timers.discard(self.timer_id)


def _best_ratio(p, q, limit=65536):
    """The fraction x/y closest to p/q with both terms below limit: continued fractions, as rp2_dma.c does."""
    h, k, h0, k0 = 1, 0, 0, 1
    while q:
        a = p // q
        h1, k1 = a * h + h0, a * k + k0
        if h1 >= limit or k1 >= limit:
            n = (limit - 1 - h0) // h if h else limit
            if k:
                n = min(n, (limit - 1 - k0) // k)
            if n != limit and n > 0 and n >= (a + 1) // 2:
                return n * h + h0, n * k + k0
            break
        p, q = q, p % q
        h0, k0, h, k = h, k, h1, k1
    return h, k


# ---- Flash and the BOOTSEL button ------------------------------------------------------------------------------
_FLASH_BYTES = 2 * 1024 * 1024  # a Pico's flash chip
_STORAGE_BYTES = 1441792  # MICROPY_HW_FLASH_STORAGE_BYTES of RPI_PICO: 1408 KiB at the top of the flash
_BLOCK = 4096  # one flash sector


class Flash:
    """The flash kept for files, or a part of it: the block device behind the filesystem."""

    def __init__(self, *, start=-1, len=-1):
        if start == -1:
            start = 0
        elif not (0 <= start < _STORAGE_BYTES and start % _BLOCK == 0):
            raise ValueError
        if len == -1:
            len = _STORAGE_BYTES - start
        elif not (0 < len and start + len <= _STORAGE_BYTES and len % _BLOCK == 0):
            raise ValueError
        self._offset = _FLASH_BYTES - _STORAGE_BYTES + start  # where it starts in the flash chip
        self._size = len
        self._xip = 0x10000000 + self._offset  # the address its buffer has, as uctypes.addressof() reports it

    def ioctl(self, op, arg):
        return {4: self._size // _BLOCK, 5: _BLOCK}.get(op, 0)  # 4: block count, 5: block size


def bench_boot():
    """What the rp2 port's frozen _boot.py does at every start-up, before boot.py: mount Flash() at /, or format it."""
    import vfs

    bdev = Flash()
    try:
        fs = vfs.VfsLfs2(bdev, progsize=256)
        print("boot: mounted the", bdev.ioctl(4, 0), "block filesystem at /")
    except OSError as e:
        print("boot: VfsLfs2(Flash()) raised OSError", e.args[0], "so the boot code formats all", bdev.ioctl(4, 0), "blocks")
        vfs.VfsLfs2.mkfs(bdev, progsize=256)
        fs = vfs.VfsLfs2(bdev, progsize=256)
    vfs.mount(fs, "/")


def bootsel_button():
    """Reads the BOOTSEL button through the flash's chip-select line: 1 while it is pressed."""
    global _bootsel_reads
    _bootsel_reads += 1
    return _button


_button, _bootsel_reads = 0, 0
BOOTSEL_PAUSE_US = 8  # MICROPY_HW_BOOTSEL_DELAY_US: the flash and interrupts stay off at least this long per read


def bench_press(pressed):
    """Hold the BOOTSEL button down (True) or let it go (False)."""
    global _button
    _button = 1 if pressed else 0

rp2/machine.py

"""A stand-in for machine.Pin, enough for the rp2 examples beside it. Never copy this file to a board."""

FAKE = True


class Pin:
    OUT, IN = 1, 0

    def __init__(self, id, mode=-1, value=None):
        self.id = id

    def __repr__(self):
        return "Pin(GPIO%d)" % self.id


def freq():
    return 125_000_000

Every period took 26 cycles, 13 high and 13 low, so the carrier is 988,020 Hz ÷ 26 = 38,000.8 Hz, and 24 periods lasted 631.6 µs. Nothing in the program measured time. The numbers follow from the instructions alone, and they stay the same while Python collects garbage or handles an interrupt, because the state machine runs beside the processor. When the count runs out, the jump falls through, the program wraps to pull() without spending a cycle, and the state machine waits there with the pin low until the next put().

Putting the number of periods into X

jmp(x_dec, "period") tests X before it decrements it, so a loop that starts with X = n runs n + 1 times: sm.put(24) sends 25 periods. Put n − 1, as the program does, and write the reason in a comment.

Checkpoint

Check: Counting a PIO program's cycles

  1. Question 1 of 2 A loop is set(pins, 1)[4], then set(pins, 0)[3], then jmp(x_dec, "loop"). How long is one period, and how much of it is the pin high?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 10 cycles, 5 of them high

    Count one cycle per instruction plus its delay: 5 high (set and 4 delay cycles), 4 low (set and 3) and 1 for the jump, also low. The burst of the lesson works the same way: 13 high and 13 low in 26 cycles.

  2. Question 2 of 2 The burst program should send 10 carrier periods. Which value does Python put into the FIFO?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 9

    The program runs the period once, then jumps back while X is not zero, decrementing each time. Starting at X = n it runs n + 1 periods, so put n - 1, as the lesson's sm.put(24 - 1) does.

freq sets the length of a cycle, in steps of 1/256 of a system clock

A state machine cannot run at any frequency you name. StateMachine.init() turns freq into a clock divider: A circuit that makes one cycle of a slower clock last a chosen number of cycles of a faster one. A PIO state machine's divider counts system clocks in steps of 1/256.: the rp2 port multiplies the system clock by 256 and divides by freq, rounding down, which is 125_000_000 * 256 // freq on a Pico at its boot clock, and loads the result as a whole number and a fraction in 256ths, the 16-bit integer and 8-bit fraction that the RP2040 datasheet gives every state machine. The divisor runs from 1 to 65,536, so at the Pico’s 125 MHz the slowest state machine runs at 1,908 Hz, as the StateMachine documentation says, and freq=1_907 raises ValueError. The divider.py tab of the program above asks for seven frequencies and prints what each becomes:

freq asked Divider One cycle Real frequency System clocks a cycle
10,000,000 12 + 128/256 100.0 ns 10,000,000.00 Hz 12 or 13
1,000,000 125 + 0/256 1000.0 ns 1,000,000.00 Hz 125, every cycle
988,000 126 + 132/256 1012.1 ns 988,020.25 Hz 126 or 127
2,000 62500 + 0/256 500000.0 ns 2,000.00 Hz 62500, every cycle
1,908 65513 + 160/256 524109.0 ns 1,908.00 Hz 65513 or 65514
1,907 ValueError: freq out of range

Two things follow. The frequency you get is close to the one you asked for, but often not equal: 988,000 became 988,020.25 Hz, because the divider keeps only whole 256ths. And a divider with a fraction does not make every cycle the same length. The datasheet’s divider is a first-order delta-sigma, so the state machine’s cycles last 126 or 127 system clocks, 1,008 ns or 1,016 ns, and only their average is 1,012.1 ns. A whole divider such as 125 makes every cycle exactly 1 µs.

So the carrier has a choice. At freq=988_000 its average is 38,000.8 Hz and an edge can move by one system clock, 8 ns, which is 0.06 % of the 13.2 µs half period. At freq=1_000_000 every edge is exact, but the carrier is 1,000,000 ÷ 26 = 38,461.5 Hz, 1.2 % fast on every period. Choose a whole divider when a protocol needs every cycle the same, and a fraction when the average frequency matters more.

Key idea

A PIO program’s timing is its cycle count times the length of one cycle, and that length is the system clock’s period times the divider, a whole number of 256ths: count first, then divide.

Checkpoint

Check: The clock divider behind freq

  1. Question 1 of 2 At the Pico's 125 MHz system clock, which freq does rp2.StateMachine refuse with ValueError?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 1,800 Hz

    The port computes 125,000,000 x 256 // freq and accepts results from 256 to 65,536 x 256. Below 1,908 Hz the divider would pass 65,536, so freq=1_907 and slower raise "freq out of range".

  2. Question 2 of 2 A protocol needs every state machine cycle to last exactly the same time. Which freq gives that at 125 MHz?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 5,000,000 Hz

    A fractional divider is a first-order delta-sigma: cycles alternate between the whole parts below and above the ratio. Only a freq that divides 125 MHz exactly, such as 5 MHz or 1 MHz, makes every cycle the same.

DMA moves data by itself, and counts transfers, not bytes

The RP2040’s DMA: Direct memory access: hardware channels that copy data between memory and peripherals by themselves, so the processor is free while a transfer runs. controller has 12 channels. Each one reads from one address and writes to another, again and again, adding to either address or to both after each transfer, while the processor does other work. rp2.DMA() claims a free channel, config() gives it its addresses, a count and a control word, and trigger=True starts it. Any object with the buffer protocol can stand for an address, a bytearray or a state machine’s FIFO.

The control word holds the details, and pack_ctrl() builds it from named fields. On the RP2040 its default for channel 0 is 0x3f8039: 4-byte transfers (size 2), both addresses incrementing, treq_sel 0x3f, which means as fast as the bus allows, and chain_to the channel itself, which means no chaining. The program prints it field by field, copies 1,024 bytes, and then makes the slip that the documentation’s own example warns about in a comment:

A DMA copy and its control word, then DMATimer pacing

rp2/dma_copy.py

"""A DMA copy on a Pico: the control word, a count in transfers, and the slip, on the model of rp2 beside this file."""
import rp2

src = bytearray(range(256)) * 4  # 1,024 bytes
dst = bytearray(1024)
d = rp2.DMA()
ctrl = d.pack_ctrl()  # the default for this channel
print(d, "default ctrl:", hex(ctrl))
f = rp2.DMA.unpack_ctrl(ctrl)
print("  size", f["size"], "(4-byte words), inc_read", f["inc_read"], "inc_write", f["inc_write"])
print("  treq_sel", hex(f["treq_sel"]), "(no pacing), chain_to", f["chain_to"], "(itself), irq_quiet", f["irq_quiet"])

d.config(read=src, write=dst, count=len(src) // 4, ctrl=ctrl, trigger=True)
while d.active():
    pass
print("count", len(src) // 4, "words: copied", dst == src)

# Bytes to the TX FIFO of PIO 0's state machine 0: one byte a transfer, the same address, paced by its request.
to_pio = d.pack_ctrl(size=0, inc_write=False, treq_sel=(0 << 3) + 0)
print("byte transfers to PIO0 SM0:", hex(to_pio))

try:
    d.config(read=src, write=dst, count=len(src), ctrl=ctrl, trigger=True)  # count in bytes: the slip
except rp2.BenchError as e:
    print("BenchError:", e)
d.close()

Output

DMA(0) default ctrl: 0x3f8039
  size 2 (4-byte words), inc_read 1 inc_write 1
  treq_sel 0x3f (no pacing), chain_to 0 (itself), irq_quiet 1
count 256 words: copied True
byte transfers to PIO0 SM0: 0x200011
BenchError: 1024 transfers of 4 bytes move 4096 bytes, but the read buffer holds 1024 and the write buffer holds 1024: on a board the channel carries on into the memory after the buffer

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dma_copy.py

rp2/dma_pace.py

"""DMATimer (MicroPython 1.29) paces transfers at X/Y of the 125 MHz system clock, on the model of rp2 beside this file."""
import rp2

timers = []
print("asked      timer        ratio (X, Y)   freq read back  treq_sel")
for want in (10_000, 44_100, 48_000, 1_000_000):
    t = rp2.DMATimer(freq=want)
    timers.append(t)  # keep a reference, or the timer is freed and reused
    print(f"{want:>9,}  {str(t):<12} {str(t.ratio):<14} {t.freq:>10,} Hz     {hex(int(t))}")

try:
    rp2.DMATimer(freq=8_000)
except OSError as e:
    print("a fifth timer: OSError", e)
for t in timers:
    t.close()
try:
    rp2.DMATimer(freq=1_000)
except ValueError as e:
    print("1,000 Hz: ValueError:", e, "(the lowest is 125,000,000 // 65,535 = 1,907 Hz)")

Output

asked      timer        ratio (X, Y)   freq read back  treq_sel
   10,000  DMATimer(0)  (1, 12500)         10,000 Hz     0x3b
   44,100  DMATimer(1)  (15, 42517)        44,100 Hz     0x3c
   48,000  DMATimer(2)  (6, 15625)         48,000 Hz     0x3d
1,000,000  DMATimer(3)  (1, 125)        1,000,000 Hz     0x3e
a fifth timer: OSError [Errno 16] EBUSY
1,000 Hz: ValueError: value out of range (the lowest is 125,000,000 // 65,535 = 1,907 Hz)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dma_pace.py

rp2.py (the model) · rp2/rp2.py

"""A model of MicroPython's rp2 module on a Raspberry Pi Pico (RP2040, 125 MHz), so rp2 code runs on a computer.

It follows the MicroPython 1.29.0 documentation and the rp2 port's source: a subset of the PIO assembler of
ports/rp2/modules/rp2.py (set, mov, pull, jmp, nop, label, wrap_target, wrap and [delay]), the clock divider of
StateMachine.init() in ports/rp2/rp2_pio.c, the DMA control fields and defaults of ports/rp2/rp2_dma.c with the
RP2040's bit positions, the DMATimer ratio search of the same file, and rp2.Flash(start, len) of
ports/rp2/rp2_flash.c. Functions whose names start with bench_ are not MicroPython: they run a state machine for a
number of cycles, or report what the hardware would have done. Never copy this file to a board.
"""

FAKE = True
SYS_CLK_HZ = 125_000_000  # machine.freq() after boot on a Pico (rp2 quick reference)
_MASK32 = 0xFFFFFFFF


class PIOASMError(Exception):
    pass


class BenchError(Exception):
    """What the hardware would do silently, said out loud by the model."""


class PIO:
    IN_LOW, IN_HIGH, OUT_LOW, OUT_HIGH = 0, 1, 2, 3
    SHIFT_LEFT, SHIFT_RIGHT = 0, 1
    JOIN_NONE, JOIN_TX, JOIN_RX = 0, 1, 2


class _Instr:
    def __init__(self, op, args):
        self.op, self.args, self.delay = op, args, 0

    def __getitem__(self, cycles):  # set(pins, 1) [12]
        if not 0 <= cycles <= 31:
            raise PIOASMError("delay too large")
        self.delay = cycles
        return self


class _Program:
    def __init__(self, set_init):
        self.set_init, self.instrs, self.labels = set_init, [], {}
        self.wrap_target, self.wrap = 0, None

    def _add(self, op, *args):
        ins = _Instr(op, args)
        self.instrs.append(ins)
        return ins

    def names(self):
        """What a PIO function sees as globals while asm_pio() assembles it."""
        def set_(dest, data):
            if not 0 <= data <= 31:
                raise PIOASMError("set takes a value from 0 to 31")
            return self._add("set", dest, data)

        def jmp(cond, label=None):
            return self._add("jmp", "always", cond) if label is None else self._add("jmp", cond, label)

        def label(name):
            self.labels[name] = len(self.instrs)

        def wrap_target():
            self.wrap_target = len(self.instrs)

        def wrap():
            self.wrap = len(self.instrs) - 1

        return {
            "set": set_, "jmp": jmp, "label": label, "wrap_target": wrap_target, "wrap": wrap,
            "nop": lambda: self._add("nop"), "pull": lambda: self._add("pull"),
            "mov": lambda dest, src: self._add("mov", dest, src),
            "pins": "pins", "x": "x", "y": "y", "osr": "osr",
            "x_dec": "x_dec", "y_dec": "y_dec", "not_x": "not_x", "not_y": "not_y",
        }


def asm_pio(*, set_init=None, **settings):
    """Assemble the decorated function's instructions (as @rp2.asm_pio does, for the subset above)."""
    def dec(f):
        prog = _Program(set_init)
        names = prog.names()
        gl = f.__globals__
        saved = {k: gl[k] for k in names if k in gl}
        gl.update(names)
        try:
            f()
        finally:
            for k in names:
                if k in saved:
                    gl[k] = saved[k]
                else:
                    gl.pop(k, None)
        if prog.wrap is None:
            prog.wrap = len(prog.instrs) - 1
        return prog
    return dec


class StateMachine:
    """One of the RP2040's 8 state machines (2 PIO blocks of 4), running at a divided system clock."""

    def __init__(self, id, program=None, freq=-1, *, set_base=None):
        if not 0 <= id <= 7:
            raise ValueError("invalid StateMachine")
        self.id = id
        if program is not None:
            self.init(program, freq, set_base=set_base)

    def init(self, program, freq=-1, *, set_base=None):
        if len(program.instrs) > 32:
            raise OSError(12, "ENOMEM")  # a PIO block holds 32 instructions
        if freq < 0:
            div = 256  # the default: run at the system clock
        elif freq == 0:
            div = 65536 * 256  # a divider of 0 means 65536, the slowest
        else:
            div = SYS_CLK_HZ * 256 // freq  # ports/rp2/rp2_pio.c: clkdiv in 1/256 steps
            if not 256 <= div <= 65536 * 256:
                raise ValueError("freq out of range")
        self._div, self._prog = div, program
        self._pc, self._delay, self._x, self._y, self._osr = 0, 0, 0, 0, 0
        self._fifo, self._running = [], False
        self.level = 1 if program.set_init == PIO.OUT_HIGH else 0

    def active(self, value=None):
        if value is not None:
            self._running = bool(value)
        return self._running

    def put(self, value):
        if len(self._fifo) == 4 and not self._running:
            raise BenchError("put() would wait for ever: the 4-word TX FIFO is full and nothing pulls")
        self._fifo.append(value & _MASK32)

    def tx_fifo(self):
        return len(self._fifo)

    def __repr__(self):
        return "StateMachine(%d)" % self.id

    def _cycle(self):
        """One state machine cycle: an instruction, a delay cycle or a stall."""
        if self._delay:
            self._delay -= 1
            return "delay"
        ins = self._prog.instrs[self._pc]
        nxt = self._pc + 1
        if ins.op == "pull":
            if not self._fifo:
                return "stall"  # a blocking pull waits; its delay starts only after it completes
            self._osr = self._fifo.pop(0)
        elif ins.op == "mov":
            dest, src = ins.args
            setattr(self, "_" + dest, getattr(self, "_" + src))
        elif ins.op == "set":
            dest, data = ins.args
            if dest == "pins":
                self.level = data & 1
            else:
                setattr(self, "_" + dest, data)
        elif ins.op == "jmp":
            cond, target = ins.args
            take = True
            if cond in ("x_dec", "y_dec"):
                reg = "_" + cond[0]
                take = getattr(self, reg) != 0  # tested before the decrement, which always happens
                setattr(self, reg, (getattr(self, reg) - 1) & _MASK32)
            elif cond in ("not_x", "not_y"):
                take = getattr(self, "_" + cond[-1]) == 0
            if take:
                nxt = self._prog.labels[target]
        if self._pc == self._prog.wrap and nxt == self._pc + 1:
            nxt = self._prog.wrap_target  # wrapping costs no cycle
        self._pc, self._delay = nxt, ins.delay
        return ins.op


def bench_divider(sm):
    """The (whole, frac) clock divider the port set: one cycle lasts whole + frac/256 system clocks."""
    return sm._div // 256, sm._div % 256


def bench_cycle_ns(sm):
    """The average length of one state machine cycle in nanoseconds."""
    return sm._div / 256 * 1e9 / SYS_CLK_HZ


def bench_run(sm, cycles):
    """Run sm for a number of cycles; return what each cycle did and the pin level during it."""
    if not sm._running:
        raise BenchError("the state machine is not active")
    return [(sm._cycle(), sm.level) for _ in range(cycles)]


# ---- DMA ------------------------------------------------------------------------------------------------------
# (name, lowest bit, width) of the RP2040's CH0_CTRL_TRIG register, in the order rp2_dma.c lists them.
_CTRL_FIELDS = (
    ("enable", 0, 1), ("high_pri", 1, 1), ("size", 2, 2), ("inc_read", 4, 1), ("inc_write", 5, 1),
    ("ring_size", 6, 4), ("ring_sel", 10, 1), ("chain_to", 11, 4), ("treq_sel", 15, 6), ("irq_quiet", 21, 1),
    ("bswap", 22, 1), ("sniff_en", 23, 1), ("busy", 24, 1), ("write_err", 29, 1), ("read_err", 30, 1),
    ("ahb_err", 31, 1),
)
_READ_ONLY = ("busy", "ahb_err")
_PERMANENT = 0x3F  # treq_sel: no pacing, run at full speed
_claimed_channels, _claimed_timers = set(), set()


class DMA:
    """One of the RP2040's 12 DMA channels, claimed for this object."""

    def __init__(self):
        free = [c for c in range(12) if c not in _claimed_channels]
        if not free:
            raise OSError(16, "EBUSY")
        self.channel = free[0]
        _claimed_channels.add(self.channel)
        self._busy = False

    def __repr__(self):
        return "DMA(%d)" % self.channel

    def pack_ctrl(self, default=None, **fields):
        value = default
        if value is None:  # quiet, unpaced, both addresses incrementing, 4-byte words, enabled, chained to itself
            value = 1 << 21 | _PERMANENT << 15 | 1 << 5 | 1 << 4 | 2 << 2 | 1 | (self.channel & 0xF) << 11
        known = {name: (shift, width) for name, shift, width in _CTRL_FIELDS}
        for name, v in fields.items():
            if name not in known:
                raise TypeError("unexpected keyword argument '%s'" % name)
            if name in _READ_ONLY:
                continue
            shift, width = known[name]
            mask = (1 << width) - 1
            v = int(v)
            if v & mask != v:
                raise ValueError("bad field value")
            value = value & ~(mask << shift) | v << shift
        return value

    @staticmethod
    def unpack_ctrl(value):
        return {name: value >> shift & (1 << width) - 1 for name, shift, width in _CTRL_FIELDS}

    def config(self, read=None, write=None, count=None, ctrl=None, trigger=False):
        self._read, self._write, self._count, self._ctrl = read, write, count, ctrl
        if trigger:
            self._run()

    def _run(self):
        f = DMA.unpack_ctrl(self._ctrl)
        width = 1 << f["size"]
        total = self._count * width
        short = [(name, len(buf)) for name, buf, inc in (("read", self._read, f["inc_read"]),
                                                          ("write", self._write, f["inc_write"]))
                 if inc and total > len(buf)]
        if short:
            raise BenchError(
                "%d transfers of %d bytes move %d bytes, but %s: on a board the channel carries on into the memory "
                "after the buffer" % (self._count, width, total,
                                      " and ".join("the %s buffer holds %d" % s for s in short))
            )
        if f["inc_read"] and f["inc_write"]:
            self._write[:total] = self._read[:total]
        self._busy = False

    def active(self, value=None):
        return self._busy

    def close(self):
        _claimed_channels.discard(self.channel)


class DMATimer:
    """One of the 4 DMA pacing timers: it asks for a transfer X/Y times per system clock cycle."""

    def __init__(self, timer_id=None, *, freq=None, ratio=None):
        if timer_id is not None and (not 0 <= timer_id <= 3 or timer_id in _claimed_timers):
            raise ValueError("value out of range") if not 0 <= timer_id <= 3 else OSError(16, "EBUSY")
        if timer_id is None:
            free = [t for t in range(4) if t not in _claimed_timers]
            if not free:
                raise OSError(16, "EBUSY")
            timer_id = free[0]
        self.timer_id = timer_id
        _claimed_timers.add(timer_id)
        self._x, self._y = 0, 0
        if ratio is not None:
            self.ratio = ratio
        elif freq is not None:
            self.freq = freq

    @property
    def ratio(self):
        return (self._x, self._y)

    @ratio.setter
    def ratio(self, xy):
        x, y = xy
        if not (1 <= x <= 65535 and 1 <= y <= 65535 and x <= y):
            raise ValueError("value out of range")
        self._x, self._y = x, y

    @property
    def freq(self):
        return SYS_CLK_HZ * self._x // self._y  # read back as a whole number of hertz

    @freq.setter
    def freq(self, hz):
        if hz > SYS_CLK_HZ or SYS_CLK_HZ // 65535 > hz:
            raise ValueError("value out of range")
        self._x, self._y = _best_ratio(hz, SYS_CLK_HZ)

    def __int__(self):
        return 0x3B + self.timer_id  # the treq_sel value of this timer

    def __repr__(self):
        return "DMATimer(%d)" % self.timer_id

    def close(self):
        _claimed_timers.discard(self.timer_id)


def _best_ratio(p, q, limit=65536):
    """The fraction x/y closest to p/q with both terms below limit: continued fractions, as rp2_dma.c does."""
    h, k, h0, k0 = 1, 0, 0, 1
    while q:
        a = p // q
        h1, k1 = a * h + h0, a * k + k0
        if h1 >= limit or k1 >= limit:
            n = (limit - 1 - h0) // h if h else limit
            if k:
                n = min(n, (limit - 1 - k0) // k)
            if n != limit and n > 0 and n >= (a + 1) // 2:
                return n * h + h0, n * k + k0
            break
        p, q = q, p % q
        h0, k0, h, k = h, k, h1, k1
    return h, k


# ---- Flash and the BOOTSEL button ------------------------------------------------------------------------------
_FLASH_BYTES = 2 * 1024 * 1024  # a Pico's flash chip
_STORAGE_BYTES = 1441792  # MICROPY_HW_FLASH_STORAGE_BYTES of RPI_PICO: 1408 KiB at the top of the flash
_BLOCK = 4096  # one flash sector


class Flash:
    """The flash kept for files, or a part of it: the block device behind the filesystem."""

    def __init__(self, *, start=-1, len=-1):
        if start == -1:
            start = 0
        elif not (0 <= start < _STORAGE_BYTES and start % _BLOCK == 0):
            raise ValueError
        if len == -1:
            len = _STORAGE_BYTES - start
        elif not (0 < len and start + len <= _STORAGE_BYTES and len % _BLOCK == 0):
            raise ValueError
        self._offset = _FLASH_BYTES - _STORAGE_BYTES + start  # where it starts in the flash chip
        self._size = len
        self._xip = 0x10000000 + self._offset  # the address its buffer has, as uctypes.addressof() reports it

    def ioctl(self, op, arg):
        return {4: self._size // _BLOCK, 5: _BLOCK}.get(op, 0)  # 4: block count, 5: block size


def bench_boot():
    """What the rp2 port's frozen _boot.py does at every start-up, before boot.py: mount Flash() at /, or format it."""
    import vfs

    bdev = Flash()
    try:
        fs = vfs.VfsLfs2(bdev, progsize=256)
        print("boot: mounted the", bdev.ioctl(4, 0), "block filesystem at /")
    except OSError as e:
        print("boot: VfsLfs2(Flash()) raised OSError", e.args[0], "so the boot code formats all", bdev.ioctl(4, 0), "blocks")
        vfs.VfsLfs2.mkfs(bdev, progsize=256)
        fs = vfs.VfsLfs2(bdev, progsize=256)
    vfs.mount(fs, "/")


def bootsel_button():
    """Reads the BOOTSEL button through the flash's chip-select line: 1 while it is pressed."""
    global _bootsel_reads
    _bootsel_reads += 1
    return _button


_button, _bootsel_reads = 0, 0
BOOTSEL_PAUSE_US = 8  # MICROPY_HW_BOOTSEL_DELAY_US: the flash and interrupts stay off at least this long per read


def bench_press(pressed):
    """Hold the BOOTSEL button down (True) or let it go (False)."""
    global _button
    _button = 1 if pressed else 0

count is a number of transfers, and with the default size of 2 each transfer moves 4 bytes, so 1,024 bytes are 256 transfers. The last call passed len(src), 1,024 transfers of 4 bytes, 4,096 bytes. The model refused, but a board raises nothing: the channel reads past src and writes 3,072 bytes into whatever follows dst in RAM, and the program fails later, somewhere unrelated.

Passing a length in bytes as count

d.config(read=src, write=dst, count=len(src), ctrl=d.pack_ctrl(), trigger=True) moves four times too much. Write count=len(src) // 4 for the default word size, or count=len(src) with pack_ctrl(size=0) for byte transfers.

Checkpoint

Check: DMA transfers and their count

  1. Question 1 of 2 You copy 600 bytes between two bytearrays with d.pack_ctrl() left at its default. Which count do you pass?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 150

    count is a number of transfers of size bytes. With the default word size divide the length by 4; for an odd length, use pack_ctrl(size=0) and count bytes.

  2. Question 2 of 2 What does treq_sel 0x3f, the default of pack_ctrl(), mean?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No pacing, so the channel copies as fast as the bus allows

    The default control word 0x3f8039 packs size 2, both addresses incrementing, treq_sel 0x3f and chain_to the channel itself: a plain, unpaced copy.

A request paces each transfer: a state machine’s DREQ, or a DMATimer

At treq_sel 0x3f a channel copies as fast as the bus allows, which suits memory to memory. A peripheral needs data at its own pace, so treq_sel names who asks for each transfer: a DREQ: Data request: the signal by which a peripheral tells a DMA channel that it can take or give one more item, so the channel transfers at the peripheral's pace.. On the RP2040 the transmit FIFO of PIO block p, state machine n, asks with DREQ (p << 3) + n, the formula of the DMA documentation. To feed bytes to PIO 0’s state machine 0, the program’s middle step packs size=0 for one byte a transfer, inc_write=False because the FIFO’s address never moves, and treq_sel=0, which gives 0x200011. Without the request the channel would write faster than the program pulls, and the RP2040 datasheet says a write to a full FIFO has no effect: those bytes would be lost. Follow a paced transfer:

Interactive · message by message A DMA channel feeding a state machine at its own pace Step through a paced transfer of 1,024 bytes from src to the FIFO of PIO 0's state machine 0.
A DMA channel feeding a state machine at its own pace, step 6 of 6. Messages so far: Your program to DMA channel 0: config(..., trigger=True); PIO0 SM0 to DMA channel 0: DREQ 0: room; DMA channel 0 to PIO0 SM0: one byte of src; PIO0 SM0 to DMA channel 0: DREQ 0 again; DMA channel 0 to Your program: active() is False.Your programDMA channel 0PIO0 SM0TX FIFO of 4 wordsconfig(..., trigger=True)DREQ 0: roomone byte of srcDREQ 0 againactive() is False

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Configure and start. The program gives the channel src as the read address, the state machine as the write address, a count of 1,024 transfers and the control word 0x200011 (bytes, a fixed write address, treq_sel 0), and starts it.
  2. A request. The FIFO has room, so PIO 0's state machine 0 raises its data request, DREQ 0. Only now may the channel move one item.
  3. One byte moves. The channel reads one byte from src, adds 1 to its read address, and writes the byte to the FIFO's address, which stays the same. The count drops by one.
  4. The FIFO fills. When the FIFO holds 4 words the request drops and the channel waits. Unpaced, it would keep writing, and the datasheet says a write to a full FIFO has no effect, so those bytes would be lost.
  5. pull() sets the pace. Each pull() of the state machine frees a word, the request rises again and the next byte moves. The transfer runs exactly as fast as the PIO program consumes.
  6. Count 0. After 1,024 transfers the count is 0 and the channel stops. The CPU copied nothing; it polls active(), which now returns False, or takes the channel's interrupt.

Memory and peripherals without a request of their own can be paced by one of the DMA controller’s four pacing timers, which MicroPython 1.29 opens as rp2.DMATimer. A timer asks for a transfer X/Y times per system clock cycle, and DMATimer(freq=...) finds the closest fraction with both terms below 65,536. The dma_pace.py tab asked for four rates: 44,100 Hz became (15, 42517), which reads back as 44,100 Hz because 125,000,000 × 15 ÷ 42,517 = 44,100.007, and int(timer) is the treq_sel value to pass, 0x3b to 0x3e. A fifth timer raised OSError: [Errno 16] EBUSY, and below 1,907 Hz, 1/65,535 of the clock, ValueError.

Keep the timer object

The documentation warns that a pacing timer is released when its DMATimer object is garbage collected. Keep a reference for as long as the transfer runs, or another caller may claim the timer and change its rate.

Checkpoint

Check: Pacing a channel with a DREQ or a DMATimer

  1. Question 1 of 2 Which treq_sel paces a transfer into the TX FIFO of PIO 1's state machine 2 on an RP2040?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 10

    PIO 0's TX FIFOs ask with requests 0 to 3 and PIO 1's with 8 to 11, so (p << 3) + n picks the one you need.

  2. Question 2 of 2 A function creates rp2.DMATimer(freq=44_100), starts a long audio transfer paced by it, and returns without keeping the timer object. What can go wrong?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The garbage collector can free the timer, and another caller may claim it and change the rate mid-transfer

    A DMATimer is a claim on one of four pacing timers. When the object is garbage collected the claim ends, so code that needs the rate for the whole transfer keeps the object, for example in a global or an attribute.

rp2.Flash and the BOOTSEL button pause the flash, so use them rarely

rp2.Flash() is the block device: An object that stores data in numbered blocks of one size and offers readblocks(), writeblocks() and ioctl() for the block count, the block size and erasing. MicroPython's filesystems sit on one, such as rp2.Flash or an SD card. More in “Filesystems: LittleFS, FAT and vfs.mount” that the board’s start-up code mounts at / as LittleFS: on a Pico, the 352 blocks of 4,096 bytes kept for files, as the filesystems lesson showed. Its keyword arguments start and len, both multiples of 4,096 inside that area, hand out a part of it. The program below takes two parts, then tries to keep the second one free for raw records:

rp2.Flash(start, len) on a Pico, and a split the boot code undoes

rp2/flash_split.py

"""rp2.Flash(start, len) on a Pico, and what the boot code does to a split, on the models of rp2, uctypes and vfs beside this file."""
import rp2
import uctypes
import vfs

whole = rp2.Flash()  # what the board's boot code mounts at /
print("Flash():", whole.ioctl(4, 0), "blocks of", whole.ioctl(5, 0), "bytes, read at", hex(uctypes.addressof(whole)))
files = rp2.Flash(len=1024 * 1024)  # 1 MiB for a filesystem
log = rp2.Flash(start=1024 * 1024)  # the rest for raw records
print("Flash(len=1 MiB):", files.ioctl(4, 0), "blocks from", hex(uctypes.addressof(files)))
print("Flash(start=1 MiB):", log.ioctl(4, 0), "blocks from", hex(uctypes.addressof(log)))
for kwargs in ({"start": 1000}, {"len": 2 * 1024 * 1024}, {"start": 4096, "len": 6000}):
    try:
        rp2.Flash(**kwargs)
    except ValueError:
        print("Flash(%s): ValueError" % ", ".join("%s=%d" % kv for kv in kwargs.items()))

rp2.bench_boot()  # the first start-up of a board with stock firmware
rp2.bench_boot()  # every start-up after it
vfs.umount("/")
vfs.VfsLfs2.mkfs(files, progsize=256)  # a smaller filesystem, to keep the log area free
print("mkfs on Flash(len=1 MiB): a", files.ioctl(4, 0), "block filesystem")
rp2.bench_boot()  # the next start-up

Output

Flash(): 352 blocks of 4096 bytes, read at 0x100a0000
Flash(len=1 MiB): 256 blocks from 0x100a0000
Flash(start=1 MiB): 96 blocks from 0x101a0000
Flash(start=1000): ValueError
Flash(len=2097152): ValueError
Flash(start=4096, len=6000): ValueError
boot: VfsLfs2(Flash()) raised OSError 22 so the boot code formats all 352 blocks
boot: mounted the 352 block filesystem at /
mkfs on Flash(len=1 MiB): a 256 block filesystem
boot: VfsLfs2(Flash()) raised OSError 22 so the boot code formats all 352 blocks

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 flash_split.py

rp2.py (the model) · rp2/rp2.py

"""A model of MicroPython's rp2 module on a Raspberry Pi Pico (RP2040, 125 MHz), so rp2 code runs on a computer.

It follows the MicroPython 1.29.0 documentation and the rp2 port's source: a subset of the PIO assembler of
ports/rp2/modules/rp2.py (set, mov, pull, jmp, nop, label, wrap_target, wrap and [delay]), the clock divider of
StateMachine.init() in ports/rp2/rp2_pio.c, the DMA control fields and defaults of ports/rp2/rp2_dma.c with the
RP2040's bit positions, the DMATimer ratio search of the same file, and rp2.Flash(start, len) of
ports/rp2/rp2_flash.c. Functions whose names start with bench_ are not MicroPython: they run a state machine for a
number of cycles, or report what the hardware would have done. Never copy this file to a board.
"""

FAKE = True
SYS_CLK_HZ = 125_000_000  # machine.freq() after boot on a Pico (rp2 quick reference)
_MASK32 = 0xFFFFFFFF


class PIOASMError(Exception):
    pass


class BenchError(Exception):
    """What the hardware would do silently, said out loud by the model."""


class PIO:
    IN_LOW, IN_HIGH, OUT_LOW, OUT_HIGH = 0, 1, 2, 3
    SHIFT_LEFT, SHIFT_RIGHT = 0, 1
    JOIN_NONE, JOIN_TX, JOIN_RX = 0, 1, 2


class _Instr:
    def __init__(self, op, args):
        self.op, self.args, self.delay = op, args, 0

    def __getitem__(self, cycles):  # set(pins, 1) [12]
        if not 0 <= cycles <= 31:
            raise PIOASMError("delay too large")
        self.delay = cycles
        return self


class _Program:
    def __init__(self, set_init):
        self.set_init, self.instrs, self.labels = set_init, [], {}
        self.wrap_target, self.wrap = 0, None

    def _add(self, op, *args):
        ins = _Instr(op, args)
        self.instrs.append(ins)
        return ins

    def names(self):
        """What a PIO function sees as globals while asm_pio() assembles it."""
        def set_(dest, data):
            if not 0 <= data <= 31:
                raise PIOASMError("set takes a value from 0 to 31")
            return self._add("set", dest, data)

        def jmp(cond, label=None):
            return self._add("jmp", "always", cond) if label is None else self._add("jmp", cond, label)

        def label(name):
            self.labels[name] = len(self.instrs)

        def wrap_target():
            self.wrap_target = len(self.instrs)

        def wrap():
            self.wrap = len(self.instrs) - 1

        return {
            "set": set_, "jmp": jmp, "label": label, "wrap_target": wrap_target, "wrap": wrap,
            "nop": lambda: self._add("nop"), "pull": lambda: self._add("pull"),
            "mov": lambda dest, src: self._add("mov", dest, src),
            "pins": "pins", "x": "x", "y": "y", "osr": "osr",
            "x_dec": "x_dec", "y_dec": "y_dec", "not_x": "not_x", "not_y": "not_y",
        }


def asm_pio(*, set_init=None, **settings):
    """Assemble the decorated function's instructions (as @rp2.asm_pio does, for the subset above)."""
    def dec(f):
        prog = _Program(set_init)
        names = prog.names()
        gl = f.__globals__
        saved = {k: gl[k] for k in names if k in gl}
        gl.update(names)
        try:
            f()
        finally:
            for k in names:
                if k in saved:
                    gl[k] = saved[k]
                else:
                    gl.pop(k, None)
        if prog.wrap is None:
            prog.wrap = len(prog.instrs) - 1
        return prog
    return dec


class StateMachine:
    """One of the RP2040's 8 state machines (2 PIO blocks of 4), running at a divided system clock."""

    def __init__(self, id, program=None, freq=-1, *, set_base=None):
        if not 0 <= id <= 7:
            raise ValueError("invalid StateMachine")
        self.id = id
        if program is not None:
            self.init(program, freq, set_base=set_base)

    def init(self, program, freq=-1, *, set_base=None):
        if len(program.instrs) > 32:
            raise OSError(12, "ENOMEM")  # a PIO block holds 32 instructions
        if freq < 0:
            div = 256  # the default: run at the system clock
        elif freq == 0:
            div = 65536 * 256  # a divider of 0 means 65536, the slowest
        else:
            div = SYS_CLK_HZ * 256 // freq  # ports/rp2/rp2_pio.c: clkdiv in 1/256 steps
            if not 256 <= div <= 65536 * 256:
                raise ValueError("freq out of range")
        self._div, self._prog = div, program
        self._pc, self._delay, self._x, self._y, self._osr = 0, 0, 0, 0, 0
        self._fifo, self._running = [], False
        self.level = 1 if program.set_init == PIO.OUT_HIGH else 0

    def active(self, value=None):
        if value is not None:
            self._running = bool(value)
        return self._running

    def put(self, value):
        if len(self._fifo) == 4 and not self._running:
            raise BenchError("put() would wait for ever: the 4-word TX FIFO is full and nothing pulls")
        self._fifo.append(value & _MASK32)

    def tx_fifo(self):
        return len(self._fifo)

    def __repr__(self):
        return "StateMachine(%d)" % self.id

    def _cycle(self):
        """One state machine cycle: an instruction, a delay cycle or a stall."""
        if self._delay:
            self._delay -= 1
            return "delay"
        ins = self._prog.instrs[self._pc]
        nxt = self._pc + 1
        if ins.op == "pull":
            if not self._fifo:
                return "stall"  # a blocking pull waits; its delay starts only after it completes
            self._osr = self._fifo.pop(0)
        elif ins.op == "mov":
            dest, src = ins.args
            setattr(self, "_" + dest, getattr(self, "_" + src))
        elif ins.op == "set":
            dest, data = ins.args
            if dest == "pins":
                self.level = data & 1
            else:
                setattr(self, "_" + dest, data)
        elif ins.op == "jmp":
            cond, target = ins.args
            take = True
            if cond in ("x_dec", "y_dec"):
                reg = "_" + cond[0]
                take = getattr(self, reg) != 0  # tested before the decrement, which always happens
                setattr(self, reg, (getattr(self, reg) - 1) & _MASK32)
            elif cond in ("not_x", "not_y"):
                take = getattr(self, "_" + cond[-1]) == 0
            if take:
                nxt = self._prog.labels[target]
        if self._pc == self._prog.wrap and nxt == self._pc + 1:
            nxt = self._prog.wrap_target  # wrapping costs no cycle
        self._pc, self._delay = nxt, ins.delay
        return ins.op


def bench_divider(sm):
    """The (whole, frac) clock divider the port set: one cycle lasts whole + frac/256 system clocks."""
    return sm._div // 256, sm._div % 256


def bench_cycle_ns(sm):
    """The average length of one state machine cycle in nanoseconds."""
    return sm._div / 256 * 1e9 / SYS_CLK_HZ


def bench_run(sm, cycles):
    """Run sm for a number of cycles; return what each cycle did and the pin level during it."""
    if not sm._running:
        raise BenchError("the state machine is not active")
    return [(sm._cycle(), sm.level) for _ in range(cycles)]


# ---- DMA ------------------------------------------------------------------------------------------------------
# (name, lowest bit, width) of the RP2040's CH0_CTRL_TRIG register, in the order rp2_dma.c lists them.
_CTRL_FIELDS = (
    ("enable", 0, 1), ("high_pri", 1, 1), ("size", 2, 2), ("inc_read", 4, 1), ("inc_write", 5, 1),
    ("ring_size", 6, 4), ("ring_sel", 10, 1), ("chain_to", 11, 4), ("treq_sel", 15, 6), ("irq_quiet", 21, 1),
    ("bswap", 22, 1), ("sniff_en", 23, 1), ("busy", 24, 1), ("write_err", 29, 1), ("read_err", 30, 1),
    ("ahb_err", 31, 1),
)
_READ_ONLY = ("busy", "ahb_err")
_PERMANENT = 0x3F  # treq_sel: no pacing, run at full speed
_claimed_channels, _claimed_timers = set(), set()


class DMA:
    """One of the RP2040's 12 DMA channels, claimed for this object."""

    def __init__(self):
        free = [c for c in range(12) if c not in _claimed_channels]
        if not free:
            raise OSError(16, "EBUSY")
        self.channel = free[0]
        _claimed_channels.add(self.channel)
        self._busy = False

    def __repr__(self):
        return "DMA(%d)" % self.channel

    def pack_ctrl(self, default=None, **fields):
        value = default
        if value is None:  # quiet, unpaced, both addresses incrementing, 4-byte words, enabled, chained to itself
            value = 1 << 21 | _PERMANENT << 15 | 1 << 5 | 1 << 4 | 2 << 2 | 1 | (self.channel & 0xF) << 11
        known = {name: (shift, width) for name, shift, width in _CTRL_FIELDS}
        for name, v in fields.items():
            if name not in known:
                raise TypeError("unexpected keyword argument '%s'" % name)
            if name in _READ_ONLY:
                continue
            shift, width = known[name]
            mask = (1 << width) - 1
            v = int(v)
            if v & mask != v:
                raise ValueError("bad field value")
            value = value & ~(mask << shift) | v << shift
        return value

    @staticmethod
    def unpack_ctrl(value):
        return {name: value >> shift & (1 << width) - 1 for name, shift, width in _CTRL_FIELDS}

    def config(self, read=None, write=None, count=None, ctrl=None, trigger=False):
        self._read, self._write, self._count, self._ctrl = read, write, count, ctrl
        if trigger:
            self._run()

    def _run(self):
        f = DMA.unpack_ctrl(self._ctrl)
        width = 1 << f["size"]
        total = self._count * width
        short = [(name, len(buf)) for name, buf, inc in (("read", self._read, f["inc_read"]),
                                                          ("write", self._write, f["inc_write"]))
                 if inc and total > len(buf)]
        if short:
            raise BenchError(
                "%d transfers of %d bytes move %d bytes, but %s: on a board the channel carries on into the memory "
                "after the buffer" % (self._count, width, total,
                                      " and ".join("the %s buffer holds %d" % s for s in short))
            )
        if f["inc_read"] and f["inc_write"]:
            self._write[:total] = self._read[:total]
        self._busy = False

    def active(self, value=None):
        return self._busy

    def close(self):
        _claimed_channels.discard(self.channel)


class DMATimer:
    """One of the 4 DMA pacing timers: it asks for a transfer X/Y times per system clock cycle."""

    def __init__(self, timer_id=None, *, freq=None, ratio=None):
        if timer_id is not None and (not 0 <= timer_id <= 3 or timer_id in _claimed_timers):
            raise ValueError("value out of range") if not 0 <= timer_id <= 3 else OSError(16, "EBUSY")
        if timer_id is None:
            free = [t for t in range(4) if t not in _claimed_timers]
            if not free:
                raise OSError(16, "EBUSY")
            timer_id = free[0]
        self.timer_id = timer_id
        _claimed_timers.add(timer_id)
        self._x, self._y = 0, 0
        if ratio is not None:
            self.ratio = ratio
        elif freq is not None:
            self.freq = freq

    @property
    def ratio(self):
        return (self._x, self._y)

    @ratio.setter
    def ratio(self, xy):
        x, y = xy
        if not (1 <= x <= 65535 and 1 <= y <= 65535 and x <= y):
            raise ValueError("value out of range")
        self._x, self._y = x, y

    @property
    def freq(self):
        return SYS_CLK_HZ * self._x // self._y  # read back as a whole number of hertz

    @freq.setter
    def freq(self, hz):
        if hz > SYS_CLK_HZ or SYS_CLK_HZ // 65535 > hz:
            raise ValueError("value out of range")
        self._x, self._y = _best_ratio(hz, SYS_CLK_HZ)

    def __int__(self):
        return 0x3B + self.timer_id  # the treq_sel value of this timer

    def __repr__(self):
        return "DMATimer(%d)" % self.timer_id

    def close(self):
        _claimed_timers.discard(self.timer_id)


def _best_ratio(p, q, limit=65536):
    """The fraction x/y closest to p/q with both terms below limit: continued fractions, as rp2_dma.c does."""
    h, k, h0, k0 = 1, 0, 0, 1
    while q:
        a = p // q
        h1, k1 = a * h + h0, a * k + k0
        if h1 >= limit or k1 >= limit:
            n = (limit - 1 - h0) // h if h else limit
            if k:
                n = min(n, (limit - 1 - k0) // k)
            if n != limit and n > 0 and n >= (a + 1) // 2:
                return n * h + h0, n * k + k0
            break
        p, q = q, p % q
        h0, k0, h, k = h, k, h1, k1
    return h, k


# ---- Flash and the BOOTSEL button ------------------------------------------------------------------------------
_FLASH_BYTES = 2 * 1024 * 1024  # a Pico's flash chip
_STORAGE_BYTES = 1441792  # MICROPY_HW_FLASH_STORAGE_BYTES of RPI_PICO: 1408 KiB at the top of the flash
_BLOCK = 4096  # one flash sector


class Flash:
    """The flash kept for files, or a part of it: the block device behind the filesystem."""

    def __init__(self, *, start=-1, len=-1):
        if start == -1:
            start = 0
        elif not (0 <= start < _STORAGE_BYTES and start % _BLOCK == 0):
            raise ValueError
        if len == -1:
            len = _STORAGE_BYTES - start
        elif not (0 < len and start + len <= _STORAGE_BYTES and len % _BLOCK == 0):
            raise ValueError
        self._offset = _FLASH_BYTES - _STORAGE_BYTES + start  # where it starts in the flash chip
        self._size = len
        self._xip = 0x10000000 + self._offset  # the address its buffer has, as uctypes.addressof() reports it

    def ioctl(self, op, arg):
        return {4: self._size // _BLOCK, 5: _BLOCK}.get(op, 0)  # 4: block count, 5: block size


def bench_boot():
    """What the rp2 port's frozen _boot.py does at every start-up, before boot.py: mount Flash() at /, or format it."""
    import vfs

    bdev = Flash()
    try:
        fs = vfs.VfsLfs2(bdev, progsize=256)
        print("boot: mounted the", bdev.ioctl(4, 0), "block filesystem at /")
    except OSError as e:
        print("boot: VfsLfs2(Flash()) raised OSError", e.args[0], "so the boot code formats all", bdev.ioctl(4, 0), "blocks")
        vfs.VfsLfs2.mkfs(bdev, progsize=256)
        fs = vfs.VfsLfs2(bdev, progsize=256)
    vfs.mount(fs, "/")


def bootsel_button():
    """Reads the BOOTSEL button through the flash's chip-select line: 1 while it is pressed."""
    global _bootsel_reads
    _bootsel_reads += 1
    return _button


_button, _bootsel_reads = 0, 0
BOOTSEL_PAUSE_US = 8  # MICROPY_HW_BOOTSEL_DELAY_US: the flash and interrupts stay off at least this long per read


def bench_press(pressed):
    """Hold the BOOTSEL button down (True) or let it go (False)."""
    global _button
    _button = 1 if pressed else 0

uctypes.py (the model) · rp2/uctypes.py

"""A stand-in for uctypes.addressof(), enough for the rp2 examples beside it. Never copy this file to a board."""

FAKE = True


def addressof(obj):
    """The address of an object's buffer: for rp2.Flash, where the processor reads its first byte through XIP."""
    address = getattr(obj, "_xip", None)
    if address is None:
        raise TypeError("object with buffer protocol required")
    return address

vfs.py (the model) · rp2/vfs.py

"""A stand-in for vfs.VfsLfs2, enough to show what a mount checks. Never copy this file to a board.

LittleFS keeps the block count of the device it was made on in its superblock, at the start of the device, and its
mount refuses a superblock whose block count differs from the device's (lib/littlefs/lfs2.c, "Invalid block count").
"""

FAKE = True
_superblocks = {}  # flash offset -> the block count written there by mkfs
_mounted = {}


class VfsLfs2:
    def __init__(self, bdev, progsize=256):
        made = _superblocks.get(bdev._offset)
        if made != bdev.ioctl(4, 0):
            raise OSError(22, "EINVAL")  # no filesystem here, or one of another size
        self.bdev = bdev

    @staticmethod
    def mkfs(bdev, progsize=256):
        for start in [o for o in _superblocks if bdev._offset <= o < bdev._offset + bdev._size]:
            del _superblocks[start]  # formatting erases what was there
        _superblocks[bdev._offset] = bdev.ioctl(4, 0)


def mount(fs, path):
    _mounted[path] = fs


def umount(path):
    _mounted.pop(path, None)

The area for files starts 640 KiB into the flash, at offset 0xa0000, and the processor reads it at address 0x100a0000, the address uctypes.addressof() printed. Flash(start=1024 * 1024) is the last 96 blocks, from 0x101a0000, Flash(len=1024 * 1024) the first 256, and a start or length that is not a whole number of blocks, or that runs past the area, raises ValueError.

The split itself failed, and on stock firmware it always does. The rp2 port’s frozen _boot.py mounts Flash(), all 352 blocks, before boot.py runs, and formats them if the mount fails. LittleFS refuses a superblock whose block count differs from the device’s, so the 256-block filesystem made by mkfs raised OSError 22 at the next start-up, and the boot code formatted the whole area, files and all. A raw area beside the files needs firmware built with a smaller MICROPY_HW_FLASH_STORAGE_BYTES or its own boot code; on stock firmware, keep raw records in a file.

Writing to the flash has a cost of its own. The program runs from that same flash, read through XIP: Execute in place: running a program straight from external flash, read through a cache. While that flash is erased or written, no code can be read from it., and while a sector is erased or programmed the port turns interrupts off and pauses the other core. PIO state machines, and DMA transfers that do not read the flash, keep running, but every Python callback waits. bootsel_button() costs a pause too, because the BOOTSEL button sits on the flash’s chip-select line:

Interactive · stage by stage What one call of bootsel_button() does Step through one read of the BOOTSEL button and see why the flash is unusable while it runs.
What one call of bootsel_button() does, step 5 of 5. Showing Interrupts off, QSPI_SS let go, Wait at least 8 µs, Read the line, Line back. In focus: Line back.Interrupts offthe other core held tooQSPI_SS let goflash cannot be readWait at least 8 µsno code from flash runsRead the linelow: pressed, returns 1Line back

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Interrupts off. The BOOTSEL button shares the flash chip's select line, QSPI_SS. Code is read from that flash, so the port first turns interrupts off and holds the other core, which might be running from flash.
  2. The line is let go. The port stops driving QSPI_SS, so the button alone decides its level. From here until the end, nothing can be read from the flash.
  3. The wait. A busy loop that runs from RAM waits more than 8 µs, MICROPY_HW_BOOTSEL_DELAY_US, for the line to settle. Timer and pin interrupts that arrive now wait as well.
  4. The read. A pressed button pulls the line low, so the function will return 1 for low and 0 for high, as the rp2 documentation says.
  5. Back to normal. The port drives QSPI_SS again, ends the atomic section and returns. At 1,000 calls a second this costs at least 8 ms of every second; at a few calls a second it costs nothing you could see.

Each call holds the flash away for at least 8 µs, MICROPY_HW_BOOTSEL_DELAY_US, with interrupts off. Polled in a loop 1,000 times a second, that is at least 8 ms of every second with no interrupt served; read a few times a second, it is nothing.

Checkpoint

Check: rp2.Flash parts and the BOOTSEL button

  1. Question 1 of 2 On stock Pico firmware you run vfs.VfsLfs2.mkfs(rp2.Flash(len=1024 * 1024)) to keep the rest of the area for a raw log. What happens at the next start-up?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The boot code fails to mount Flash() and formats all 352 blocks, so the new filesystem is lost

    The lesson's flash_split.py shows it: after mkfs on a part, the next boot printed OSError 22 and formatted all 352 blocks. A raw area needs firmware with a smaller storage size or its own boot code.

  2. Question 2 of 2 A main loop calls rp2.bootsel_button() 2,000 times a second. For at least how many milliseconds of each second are interrupts off?

    Type a number.

    A hint

    Multiply the calls by the shortest pause of one call.

    Show the answer: Question 2 of 2

    Answer: 16 ms

    Each call keeps interrupts off and the flash away for at least 8 µs, MICROPY_HW_BOOTSEL_DELAY_US: 2,000 x 8 µs is 16 ms of every second. Read the button a few times a second instead.

The RP2350 keeps the API and adds blocks, channels and cores

The Pico 2 and Pico 2 W use the RP2350, and the same rp2 module drives it. The PIO tutorial calls basic PIO programs fully compatible between the two chips, with StateMachine ids from 0 to 11 on the RP2350:

Feature RP2040: Pico, Pico W RP2350: Pico 2, Pico 2 W
Processors 2 Arm Cortex-M0+, 133 MHz 2 Arm Cortex-M33 or 2 Hazard3 RISC-V, 150 MHz
SRAM 264 KB 520 KB
PIO 2 blocks, 8 state machines 3 blocks, 12 state machines
DMA 12 channels, 4 pacing timers 16 channels, 4 pacing timers
GPIO 30 30 (RP2350A) or 48 (RP2350B)
PWM channels 16 24
Inline assembler asm_thumb asm_thumb, or asm_rv32 in RISC-V mode
Firmware RPI_PICO, RPI_PICO_W RPI_PICO2, RPI_PICO2_W, each with a RISCV variant
Flash kept for files 1,408 KiB on a Pico 3 MiB on a Pico 2

The chip runs either pair of processors, chosen by the firmware: the RISCV variant of RPI_PICO2 or RPI_PICO2_W starts the Hazard3 cores, and since MicroPython 1.25 it has the @micropython.asm_rv32 inline assembler in place of asm_thumb. The native and viper emitters compile for whichever cores run. The tutorial also warns that the two chips reset their GPIO pins differently, so a program that relies on a pin’s state after reset may need changes: set every pin a program uses, as the pins lesson recommends anyway.

One codebase for both chips

Ask the hardware instead of guessing the board: sys.implementation._machine names the board and its chip, and rp2.StateMachine(8) raises ValueError on an RP2040, which has no third PIO block.

Checkpoint

Check: What the RP2350 adds

  1. Question 1 of 2 How many PIO state machines does the RP2350 have?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 12

    The RP2350 adds a third PIO block, so 12 state machines, and 16 DMA channels instead of 12. Basic PIO programs run on either chip.

  2. Question 2 of 2 A module uses @micropython.asm_thumb for a hot loop. On which firmware does it stop working?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The RISCV variant of RPI_PICO2

    The RP2350 starts either its Arm or its RISC-V cores, chosen by the firmware. Since 1.25 the RISC-V build has the asm_rv32 inline assembler; Thumb code needs the Arm build.

Practise: a bit-timing table for a PIO encoder

A pixel chip that reads a 1 as a long pulse and a 0 as a short one is driven by a PIO program of fixed cycle counts per bit, like the burst above. Before such a program goes near a board, its timing can be worked out on paper. Write the two functions of pio_timing.py: the divider the port sets for a freq, and the high and low time of every bit of some bytes:

Exercise · Medium · Python

Work out a PIO encoder's divider and bit timings

Write the two functions of pio_timing.py, which work out on a computer what a PIO encoder will send.

divider(freq, sys_hz=125_000_000) returns the clock divider that rp2.StateMachine(freq=freq) sets, as a pair (whole, frac). The port computes value = sys_hz * 256 // freq, refuses a value below 256 or above 65536 * 256 with ValueError, and splits it into whole = value // 256 and frac = value % 256.

bit_pulses(data, one_high, zero_high, bit_cycles, freq, sys_hz=125_000_000) returns one (high_ns, low_ns) pair for every bit of the bytes in data, most significant bit first. A 1 stays high for one_high cycles and a 0 for zero_high cycles, and every bit lasts bit_cycles cycles. One cycle lasts value / 256 system clock periods, the divider's average; round each time to the nearest nanosecond with round(). Unless 0 < zero_high < one_high < bit_cycles, raise ValueError.

Starter code · pio_timing.py

SYS_HZ = 125_000_000  # the Pico's system clock after boot


def divider(freq, sys_hz=SYS_HZ):
    """The (whole, frac) clock divider StateMachine(freq=freq) sets."""
    return (0, 0)


def bit_pulses(data, one_high, zero_high, bit_cycles, freq, sys_hz=SYS_HZ):
    """One (high_ns, low_ns) pair per bit of data, most significant bit first."""
    return []
The sample tests · test_pio_timing.py
from pio_timing import bit_pulses, divider


def raises(fn, *args):
    try:
        fn(*args)
    except ValueError:
        return True
    return False


def test_whole_divider():
    """freq=2_000 is a whole divider of 62,500"""
    assert divider(2_000) == (62500, 0)


def test_fractional_divider():
    """freq=988_000 is 126 + 132/256, as in the lesson"""
    assert divider(988_000) == (126, 132)


def test_divider_range():
    """refuses a freq below 1,908 Hz or above the system clock"""
    assert raises(divider, 1_907)
    assert raises(divider, 200_000_000)
    assert divider(1_908) == (65513, 160)


def test_one_byte():
    """0x80 at 8 MHz: a long pulse, then seven short ones"""
    assert bit_pulses(b"\x80", 7, 3, 10, 8_000_000) == [(875, 375)] + [(375, 875)] * 7


def test_bit_order():
    """two bytes, most significant bit first"""
    pulses = bit_pulses(b"\x01\x40", 7, 3, 10, 8_000_000)
    assert len(pulses) == 16
    assert [p[0] for p in pulses].count(875) == 2
    assert pulses[7] == (875, 375) and pulses[9] == (875, 375)


def test_fractional_cycle():
    """at 988 kHz one cycle averages 1012.125 ns"""
    assert bit_pulses(b"\xff", 3, 1, 4, 988_000)[0] == (3036, 1012)


def test_bad_timings():
    """refuses pulses that do not fit the bit"""
    assert raises(bit_pulses, b"\x00", 3, 3, 10, 8_000_000)
    assert raises(bit_pulses, b"\x00", 10, 3, 10, 8_000_000)
    assert raises(bit_pulses, b"\x00", 7, 0, 10, 8_000_000)
A hint

One cycle in nanoseconds is value / 256 * 1e9 / sys_hz, with the same value that divider() computes. For each byte, take its bits from 7 down to 0 with (byte >> bit) & 1, pick the high cycle count, and turn both the high part and the rest of the bit into nanoseconds.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Then check yourself on cycles, dividers, DMA, the flash and the RP2350, one question at a time:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 What does a blocking pull() do when the TX FIFO is empty?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: It stalls and runs again on every cycle until a word arrives; its delay starts after that

    Blocking is the default for pull(). The program counter stays on the instruction, and any delay written after it counts only from the cycle the word arrives.

  2. Question 2 of 12 A state machine runs at freq=5_000_000, a whole divider of 25, and one bit of an encoder takes 7 of its cycles. How long is one bit?

    Type a number.

    A hint

    One cycle at 5 MHz lasts 1 / 5,000,000 s.

    Show the answer: Question 2 of 12

    Answer: 1.4 µs (anything from 1.399 to 1.401 counts)

    At 5 MHz one cycle lasts 200 ns, so 7 cycles last 1,400 ns, 1.4 µs. Count the cycles, then multiply by the length of one.

  3. Question 3 of 12 Which divider does StateMachine(freq=3_000_000) set at 125 MHz?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: 41 + 170/256

    The real frequency is 125,000,000 x 256 / 10,666 = 3,000,187.5 Hz, a little fast, with cycles of 41 or 42 system clocks.

  4. Question 4 of 12 The 26-cycle carrier program must keep every edge exactly on time, and a carrier up to 2 % fast is acceptable. Which setting fits best?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: freq=1_000_000

    Exact edges need a whole divider; the limit on frequency error then decides which whole divider. 125 gives 1.2 %, inside 2 %.

  5. Question 5 of 12 A channel runs 512 transfers with pack_ctrl(size=1). How many bytes does it move?

    Type a number.

    Show the answer: Question 5 of 12

    Answer: 1024 bytes

    size 1 is a half word, 2 bytes, so 512 transfers move 1,024 bytes. Size 0 is a byte and size 2 a 4-byte word.

  6. Question 6 of 12 What does a board do when a DMA count asks for more transfers than the buffers hold?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: It carries on through the memory after the buffers, with no error

    An overrun corrupts the heap silently, and the program fails later somewhere unrelated, which is why count must be worked out from the transfer size.

  7. Question 7 of 12 What is int(t) for t = rp2.DMATimer(freq=48_000)?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: The treq_sel value that selects this timer, from 0x3b to 0x3e

    pack_ctrl(treq_sel=timer) works because int(timer) gives the selector, 0x3b for timer 0 up to 0x3e for timer 3.

  8. Question 8 of 12 You stream 2,048 bytes to a state machine with treq_sel=0x3f instead of its DREQ. What happens?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: The channel writes far faster than the program pulls, and every write to the full FIFO is lost

    A peripheral sets the pace through its request, DREQ (p << 3) + n for a PIO TX FIFO. Unpaced transfers suit memory only.

  9. Question 9 of 12 How many 4,096-byte blocks does rp2.Flash(start=1024 * 1024) cover on a Pico?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: 96

    1,441,792 bytes kept for files minus 1,048,576 is 393,216 bytes, 96 blocks, from flash offset 0x1a0000.

  10. Question 10 of 12 A timer callback must run every millisecond, and the main loop saves a file every second. What does each save do to the callback?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: The callback runs late while sectors are erased and programmed, because interrupts are off

    Code runs from the flash through XIP, so nothing that might run from it may run during an erase or a write. PIO state machines and DMA transfers from RAM keep going.

  11. Question 11 of 12 How many DMA channels does the RP2350 have?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: 16

    The pico-sdk's platform definitions give 16 DMA channels and 4 DMA timers for the RP2350, 12 and 4 for the RP2040.

  12. Question 12 of 12 Pico code uses StateMachine ids 0 to 7, rp2.DMA and asm_thumb. Moved to a Pico 2 with the standard RPI_PICO2 firmware, what must change?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: Nothing in the API; check the pins it relies on after reset

    The PIO tutorial calls basic programs fully compatible; it warns only about different GPIO reset states, so set every pin a program uses.

Interview questions

Warm-up (fresher): Why can a PIO state machine keep timing that a Python loop on the same chip cannot?

A state machine is a small processor beside the CPU with its own program of up to 32 instructions. Every instruction takes exactly one of its cycles unless it waits for data, and delays are written in cycles, so its timing is a count: the burst in this lesson spends 13 cycles high and 13 low in every period. A Python loop shares the CPU with the interpreter, the garbage collector and interrupt handlers, and none of them promises a cycle count, so its pulses stretch whenever something else runs. The state machine also keeps going while Python is busy, as long as its FIFO has data. Python’s job becomes feeding it, with put() or a DMA channel.

Two longer rp2 questions follow; the link under each leads to what it tests, the first step of a good reply and the follow-ups to expect:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A Pico sends an infrared carrier from a PIO program of 26 cycles a period at freq=1_000_000, and the 38 kHz receiver works poorly. A colleague proposes freq=988_000. What does each setting give, what does the change cost, and how else could you get closer to 38 kHz?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A Pico logger reads a sensor in a 1 ms timer callback, polls the BOOTSEL button as fast as its main loop runs and appends a record to a file every second. Readings arrive late. After a change that wrote raw records to rp2.Flash(start=...), every file vanished. Explain both faults and fix the design.

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why can a PIO state machine keep timing that a Python loop cannot?

    Each instruction takes exactly one cycle unless it stalls, plus up to 31 delay cycles written as [n], and the state machine runs beside the processor, untouched by garbage collection and interrupts.

  2. How many times does a loop on jmp(x_dec, ...) run when X starts at n?

    n + 1 times, because the jump tests X before it decrements it. To send n periods, put n − 1, as sm.put(24 - 1) does for 24.

  3. How does a StateMachine's freq become its clock divider on a Pico?

    125_000_000 * 256 // freq: a whole number and a fraction in 256ths, from 1 to 65,536. The slowest state machine runs at 1,908 Hz, and freq=1_907 raises ValueError.

  4. What does a fractional PIO divider cost?

    Uneven cycles: at freq=988_000 they last 126 or 127 system clocks, and only their average is right. A whole divider, such as 125 for 1 MHz, makes every cycle the same.

  5. What does a DMA channel's count mean?

    A number of transfers of size bytes, not a number of bytes: with the default size of 4-byte words, 1,024 bytes are count=256. A count in bytes overruns both buffers without an error.

  6. How do you pace a DMA channel that feeds a PIO state machine?

    Set treq_sel to the FIFO's DREQ, (p << 3) + n for PIO block p and state machine n, with size=0 for bytes and inc_write=False, so no byte is written to a full FIFO.

  7. Why poll rp2.bootsel_button() only a few times a second?

    The button sits on the flash's chip-select line, so each call keeps the flash away for at least 8 µs with interrupts off.

  8. What does the RP2350 add for MicroPython over the RP2040?

    12 PIO state machines, 16 DMA channels, 520 KB of SRAM and 24 PWM channels, under the same rp2 module; its RISCV firmware variants use asm_rv32 instead of asm_thumb.

Key takeaways

  • A PIO state machine runs one instruction per cycle plus the delays you write, so a program’s pulses are counted: the burst spends 26 cycles a period, 13 high and 13 low, and jmp(x_dec) loops X + 1 times.
  • freq becomes a divider of whole 256ths of the 125 MHz clock: the slowest is 1,908 Hz, a fraction gets the average right but makes cycles 126 or 127 clocks long, and a whole divider makes every cycle the same.
  • A DMA channel’s count is in transfers of size bytes, so 1,024 bytes are 256 word transfers, and a count in bytes overruns both buffers without an error.
  • treq_sel paces a channel: a PIO FIFO’s DREQ is (p << 3) + n, and a DMATimer asks at X/Y of the system clock, for as long as you keep the object.
  • rp2.Flash(start, len) hands out parts of the file area, but stock firmware formats the whole area at start-up when its filesystem has another size, so raw records go in a file; flash writes and bootsel_button() turn interrupts off, so both belong outside fast loops.
  • The RP2350 runs the same rp2 code with 12 state machines and 16 DMA channels, and its RISC-V mode trades asm_thumb for asm_rv32.

Terms in this lesson

clock divider
A circuit that makes one cycle of a slower clock last a chosen number of cycles of a faster one. A PIO state machine's divider counts system clocks in steps of 1/256.
DMA
Direct memory access: hardware channels that copy data between memory and peripherals by themselves, so the processor is free while a transfer runs.
DREQ
Data request: the signal by which a peripheral tells a DMA channel that it can take or give one more item, so the channel transfers at the peripheral's pace.
FIFO
First in, first out: a small queue in hardware. Each PIO state machine has a 4-word FIFO for data from the processor and one for data back to it.
PIO state machine
One of the small processors in the programmable I/O blocks of the RP2040 and RP2350. It runs a program of up to 32 instructions, one per cycle of its own clock, to drive and read pins with exact timing.
XIP
Execute in place: running a program straight from external flash, read through a cache. While that flash is erased or written, no code can be read from it.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.