TinyGo intro

Update 2023-11-25. I got some technical feedback in this mastodon thread, so I updated my post based on it.

Introduction

I went through a Go tutorial back in February and discovered the TinyGo project, which supports running a subset of Go on microcontrollers. It's similar to MicroPython, but this project appears to support a wider variety of hardware, including the Arduino Nano, which I have.

In this article I'll describe how to set up TinyGo build environment for Arduino Nano on Debian 11.

Getting started

I followed these articles about getting started with Go and reference pinout on Arduino nano

For the host system, the setup involves installing the TinyGo and Avrdude packages:

$ wget https://github.com/tinygo-org/tinygo/releases/download/v0.30.0/tinygo_0.30.0_amd64.deb
$ sudo apt install -y ./tinygo_0.30.0_amd64.deb
$ sudo apt install -y avrdude

To test the toolchain I tried running the "hello world" of embedded systems -- blinking an onboard LED (Tinygo Lesson 0).

Unfortunately, I encountered a few issues when trying to use the tinygo flash subcommand, resulting in this error:

$ tinygo flash -target=arduino-nano main.go
avrdude: stk500_recv(): programmer is not responding
avrdude: stk500_getsync() attempt 1 of 10: not in sync: resp=0x00
avrdude: stk500_recv(): programmer is not responding
avrdude: stk500_getsync() attempt 2 of 10: not in sync: resp=0x00

It appears there are some problems with code integration under the hood, so I tried running individual steps for building and uploading. To manually run the upload step, I had to install arduino-cli using the following command:

curl -fsSL \
    https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
    | sh

I tried again, this time using tinygo build followed by arduino-cli upload. This sequence of commands triggered a code path that detects and installs any missing Arduino dependencies.

$ tinygo build -scheduler=tasks -target=arduino -o test.hex ./main.go
$ arduino-cli upload -b arduino:avr:nano -p /dev/ttyUSB0  -i test.hex .
Downloading missing tool builtin:ctags@5.8-arduino11...
builtin:ctags@5.8-arduino11 downloaded
Installing builtin:ctags@5.8-arduino11...
Skipping tool configuration....
builtin:ctags@5.8-arduino11 installed
Downloading missing tool builtin:serial-discovery@1.4.0...
builtin:serial-discovery@1.4.0 downloaded
Installing builtin:serial-discovery@1.4.0...
Skipping tool configuration....
builtin:serial-discovery@1.4.0 installed
Downloading missing tool builtin:mdns-discovery@1.0.8...
builtin:mdns-discovery@1.0.8 downloaded
Installing builtin:mdns-discovery@1.0.8...
Skipping tool configuration....
builtin:mdns-discovery@1.0.8 installed
Downloading missing tool builtin:serial-monitor@0.13.0...
builtin:serial-monitor@0.13.0 downloaded
Installing builtin:serial-monitor@0.13.0...
Skipping tool configuration....
builtin:serial-monitor@0.13.0 installed

Although this didn't fix the original error caused by running tinygo flash, it brought me closer to a solution. Later on, I found an issue describing a workaround in Issue #1580 of TinyGo project

$ tinygo build -scheduler=tasks -target=arduino-nano-new -o test.hex ./main.go
$ arduino-cli upload -b arduino:avr:nano -p /dev/ttyUSB0  -i test.hex .

Where /dev/ttyUSB0 is the port used by Arduino Nano. Sometimes the board jumps between /dev/ttyUSB0 and /dev/ttyUSB1 when being repeatedly reconnected.

1602 display

I also happened to have a 1602 display and TinyGo happened to have a library for it. So I ran a few sample programs that use the display.

Hello, World

Now that I could upload code to the board, I followed the Displaying text on an HD44780 16x2 display tutorial for connecting the display over I2C and used the tinygo hd44780i2c drivers.

package main

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

func main() {
    i2c := machine.I2C0
    i2c.Configure(machine.I2CConfig{})

    lcd := hd44780i2c.New(machine.I2C0, 0x27)
    lcd.Configure(hd44780i2c.Config{
        Width:  16,
        Height: 2,
    })
    lcd.Print([]byte(" Hello, world\n LCD 16x02"))
}

Additional external dependencies are required to run the application. The command to download the dependencies is go mod init .

Everything worked, "Hello, World" displayed on the screen.

Hello world on 16x2 line controller

Here's the same message with visible wiring for HD44780 16x2 display.

Reference wiring for the controller.

Spinner

Then I used another sample program for the screen, a spinner.

package main

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

func write_progress_spinner(d *hd44780i2c.Device, counter uint) {
    spinner_pos := "|/-\\"
    d.SetCursor(15, 1)
    pos := counter % uint(len(spinner_pos))
    d.Print([]byte(string(spinner_pos[pos])))
}

func main() {
    i2c := machine.I2C0
    i2c.Configure(machine.I2CConfig{
        Frequency: machine.TWI_FREQ_400KHZ,
    })
    lcd := hd44780i2c.New(machine.I2C0, 0x27)
    lcd.Configure(hd44780i2c.Config{
        Width:  16,
        Height: 2,
    })
    lcd.Print([]byte("Counting: "))
    var counter uint = 0 // only goes to 512
    for true {
        time.Sleep(250 * time.Millisecond)
        lcd.SetCursor(10, 0)
        strconv.FormatUint(uint64(counter), 10)
        lcd.Print([]byte(strconv.FormatUint(uint64(counter), 10)))
        write_progress_spinner(&lcd, counter)
        counter += 1
    }
}

One issue I encountered, as the error displayed in the image below -- this supposed to be a backslash symbol. I tried different escape sequences for the backslash, but none of them worked.

It could be that the snippet above has bugs in it, or there have been issues reported with the combination of LLVM implementation for the Go compiler and Arduino Nano. The TinyGo project recommends using Raspberry Pico.

Backslash display error

Counter

Another example program I've tried is counter, just counting up the numbers. Here's source code:

package main

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

func main() {
    i2c := machine.I2C0
    i2c.Configure(machine.I2CConfig{})

    lcd := hd44780i2c.New(machine.I2C0, 0x27)
    lcd.Configure(hd44780i2c.Config{
        Width:  16,
        Height: 2,
    })
    lcd.SetCursor(0, 0)
    lcd.Print([]byte("Counting: "))
    var counter = 0
    for true {
        if (counter % 100 == 0){
            lcd.SetCursor(0, 1)
            lcd.Print([]byte(strconv.Itoa(counter)))
        }
        counter += 100
    }

}

Reset during count

I used to have an issue where where the Arduino board would reset at random intervals when counting up. I tried debugging the issue by displaying only every 100th number to see if this might be an issue with the library or some kind of integer overflow error, but no luck. Sometimes the counter would go up to 10,000 or 20,000; other times, the counter would reset almost immediately.

Counter reboot

Update 2023-11-25. This issue has been resolved by setting this parameter -target=arduino-nano-new for the build command and this parameter -b arduino:avr:nano for the upload command. i.e.

$ tinygo build -scheduler=tasks -target=arduino-nano-new -o main_counter_nano.hex ./main_counter.go
$ arduino-cli upload -b arduino:avr:nano -p /dev/ttyUSB0  -i main_counter_nano.hex .

Explicit types

I tried setting explicit types, like uint16, but in the end I got following error.

$ tinygo build -scheduler=tasks -target=arduino -o main_counter.hex ./main_counter.go
error: interp: ptrtoint integer size does not equal pointer size

Update 2023-11-25. This has been fixed as of 2023-11-03

Now tinygo display potential overflow when trying to print uint16 directly.

package main

import (
    "fmt"
    "machine"
    "tinygo.org/x/drivers/hd44780i2c"
)

func main() {
    i2c := machine.I2C0
    i2c.Configure(machine.I2CConfig{})

    lcd := hd44780i2c.New(machine.I2C0, 0x27)
    lcd.Configure(hd44780i2c.Config{
        Width:  16,
        Height: 2,
    })
    lcd.SetCursor(0, 0)
    lcd.Print([]byte("Counting: "))
    var counter uint16 = 0
    for true {
        if counter%100 == 0 {
            lcd.SetCursor(0, 1)
            lcd.Print([]byte(fmt.Sprintf("%v", counter)))
        }
        counter += 100
    }

}
tinygo:ld.lld: error: section '.text' will not fit in region 'FLASH_TEXT': overflowed by 11286 bytes
tinygo:ld.lld: error: section '.text' will not fit in region 'FLASH_TEXT': overflowed by 11356 bytes
tinygo:ld.lld: error: section '.text' will not fit in region 'FLASH_TEXT': overflowed by 11386 bytes
tinygo:ld.lld: error: too many errors emitted, stopping now (use --error-limit=0 to see all errors)
failed to run tool: ld.lld
error: failed to link /tmp/tinygo2493042965/main: exit status 1

As silly as this is, converting uint to int the following compiles. This is rather silly as internally Arduino Uno stores ints and 16 bit value, so I might as well drop using uint16 and just use int or uint.

package main

import (
    "machine"
    "strconv"

    "tinygo.org/x/drivers/hd44780i2c"
)

func main() {
    i2c := machine.I2C0
    i2c.Configure(machine.I2CConfig{})

    lcd := hd44780i2c.New(machine.I2C0, 0x27)
    lcd.Configure(hd44780i2c.Config{
            Width:  16,
            Height: 2,
    })
    lcd.SetCursor(0, 0)
    lcd.Print([]byte("Counting: "))
    var counter uint16 = 0
    for true {
            if counter%100 == 0 {
                    lcd.SetCursor(0, 1)
                    lcd.Print([]byte(strconv.Itoa(int(counter))))
            }
            counter += 100
    }

}

This code compiles With the following parameters

$ tinygo build -scheduler=tasks -target=arduino-nano-new -o main_counter_nano.hex ./main_counter.go

Conclusion

Learning a new programming language is exciting, but it also means that I don't have all the knowledge about debugging the specific toolchain. In addition, working on a platform that I'm not familiar with doesn't help; there are just too many unknowns.

I should probably get one of the better supported boards and try this again. At least then I will know for sure if I'm making an error.

Update 2023-11-25 Rasberry Pi Pico, a board of choice for tiny go and many similar projects, seem to be widely availabe now. I was able to order a coupe for $4 CAD a piece from a local distributor.