みつきんのメモ

組み込みエンジニアです。Interface誌で「My オリジナルLinuxの作り方」連載中

TinyGoをSTM32F4Dicoveryで動かす

はじめに

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

この内外部から呼び出される可能性があるのは大文字で始まっているConfigureSetのみ。 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の値を使用できる。

f:id:mickey_happygolucky:20190416115524p:plain
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のコードを修正して実機で確認するというサイクルが手軽に回せるので開発しやすいと思った。