Getting started with TinyGo for IoT Development

using VSCode (PlatformIO) for an Arduino board.
Getting started with TinyGo for IoT Development using VSCode (PlatformIO) for an Arduino board.

Explore using TinyGo for your IoT adventures on Arduino. Setting up - tooling, terminal goodies, VSCode with uploading your first sketch and some interesting bits about TinyGo in the new v0.15.x release.

What is TinyGo?

TinyGo is a new (ish) LLVM-based Compiler that supports a subset of the full Go Language and data-types, a hardware abstraction layer and it’s own runtime implementation (tiny’er than Go). Including a leaner number of Go Standard packages all geared towards IoT devices (and target architectures) that need a small footprint (both in binary size and memory utilisation).

As of the latest 0.15 release, TinyGo now includes support for the popular ESP32 & ESP8266 microcontrollers with more boards added frequently. What’s even cooler, is you can also muse about with the Nintendo Switch and Gameboy Advance - though they are still in early development. If that wasn’t enough, you can also build highly optimised Web Assembly builds too.

TinyGo’s support for hardware sensors and devices are also maturing with most of the popular devices supported.

The latest release also brings support for Bluetooth Low Energy (BLE) via the Nordic nRF51 & nRF52 SoCs too - like the Adafruit Feather nRF52 Kit.

There’s a mark-sweep garbage collector (on platforms outside of AVR) which is invoked when the heap is exhausted or when you force a GC Collection via runtime.GC(). One of the smart ways that TinyGo ensures your heap allocations are low is via escape analysis and where possible, optimise it out.

Go Routines are based on the async-await pattern synonymous with .NET’s async/await or Javascript’s async/await borrowing the C++ implementation used in CLang/LLVM via Coroutines in LLVM. An in-depth write up is available on Ayke van Laethem’s blog post on Goroutines in TinyGo with examples.

The TinyGo compiler pipeline relies heavily on the existing Golang Compiler pipeline and uses it for the parsing and type checking as well as the SSA construction but before this gets to the LLVM infrastructure, TinyGo has a go at optimising for both memory allocation and size constraints which is then further optimised by the LLVM optimiser and patched for the target architecture before the final binary is baked.

TinyGo Compiler Architecture - The journey from Blink.go to tiny main.hex ready to flash.
Figure 1. TinyGo Compiler Architecture - The journey from Blink.go to tiny main.hex ready to flash.

They’ve documented the TinyGo compiler internals, including how they implemented interrupts in TinyGo and the ways they’ve optimised heap allocation.

How Tiny are we talking?

Sometimes it’s worth taking a simple go example and seeing the compilation in Go (v1.15.2) vs TinyGo (v0.15). This was done on Linux (you can also use WSL2) as we can’t build Windows binaries for TinyGo yet.


package main

func main() {
	println("hello world")
}

Compiling with standard Go Compiler:


go build -o ./helloworld-go helloworld.go

Now with TinyGo:


tinygo build -o ./helloworld-tinygo helloworld.go

The size difference? Quite massive:

1.2M helloworld-go
21K  helloworld-tinygo

That’s some serious trimming and optimising, but hangon, what kind of hello world is that? You’re not even importing fmt?

OK let’s try a real token helloworld:


package main

import "fmt"

func main() {
	fmt.Println("hello fmt world")
}
2.1M helloworld-go
253K helloworld-tinygo

Sure it’s a little less tiny but still a significantly bit leaner than the go binary.

Here’s a smattering of different versions and binary sizes for reference. The first example is Compact and the latter is Standard with Go 1.13 vs 1.15.2 and TinyGo 0.15 vs TinyGo 0.9.

Charting the binary size differences of Go vs TinyGo in KB.
Figure 2. Charting the binary size differences of Go vs TinyGo in KB.

The raw data:

1.1M helloworld-compact-go-1.13
1.2M helloworld-compact-go-1.15.2
 21K helloworld-compact-tinygo-0.15
 20K helloworld-compact-tinygo-0.9
2.0M helloworld-standard-go-1.13
2.1M helloworld-standard-go-1.15.2
253K helloworld-standard-tinygo-0.15
149K helloworld-standard-tinygo-0.9

Note that the Go binaries are slowly increasing in size too.

Setting up TinyGo

Let’s take a look at how we get TinyGo setup so you can start writing in TinyGo for your tiny devices. Get the tooling setup, install some awesome goodies for your terminal, setup VSCode to make it a seamless experience and finally, upload a simple Blinky example to Arduino to test our setup.

For this example, I’ll be using an Arduino ATmega 328P based clone made by an Australian company named Freetronics called the Freeduino EtherTen.

Freeduino EtherTen Arduino Compatible ATmega 328P Board
Figure 3. Freeduino EtherTen Arduino Compatible ATmega 328P Board

But if you have any Arduino clone or original or the many supported boards, you’ll be able to follow along!

We’re going to cover the Windows and macOS install but, you can also run TinyGo via Docker. However, you’ll have to flash your device on the host OS as you can’t do it within Docker itself.

Windows via scoop

Keep in mind that (currently) we can’t compile TinyGo binaries for Windows and it only supports MCU and WASM targets, but we can use TinyGo on Windows via Scoop.

The best thing about Scoop is that there’s no need for elevated privileges when installing software like Chocolatey needs.

You should already have installed Go v1.14+, if you haven’t, install that first:


scoop install go

Now let’s get TinyGo - the latest is v0.15.0 as of writing:


scoop install tinygo

The package will update your $PATH to include the location of TinyGo too.

One extra thing we’ll do is to include the AVR-GCC toolchain for our Arduino board, that’s also in Scoop.


scoop install avr-gcc

Verify TinyGo is installed correctly with a version and AVR-GCC Toolchain works with --version flag.


tinygo version
tinygo version 0.15.0 windows/amd64 (using go version go1.15 and LLVM version 10.0.1)
avr-gcc --version
avr-gcc.exe (GCC) 10.1.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

macOS via brew

Installing on macOS is trivial by using brew by adding the tinygo-org tap. Keep in mind you should have a modern version of Go v1.14+ installed already.


brew tap tinygo-org/tools
brew install tinygo

To setup the AVR-GCC toolchain, add the osx-cross tap and install:


brew tap osx-cross/avr
brew install avr-gcc avrdude

Verify TinyGo is installed correctly with a version and AVR-GCC Toolchain works with --version flag.


tinygo version
tinygo version 0.15.0 darwin/amd64 (using go version go1.15 and LLVM version 10.0.1)
avr-gcc --version
avr-gcc (Homebrew AVR GCC 9.3.0) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Install drivers for TinyGo

Once TinyGo is installed, in order to query sensors and other hardware devices, we need to install the drivers.

Easily done via go get:


go get -u tinygo.org/x/drivers

Full set of supported drivers are on the TinyGo Drivers Repo.

Setup tinygo-autocmpl for Terminals

The next step is to setup some terminal goodies for auto-completing TinyGo commands through the tinygo-autocmpl by Masaaki Takasago. This makes it super easy for resolving targets and switches for TinyGo on bash and zsh.

Autocomplete targets and other arguments easily.
Figure 4. Autocomplete targets and other arguments easily.

First go get it:


go get github.com/sago35/tinygo-autocmpl

Then based on your shell, add the following to your ~/.bashrc profile:

For bash:

eval "$(tinygo-autocmpl --completion-script-bash)"

For zsh:

eval "$(tinygo-autocmpl --completion-script-zsh)"

Finally, reload it with:

source ~/.bashrc

Setup VSCode for TinyGo

There’s a whole section on TinyGo IDE Integration, but if you’re using VSCode, get the TinyGo Extension which will update the Go Tool environment variables with the target of your choice too. Bring up the command palette and set the target microcontroller with TinyGo target.

Set your target microcontroller within VSCode.
Figure 5. Set your target microcontroller within VSCode.

In the next article, we’ll setup Jetbrains GoLand (which is what I use mostly now).

First Arduino Sketch - Blinky

Now that we’ve setup everything, let’s get cracking on our first sketch for our Arduino board. The simplest hello world, is that to blink the onboard LED with a delay heartbeat, affectionately we dub this blinky.go.

When you install TinyGo, you’ll find some examples in tinygo/src/examples.

Let’s get our Arduino’s onboard LED to blink for a second, then turn itself off and pulse 5 times.


package main

import (
	"machine"
	"time"
)

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

		led.High()
		time.Sleep(time.Millisecond * 1000)
		for i := 0; i < 5; i++ {
			led.Low()
			time.Sleep(time.Millisecond * 250)

			led.High()
			time.Sleep(time.Millisecond * 250)
		}
	}
}

To upload the above (let’s call that blink.go) we simply ask tinygo to flash to the target of arduino (or whichever device you have, the above is portable across a lot of boards).

Here’s my output on Windows - note that specifying the port is optional as of TinyGo 0.13+, but you can force specify the port with -port=/dev/[PORT].


tinygo flash -target arduino src/blink.go

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.01s

avrdude: Device signature = 0x1e950f (probably m328p)
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "C:\...\tinygo166763415\main.hex"
avrdude: writing flash (996 bytes):

Writing | ################################################## | 100% 0.16s

avrdude: 996 bytes of flash written
avrdude: verifying flash memory against C:\...\tinygo166763415\main.hex:
avrdude: load data flash data from input file C:\...\tinygo166763415\main.hex:
avrdude: input file C:\...\tinygo166763415\main.hex contains 996 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 0.13s

avrdude: verifying ...
avrdude: 996 bytes of flash verified

avrdude: safemode: Fuses OK (E:00, H:00, L:00)

avrdude done.  Thank you.

Under the covers, TinyGo uses avrdude to flash the output of the machine code.

Your board should be blinking something like this:

TinyGo Blinkn on Arduino.
Figure 6. TinyGo Blinkn on Arduino.

Let’s dive deeper - Machine package

From the example below, let’s take a look at some of the important parts to it specific to TinyGo:


import (
	"machine"
	"time"
)

The machine package is the main backbone (the hardware abstraction layer) to all our interactions with the board or target, for the particular Arduino example above, it’s defined in the Arduino Machine API definition.

When we create a new supported board, we define the mapping of the pins to something meaningful, for example the default onboard LED pin is set to D13. This should be similar across a variety of boards which show board status.


const LED Pin = D13

Outside of the Pin Maps, it contains the processor frequency, bus setup and configuration for that specific board (or target).


func main() {
	led := machine.LED
	led.Configure(machine.PinConfig{Mode: machine.PinOutput})    

In the main, we’re first going to configure our LED pin to be an output pin (keep in mind that GPIO pins can be either an input or output configuration), then we can instruct TinyGo to turn our configured led to be ON (led.High()) or OFF (led.Low()).


for {
		led.Low()
		time.Sleep(time.Millisecond * 1000)		

		led.High()
		time.Sleep(time.Millisecond * 1000)

The boiler plate code around these fragments are standard Go code you already know and love - including the for loop and the time package used to delay/sleep.

Next time we’re going to dive into some GoRoutines and play with some sensors which is where the real interesting things happen and shows off some TinyGo prowness.

Play in the TinyGo Playground

A really neat little online learning tool is the TinyGo Playground where you can paste the above example and even play with GoRoutines in TinyGo like in tinygo\src\examples\blinky2 - try it with the Phytec reel board.

References

Related Articles