Tinygo on MCUs

Spend a few minutes this morning trying tinygo on a Microbit and a Arduino Uno.

(Host system is Arch Linux)

Arduino UNO

package main

import (
        "machine"
        "time"
)

func main() {
        led := machine.LED
        led.Configure(machine.PinConfig{Mode: machine.PinOutput})
        for {
                led.Low()
                time.Sleep(time.Millisecond * 100)

                led.High()
                time.Sleep(time.Millisecond * 100)
        }
}
  • pacman -s avr-gcc avr-libc avrdude tinygo
  • cd to this directory
  • tinygo flash -target arduino

It worked! I then tried a go-routine, which would not compile and learned this:

  • scheduler: Use the specified scheduler. The default scheduler varies by
    platform. For example, AVR currently defaults to none because it has such
    limited memory while asyncify and tasks are used for other platforms.
    Normally you do not need to override the default except on AVR where you can
    optionally select the tasks scheduler if you want concurrency.
    • scheduler=tasks The tasks scheduler is a scheduler much like an RTOS
      available for non-WASM platforms. This is usually the preferred scheduler.
    • scheduler=asyncify The asyncify scheduler is a scheduler for WASM based
      off of Binaryen’s Asyncify Pass.
    • scheduler=none The none scheduler disables scheduler support, which
      means that goroutines and channels are not available. It can be used to
      reduce firmware size and RAM consumption if goroutines and channels are
      not needed.

Microbit

Made this program a little more complex with two goroutines:

package main

import (
        "machine"
        "time"
)

func main() {
        ledCol1 := machine.LED_COL_1
        ledCol2 := machine.LED_COL_2
        ledRow1 := machine.LED_ROW_1
        ledCol1.Configure(machine.PinConfig{Mode: machine.PinOutput})
        ledCol2.Configure(machine.PinConfig{Mode: machine.PinOutput})
        ledRow1.Configure(machine.PinConfig{Mode: machine.PinOutput})
        ledRow1.High()
        go func() {
                for {
                        ledCol1.Low()
                        time.Sleep(time.Millisecond * 500)

                        ledCol1.High()
                        time.Sleep(time.Millisecond * 500)
                }
        }()

        go func() {
                for {
                        ledCol2.Low()
                        time.Sleep(time.Millisecond * 100)

                        ledCol2.High()
                        time.Sleep(time.Millisecond * 100)
                }
        }()

        select {}
}
  • cd to this directory
  • tinygo flash -target microbit

It worked! Two LEDs are flashing at different rates. This is a more capable 32-bit CPU so goroutines work.

Not sure what host tools are needed, arm-gcc, etc. was already installed – not sure what it uses under the hood. I ran strace on tinygo and did not see any evidence it is calling gcc.

Tinygo has advanced a lot since I last looked at it and appears to be useable for lab work, and perhaps even product programming. The development experience is quite nice.

Was also impressed how fast the programs built and flashed – at least as fast as anything else I’ve used.

Here is a repo with test code/notes:

I was able to run a version on the Arduino that had a goroutine and running tinygo with the -scheduler tasks option. However, when I tried to use channels, I got a compile error.

TinyGo provides a nice -size option which shows how much flash and ram is being used:

Arduino AVR

[cbrake@ceres arduino-uno]$ tinygo flash -target=arduino -size full
   code  rodata    data     bss |   flash     ram | package
------------------------------- | --------------- | -------
    622       0       0      31 |     622      31 | (unknown)
      4       0       0       0 |       4       0 | /scratch/tinygo/blink/usr/lib/tinygo/src/device/avr
     64       0       0       0 |      64       0 | /scratch/tinygo/blink/usr/lib/tinygo/targets
      0       0       0     512 |       0     512 | C stack
    160       0       0       0 |     160       0 | device/avr
     92       0       0     130 |      92     130 | machine
     14       0       0       0 |      14       0 | main
   1248      90       0       0 |    1338       0 | runtime
     22       0       0       0 |      22       0 | runtime/interrupt
    144       0       0       0 |     144       0 | runtime/volatile
------------------------------- | --------------- | -------
   2370      90       0     673 |    2460     673 | total

Arduino AVR with scheduler

[cbrake@ceres arduino-uno]$ tinygo flash -target=arduino -scheduler=tasks -size full
   code  rodata    data     bss |   flash     ram | package
------------------------------- | --------------- | -------
    622       0       0      49 |     622      49 | (unknown)
      4       0       0       0 |       4       0 | /scratch/tinygo/blink/usr/lib/tinygo/src/device/avr
     98       0       0       0 |      98       0 | /scratch/tinygo/blink/usr/lib/tinygo/src/internal/task
     80       0       0       0 |      80       0 | /scratch/tinygo/blink/usr/lib/tinygo/src/runtime
     64       0       0       0 |      64       0 | /scratch/tinygo/blink/usr/lib/tinygo/targets
      0       0       0     512 |       0     512 | C stack
    160       0       0       0 |     160       0 | device/avr
    454      24       0       0 |     478       0 | internal/task
     92       0       0     130 |      92     130 | machine
     14       0       0       0 |      14       0 | main
   4000     125       0       0 |    4125       0 | runtime
     88       0       0       0 |      88       0 | runtime/interrupt
    144       0       0       0 |     144       0 | runtime/volatile
------------------------------- | --------------- | -------
   5820     149       0     691 |    5969     691 | total

Microbit

[cbrake@ceres microbit]$ tinygo flash -target=microbit -size full
   code  rodata    data     bss |   flash     ram | package
------------------------------- | --------------- | -------
    320       0      16      46 |     336      62 | (unknown)
     48       0       0       0 |      48       0 | /build/tinygo/src/tinygo/build/tinygo/src/tinygo/llvm-project/compiler-rt/lib/builtins/arm
     12       0       0       0 |      12       0 | /scratch/tinygo_experiments/microbit/usr/lib/tinygo/src/device/arm
      4       0       0       0 |       4       0 | /scratch/tinygo_experiments/microbit/usr/lib/tinygo/src/device/nrf
     64       0       0       0 |      64       0 | /scratch/tinygo_experiments/microbit/usr/lib/tinygo/src/internal/task
     22       0       0       0 |      22       0 | /scratch/tinygo_experiments/microbit/usr/lib/tinygo/src/runtime
      6       0       0       0 |       6       0 | C nrfx
      0       0       0    2048 |       0    2048 | C stack
     74       0       0       0 |      74       0 | device/arm
     40       0       0       0 |      40       0 | device/nrf
    244      24       0       0 |     268       0 | internal/task
     14       0       0       0 |      14       0 | lib/picolibc/newlib/libc/string
    888       0       0       0 |     888       0 | llvm-project/compiler-rt/lib/builtins
     68       0       0     130 |      68     130 | machine
    184       0       0       0 |     184       0 | main
   1870     146       0       0 |    2016       0 | runtime
    100       0       0       0 |     100       0 | runtime/volatile
------------------------------- | --------------- | -------
   3958     170      16    2224 |    4144    2240 | total

The scheduler adds considerable overhead, but it’s interesting more flash is required on the AVR than the ARM MCU.

The ATMega328P MCU on the Arduino has 32KB of flash and 2KB of SRAM, so in either case it seems we have enough left to write a sizable application.

The nRF51822 MCU in the microbit v1 has 256KB of flash and 16KB SRAM, so we have lots of space in this device for applications, however I’m not sure if the nRF stack requires some space as well that is not reported here.

The microbit v2 has 512KB of flash and 128KB of SRAM.

Arm instruction set produced most compact code in my experience I have seen WebKit compiled for
Arm 32bit is almost 20% smaller compared to mips everything else remaining same. It will be interesting to see how density compares with RISCV