tinygo-org / tinygo

Go compiler for small places. Microcontrollers, WebAssembly (WASM/WASI), and command-line tools. Based on LLVM.
https://tinygo.org
Other
15.4k stars 909 forks source link

Design: How should we support RP2040 PIO? #1953

Closed kenbell closed 5 months ago

kenbell commented 3 years ago

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 commented 3 years ago

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
soypat commented 3 years ago

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.

aykevl commented 3 years ago

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:

kenbell commented 3 years ago

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.

soypat commented 3 years ago

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:

kenbell commented 3 years ago

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:

kenbell commented 3 years ago

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.

deadprogram commented 5 months ago

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!