Play with RP2040 MCU PIO Assembler

With the trend of ROE (Rust on Everything), cool kids (respectfully) have worked hard to port Rust Embedded toolchain to RP2040 MCU, in addition to many other MCUs already.

After the ubiquitous Micro-Python (also Circuit-Python) and Arduino toolchains, Rust now becomes another development platform for Raspberry Pi Foundation’s RP2040 MCU, which similar to STM32 Bluepill, provides a cheap and versatile piece of hardware. RP2040 helps you learn about ARM MCU, digital design from its excellent documentation, and Rust embedded programming in general.

The Rust rp-hal library (link) provides detailed setup information to get you started.

One interesting feature of RP2040 is its self-driving GPIO pins, called PIO. PIO has its own 9 instructions set, bunch of registers, full access to GPIO pins.

Parallax Propeller 2’s smart pin idea is kind of similar, but not exactly the same … (https://forums.parallax.com/discussion/173167/rpi-pico-do-creators-users-reviewers-know-about-the-propeller-2)

In this article, we will examine a few PIO programs, as well as their disassembled instructions, as the output from the Rust assembler (pio-rs).

1. square wave output

let pm = pio_proc::pio_asm!(
"set pindirs, 1",
".wrap_target",
"set pins, 0 [5]",
"set pins, 1 [5]",
".wrap",
).program;
for x in &pm.code {
println!("{:#018b}",x)

}

Rust assembler outputs three instructions:

0b111 00000 100 00001
0b111 00101 000 00000
0b111 00101 000 00001

As you can see, these are three SET instructions

https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf (page 328)
  • The leading opcode 111 identifies the SET instruction (Yes, PUSH and PULL share the same opcode, that’s why 3-bit can encode 9 instructions)
  • five-clock-cycle delay is encoded in BIT12 to BIT8 field
  • 100 in “Destination” field points at PINDIR
  • SET value stays at the last 5 bits, ZERO or ONE in this case

In addition, Rust assembler also spits out some control register settings that are not represented by these three SET instructions, i.e. wrap source=2, and target=1, which allows the second and third instructions to loop endlessly:

Program { code: [57473, 58624, 58625], origin: None, wrap: Wrap { source: 2, target: 1 }, side_set: SideSet { opt: false, bits: 0, max: 0, pindirs: false } }

2. WS2812 driver

The shortest PIO code (only four-instruction) to drive WS2812 NEO-pixel, as is explained in details in RP2040 SDK document, page 37 (https://datasheets.raspberrypi.com/pico/raspberry-pi-pico-c-sdk.pdf)

Here is also the Adafruit version for reference (https://learn.adafruit.com/intro-to-rp2040-pio-with-circuitpython/using-pio-to-drive-a-neopixel)

let pm = pio_proc::pio_asm!(
".side_set 1",
".wrap_target",
"bitloop:",
"out x, 1 side 0 [6]",
"jmp !x do_zero side 1 [3]",
"do_one:",
"jmp bitloop side 1 [4]",
"do_zero:",
"nop side 0 [4]",
".wrap",
).program;
for x in &pm.code {
println!("{:#018b}",x)

}

Rust assembler outputs only four instructions:

0b011 0 0110 001 00001
0b000 1 0011 001 00011
0b000 1 0100 000 00000
0b101 0 0100 010 00 010
  • 1-bit MSB out of 5-bit delay/side-set field “side-effect” sets GPIO to ONE or ZERO
  • the remaining 4-bit sets the delay as clock cycles: 6–3–4–4
  • JMP address = 3 or 0, are calculated by the assembler, to reload PC register
  • two JMP instructions have different jump conditions: 001 vs 000 (always jump)
  • NOP is a MOV instruction in disguise

3. Summary

We’ve been focusing on the PIO instructions themselves. The other piece of the puzzle is to set extra control registers and release GPIO from MCU side. (see PIO examples in Rust HAL: https://github.com/rp-rs/rp-hal/blob/main/rp2040-hal/examples/pio_side_set.rs)

Interestingly, after all these programming languages, compilers/assemblers, in the end, it boils down to a few binary instructions to the digital machine for execution. Yet, such simple instructions can achieve quite the complexity of many digital IO operations.

That is the beauty of digital designs.

In the case of PIO, due to its simplicity, you can almost hand assemble the instructions bit-by-bit yourself, and you can recognize the instruction bit patterns quite easily.

In the early days of computers, (https://www.dcs.warwick.ac.uk/~edsac/) for example, you see register or memory bits flickering on/off when a program is executing. These bit patterns are actually pretty useful debugging tool, for example to tell whether a numerical computation is converging or not.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
circuit4u

circuit4u

memento of electronics and fun exploration for my future self