TinyGo: A Go Compiler for Micro-controllers

For micro-controllers like the Arduino or the BBC Micro:bit there are a choice of different programming languages, and now Go (or Golang) can be used for embedded solutions.

Go is an open source language that was developed and supported by Google. Go is a compiled language that runs on MS-Windows, MacOS and Linux.

The TinyGo project implements the exact same Go programming language, however TinyGo uses a different compiler that creates applications on a variety of different micro-controllers.

In this blog I wanted to document some of my notes on building an apps that ran on Arduino and a BBC Micro:bit hardware.

Some key take-aways are:

  • TinyGo is easy to install and flash packages to a micro-controller.
  • The same Go program can potentially run on totally different controllers (some comments on this below).
  • The TinyGo project is still in development so it’s missing some common libraries, full Wifi support being the most important one.

Getting Started

For your specific installation requirements see: https://tinygo.org/getting-started/install/

The typical TinyGo program will have three sections: 1) a definition of libraries used, 2) a setup of hardware and variables, and 3) an infinite loop to run some actions:

Arduino Uno 7 Segment Display Project

For a first project I wanted to show some numbers on a 7 segment display with an Arduino Uno.

The TinyGo site has a list of supported devices: https://tinygo.org/docs/reference/devices/. It’s important to realize that not all the libraries are support on all micro-controllers. There are examples for these drivers at: https://github.com/tinygo-org/drivers/tree/release/examples.

Unfortunately there aren’t examples for all the drivers, so in some cases I needed to look at the source code and reference examples done in Arduino C.

Below is the Go source for a program that will setup the tm1637 segment display on :

//
// Example program to write to a 7 segment display
//
package main

import (
    "machine"
    "time"
    "tinygo.org/x/drivers/tm1637"
)

func main() {
    // Define hardware, clk on pin D2, di (data) on pin D3
    mydisplay := tm1637.New(machine.D2, machine.D3, 7) 

    println("Starting...")
    val := 1

    for {       
        val = val + 1
        print("val: ")
        println(val)
        mydisplay.ClearDisplay()
        mydisplay.DisplayNumber(int16(val))
        time.Sleep(time.Millisecond * 1000)
    }
}

To run this example I needed to create a new directory with the source, initialize the project, get libraries and then build/flash to the controller:

# make a directory for my project
mkdir tm1637
cd tm1637
# initialize a Go project, this project is: tm1637_uno
go mod init tm1637_uno
# get tinygo.org/x/drivers/tm1637 for this app
go get tinygo.org/x/drivers/tm1637
# tidy up project file with tinygo.org/x/drivers/tm1637
go mod tidy
# build and flash project to Arduino 
tinygo flash  -target=arduino tm1637_uno.go  

There are some options on the tinygo compiler, for this example flash -target=arduino will download the application to any Arduino hardware (Uno, Mega, Nano etc.). Tinygo will find the controller so you don’t need to specific a port, however if you have multiple Arduino modules connected you can use the -port option to specify a port.

USB Serial Output

Typically you include USB Serial print statements for debugging. This output can be viewed using a number of different packages, like the Arduino IDE monitor option.

If you are working in a Linux shell, a couple of lines of bash can be used:

# in Linux the default port is: /dev/ttyACM0
# the Arduino Uno default speed is 9600
stty -F  9600 
while read line < /dev/ttyACM0; do echo "$line"; done

Another option is to write a Go USB Serial program. This option has the advantage in that a terminal based program can be used to create custom views of the micro-controller data (I’ll do this later and add some bar charts).

Below is the Go source code for a USB serial interface:

//
// USB Serial program to output data from a Micro-controller
//
package main

import "log"
import "github.com/tarm/serial"
import	"bufio"
// adjust port and speed for your setup
const SERIAL_PORT_NAME = "/dev/ttyACM0"
const SERIAL_PORT_BAUD = 9600

func main() {
	conf := &serial.Config{Name: SERIAL_PORT_NAME, Baud: SERIAL_PORT_BAUD}
	ser, err := serial.OpenPort(conf)
	if err != nil {
		log.Fatalf("serial.Open: %v", err)
	}
	scanner := bufio.NewScanner(ser)
	for scanner.Scan() {
          println(scanner.Text())
	}
	if scanner.Err() != nil {
	  log.Fatal(err)
	}
}

To build this project (source file is: main.go), the standard Go (not TinyGo) compiler is used :

# create a source directory and put main.go into this directory
mkdir USB_Serial
cd USB_Serial
# initialize the project
go mod init main
# get the serial library
go get github.com/tarm/serial
go mod tidy
# build the file, call the executable: usbserial
go build -o usbserial main.go
# run the file and see some sample output
./usbserial

val: 918
val: 919
val: 920
...

Micro:bit Sensor Project

For my next project I wanted to do something more complex. I connected to a BME280 temperature/pressure/humidity sensor and presented the results on 16×2 LCD display, and on a USB serial connection to a laptop.

Both BME280 sensor and the 16×2 LCD display used the I2C bus, so I used a small breadboard to connect the SDA and SCL pins together. It’s important to note that the Micro:bit only has 3.3V power so 5V LCD display units won’t work. My LCD display uses the hd44780i2c driver.

My project code was as follows:

//
// project1.go - show sensor data on a 2x16 LCD Display
//
package main

import (
	"machine"
	"strconv"
	"time"
	"tinygo.org/x/drivers/bme280"
    "tinygo.org/x/drivers/hd44780i2c"
)

func main() {
      // setup sensors
	  machine.I2C0.Configure(machine.I2CConfig{})
	  sensor := bme280.New(machine.I2C0)
	  sensor.Configure()

	connected := sensor.Connected()
	if !connected {
		println("BME280 not detected")
	}
	println("BME280 detected")
        // setup 2x16 screen
        mydisplay := hd44780i2c.New(machine.I2C0, 0x3F)
        mydisplay.Configure(hd44780i2c.Config{Width:16 , Height:2,})
        mydisplay.CursorOn(false)

	for {
		mtemp, _ := sensor.ReadTemperature()
        temp := int(mtemp/1000)

		mpress, _ := sensor.ReadPressure()
		press :=int(mpress/100000)

		mhum, _ := sensor.ReadHumidity()
        hum := int(mhum/100)

        mydisplay.ClearDisplay()
		msg :=  strconv.Itoa(temp) + " C  " + strconv.Itoa(press) + " hPa\n" + strconv.Itoa(hum) + " %"
        println(temp, press, hum)
        mydisplay.Print( []byte(msg) )
		time.Sleep(2 * time.Second)
	}
}

The serial output was sent as integers with no labels or units, I did this for the next step and that was to create a custom terminal program.

Terminal Output

There are a number of different options on how to present the serial output.

I wanted to keep things basic so I used the earlier USB serial code and I included some ANSI terminal codes.

Below is the Go source. I added some ANSI constants for clearing, resetting and color definitions.

The screen will refresh every time some new data is received from the Microbit.

package main

import "fmt"
import "log"
import "strings"
import "github.com/tarm/serial"
import "bufio"
import "strconv"

const SERIAL_PORT_NAME = "/dev/ttyACM0"
const SERIAL_PORT_BAUD = 115200

const clear = "\033[2J"
const reset = "\033[0m"
const uline = "\033[4m"
const nouline = "\033[24m"

// define some foreground colors
const red = "\033[38;5;1m"
const green = "\033[38;5;10m"
const magenta = "\033[38;5;13m"
const purple = "\033[38;5;5m"
const yellow = "\033[38;5;11m"

// Use ANSI color and fill character to show scaled bars with values 
func show_bar(inval string, label string, units string, toprange int, scale int, color string) { 
    bar1, _ := strconv.Atoi(inval) ; // convert USB serial text to int
    bar1 = bar1 * 50 / toprange    ; // scale string input to bar
    bar2 := scale - bar1
    print(color)
    fmt.Printf("%15s %s%s %s %s\n\n", label, strings.Repeat("█",bar1), strings.Repeat("░",bar2), inval, units)

}

func main() {
    conf := &serial.Config{Name: SERIAL_PORT_NAME, Baud: SERIAL_PORT_BAUD}
    ser, err := serial.OpenPort(conf)
    if err != nil {
        log.Fatalf("serial.Open: %v", err)
    }
    scanner := bufio.NewScanner(ser)
    for scanner.Scan() {
        println(clear, uline)
        println(" Micro:Bit Sensor Values ",nouline,"\n")
        values := strings.Split(scanner.Text(), " ")

	show_bar(values[0],"Temperature:","C", 40, 50, magenta)
        show_bar(values[1],"Pressure:","kPa", 1020, 50, green)
        show_bar(values[2],"Humidity:","%", 100, 50, yellow)
        println(reset) ; // Put terminal back to reset mode
    }
    if scanner.Err() != nil {
        log.Fatal(err)
    }
}

If you are looking at doing some more creative terminal programs there are some great libraries, for example: https://github.com/gizak/termui

Same Code on Different Modules

When I looked at running the same Go code on different modules I noticed:

  • Different hardware had different machine libraries, for example Microbit defines pins as P1, P2… , but Arduino defines pins as D1, D2 …. This will unfortunately means the same code can’t run directly.
  • I2C bus interfaces use SCL and SDA definitions which are uniform for all controllers, so my 2nd project would run on probably all the platforms without any code changes.
  • Libraries are hardware dependent, so a library that works on an Arduino may not work on other devices

Final Comments

Because TinyGo is still in development I found it a little challenging because of the lack of documentation. I especially missed having a Wifi library.

For Arduino I would definitely stick to the default C/C# interface, however for the Microbit C/C# isn’t a good option so I can see using TinyGo.

If you are looking at learning Go and you want to start with some small projects TinyGo might be a good option.