TinyGo on Arduino Uno: An Introduction

TinyGo on Arduino Uno: An Introduction © CC BY-NC-SA

Run Golang on this old but still popular 8-bit AVR microcontroller.

  • 11,102 views
  • 1 comment
  • 10 respects

Components and supplies

Apps and online services

About this project

Note: this article has been updated for TinyGo 0.15.0. I will stop update this article any further. Check out the official site for more information.

TinyGo - Golang for microcontrollers

TinyGo - "Go for small places", now officially sponsored by Google - is one of the new programming language for microcontrollers. By using a LLVM-based compiler, it can generate binary files small enough to fit into microcontrollers, including 8-bit AVR boards with very limited RAM.

Since TinyGo is mainly designed for 32-bit microcontrollers, these AVR boards have limitations - they are not able to perform some features of TinyGo and may only be able to run smaller scripts.

On the other hand, Arduino Uno and Arduino Nano are still one of the most popular beginner's Maker board, and their clones can be bought cheaply. If you accidentally toast one you won't lose much. And for some people living in certain area around the globe (like me), open-sourced Unos are far more accessible than Cortex M0/M4 or nRF52 products.

Also, Go (Golang) has became rapidly popular in recent years. Learn Go basics on an interactive physical device may be more interesting than learning solely on a computer. It's also exciting to see something else can run on Arduino Uno other than the powerful but intimidating C++. (No more problems caused by forgotten semicolons! yay!)

Uno may not be ideal TinyGo, but it is still capable to do many things.

Setup - Windows

Tested on Windows 10.

1. Download Go and install it under C:\. (The installer should automatically added "C:\Go\bin" to your system's PATH variable.)

2. Download TinyGo and unzip it to C:\. Look for lastest version like tinygo0.15.0.windows-amd64.zip.

Note: only TinyGo 0.14.1 and above support Go 1.15.

3. Open Command Prompt and add TinyGo path to your system PATH variable:

set PATH=%PATH%;"C:\tinygo\bin"

You may need to restart the Command Prompt for the new PATH to take effect.

Personally, I would recommend to set PATH manually via Control Panel -> System -> Advanced System Settings -> Environment Variables.

4. Check if both paths are added correctly:

tinygo version

You should see something like

tinygo version 0.15.0 windows/amd64 (using go version go1.15.2 and LLVM version 10.0.1)

5. Download avr-gcc/avrdude and unzip it to C:\. These tools are needed to upload compiled binary files to Arduino Uno.

6. Add path to PATH as well (the following is the path for avr-gcc-10.1.0-x64-windows.zip):

set PATH=%PATH%;"C:\avr-gcc-10.1.0-x64-windows\bin";

You can also use avr-gcc.exe and avrdude.exe provided by Arduino IDE if you had it installed (search it yourself). You'll need to add path for both respectively.

Setup - Linux

Tested on a Utuntu 18.04 virtual machine and my Raspberry Pi 3B+ (Raspbian Buster).

1. Open Terminal, update and upgrade system:

sudo apt-get update
sudo apt-get dist-upgrade -y

2. Install Go:

sudo apt-get install golang-go

On Ubuntu you might get an older Go version (like 1.10), which does not work for TinyGo. Run the following commands and you'll be able to install newer Golang:

sudo add-apt-repository ppa:longsleep/golang-backports
sudo apt update

3. Download and unpack TinyGo (Ubuntu/Debian):

wget https://github.com/tinygo-org/tinygo/releases/download/v0.15.0/tinygo_0.15.0_amd64.deb
sudo dpkg -i tinygo_0.15.0_amd64.deb

If you are using a Raspberry Pi: (newest version might not be available until a while after new releases:)

wget https://github.com/tinygo-org/tinygo/releases/download/v0.15.0/tinygo_0.15.0_arm.deb
sudo dpkg -i tinygo_0.15.0_arm.deb

4. Add path to bashrc:

nano ~/.bashrc

Scroll down to the end, add this line, press Ctrl+X then Y to save:

export PATH=$PATH:/usr/local/tinygo/bin

5. Restart system or execute the previous line in terminal as well (so it will take effect immediately).

6. Check installation/path:

tinygo version
tinygo version 0.14.1 linux/arm (using go version go1.15 and LLVM version 10.0.1)

7. Install other required tools for Arduino Uno:

sudo apt-get install gcc-avr
sudo apt-get install avr-libc
sudo apt-get install avrdude

Upgrade TinyGo in the future

For Windows, simply delete the tinygo directory and download/unzip the new release file.

For Linux, simply repeat the wget/dpkg commands with the new release version.

Compile and upload TinyGo code

In Command Prompt (Windows) or Terminal (Linux), this is how you compile and upload the official example blinky1.go to your Uno:

tinygo flash -target arduino -port COMX examples/blinky1

On Windows, open Device Manager and see which COM port your Arduino Uno uses.

On Linux systems the port should be /dev/ttyACM0 or may be /dev/ttyUSB0. You can use the following command to check com ports:

dmesg | grep tty

Or, you can install Arduino IDE and look at which port is available in Tools -> Port (works on both Windows and Linux).

Target arduino is for Arduino Uno. For Arduino Nano, use target arduino-nano. Nano is basically a smaller Uno with same pins and also .ATmega328P-based.

To write your own code, for example, I added a folder on Windows

C:\Users\username\go\src\main

Or on Linux

/home/username/go/src/main

and save a text file main.go in it. Then I can compile and upload this file by the following command: (same on Windows and Linux)

tinygo flash -target arduino -port COMX main

You will see something like this if it's successful:

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:\Users\Admin\AppData\Local\Temp\tinygo454908831\main.hex"
avrdude: writing flash (558 bytes):
Writing | ################################################## | 100% 0.15s
avrdude: 558 bytes of flash written
avrdude: verifying flash memory against C:\Users\Admin\AppData\Local\Temp\tinygo454908831\main.hex:
avrdude: load data flash data from input file C:\Users\Admin\AppData\Local\Temp\tinygo454908831\main.hex:
avrdude: input file C:\Users\Admin\AppData\Local\Temp\tinygo454908831\main.hex contains 558 bytes
avrdude: reading on-chip flash data:
Reading | ################################################## | 100% 0.13s
avrdude: verifying ...
avrdude: 558 bytes of flash verified
avrdude: safemode: Fuses OK (E:00, H:00, L:00)
avrdude done. Thank you.

You can also try out the TinyGo Playground.

Blinky: digital output, loop and delay

A pin or GPIO (general purpose input/output) can be used to control external devices, for example a LED. It can be switched on (output High voltage) or off (output Low voltage) hence making the LED light up or not.

Below is the most basic code to "blink" the built-in LED on pin 13 (very slightly modified from the official example):

package main

import (
"machine"
"time"
)

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

Blinky (alternate version)

Here we use an external LED connected to pin 13.

Be noted that pin 13 does not have a built-in resistor. A 220 ohms resistor should be added between the pin and LED as protection, otherwise the LED might be damaged by higher voltage.

package mainimport (
"machine"
"time"
)

func main() {

led := machine.D13
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
ledSwitch := true

for {
led.Set(ledSwitch)
ledSwitch = !ledSwitch
delay(500)
}

}

func delay(t uint32) {
time.Sleep(time.Duration(1000000 * t))
}

machine.LED is pre-defined in the machine module, which is equal to machine.D13.

Beware that in 0.13.1 or earlier version, machine.D13 is expressed as machine.Pin(13) for all AVR boards.

You can declare a variable without specify the type if you give it a value at declaration. Here the variable led would automatically set as machine.Pin type - := (short variable declaration operator) is as same as var but can be only used in a function (for example, main()).

Also, I wrote a reusable delay function which make things slightly easier. The smallest unit of time.Duration (int16) is nanosecond; times 1, 000, 000 and it's a millisecond.

(In case you wondered, time.Since() doesn't work on Uno. I tried.)

Blinky with Goroutine!

It's possible to run Goroutines (concurrent/parallel threads) on Arduino Uno - and quite easy too. However, you'll need to enable the scheduler while uploading your script:

tinygo flash -scheduler coroutines -target arduino -port COMX main

The following code will light up two LEDs on pin 2 and 3 with different intervals: (Although, due to hardware limitations, the interval time would be pretty inaccurate.)

package mainimport (
"machine"
"time"
)

func main() {
go led1() // goroutine
led2()
}

func led1() {
led := machine.D2
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
led.High()
delay(500)
led.Low()
delay(500)
}
}

func led2() {
led := machine.D3
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
led.High()
delay(400)
led.Low()
delay(400)
}
}

func delay(t int64) {
time.Sleep(time.Duration(1000000 * t))
}

You can even make these two functions both as Goroutines. To do that, a small delay is needed at the end of main() (which is actually a Goroutine itself):

func main() {
go led1()
go led2()
delay(100)
}

Knight Rider LED array (simpler version)

Now we will make a row of 9 LEDs blink one by one, toward one and then back. Using pin 2-10.

package mainimport (
"machine"
"time"
)func main() {

leds := []machine.Pin{
machine.D2,
machine.D3,
machine.D4,
machine.D5,
machine.D6,
machine.D7,
machine.D8,
}

for i := 0; i < len(leds); i++ {
leds[i].Configure(machine.PinConfig{Mode: machine.PinOutput}})
}

for {

for i := 0; i < len(leds); i++ {
leds[i].High()
delay(75)
leds[i].Low()
}

for i := len(leds) - 1; i >= 0; i-- {
leds[i].High()
delay(75)
leds[i].Low()
}

}
}

func delay(t uint16) {
time.Sleep(time.Duration(1000000 * uint32(t)))
}

Knight Rider LED array (better version)

package mainimport (
"machine"
"time"
)

func main() {

delay := func(t uint16) {
time.Sleep(time.Duration(1000000 * uint32(t)))
}

leds := []machine.Pin{
machine.D2,
machine.D3,
machine.D4,
machine.D5,
machine.D6,
machine.D7,
machine.D8,
}

for _, led := range leds {
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
}

index, delta := 0, 1

for {

for i, led := range leds {
led.Set(i == index)
}

index += delta

if index == 0 || index == len(leds)-1 {
delta *= -1
}

delay(75)
}
}

This time delay is an implicit function declared inside main(). We also use a different for loop to iterate all elements in the array.

Set() method of LED structs accept a bool parameter (true/false). In Go you cannot convert int to bool; you'll have to use logical operators.

Button: digital input

Pins can be used to read digital signals (High/Low voltage) as well. Here we will use a push button (or switch) on pin 8 to turn on/off the LED on pin 13.

In order to switch between High/Low states, the button needs a 10K Ohms pull-up resistor.

package mainimport (
"machine"
"time"
)

func main() {

button := machine.D2
button.Configure(machine.PinConfig{Mode: machine.PinInput})

led := machine.D9
led.Configure(machine.PinConfig{Mode: machine.PinOutput})

for {
led.Set(!button.Get())
time.Sleep(time.Millisecond * 100)
}
}

Get() method returns a bool value (true/false).

You can also use internal pull-up resistor, which allows you to use only 2 wires (signal/ground) to connect the button:

button := machine.D2
button.Configure(machine.PinConfig{Mode: machine.PinInputPullup})

PWM: analog output

PWM (Pulse Width Modulation) is to turn on/off the pin at certain percentage of the time to generate a specific voltage between Low and High (5V on Uno), which can be used to change brightness level of a LED or change the speed of a DC motor (via a motor board).

On Uno (and Nano) only pin 3, 5, 6, 9, 10 and 11 (with the ~ mark beside it) can be used to output PWM.

This example makes the LED on pin 9 "breathes" - light up and fade gradually, The circuit is as same is the blinky code.

package mainimport (
"machine"
"time"
)

func main() {

machine.InitPWM()
led := machine.PWM{machine.D9}
led.Configure()

duty, delta := 0, 1024

for {

led.Set(uint16(duty))
duty += delta

if duty < 0 || duty > 65535 {
delta *= -1
duty += delta
}

time.Sleep(time.Millisecond * 25)
}
}

PWM's Set() method accepts a uint16 parameter (0-65535), which is the PWM duty cycle. However, Unos only have 8-bit PWM resolution, so smaller changes may not be visible.

Right now it's not supported to change the board's PWM frequency, which is useful to control passive piezo buzzers and hobby servo motors.

Sensor: analog input

Some of the pins on Arduino Uno have ADC (Analog-to-Digital Converter), which allows them to read voltage as numbers.

Here we use a LDR (photoresistor) to output analog readings to pin A0 based on light levels and turn on/off the LED on pin 13. In order to read the changes of voltage, we'll need a voltage divider circuit with a 10K Ohms resistor.

mackage mainimport (
"machine"
"time"
)

func main() {

machine.InitADC()
ldr := machine.ADC{machine.ADC0}
ldr.Configure()

led := machine.Pin(machine.D9)
led.Configure(machine.PinConfig{Mode: machine.PinOutput})

for {

print(ldr.Get())

if ldr.Get() > 40000 {
led.Set(true)
} else {
led.Set(false)
}

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

There are pre-defined analog pins in the machine module as machine.ADC0 to machine.ADC5.

As PWM, ADC's Get() method returns a uint16 value (0-65535).

Here I set the threshold as 40000. Actual threshold may be different depending on your lighting condition. You can use a terminal software like Tera Term or Arduino IDE's serial monitor to read the output value from print().

Conclusion

TinyGo is still a language in active development, however there are already a lot of progress achieved. It demonstrated that you can indeed run Golang in small places, and It supports various boards (like BBC micro:bit, Arduino Nano 33 IoT, Adafruit ItsyBitsy M4, even Nintendo Switch) and external electronic modules. Arduino Uno and Nano can be an excellent start, and they certainly won't be your last.

Code

TinyGo
Go compiler for small places. Microcontrollers, WebAssembly, and command-line tools. Based on LLVM. https://tinygo.org

Comments

Similar projects you might like

Stepper - A First Introduction to Nema 17

by Ingo Lohs

  • 16,708 views
  • 7 comments
  • 66 respects

Multifunctional Watch With Arduino Uno

Project showcase by shaqibmusa94

  • 5,046 views
  • 2 comments
  • 10 respects

Control Arduino Uno Using ESP8266 WiFi Module and Blynk App

Project tutorial by Adithya TG

  • 49,488 views
  • 14 comments
  • 47 respects

3-Bit Binary Calculator Using Arduino Uno

Project showcase by 22warehamD

  • 5,815 views
  • 9 comments
  • 13 respects
Add projectSign up / Login