Closed kenbell closed 5 months 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:
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.
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:
//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.
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.
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.
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.
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:
pioasm
.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?
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
// ...
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.
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?
I'm currently thinking a code-generation approach would be best, with both:
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
(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.
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:
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:
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.
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!
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:
TinyGo Options
What model should we use for TinyGo?
Some options:
//go:generate
arm.Asm
pioapp.AddInstruction(pio.MOV,xxx,xx,xx)
arm.Asm
) and similar(-ish) to CircuitPythonpio.Asm
as a special caseReferences:
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