TinyGo Tutorial


この記事はQiita AdventCalendar 2019 Go6の17日目のエントリです。

「TinyGoでマイコン開発に入門してみよう!」という内容で現状のTinyGoをご紹介。

TinyGoって?

Goのフロントエンドを使ってバックエンドにLLVMを利用する形で マイコンに向いたバイナリを出力可能にしたものです。

特徴

  • Goのマイコン向けサブセット
  • AVRやARMシリーズに対応
  • WASMやPC向けバイナリも出力可能
  • LLVMバックエンドのため高効率(サイズ、処理性能)

なぜGoはマイコンに向かないのか?

  • OS依存が大きいとかランタイムが大きいなど
  • ネットワークライブラリがモノリシックで巨大
  • メモリの利用方法が富豪的
  • 組込に必要なGCの保留やvolatile機能の不足

TinyGoがサポートする標準ライブラリ

まだ、本家Goとの差異はいくつか残っていてその影響で半数ほどのパッケージはそのまま利用できません。

  • goroutineはLLVMのcoroutineに置き換えられている
  • interface型をmapにキーにできない
  • reflectで構造体フィールドのタグにアクセスできない

ここに一覧 https://tinygo.org/lang-support/stdlib/

WASM出力について

  • 本家に比べ5〜20分の1くらいの小さなサイズのバイナリを出力できる。
  • TinyGo用にJSONパッケージができた。
  • もう一息なところまで来てると思うんだけど。
  • v0.9〜0.10は若干問題があって推奨されない状況(Go1.13での変更に追従できていない)

Goフロントエンド+LLVMバックエンド

  • GoのSSA最適化出力をLLVMのIRに変換
  • LLVMの最適化を経由して各種アーキテクチャのバイナリを出力
  • ダブル最適化で本家よりも性能やサイズの効率を上回ることがある

TinyGoの使い方

現状、あらゆるプラットフォームでdockerを利用するのがオススメです。 ネイティブを入れるよりは煩雑さや処理時間がおそかったりしますが、 環境の再現性という意味ではもっとも高いので。 トラブった時にコミュニティに相談する時もお互いの疎通が楽です。

カレントフォルダにあるコード群をTinyGoでビルドするのには以下のようにします。 作成するプロジェクトのインポートパスがgithub.com/Hoge/FugaAppで標準tinygoでビルドする場合。

docker run -it --rm -v $PWD:/go/src/github.com/Hoge/FugaApp \
-w /go/src/github.com/Hoge/FugaApp \
-e GOPATH=/go \
tinygo/tinygo tinygo build -o app.hex -target circuitplay-express .

作成するプロジェクトのインポートパスがgithub.com/Hoge/FugaAppで標準tinygo-dev版でビルドする場合。

docker run -it --rm -v $PWD:/go/src/github.com/Hoge/FugaApp \
-w /go/src/github.com/Hoge/FugaApp \
-e GOPATH=/go \
tinygo/tinygo-dev tinygo build -o app.hex -target circuitplay-express .

これだけです。Docker環境さえあれば、TinyGo専用の環境を作る必要はありません。

ドライバーを利用するには?

go-moduleによる「go mod vendor」コマンドでカレントフォルダに引き込みます。 ただし、現状Go1.13のgo-moduleによるパス検査の厳格化によりTinyGo専用パッケージパスがエラーでハネられます。 (この挙動はきっと将来に解決するはずです)

実際には以下のようなコマンドを実行します

go get giolang.org/dl/go1.12.14
go1.12.14 download
go1.12.14 mod vendor

これで依存ライブラリはカレントフォルダのvendorフォルダ以下に複製されるので、 docker経由のビルドコマンドで問題なくビルドすることができます。

余談ですがデフォルトと異なるGoバージョンを利用するのは上記のコマンドがオススメです。 理想的な「GOXXXX」環境変数の設定をしつつGOPATHはそのままクロスバージョンで使えるので。 (gvmやgoenvはもっとくっきり分離しちゃうためエディタのと連携は難しい)

サポート済みアーキテクチャ

  • AVR
  • ARM(Cortex-M)
    • ATSAMDシリーズ
    • STMシリーズ
    • nRF51、nRF52シリーズ
  • RISC-V

Xtensa(ESPシリーズ)はLLVM本家の対応待ち (esspressoブランチはLLVM9対応できたっぽい)

サポート済みマイコンボード

この半年ほどで倍増しました。

  • Adafruit Circuit Playground Express
  • Adafruit Feather M0
  • Adafruit Feather M4
  • Adafruit ItsyBitsy M0
  • Adafruit ItsyBitsy M4
  • Adafruit Metro M4 Express AirLift
  • Adafruit Trinket M0
  • Arduino Nano33 IoT
  • Arduino Uno
  • BBC micro:bit
  • Bluepill
  • Digispark
  • Game Boy Advance
  • HiFive1 RevB
  • Nucleo F103RB
  • PCA10031
  • PCA10040
  • PCA10056
  • PineTime
  • STM32F4 Discovery
  • X9 Pro Smartwatch
  • nRF52840-MDK
  • reel board

カスタムボードを利用するには?

  • custom.jsonとcustom.ldを用意してプロジェクトルートに置く
  • -target custom.json指定にてカスタムボードを扱える
  • machineのボード特有定義が存在しないので
  • その定義の肩代わりをアプリ作者が代行する

nRF52840コンパチボードの場合

custom.json

{
  "inherits": ["cortex-m"],
  "llvm-target": "armv7em-none-eabi",
  "build-tags": ["nrf52840", "nrf", "pca10056"],
  "cflags": [
    "--target=armv7em-none-eabi",
    "-mfloat-abi=soft",
    "-Qunused-arguments",
  ],
  "ldflags": ["-T", "custom.ld"],
  "extra-files": ["lib/nrfx/mdk/system_nrf52840.c", "src/device/nrf/nrf52840.s"]
}

custom.ld

MEMORY
{
    FLASH_TEXT (rw) : ORIGIN = 0x00000000 + 0x00026000 , LENGTH = 1M - 0x00026000
    RAM (xrw)       : ORIGIN = 0x20000000 + 0x000039c0,  LENGTH = 256K  - 0x000039c0
}
_stack_size = 4K;
INCLUDE "targets/arm.ld"

nRF52840のスペックシートによると

  • FLASHメモリは0x000000000から1MiB
  • RAMは0x20000000から256KiB
  • 0x00000000 -> 0x26000まではsoftdevice領域
  • 0x2000000 -> 0x200039c0まではsoftdeviceが利用

ビルド方法

docker run -it --rm -v $PWD:/go/src/github.com/Hoge/FugaApp \
-w /go/src/github.com/Hoge/FugaApp \
-e GOPATH=/go \
tinygo/tinygo tinygo build -o app.hex -target custom.json .

インラインアセンブラ

  arm.Asm("wfi")
  arm.AsmFull(`
    str {value}, {result}
    `,
    map[string]interface{}{
      "value":  1
      "result": &dest,
    }
  )

volatile

import "runtime/volatile"
func foo() {
  var i volatile.Register32
  for{
    i++
  }
}

レジスタアクセス(nRF52例)

import "device/nrf"

func foo() {
  nrf.UART0.PSELTXD.Set(8)
}

レジスタの名称はスペックシートで確認

タイマー割り込み(nRF52例)

割り込みハンドラは一式がWeak宣言されていて、 同名の関数をエクスポートすることで上書きできます。

//go:export TIMER1_IRQHandler
func timerHandler(ptr uint32) {
  // do something
  if nrf.TIMER1.EVENTS_COMPARE[0].Get() != 0 {
    nrf.TIMER1.EVENTS_COMPARE[0].Set(0)
  }
}

タイマーの設定はレジスタアクセスにて。 詳しくはNVICのスペックを参考に。

CGOでC資産を利用

Go本家とやり方は同じ

/*
#include "sdk_config.h"
#include "SEGGER_RTT.h"
*/
import "C"

clang-cでCコード部分はコンパイルされます。 そこへ渡したいFLAGSはcustom.jsonのcflagsに追記します。 (例えばBLEのSDKヘッダファイルへのインクルードパス追加など)

高機能ハードウェア

  • カメラモジュール
  • 高機能センサなど
  • LCD/OLED/E-Ink
  • BLE/Bluetooth
  • Ether/Wi-Fi
  • USB機器/ホスト
  • LoRa/3G/LTE

TinyGoがArduinoに統合予定と発表

TinyGo on Arduino

CGOによるBLE実装例

https://github.com/aykevl/go-bluetooth

BLEサポート準備中

プロポーザル

type UUID [4]uint32
type Address [6]uint8
type Bluetooth struct {
    // ...
}
func (b *Bluetooth) Enable(config BluetoothConfig) error {}
func (b *Bluetooth) Disable() error {}
func (b *Bluetooth) Advertise(interval int8, advertisement, scanResponse []byte) {}
type ScanResult struct {
    Address
    // ...
}
func (b *Bluetooth) Scan(callback func(*ScanResult)) error {}
func (b *Bluetooth) StopScan() error {}

余談

GoとTinyGoの比較実験

以下のコードをGoとTinyGoとでビルドして・・・

package main
func recurse(n int) int {
	if n <= 0 {
		return 0
	}
	return n + recurse(n-1)
}
func main() {
	println(recurse(2000000))
}

性能比較

$ /usr/bin/time -l ./exp-go
2000001000000
        0.42 real         0.17 user         0.04 sys
 132681728  maximum resident set size
$ /usr/bin/time -l ./exp-tinygo
2000001000000
        0.25 real         0.00 user         0.00 sys
    700416  maximum resident set size

TinyGoの方が早い!!&メモリ使用量少ない!!

ファイルサイズ

$ ls -lh
...
-rwxr-xr-x@ 1 nobo  staff   1.1M 10 24 16:59 exp-go
-rwxr-xr-x@ 1 nobo  staff    13K 10 24 16:59 exp-tinygo
...

TinyGoの出力サイズが1.1%!!

こういうの鵜呑みにしないこと!

  • Goは実用にフォーカスしてる
  • Goでの再帰呼び出しはメモリを浪費し遅くなる
  • LLVMは末尾再帰除去を含む広範囲の最適化を行う
  • Goの出力はlibc相当を内包してる
  • libcは2MiBくらいある

TinyGoのPros

  • Goの最適化とLLVMの最適化の両方が利く
  • Cの資産をCGO経由で利用可能
  • Goの資産を取り込めるようになる予定
  • Goの良さの多くを継承している
  • AVR系を除きGCを持っていてメモリ管理が楽
  • 組込開発に必要な基本フィーチャーは出そろってきた
  • ATSAMD向けUSBCDCサポートが追加

TinyGoのCons

  • LLVMバックエンドが重い
  • 環境づくりもビルドタイムも時間が必要
  • goroutineが本物ではない(LLVMのcoroutine)
  • 構造体フィールドタグにアクセスできない(鋭意対応に向けて活動中ではある)
  • 標準jsonエンコーダなどが動かない

まとめ

  • 公開からたった一年半で急成長中
  • WASMもだいぶ使えるようになってきた
  • WebGLサンプルが圧縮で9KBサイズになった事例あり
  • 本家の代わりに使う、WASM勢、LLVM勢などが参入する可能性
  • RISC-V、ゲームボーイアドバンスの開発も可能になった
  • TinyGoがWindowsでも動くようになった
  • $25のスマートウォッチもTinyGoで開発できるよ!