tinygo icon indicating copy to clipboard operation
tinygo copied to clipboard

Design: How should we support RP2040 PIO?

Open kenbell opened this issue 3 years ago • 7 comments

This issue is a placeholder to discuss how we should support RP2040 PIO

Background

The Pico SDK comes with pioasm, which is a separate tool to assemble PIO instructions (+ interleaved Python / C). It's run as a code generator, run before compiling the main application logic and generating python/C code. pioasm can also output the binary as hex.

CircuitPython comes with it's own assembler (adafruit_pioasm) which has a few language differences. It can be used inline in CircuitPython code:

hello = """
.program hello
loop:
    pull
    out pins, 1
; This program uses a 'jmp' at the end to follow the example.  However,
; in a many cases (including this one!) there is no jmp needed at the end
; and the default "wrap" behavior will automatically return to the "pull"
; instruction at the beginning.
    jmp loop
"""

assembled = adafruit_pioasm.assemble(hello)

TinyGo Options

What model should we use for TinyGo?

Some options:

  1. Use pioasm with //go:generate
  2. Have an inline primitive, similar to arm.Asm
  3. Allow coding by doing function call per instruction, e.g pioapp.AddInstruction(pio.MOV,xxx,xx,xx)

Quick evaluation...

Option Pros Cons
1 Quick to implement Extra dependencies to use TinyGo - install/compile Pico SDK
2 Familiar model (like arm.Asm) and similar(-ish) to CircuitPython Relatively complex to implement pioasm in Go, some complexity in TinyGo to treat pio.Asm as a special case
3 Relatively quick to implement Relatively non-intuitive (personal opinion?)

References:

https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf section 3.3 https://datasheets.raspberrypi.org/pico/raspberry-pi-pico-c-sdk.pdf section 3.3 https://learn.adafruit.com/intro-to-rp2040-pio-with-circuitpython

kenbell avatar Jun 16 '21 05:06 kenbell

My personal preference is to do pio.Asm approach. Apps using PIO would be something like this, based on CircuitPython:

import "machine/pico/pio"

hello := pio.Asm(`
.program hello
loop:
    pull
    out pins, 1
; This program uses a 'jmp' at the end to follow the example.  However,
; in a many cases (including this one!) there is no jmp needed at the end
; and the default "wrap" behavior will automatically return to the "pull"
; instruction at the beginning.
    jmp loop
`)

sm = pio.StateMachine{
    binary: hello,
    frequency: 2000,
    firstOutPin: board.LED,
}
sm.Start()

for {
    sm.Write([]byte{1})
    time.Sleep(500 * time.Millisecond)
    sm.write([]byte{0})
    time.sleep(500 * time.Millisecond)
}

pio.Asm(code string) would be translated by:

  1. Assemble the PIO code
  2. Output the binary PIO code as constant in LLVM IR
  3. Replace pio.Asm call with a pointer to the binary PIO code

kenbell avatar Jun 16 '21 05:06 kenbell

I'm not sure I understand how //go:generate would work. Would the assembly be in a separate file?

I personally like pio.Asm since the assembly is inside the code and easier to find than if it were a tag. It's also great that it's similar to how CicuitPython does it.

soypat avatar Jun 16 '21 14:06 soypat

Yes, we will definitely want to have some support for PIO eventually. It's extremely powerful, I don't think I've seen anything this powerful in any other MCU I looked at. However, adding good support for it probably won't be trivial.

I should note that the ESP32 has something somewhat similar: it has a ULP coprocessor mainly intended for low-power operations. We might want to keep this in mind so that anything we come up with for PIO, can also work in a similar way for the ESP32.

I'll go through the options:

1. //go:generate

This could work, but note that //go:generate is not called at build time. It has to be run manually. So how this could work is that you make it generate Go code (e.g. an uint16 slice, similar to C header files) and store this along side the other files. It could even work in a single file, modifying a Go source file. For example:

//go:generate <some program that modifies the source code>

// PIO PROGRAM
// .program hello
// loop:
//    pull
//    out pins, 1
//    jmp loop
var someProgram = []uint16{
    // ...
}

func main() {
    // ...
}

Go code is in fact quite easy to modify this way using the built-in go/parser package. The program could look for the header (here // PIO PROGRAM, I couldn't come up with a better identifier right away) and modify the array.

One big advantage is that this does not require compiler changes specifically for the RP2040, which would then need to be maintained forever. It also doesn't need to have the SDK installed at build time, only when re-generating the PIO program.

2. pio.Asm

Very interesting approach, and indeed quite similar to the arm.Asm API in TinyGo. Note that I'm not too excited about the arm.Asm API, but it was the best I could come up with at the time. The problem is that it looks like regular Go code but it has different semantics: the parameter has to be a constant.

For PIO, this could certainly work. However, it has a big drawback: it requires a PIO assembler at build time. I would really like to avoid having to call out to pioasm at build time (especially inside the machine package). One solution is to reimplement it in Go, but that's also a lot of work and most likely leads to some language inconsistencies (like in the case of CircuitPython).

One variant on this could be embed-style programs, something like this:

//go:pioasm
// pull
// out pins, 1
// etc
var pioProgram []uint16

This also requires changes to the compiler, but in different places.

3. Manually assembling instructions

I like this approach from an implementers point of view, but I agree it's not at all easy to use. Especially things like labels would make things complicated. I think there are better ways to support PIO in TinyGo.

4. Domain specific language, maybe?

Something I've been thinking about while looking at all this, is whether it is possible to write a domain specific language for deterministic execution of code. I've been thinking a bit about this already in the case of WS2812, which is currently implemented directly in assembly.

The idea would be to define a new (small!) language with a limited number of variables that can be compiled either directly to assembly (ARM, AVR, Xtensa, ...) or to the special PIO code. There are various things that make this hard, such as that many chips don't have deterministic jump instructions (especially faster chips like Cortex-M4 and up). This is probably left for the future, and we'll probably want to write raw PIO instructions anyway for more control over the output.

Conclusion

Thank you for starting the discussion on this topic! I agree there is not one obviously best way to implement this feature, but I agree it would be great to have.

Some considerations, in short:

  • The ESP32 ULP coprocessor could be good for comparison.
  • It would be a good idea to avoid depending on external tools such as pioasm.
  • It would be great if this could be implemented without needing any compiler changes, especially as it is to support just one chip (the rp2040).

aykevl avatar Jun 17 '21 11:06 aykevl

ESP32 ULP

Looking at the ESP32 ULP docs, it looks like variables can be allocated from RTC_SLOW_MEM to be shared with the main processor, whereas it looks like PIO uses FIFOs. To support ULP, we'd need an approach that can output object files that can be combined into the tinygo app using the linker?

Ref - ULP program variables: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/ulp.html#accessing-the-ulp-program-variables

If we go the comment approach, ULP would look something like this?

//go:ulpasm
//                      .global measurement_count
//  measurement_count:  .long 0
//
//                      move r3, measurement_count
//                      ld r3, r3, 0
var ulpProgram []uint16

//go:extern ulpasm.measurement_count
var measurementCount uint16 

To avoid the //go:extern, we'd probably have to go for the code-generation approach to spit out a .go file with the program binary + exposed symbols?

Comment syntax

To make it more 'pluggable', would it make sense to define the pragma like this: //go:asm <lang>

Would then have:

//go:asm ulp
//    ...
...
//go:asm pio
//    ...

CGO-style?

Another option would be to make this more like CGO? So...

//                      .global measurement_count
//  measurement_count:  .long 0
//
//                      move r3, measurement_count
//                      ld r3, r3, 0
import "ulp"

and

// pull
// out pins, 1
// etc
import "pio"

I don't really like having 'magic' import packages, and I think could be super-confusing for people trying to track down these mysterious packages - so I'm not advocating for this one, but thought it's another option we could think about.

pioasm

There's a formal spec for the PIO language in the pioasm code, so it should be possible to create a Go implementation of pioasm that meets the official Pico syntax: https://github.com/raspberrypi/pico-sdk/blob/master/tools/pioasm/lexer.ll and https://github.com/raspberrypi/pico-sdk/blob/master/tools/pioasm/parser.yy

If we do pick an approach where we want a Go implementation of pioasm to avoid external tool dependency, it should be a separate github repo?

My current thoughts

I'm currently thinking a code-generation approach would be best, with both:

  1. See if we can contribute a 'Go' output format for the official pioasm tool (currently 'C', 'Python', 'Hex') and embrace use of //go:generate pioasm -o go foo.pio foo.go

  2. (with more work) support the comment / pragma approach using a Go implementation of pioasm, which would avoid the need for a 3rd party tool

I think the first option (contribute to official pioasm tool) is probably a lot less work to get an initial level of support, and also exposes the wider RP2040 community to TinyGo.

kenbell avatar Jun 20 '21 23:06 kenbell

I'll be watching from the sidelines since most of this exceeds my knowledge on the subject. I'll leave one idea on the drawing board:

  • It would be nice if I could look at someone's PIO assembly written in C/Python and have it easily ported to Go. this is nice since it also means many tutorials available on youtube on PIO also apply to TinyGo

soypat avatar Jun 21 '21 01:06 soypat

I've created a proof of concept of using the 'official' pioasm tool to output Go code using //go:generate....

This is the change to the pioasm tool: https://github.com/raspberrypi/pico-sdk/compare/master...kenbell:pioasm-go-output This is an example app that uses //go:generate: https://github.com/kenbell/tinygo-rp2040-pio-example

Since there's no support for actually using the PIO binary yet, the app doesn't do anything.

In the example:

  • test.pio is the PIO source
  • ws2812_pio.go is the generate Go code

kenbell avatar Jun 25 '21 05:06 kenbell

I'd like to get some feedback on the implementation I've put here: https://github.com/tinygo-org/tinygo/pull/1983

It would be great if someone else could try it out and indicate whether the APIs make sense (I've tried to mirror the APIs from the c-sdk, but in Go style).

The pioasm tool from the Pico c-sdk would have dependencies on this API, so we should make sure it's the right API for the long-term if we go this route.

kenbell avatar Jul 03 '21 16:07 kenbell

Now that the TinyGo PIO support landed in the main pico SDK repo I think we can close this issue. Thank you very much to @kenbell and @soypat for making it happen!

deadprogram avatar May 12 '24 12:05 deadprogram