SNSへはこちら

自作マイコンボードでGoを動かす

続いて、TinyGo を手持ちのマイコンボードで動かしていきます。そのためには、TinyGo をインストールしたディレクトリに設定ファイルを入れることが必要です。
なので、予めインストールディレクトリに移動しておきます。僕の場合は /usr/local/Cellar/tinygo/0.13.1 でした。

なお動かすのに用いたマイコンは STM32F042 です。最近これ好きなんですよね〜〜
ただ、注意点があります。SPI, I2C 共に、送信のみでしかテストしていません。受信に関しては雑に「ビルドエラーが出ないように修正した」程度で、動作は保証しません。よって、GitHub にてプルリクも送りません(という口実作りでもある)。

参考

マイコンボード作った

突然ですが、最近手ハンダでマイコンボードを作りました。USB で書き込みを行えて、かつ動作確認用の LED を 5つ 搭載しただけの簡単なボードです。LED を基板裏側に配置したので、すりガラスみたいに光がいい感じに散って、とてもシャレオツな雰囲気です。

今回はこちらの自作ボードで Go を動かしたいなあと思いますね。ターゲット名は myf042 にします(My F042 Board だから)。

ファイル構造

簡単に、設定ファイルのあるディレクトリを見てみます。

targets

ここには構成やコンパイラオプションを記した .json ファイル、リンカスクリプト、スタートアップコードがあります。ざっと見ていきましょう。

スタートアップコード

.s ファイルとして用意され、アーキテクチャ毎に用意されています。今回は別ディレクトリにスタートアップコードがそのまま使える形で用意されているので、特にいじりません。
いろいろな型番のものが沢山あるのを見た時「すげー」って言ってしまいました。今後の対応マイコンが増えることに期待です!

リンカスクリプト

リンカスクリプトはチップ毎に用意されていて、それぞれが各アーキテクチャのリンカスクリプトをインクルードしています。例えば stm32f103rb.ld を見てみましょう。

MEMORY
{
    FLASH_TEXT (rw) : ORIGIN = 0x08000000, LENGTH = 128K
    RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 20K
}

_stack_size = 2K;

INCLUDE "targets/arm.ld" /* ここでインクルード */

targets/arm.ld には SECTIONS コマンドが書かれています。つまりここでは MEMORY コマンドとスタックサイズの定義を行えば OK ということみたい。

json

ボード毎に用意されています。「特にマイコンボードでは無く動かす場合はどうするの」という話は置いておきましょう。
上の例で示した STM32F103 は、BluePill ボードとして使われています。

{
    "inherits": ["cortex-m"],
    "llvm-target": "armv7m-none-eabi",
    "build-tags": ["bluepill", "stm32f103xx", "stm32"],
    "cflags": [
        "--target=armv7m-none-eabi",
        "-Qunused-arguments"
    ],
    "linkerscript": "targets/stm32.ld",
    "extra-files": [
        "src/device/stm32/stm32f103xx.s"
    ],
    "flash-method": "openocd",
    "openocd-interface": "stlink-v2",
    "openocd-target": "stm32f1x"
}

inherits から extra-files は必須です。後のコマンドはお好きにどうぞといった感じらしい。

書き込みの欄はバリエーションがあって、例えば AVR だと

    "flash-command": "avrdude -c arduino -p atmega328p -P {port} -U flash:w:{hex}:i"

とも書いてあります。

src/runtime

ここにはランタイムを記述したファイルがあります。runtime_xxx.go として適切なものを記述すれば OK らしい。
コードを見る限りは、以下を書けばいいっぽいです。

  • init 関数
    • マイコン起動時に呼ばれる関数。クロックや各種タイマーの初期化を行う。
  • putchar 関数
    • println とかで使えるように。通常は UART を使うようにします。
  • timerSleep 関数
    • いわゆる時間稼ぎのディレイ。

src/device

先程述べた、数多くのスタートアップコードが用意されているのがここです!また、レジスタ記述用の go ライブラリもここに。レジスタ用マクロもここで用意されています。このお陰で、虚無なレジスタ構造体等の作成をせずに済むわけです。ありがたい。
パッケージ名は stm32

src/machine

machine パッケージとしての実装を書きます。チップに固有な machine_xxx.go というファイルと、マイコンボードに固有な board_xxx.go を用意するのが一般的なようなので、今回もこれらを用意します。

やるぞ

ざっと見終わったところで、実装していきますか。これがまた一筋縄では行かない。以下が大変でした。

  • どの定義が何処にあるのかあまり把握できていなかったので、定義を探すのに手間取った
  • SPI, USART と I2C の定義やレジスタ名がもともと STM32 用として存在していたファイルと一致しない

targets

リンカスクリプト・json ファイルを用意していきます。

リンカスクリプト

ざっとこんな感じですかね。アドレスとサイズはデータシート参照。ファイル名は stm32f042x.ld です。

スタートアップコード

置きません。元からあるので。下のjson参照。

json

ごく適当に、他からコピペしてガチャガチャいじりました。あと、このマイコンは ARM Cortex-M0 なので、ターゲットが armv6m になることに注意です。
書き込みは DFU で行います。そのためにバイナリファイルがほしいので、予め objcopy で変換しておきます。
ファイル名は myf042.json です。このファイルだけは、使用するターゲット名に対してファイル名を合わせておく必要があります。

ここの build-tags は後に作る .go ファイルのビルドターゲットを決めるシンボルです。取り敢えずボード名とマイコンの型番、マイコンのシリーズ名を書いておきました。

src/runtime

このままだと各種ランタイムが無いぞって怒られるので、追加しましょう。
ここでは以下の構成にします。

  • クロック源は HSI48で、PLL 等は使わない
  • timerSleepTIM3 を使用
  • ticksSysTick を使用
    • F042 の RTC は、純粋なカウンタ値の読み出しは不可能のため
    • まあよく分からんが必要らしい

src/machine

machine_stm32f042x.go

GPIO の扱いは STM32F103 とは異なるので、F407 のものをコピーして編集。あとはもともとサポートされているペリフェラルのレジスタが、F042 のものと一致しないので、そこをゴリ押しで持ってしまいました。

board_myf042.go

取り敢えず無いとライブラリ(src/machine/machine_stm32_spi.goとか)が動かないみたい。
今回は折角自作ボードでの動作を目指すわけですから、それっぽく書きましょうか。

その他

ペリフェラルの互換性等の問題で、既存のファイルを編集します。よって、インストールした TinyGo のアップデート非推奨です。とはいっても、Homebrew でインストールしてアプデかけると自動的に更新されてしまうのですがね。そのためのメモとして記事を書いているということもあるわけです。以下は src/machine 内のファイルです。

machine_stm32_uart.go

冒頭のビルドターゲットを編集。

// +build stm32,!stm32f042x

これでOK

ふぅ... ここまで来るのが長かった...丸3日位かかりました。では src/examples に入って実行してみましょう。
予めマイコンをブートローダーモードにしておいて...

$ tinygo flash -target myf042 ./exapmles/blinky1

はい、これで PB3 につないだ LED がチカチカしたら成功です。

自作プロジェクト

動いて嬉しいので、調子に乗ってみます。

mcp4922

MicroChip 社の DAC です。SPI の送信のみで動く(受信、すなわち MISO の配線は要らない)ので、マイコンを買った時のペリフェラル動作チェック用 IC として使っています。

package main

import (
    "machine"
    "time"
)

const nss = machine.Pin(machine.PA4)

func main() {
    machine.SPI0.Configure(machine.SPIConfig{
        Frequency: 100000,
        Mode: 0,
    })

    nss.Configure(machine.PinConfig{Mode: machine.PinOutput})
    nss.High()

    const header = 0b0111
    var value uint16
    for {
        value += 8
        if (value & 0xF000) != 0 {
            value = 0
        }
        send((header << 12) | value)
        time.Sleep(time.Millisecond * 1)
    }
}

func send(data uint16) {
    tx := make([]byte, 2)
    rx := make([]byte, 2)

    tx[0] = byte(data >> 8)
    tx[1] = byte(data & 0xFF)

    nss.Low()
    machine.SPI0.Tx(tx, rx)
    nss.High()
}

aqm1620

秋月で買える 16桁 2段の LCD キャラクタ液晶。こちらも I2C の動作チェック対象です。

package main

import (
    "machine"
    "time"
)

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

    machine.I2C0.Configure(machine.I2CConfig{})
    err := lcdInit()
    if err != nil {
    }
    time.Sleep(time.Millisecond * 200)

    for {
        lcdString("Hello")
        time.Sleep(time.Millisecond * 500)
    }
}

func lcdInit() error {
    cmds := []byte{0x38, 0x39, 0x14, 0x73, 0x56, 0x6C, 0x38, 0x01, 0x0C}

    for i := 0; i < len(cmds); i++ {
        err := machine.I2C0.Tx(0x7c >> 1, []byte{0x00, cmds[i]}, nil)
        if err != nil {
            return err
        }
        time.Sleep(time.Millisecond * 100)
    }
    return nil
}

func lcdString(str string) {
    for i := 0; i < len(str); i++ {
        machine.I2C0.Tx(0x7c >> 1, []byte{0x40, str[i]}, nil)
        time.Sleep(time.Millisecond * 1)
    }
}

疑問点

これはドキュメントを読めば分かりそうですが、プロジェクト内に異なるパッケージを置くことは出来ないのですかね?多分ぼくの理解不足なのでしょうが、インポートする際にエラーが出ます。多分その方法とか、GOPATH の設定に問題がありそう。
引き続き勉強していきたいです。

フォロワーさんに教えていただきました。プロジェクト内にサブディレクトリを作れば対応できるようです。ごく簡単にですが、やってみました。

blink/ ディレクトリ内に test ディレクトリを作り、更にその中に test.go を作りました(blink/test/test.go)。中身はスッカラカンですが以下のような感じです。

package test

func Func() {

}

ここで blink/blink.go 内でこれを呼び出します。# ADD とコメントを振った行を追加しました。

package main

import(
    "machine"
    "time"
    "./test" # ADD
)

func main() {
    led := machine.LED
    led.Configure(machine.PinConfig{Mode: machine.PinOutput})
    test.Func() # ADD
    for {
        println("Hello")
        led.High()
        time.Sleep(time.Millisecond * 500)
        led.Low()
        time.Sleep(time.Millisecond * 500)
    }
}

これでビルドすると、無事にエラー無く完了します。ご教示ありがとうございました。