はじめに
TinyGoの勉強を兼ねてSTM32F4Dicoveryへ移植してみる。
LチカとHelloWorldを動かせれば良いので、次のペリフェラルのみ動かす。
- GPIO
- UART(USART2)
実際に動かしたサンプル。
- examples/brinky1(Lチカ)
- examples/brinky2(gofuncの並列Lチカ)
- examples/serial(HelloWorld)
- examples/echo(UARTでのエコー)
- examples/test(golangの基本機能のテスト)
クロック設定は、168MHzに固定
移植作業
追加したファイル
移植に当たって次のファイルを追加した。
ファイル名 | ディレクトリ | 概要 |
---|---|---|
stm32f4disco.json | targets | ターゲット定義 |
stm32f407.ld | targets | リンカスクリプト |
runtime_stm32f407.go | src/runtime | クロック初期化など |
machine_stm32f407.go | src/machine | マシン(SoC毎)定義 |
board_stm32f4disco.go | src/machine | ボード(ペリフェラル)定義 |
たった5つのファイルで新しいボードに対応できた。
詳しく見れているわけではないが、抽象化層がよくできている印象。
修正したファイル
既存のファイルの変更はsrc/machine/machine_stm32.go
のみ。
GPIOのポートの定義を追加した。
stm32f4disco.json
ターゲットをビルドするための定義ファイル。 ツールチェインやビルド設定、ターゲットへの書き込み方法、デバッガの設定などを定義する。
'inherit'でベースになる定義ファイルを取り込むことができる。
{ "inherits": ["cortex-m"], "llvm-target": "armv7em-none-eabi", "build-tags": ["stm32f4disco", "stm32f407", "stm32"], "cflags": [ "--target=armv7em-none-eabi", "-Qunused-arguments" ], "ldflags": [ "-T", "targets/stm32f407.ld" ], "extra-files": [ "src/device/stm32/stm32f407.s" ], "flash": "openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg -c 'program {hex} reset exit'", "ocd-daemon": ["openocd", "-f", "interface/stlink.cfg", "-f", "target/stm32f4x.cfg"], "gdb-initial-cmds": ["target remote :3333", "monitor halt", "load", "monitor reset", "c"] }
'build-tags'で、'ボード名'、'マシン名'、'アーキテクチャ'を設定する。 Linuxカーネルを覗いた事がある人なら、'board'、'mach'、'arch'のようにイメージすれば良さそう。 ここに指定した文字列を元に、ビルドするターゲットのソースファイルを選択しているっぽい。
使用するリンカスクリプトもここで定義する。
stm32f407.ld
RAMは192K使えるはずなんだけど、とりあえず128Kにしといた。
MEMORY { FLASH_TEXT (rw) : ORIGIN = 0x08000000, LENGTH = 1M RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K } _stack_size = 4K; INCLUDE "targets/arm.ld"
runtime_stm32f407.go
クロック関連の初期化や処理を書く。 スリープやgofuncのスケジューラから呼ばれて、タイマーなどで実際のタイミングを取るための処理。
ここで実装した関数は次の通り。
- init
- putchar
- initCLK
- initTIM3
- initTIM7
- sleepTicks
- ticks
- timerSleep
- handleTIM3
- handleTIM7
ビルド対象選択のギミック
ソースファイルの先頭に次のようなコメントがある。
// +build stm32,stm32f407
これがビルドするターゲットが「stm32,stm32f407」だった場合にこのファイルをビルドするという宣言になっているらしい。
クロック初期化
initCLK
はクロック初期化の処理。platformioや他のベアメタル系の処理を参考(ほぼパクリ)に実装した。
ここで重要なことはPLLの設定とそれぞれのプリスケーラの設定の計算。
システムクロックが168MHzになるように設定している。 外部クロックである`HSE'の値は8MHzなので、これを基準にクロックを計算する。
const ( HSE_STARTUP_TIMEOUT = 0x0500 /* PLL Options - See RM0090 Reference Manual pg. 95 */ PLL_M = 8 /* PLL_VCO = (HSE_VALUE or HSI_VLAUE / PLL_M) * PLL_N */ PLL_N = 336 PLL_P = 2 /* SYSCLK = PLL_VCO / PLL_P */ PLL_Q = 7 /* USB OTS FS, SDIO and RNG Clock = PLL_VCO / PLL_Q */ )
また、それぞれのクロックドメインを次のように設定した。
クロック | 周波数 |
---|---|
SYSCLK | 168mhz |
HCLK | 168mhz |
APB2(PCLK2) | 84mhz |
APB1(PCLK1) | 42mhz |
Sleep処理
Lチカなどで一定時間待ち合わせたりする際に必要なsleepの処理のためにTIM3を使用する
TIM3はAPB1に接続されているので、内部クロックソースの場合は42mhzになるかと思いきや、 タイマーは自分でPLLを持っているらしく何も設定しない場合は42mhzの倍の84mhzで動作するらしい。
このタイマーは、プリスケーラで分周したクロックで、ARRに設定したカウント分に到達した時に割り込みを発生させることができる。
tinygoでのsleepはマイクロ秒単位で指定なので、たとえば500ミリ秒スリープしたい場合は500000000
が渡されてくる。
参考にした実装では、タイマーのクロックを10Khzに分周して、ARRを0.1ミリ秒単位で設定することで、 スリープ解除したいタイミングで割り込みが発生するようにしていた。
なのでこのようなコードになる。
// ticks are in microseconds func timerSleep(ticks uint32) { timerWakeup = false // CK_INT = APB1 x2 = 84mhz // prescale counter down from 84mhz to 10khz aka 0.1 ms frequency. stm32.TIM3.PSC = 84000000/10000 - 1 // 8399 // set duty aka duration arr := (ticks / 100) - 1 // convert from microseconds to 0.1 ms if arr == 0 { arr = 1 // avoid blocking } stm32.TIM3.ARR = stm32.RegValue(arr) // Enable the hardware interrupt. stm32.TIM3.DIER |= stm32.TIM_DIER_UIE // Enable the timer. stm32.TIM3.CR1 |= stm32.TIM_CR1_CEN // wait till timer wakes up for !timerWakeup { arm.Asm("wfi") } }
84Mhzから10Khzを作るためにプリスケーラレジスタのPSCに8399
を設定。
10Khzは1秒に10000回振幅するので、1回の振幅に0.1ミリ秒かかる計算になる。
引数で渡されたtickはマイクロ秒単位なので、0.1ミリ秒単位になるようにARRを設定する。
割り込みハンドラの中で、スリープ解除フラグを立てるという前提で、フラグが経っていない時はwfi
で寝る。
なかなかに分かりやすい構造となっている。
割り込みを有効化するためには、NVICに対する設定を行なう次の関数を使用する。
arm.SetPriority(stm32.IRQ_TIM3, 0xc3)
arm.EnableIRQ(stm32.IRQ_TIM3)
そしてハンドラは次のようにする。
//go:export TIM3_IRQHandler func handleTIM3() { if (stm32.TIM3.SR & stm32.TIM_SR_UIF) > 0 { // Disable the timer. stm32.TIM3.CR1 &^= stm32.TIM_CR1_CEN // clear the update flag stm32.TIM3.SR &^= stm32.TIM_SR_UIF // timer was triggered timerWakeup = true } }
//go:export TIM3_IRQHandler
のコメント行がキモとなっていて、この次に定義された関数がTIM3_IRQHandler
としてエクスポートされる。
つまり、割り込みハンドラとして登録されるようになっている。
gofuncスケジューラのためのtickカウンタ
examples/blinky2
では2つのLEDをgofuncを使用して2つの関数からそれぞれ制御する。
tinygoは内部にスケジューラを持っていて、スケジューラがタイミングを取るためにticks関数を呼び出している。
ticks関数は、ボードが起動してからのカウントをマイクロ秒単位で取得する。
// number of ticks (microseconds) since start. func ticks() timeUnit { // milliseconds to microseconds return tickCount * 1000 }
ここで返すカウント値は、TIM7を使って1ミリ秒ごとにカウントアップすることで作る。(なので戻り値は1000倍している)
TIM7の設定は基本的にTIM3と同じ。違うのはARRが10固定になっている点。
PSCで10Khzを作って、0.1ミリ秒の振幅が10回行われた時、つまり1ミリ秒に1回割り込みが発生する。 その中でカウンタを増やす。
// Enable the TIM7 clock.(tick count) func initTIM7() { stm32.RCC.APB1ENR |= stm32.RCC_APB1ENR_TIM7EN // CK_INT = APB1 x2 = 84mhz stm32.TIM7.PSC = 84000000/10000 - 1 // 84mhz to 10khz(0.1ms) stm32.TIM7.ARR = stm32.RegValue(10) - 1 // interrupt per 1ms // Enable the hardware interrupt. stm32.TIM7.DIER |= stm32.TIM_DIER_UIE // Enable the timer. stm32.TIM7.CR1 |= stm32.TIM_CR1_CEN arm.SetPriority(stm32.IRQ_TIM7, 0xc1) arm.EnableIRQ(stm32.IRQ_TIM7) } ...(snip)... //go:export TIM7_IRQHandler func handleTIM7() { if (stm32.TIM7.SR & stm32.TIM_SR_UIF) > 0 { // clear the update flag stm32.TIM7.SR &^= stm32.TIM_SR_UIF tickCount++ } }
machine_stm32f407.go
マシン(SoC)固有の処理を書く。 ペリフェラルのドライバはここに書くことになる。
今回は次のドライバを実装している。
- GPIO
- UART
GPIOドライバ
GPIOドライバでは次の関数を実装している。
- getPort
- enableClock
- Configure
- setAltFunc
- Set
この内外部から呼び出される可能性があるのは大文字で始まっているConfigure
とSet
のみ。
golangでは大文字で始まるシンボルがエクスポートされるらしい。
getPort
この関数ではGPIOA〜GPIOIまでのレジスタアクセス用の構造体を取得できる。
enableClock
この関数はAHB1のGPIOxENを有効にしデバイスを有効化する。
Configure
この関数は外部から呼び出され、ピンの方向やプッシュプルなどの設定を行なう。
指定できるフラグは次のようになっている。
フラグ | 機能 | 備考 |
---|---|---|
GPIO_OUTPUT | 出力 | |
GPIO_INPUT | 入力、GPIO_INPUT_PULLDOWNと同じ | |
GPIO_INPUT_FLOATING | 入力でフローティング | |
GPIO_INPUT_PULLDOWN | 入力でプルダウン | |
GPIO_INPUT_PULLUP | 入力でプルアップ | |
GPIO_UART_TX | UARTのTX設定 | UARTドライバからのみ呼ばれる |
GPIO_UART_RX | UARTのRX設定 | UARTドライバからのみ呼ばれる |
setAltFunc
GPIOのAlternative Functionを設定する。
Set
出力ピンのHI/LOを設定する。
UARTドライバ
UARTドライバはほぼmachine_stm32f103xx.go(BluePill向け)のドライバを拝借した。
ボーレートの設定の部分だけSTM32F4Discoveryに合わせて設定した。 本来であれば9600や38400など一般的な設定は受け付けるようにするべきだが、 今回は115200に固定とした。
ボーレートを決定するにはBRRレジスタを設定する必要がある。
BRRに設定できる値はRM0090のTable 134〜143に一覧されている。
USART2はAPB2に接続されているので、クロックは42Mhzとなる。USART2_CR1のOVER8は0に設定されているのでTable 142
の値を使用できる。
BRRは指数部が4ビットの固定小数点のため、下記のように計算できる。
(整数部<<4)+(指数部*16)
なのでこのように実装している。
// Configure the UART. func (uart UART) Configure(config UARTConfig) { // Default baud rate to 115200. if config.BaudRate == 0 { config.BaudRate = 115200 } // pins switch config.TX { default: // use standard TX/RX pins PA2 and PA3 GPIO{UART_TX_PIN}.Configure(GPIOConfig{Mode: GPIO_UART_TX}) GPIO{UART_RX_PIN}.Configure(GPIOConfig{Mode: GPIO_UART_RX}) } // Enable USART2 clock stm32.RCC.APB1ENR |= stm32.RCC_APB1ENR_USART2EN /* Set baud rate(115200) OVER8 = 0, APB2 = 42mhz +----------+--------+ | baudrate | BRR | +----------+--------+ | 1200 | 0x88B8 | | 2400 | 0x445C | | 9600 | 0x1117 | | 19200 | 0x88C | | 38400 | 0x446 | | 57600 | 0x2D9 | | 115200 | 0x16D | +----------+--------+ */ stm32.USART2.BRR = 0x16c // Enable USART2 port. stm32.USART2.CR1 = stm32.USART_CR1_TE | stm32.USART_CR1_RE | stm32.USART_CR1_RXNEIE | stm32.USART_CR1_UE // Enable RX IRQ. arm.SetPriority(stm32.IRQ_USART2, 0xc0) arm.EnableIRQ(stm32.IRQ_USART2) }
デバッグ中に0x16c
をBRRに設定したままコミットしてしまった。
0x16d
を入れても問題なく動く。
board_stm32f4disco.go
このファイルではボードに実装されているピンやペリフェラルを定義する。
// +build stm32,stm32f4disco package machine const ( PA0 = portA + 0 PA1 = portA + 1 PA2 = portA + 2 PA3 = portA + 3 PA4 = portA + 4 PA5 = portA + 5 PA6 = portA + 6 PA7 = portA + 7 PA8 = portA + 8 PA9 = portA + 9 PA10 = portA + 10 PA11 = portA + 11 PA12 = portA + 12 PA13 = portA + 13 PA14 = portA + 14 PA15 = portA + 15 PB0 = portB + 0 PB1 = portB + 1 PB2 = portB + 2 PB3 = portB + 3 PB4 = portB + 4 PB5 = portB + 5 PB6 = portB + 6 PB7 = portB + 7 PB8 = portB + 8 PB9 = portB + 9 PB10 = portB + 10 PB11 = portB + 11 PB12 = portB + 12 PB13 = portB + 13 PB14 = portB + 14 PB15 = portB + 15 PC0 = portC + 0 PC1 = portC + 1 PC2 = portC + 2 PC3 = portC + 3 PC4 = portC + 4 PC5 = portC + 5 PC6 = portC + 6 PC7 = portC + 7 PC8 = portC + 8 PC9 = portC + 9 PC10 = portC + 10 PC11 = portC + 11 PC12 = portC + 12 PC13 = portC + 13 PC14 = portC + 14 PC15 = portC + 15 PD0 = portD + 0 PD1 = portD + 1 PD2 = portD + 2 PD3 = portD + 3 PD4 = portD + 4 PD5 = portD + 5 PD6 = portD + 6 PD7 = portD + 7 PD8 = portD + 8 PD9 = portD + 9 PD10 = portD + 10 PD11 = portD + 11 PD12 = portD + 12 PD13 = portD + 13 PD14 = portD + 14 PD15 = portD + 15 PE0 = portE + 0 PE1 = portE + 1 PE2 = portE + 2 PE3 = portE + 3 PE4 = portE + 4 PE5 = portE + 5 PE6 = portE + 6 PE7 = portE + 7 PE8 = portE + 8 PE9 = portE + 9 PE10 = portE + 10 PE11 = portE + 11 PE12 = portE + 12 PE13 = portE + 13 PE14 = portE + 14 PE15 = portE + 15 PH0 = portH + 0 PH1 = portH + 1 ) const ( LED = LED_BUILTIN LED1 = LED_GREEN LED2 = LED_ORANGE LED3 = LED_RED LED4 = LED_BLUE LED_BUILTIN = LED_GREEN LED_GREEN = PD12 LED_ORANGE = PD13 LED_RED = PD14 LED_BLUE = PD15 ) // UART pins const ( UART_TX_PIN = PA2 UART_RX_PIN = PA3 )
特に難しいところはない。
tinygoコマンド
ソースコードをビルドしたり、プログラムを実機に書き込んだりするにはtinygo
コマンドを使用する。
単にビルドするのであればbuild
サブコマンドを使用する。
$ tinygo build -o test.elf --target stm32f4disco examples/echo
プログラムを書き込む場合はflash
サブコマンドを使用する。flashではビルドから書き込みまでやってくれるので便利。
$ tinygo flash --target stm32f4disco examples/echo
GDBでデバッグする場合はgdb
サブコマンドを使用する。これも必要に応じてビルド、書き込みも行ってくれる。
$ tinygo gdb --target stm32f4disco examples/echo
シンボルデバッグは正直使えないため、レジスタの確認が主な用途だった。
Pull Request
せっかく作ったのでプルリクを送ってみた。
CONTRIBUTING.mdを読んでなかった ため、masterブランチに向けてプルリクを作成したり(devブランチに向けないとダメ)、 go fmtを実行していなかったりといろいろ合ったが、中の人が丁寧にアドバイスをくれた。
最終的には中の人がいろいろと手直しをしてくれてdevブランチにマージされた。
とてもいい経験になった。
まとめ
Golang初心者でも、なんとかSTM32F4DiscoveryでTinyGo動かすことができた。 ビルドシステムやコンパイラがなかなか良くできている印象。
ハードウェアの抽象化レベルが高いのか、少しのコードを足すだけで新しいボードに対応できるのはスゴいと思った。
あと、中の人がとても親切。
ただ、GDBが使えるとはいえ、シンボルでバッグがまともに使えないので(goのシンボルとは1:1にならないので?)、 UARTが使えるようになるまではデバッグがしんどかった。
環境が整ってくればかなり使えるのではないかと思う。 意外とGolangのコードを修正して実機で確認するというサイクルが手軽に回せるので開発しやすいと思った。