マイコンで使えるGo言語であるTinyGoについて調べてみたら、実際にある程度の機能のプロジェクトでも動かせそうな感じでしたので、以前、C++でコーディングしたRaspberry Pi Picoで向けのFatFsのベンチマークプロジェクトをTinyGoに移植して動かしてみました。
TinyGoでFatFs動作確認 |
TinyGo開発環境の準備
TinyGo開発環境に関しては、直接ローカルにインストールする方法もありますが、ローカルの環境を汚さずに手っ取り早く動く環境を取得するには、Docker環境を使用するのがおすすめです。 以降、Docker Desktop WSL2環境を前提に話を進めます。 まずは、Dockerを起動する前に、今回のプロジェクトをGitHubからクローンするため、Git Bashシェルなどから以下を実行します。
$ cd /d/somewhere/share $ git clone -b main https://github.com/elehobica/pico_tinygo_fatfs_test.git
次にDockerイメージの取得、実行と進みます。この説明ではDockerでの実行ディレクトリは共有ディレクトリ上で行っています。よりビルドを高速化したい場合はDockerコンテナ内のファイルシステム側にデータをコピーして行うと良いですが、今回のプロジェクト程度の規模ではWindowsファイルシステム上でビルドを行ってもほとんど問題にならないレベルと思います。
> wsl (以下 WSL2 シェルにて) $ docker pull tinygo/tinygo $ docker images $ docker run -it -v /mnt/d/somewhere/share:/share tinygo/tinygo:latest /bin/bash (以下 Dockerコンテナにて) # cd /share # cd pico_tinygo_fatfs_test
プロジェクトのビルド
Goモジュール設定
ビルドの前に、Goモジュールの設定を行っておきます。 プロジェクトのルートとなるディレクトリ(pico_tinygo_fatfs_test)の設定を行ったあと、 go mod tidyを実行すれば残りの部分は自動的に設定されます。
# go mod init pico_tinygo_fatfs_test # go mod tidy
モジュール設定出来た状態でのgo.modの内容は以下のようになっているはずです。
module pico_tinygo_fatfs_test go 1.17 require tinygo.org/x/tinyfs v0.2.0
ビルド
あとは、ターゲットを指定してtinygo buildを行えば、バイナリファイルが生成されます。
# tinygo build -target=pico -o pico_tinygo_fatfs_test.uf2
詳細なサイズ情報を表示させるには以下のようにビルドすると良いでしょう。
# tinygo build -target=pico -o pico_tinygo_fatfs_test.uf2 -size full
生成されたUF2ファイルをWindows環境に戻って、RPI-RP2ドライブ直下にコピーすれば、OKです。
配線図 / ピン設定
配線図は以下の通りです。
配線図 |
コードの説明
サンプルコードはここに置いてあるものを使用します。
pico.go
TinyGoではターゲットに関する設定はmachineパッケージにまとめられており、Raspbery Pi Picoの基本設定はこちらにまとめられています。この情報を利用して初期設定をおこなっているのが、pico.goです。冒頭の2行はtinygo build時に指定したtargetと合致する場合にのみビルド対象となるように指定するためのGo Compiler Directiveです。1行目がgo 1.17以降の書式、2行目がgo 1.16以前の書式に対応するようです。init()関数はこの場合mainパッケージ内に定義されているので、main()より先に実行されmain.goで定義されている変数の初期化を行っています。
//go:build pico // +build pico package main import ( "machine" ) func init() { spi = *machine.SPI0 sckPin = machine.GP2 sdoPin = machine.GP3 sdiPin = machine.GP4 csPin = machine.GP5 ledPin = machine.LED }
machine.Pinインターフェイス追加
main.goの最初のほうでLEDピン関連に対する操作を追加しています。
type Pin struct { *machine.Pin } func (pin Pin) Toggle() { pin.Set(!pin.Get()) } func (pin Pin) ErrorBlinkFor(count int) { for { for i := 0; i < count; i++ { pin.High() time.Sleep(250 * time.Millisecond) pin.Low() time.Sleep(250 * time.Millisecond) } pin.Low() time.Sleep(500 * time.Millisecond) } } func (pin Pin) OkBlinkFor() { for { pin.High() time.Sleep(1000 * time.Millisecond) pin.Low() time.Sleep(1000 * time.Millisecond) } }
手順としては既存インターフェイスを取り込んだ新規のTypeであるPinを定義して、そのTypeをレシーバとしたメソッドを定義していく流れになります。このようにすれば、既存のインターフェイスと新規に定義したメソッドを同様に新規のTypeに対して扱うことができ、継承のような効果を得ることができます。
FatFsライブラリ
フリーのCライブラリであるFatFsをcgo機能を利用してGoから呼び出す形で使用します。TinyGoのリポジトリにあるFatFsに対してfatfs_testと同等の機能を実現するためにカスタマイズして使用します。なお、TinyGoリポジトリのFatFsのCライブラリ自体は、ooFatというFatFsをオブジェクト指向化したものを導入しています。
今回のプロジェクトの機能追加は、go_fatfs.goに以下のGo側のメソッドを追加する形で行いました。
func (l *FATFS) GetFsType() (Type, error) func (l *FATFS) GetCardSize() (int64, error) func (f *File) Seek(offset int64) error func (f *File) Tell() (ret int64, err error) func (f *File) Rewind() (err error) func (f *File) Truncate() error func (f *File) Expand(size int64, flag bool) error
GoからCへの受け渡し
GoからCへの渡し方の部分をExpand()メソッドを例にとって説明します。
// #include <string.h> // #include <stdlib.h> // #include "./go_fatfs.h" import "C" ... func (f *File) Expand(size int64, flag bool) error { var fsz C.FSIZE_t = C.FSIZE_t(size) var opt C.BYTE; if flag { opt = 1 } else { opt = 0 } return errval(C.f_expand(f.fileptr(), fsz, opt)) }
まず、cgoを使用する場合はインクルードするCヘッダをGo Compiler Directiveで指定して、空行を入れずにimport "C"を記述する必要があります。このようにすることで、C側で定義された関数をC.f_expand()のように直接呼び出すことが出来ます。ただし、C関数に与える引数もC側の型に合わせて変換する必要があり、ここで値のコピーが発生するので、C関数をGo対応する際にはオーバーヘッドが発生します。
Cの構造体メンバへのアクセス
Cの構造体メンバへのアクセスについてはソースコードの以下の部分を抜粋して説明します。
fatfs/go_fatfs.go
... type FATFS struct { dev tinyfs.BlockDevice fs *C.FATFS } ... func (l *FATFS) Configure(config *Config) *FATFS { l.fs = C.go_fatfs_new_fatfs() l.fs.drv = gopointer.Save(l) return l } ... func (l *FATFS) GetCardSize() int64 { return int64(l.fs.csize) * int64(l.fs.n_fatent) * SectorSize }
fatfs/go_fatfs.c
FATFS* go_fatfs_new_fatfs(void) { return malloc(sizeof(FATFS)); }
まず、Go側の構造体 FATFS のメンバーとしてfsを、ff.hで定義されているFATFS構造体、つまりC.FATFS型としてTinyGo側で宣言します。 (ここではTinyGo側のFATFS構造体と、C側のFATFS構造体は別のものであることに注意してください。) このメンバーfsに対してはC側で定義したgo_fatfs_new_fatfs()関数により、Configureコール時に構造体へのポインタが代入されます。 この状態において、TinyGo側では、Cの構造体メンバーに対しては、C側で行うのと同様に、FATFSインスタンス.fs.メンバー名 でアクセスすることができます。 具体的には、GetCardSize()メソッドにおける、l.fs.csize, l.fs.n_fatentの部分がその例になります。
Type Assertion
GoならではのType Assertionが行われている部分について説明します。
fatfs/go_fatfs.go
... type File struct { fs *FATFS typ uint8 hndl unsafe.Pointer name string } ... func (l *FATFS) OpenFile(path string, flags int) (tinyfs.File, error) { ... var file = &File{fs: l, name: path} ... return file, nil }
type File interface { FileHandle IsDir() bool Readdir(n int) (infos []os.FileInfo, err error) } // FileHandle is a copy of the experimental os.FileHandle interface in TinyGo type FileHandle interface { // Read reads up to len(b) bytes from the file. Read(b []byte) (n int, err error) // Write writes up to len(b) bytes to the file. Write(b []byte) (n int, err error) // Close closes the file, making it unusable for further writes. Close() (err error) }
main.go
... f, err := filesystem.OpenFile("bench.dat", os.O_RDWR | os.O_CREATE | os.O_TRUNC) ... ff, ok := f.(*fatfs.File) ... err = ff.Seek(0) ...
go_fatfs.goのOpenFile()メソッド内では、File type (構造体)を生成してひとつ目の戻り値として返しますが、一方でOpenFile()メソッドの戻り値の定義部分には、File typeではなく、tinyfs.Fileが指定されています。tinyfs.Fileはtinyfs/tinyfs.goを参照するとtypeではなくインターフェイスとなっています。つまり、OpenFile()メソッドのリターン時に、File typeをtinyfs.Fileインターフェイスに代入したものを返していることになります。したがって、OpenFile()呼び出し側のmain.goでは、戻り値の変数からはtinyfs.Fileインターフェイスのみを扱うことができ、もとのfatfs.File typeをレシーバとしたメソッドをそのままの状態では扱うことが出来ません。このような場合に、インターフェイスに代入されたことにより、もとのtypeの情報を失ったインターフェイス変数に対して元の型を取り戻す操作 = Type Assertion を行うことができます。main.goのff, ok := f.(*fatfs.File)がそれに該当します。Type Assertionを行う際には、想定する元の型が異なり失敗することを想定に入れる必要がありますので、必ず2番目の戻り値 ok でType Assertionの成否を確認するようにします。このようにして、取り出したfatfs.File typeのff変数に対して、fatfs.FileがレシーバであるSeek()メソッドなどを作用させています。
defer文 (遅延処理)
main.go
func fatfs_test(led *Pin) (testError *TestError) { ... f, err := filesystem.OpenFile("bench.dat", os.O_RDWR | os.O_CREATE | os.O_TRUNC) if err != nil { return &TestError{ error: fmt.Errorf("open error: %s", err.Error()), Code: 3 } } defer f.Close() ... ffに対する処理 }
deferは遅延処理の予約で、関数を抜けるときに実行されます。上記の例では、ファイルのOpenとCloseは一対の処理ですので、正常にOpenが確認できたあとで、Close()をdeferしておけば、fatfs_test()関数からリターンされる直前で実行されます。
なお、TinyGoではGo言語のひとつの特徴である大域脱出としてのpanicと、panicを救済するためのrecoverは実装されていません。これはTinyGoが対象とするようなターゲットではメモリリソースが限られており、panic/recover機構にともなう利点よりも実装のコストが上回るとの判断によるようです。詳しくはここをご覧ください。TinyGoではpanicをコールした場合はそこでプログラムは異常終了しますので、処理を継続できないような深刻なエラーの場合(対処方法がないハードウェア異常など)に限定して使用するのが良さそうです。
errorにエラーコード追加
上記に記載した通り、TinyGoではpanicを上位で救済する機能はありませんので、エラー処理はpanicを使用せずに、エラーを戻り値で返すことになります。errorはインターフェイスとして定義されており、Error() stringのみを持ちますが、今回のプロジェクトではエラーコードを追加してみました。
main.go
type TestError struct { error Code int } func main() { ... err := fatfs_test(led) if err != nil { fmt.Printf("ERROR[%d]: %s\r\n", err.Code, err.Error()) led.ErrorBlinkFor(err.Code) } ... } func fatfs_test(led *Pin) (testError *TestError) { ... sd := sdcard.New(spi, sckPin, sdoPin, sdiPin, csPin) err := sd.Configure() if err != nil { return &TestError{ error: fmt.Errorf("configure error: %s", err.Error()), Code: 1 } } ... }
errorにエラーコード Codeを追加するには全く新規のエラー typeを定義する方法もありますが、簡単な方法として既存errorを取り込んだ上で、新規の要素を追加する方法を用いました。それが冒頭のTestError typeの定義となります。これにより、TestErrorは errorインターフェイスであるError()に加えて、Codeというメンバー変数を持つことができますので、エラー送出の際は、errorとCodeを含むTestError構造体を生成して返すようにすれば、受け取り側のmain()で err.Error()と err.Codeという形で参照できるようになります。今回のプロジェクトとしてはサンプルとしてエラー内容をシリアル表示しながら、エラーコードが示す回数分、LEDが点滅をしてエラーを知らせる処理を記述しました。
その他
今回のプロジェクトで使用した、マイコンを使用する際に、よく使用されるであろうその他機能をまとめて紹介します。// SPIのシリアルクロック周波数の設定 SPI_BAUDRATE_MHZ := 50 spi.SetBaudRate(SPI_BAUDRATE_MHZ * machine.MHz) // 経過時間の取得 start := time.Now() // 冒頭でコール ... millis := time.Since(start).Milliseconds() // 経過時間: ミリ秒 micros := time.Since(start).Microseconds() // 経過時間: マイクロ秒
ベンチマーク比較
最後に元のC++プロジェクトと今回のTinyGoプロジェクトでのSDカードアクセスのベンチマーク比較を記載しておきます。同じ、Sansung microSDHC EVO Plus 32GB (UHS-I U1)、FAT32フォーマットカードでの比較です。 read性能に顕著なパフォーマンス低下が見られますので、実際のアプリケーションの要求性能に応じた判断が求められると思います。
- pico_fatfs_test (C++)
===================== == pico_fatfs_test == ===================== mount ok Type is FAT32 Card size: 32.00 GB (GB = 1E9 bytes) FILE_SIZE_MB = 5 BUF_SIZE = 512 bytes Starting write test, please wait. write speed and latency speed,max,min,avg KB/Sec,usec,usec,usec 447.7192, 6896, 1007, 1142 446.4797, 7589, 1024, 1145 Starting read test, please wait. read speed and latency speed,max,min,avg KB/Sec,usec,usec,usec 974.9766, 1050, 403, 524 974.4066, 1049, 402, 524
- pico_tinygo_fatfs_test (TinyGo)
============================ == pico_tinygo_fatfs_test == ============================ mount ok Type is FAT32 Card size: 32.00 GB (GB = 1E9 bytes) FILE_SIZE_MB = 5 BUF_SIZE = 512 bytes Starting write test, please wait. write speed and latency speed,max,min,avg KB/Sec,usec,usec,usec 349.1884, 17431, 1042, 1442 361.0920, 22377, 1062, 1391 Starting read test, please wait. read speed and latency speed,max,min,avg KB/Sec,usec,usec,usec 354.7382, 22428, 1284, 1417 353.7843, 22470, 1293, 1421
速度改善編に続く。
0 件のコメント:
コメントを投稿