RP2xxxPIO: writing PIO programs inline in Swift

Hi everyone!

I’d like to share RP2xxxPIO, a Swift DSL for writing PIO programs directly inside your Embedded Swift firmware.

PIO is programmable I/O hardware on Raspberry Pi’s RP2040 and RP2350 microcontrollers. Think of it as small processors dedicated to reading and writing pins with precise timing, independently of the main CPU. It’s useful for things like custom serial interfaces, LED drivers, and digital audio.

Instead of writing a separate assembly file, you can write this:

import RP2xxxPIO

struct Pulse: PIOProgram {
    var clockDivider: Double = 1
    var pins: PIOPinMapping = .init()
    let setCount: Int? = 1

    let code = #constPIO {
        @PIOLabel var loop

        Label(loop)
        Set(.pins, 1).delay(7)
        Set(.pins, 0).delay(7)
        Jmp(to: loop)
    }
}

This describes a program that repeatedly drives an output high and low. The instructions are compiled at build time, while the pin mapping and clock divider can vary between instances:

let pulse = Pulse(
    clockDivider: 100,
    pins: .init(set: 2)
)

The optional CPicoSDK integration provides the program descriptor and state-machine configuration. Your application still explicitly loads the instructions, initializes the GPIOs, and starts the hardware. Declaring a Swift value doesn’t quietly claim resources or touch registers.

Labels that feel like Swift

Labels are one part of the API I’m particularly happy with. loop is an ordinary Swift identifier—not an assembly symbol stored in a string, or an instruction address you have to maintain.

The result builder captures the sequence of instructions and label markers. A label marks a position without emitting an instruction; the assembler resolves jumps to the resulting program-relative offsets. Moving instructions around or jumping forward doesn’t require manually updating addresses.

A useful distinction fell out of that design: a label’s identity isn’t its address. The offset belongs to a particular program. Declare a label outside the builder, and you can also use it from the surrounding Swift code to find an entry point.

Constant when possible, dynamic when needed

There are two compilation macros:

  • try #pio compiles what it can at build time and supports runtime customization.
  • #constPIO requires the entire program and its encoding options to be known at build time.

For fully constant programs, both generate the same PIO instruction words without runtime assembly. #constPIO is the reassurance: accidentally introduce something that requires runtime assembly, and the build fails. #pio keeps its throwing interface even when the program is constant.

Runtime customization looks like ordinary Swift:

func makeProgram(value: Int) throws(PIOAssemblyError) -> PIOProgramData {
    try #pio {
        Set(.x, value)
        Nop()
        Jmp(to: 0)
    }
}

Eligible constant instructions are precompiled; runtime-dependent instructions and final layout are handled on the device. Invalid dynamic operands throw an error. Definitely invalid constant programs produce compiler diagnostics.

It’s closer to partial evaluation than a JIT: the interesting question is how much work can disappear before the firmware reaches the device.

Does moving that work help?

For a matched eight-instruction program, I measured CPU instructions executed from construction until the program was ready for the SDK. The warmed, uncaptured #constPIO case took 59 instructions, compared with 14,460 for the Swift runtime assembler. With one dynamic operand, #pio used about 10% fewer instructions than runtime assembly; fully dynamic programs matched it.

Those are emulator measurements, not hardware cycle counts. The harness uses software floating-point helpers and deterministic entropy; first-use initialization and captured-label bookkeeping are measured separately. And this only changes construction costs—identical PIO words execute identically once loaded.

This started as a result-builder experiment and gradually became something I could use in firmware. There are now reference-assembler comparisons, SDK configuration checks for both chips, and a complete Pico 2 audio example.

It’s still experimental, and the API may change. The project is MIT-licensed, and the README starts with usage rather than internals. I’d love feedback from people working with Embedded Swift—or exploring how far ordinary Swift syntax can take a small, hardware-specific language.

5 Likes