This program demonstrates the use and integration of PIO assembly programs with an Oberon program. The focus is on the set-up and toolchain, not yet any real use of PIO. The PIO programs simply output a square-ish wave signal on two GPIO pins, without any interaction via the PIO FIFOs and all that good stuff.

The program was developed with Astrobe for Cortex-M0 v9.1.

Tool pio2o

The tool pio2o runs the pioasm assembler to create corresponding binary code, and then generates an Oberon module to access that code for loading it into the PIO instruction registers, including the .wrap and .wrap_target parameters.

See pio2o for more information.

Program Description

The PIO assembly source code – borrowed from the RP2040 datasheet – is contained directly in the program module PIOsquare.mod:

    .program square_wave
      set pindirs 1
      set pins 1 [1]
      set pins 0
      jmp loop

    .program square_wave_asym
      set pindirs 1
      set pins 1 [1]
      set pins 0

Keeping the PIO code in the Oberon module may or may not be a good idea, this requires more practical exploration. PIO programs usually are a few instructions (lines) only, and can encompass maximally 32 instructions.

Alternatively, PIO code can be put into a separate file with extension .pio (without the PIOBEGIN and PIOEND markers, of course). There’s a corresponding file PIOsquare.pio in the example program directory.

Running pio2o on PIOsquare.mod, either via command line, or the Astrobe tools menu, extracts the above PIO code, runs it through the pioasm assembler, and creates an Oberon module named PIOsquarePio, that is, by default Pio gets added to the source module name.1 Normally, we wouldn’t keep generated files in the repo, I include it here for easy reference in this context.

Program module PIOsquare IMPORTs PIOsquarePio, and gets the binary code via PIOsquarePio.GetCode as an array, which is then loaded into the PIO device’s instruction registers using PIO.PutCode.

Since we have two PIO programs, they need to be loaded into the instruction registers at different locations. PIOsquarePio.GetCode also provides the two address values for .wrap and .wrap_target, which need to be configured for the state machines using PIO.ConfigWrap.

By default, all state machines start executing at address zero, hence to run the program loaded at the higher address, the starting address needs to be set with PIO.SetStartAddress for one of the state machines.

See PIOsquare for all required set-up steps for the two programs and the corresponding two state machines.

Module PIO

The example program contains a rudimentary first version of device module PIO, to access the registers of the two PIO peripherals and their state machines, plus a few required procedures to implement this program. This module is likely to change before “promoting” it to the library.

Signal Output


  • yellow: output from state machine 0
  • blue: output from state machine 1

The square wave from state machine 0 (yellow) has a measured frequency of about 478 Hz. The clock divider for the state machine is set to 65,536, hence it runs at 125 MHz / 65,535 = 1.91 kHz. The PIO program above sets the GPIO pin to 1, and delays for one clock cycle using [1]. Then the pin is set to 0 in the next cycle, with the jmp taking another clock cycle, hence the state machine clock rate is divided by 4, so 1.91 kHz / 4 = 477 Hz. Close enough.

The output of state machine 1 (blue) shows the effect of using .wrap and .wrap_target. Since we can omit the jmp instruction, the second half of the wave is only one state machine clock cycle long. Using the wrap mechanics saves us one instruction and thus one storage register of the precious 32 in total per PIO device.

Output Terminals

See Set-up, one-terminal set-up. No much output, though.

Build and Run

Run pio2o on module PIOsquare to generate PIOsquarePio, then build module PIOsquare with Astrobe, and create and upload the UF2 file using abin2uf2.

Set Astrobe’s memory options as listed, and the library search path as explained.


  1. You can optionally specify a different output module name using -o module_name↩︎