PIOsquare

Overview

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.

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:

(*
  PIOBEGIN
    .program square_wave
      set pindirs 1
    loop:
      set pins 1 [1]
      set pins 0
      jmp loop

    .program square_wave_asym
      set pindirs 1
      .wrap_target
      set pins 1 [1]
      set pins 0
      .wrap
  PIOEND
*)

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.

Module PIO

The example program uses 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. his module is likely to change.

Signal Output

Traces:

  • 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 text output, though. It’s the signal that matters in this case.

Build and Run

Repository

  • libv2: for RP2040/Pico and RP2350/Pico2: PIOsquare
  • lib (v1): this version is no longer being maintained: PIOsquare

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