続いて、TinyGo を手持ちのマイコンボードで動かしていきます。そのためには、TinyGo をインストールしたディレクトリに設定ファイルを入れることが必要です。
なので、予めインストールディレクトリに移動しておきます。僕の場合は /usr/local/Cellar/tinygo/0.13.1
でした。
なお動かすのに用いたマイコンは STM32F042 です。最近これ好きなんですよね〜〜
ただ、注意点があります。SPI, I2C 共に、送信のみでしかテストしていません。受信に関しては雑に「ビルドエラーが出ないように修正した」程度で、動作は保証しません。よって、GitHub にてプルリクも送りません(という口実作りでもある)。
参考
- 頂いた助言(Twitter)
- いろいろ調べたりするきっかけになりました。ありがたい。
- TinyGo の Wiki
- カスタムボードを追加するには何をすればいいか、書いてあります。
- TinyGoをSTM32F4Dicoveryで動かす - みつきんのメモ
- 結構参考にしました。
マイコンボード作った
突然ですが、最近手ハンダでマイコンボードを作りました。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 等は使わない
timerSleep
は TIM3 を使用ticks
は SysTick を使用- 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)
}
}
これでビルドすると、無事にエラー無く完了します。ご教示ありがとうございました。