144Labの入江田です。
2軸加速度センサーIIS2ICLXブレイクアウトをArduino(nRF52840)で動かすの内容をArduinoではなくTinyGoで実装するときの方法を紹介します。
あらかじめ必要なもの
- Go v1.16.0以降(Goのインストール方法)
- TinyGo v0.17.0以降(TinyGoのインストール方法)
あとターゲットによっては追加インストールが必要になったりしますが、今回の事例では不要です。
プロジェクトの構成
プロジェクト名フォルダを作成し、Goモジュールを初期化します。
mkdir sampleProject cd sampleProject go mod init sampleProject
以下のIIS2ICLX用のドライバーソースコードの「iis2iclx_reg.c」と「iis2iclx_reg.h」をプロジェクトフォルダにダウンロードします。 https://github.com/STMicroelectronics/STMems_Standard_C_drivers/tree/6788aa613bd11340767f691c77e306fc95c05555/iis2iclx_STdC
Goラッパーの実装
iis2iclx_go.cを作成します。
stmdev_ctx_t *dev_ctx
を初期化してそれを引き出せる関数を用意するだけです。
platform_readとplatform_writeはGo側の実装を参照します。
#include "iis2iclx_go.h" extern int32_t platform_read(void *handle, uint8_t reg, uint8_t *bufp, uint16_t len); extern int32_t platform_write(void *handle, uint8_t reg, uint8_t *bufp, uint16_t len); static stmdev_ctx_t *dev_ctx = &(stmdev_ctx_t){.write_reg = platform_write, .read_reg = platform_read}; stmdev_ctx_t *getContext() { return dev_ctx; }
iis2iclx_go.hを作成します。 getContext()をexternするだけです。
#ifndef IIS2ICLX_GO_H #define IIS2ICLX_GO_H #include "iis2iclx_reg.h" extern stmdev_ctx_t *getContext(); #endif // IIS2ICLX_GO_H
iis2iclx.goを作成します。 platform_writeとplatform_readの関数定義をCの参照宣言に合うようにGoで書いてexportします。
package main /* #include "iis2iclx_go.h" #include "string.h" */ import "C" import ( "log" "machine" "time" "unsafe" ) var ( p33 = machine.Pin(2) // power 3.3V gnd = machine.Pin(3) // power gnd io4 = machine.Pin(31) // io4 sda = machine.Pin(30) // i2c sda scl = machine.Pin(29) // i3c scl ) //export platform_write func platform_write(handle unsafe.Pointer, reg uint8, buf *uint8, length uint16) int32 { b := make([]byte, length) C.memcpy(unsafe.Pointer(&b[0]), unsafe.Pointer(buf), C.uint(length)) machine.I2C1.WriteRegister(C.IIS2ICLX_I2C_ADD_L>>1, reg, b) return 0 } //export platform_read func platform_read(handle unsafe.Pointer, reg uint8, buf *uint8, length uint16) int32 { b := make([]byte, length) machine.I2C1.ReadRegister(C.IIS2ICLX_I2C_ADD_L>>1, reg, b) C.memcpy(unsafe.Pointer(buf), unsafe.Pointer(&b[0]), C.uint(len(b))) return 0 } // Device ... type Device struct { ctx *C.stmdev_ctx_t x, y float32 temperature float32 } func Setup() *Device { p33.Configure(machine.PinConfig{Mode: machine.PinOutput}) gnd.Configure(machine.PinConfig{Mode: machine.PinOutput}) gnd.Low() p33.High() machine.I2C1.Configure(machine.I2CConfig{ Frequency: machine.TWI_FREQ_100KHZ, SCL: scl, SDA: sda, }) time.Sleep(20 * time.Millisecond) dev := &Device{ctx: C.getContext()} C.iis2iclx_bus_mode_set(dev.ctx, C.IIS2ICLX_SEL_BY_HW) if dev.GetWhoAmI() != C.IIS2ICLX_ID { log.Println("get who am i failed") } C.iis2iclx_reset_set(dev.ctx, C.PROPERTY_ENABLE) rst := C.uint8_t(1) for rst != 0 { C.iis2iclx_reset_get(dev.ctx, &rst) time.Sleep(10 * time.Millisecond) } /* Enable Block Data Update */ C.iis2iclx_block_data_update_set(dev.ctx, C.PROPERTY_ENABLE) /* Set Output Data Rate */ C.iis2iclx_xl_data_rate_set(dev.ctx, C.IIS2ICLX_XL_ODR_12Hz5) /* Set full scale */ C.iis2iclx_xl_full_scale_set(dev.ctx, C.IIS2ICLX_2g) /* Configure filtering chain(No aux interface) * Accelerometer - LPF1 + LPF2 path */ C.iis2iclx_xl_hp_path_on_out_set(dev.ctx, C.IIS2ICLX_LP_ODR_DIV_100) C.iis2iclx_xl_filter_lp2_set(dev.ctx, C.PROPERTY_ENABLE) log.Println("setup completed") return dev } func (d *Device) GetWhoAmI() byte { b := make([]byte, 1) C.iis2iclx_device_id_get(d.ctx, (*uint8)(unsafe.Pointer(&b[0]))) return b[0] } func (d *Device) Update() error { reg := uint8(1) C.iis2iclx_xl_flag_data_ready_get(d.ctx, ®) if reg != 0 { /* Read acceleration field data */ accel := [2]int16{} C.iis2iclx_acceleration_raw_get(d.ctx, &accel[0]) d.x = float32(accel[0]) * 0.061 //C.iis2iclx_from_fs2g_to_mg(accel[0]) d.y = float32(accel[1]) * 0.061 //C.iis2iclx_from_fs2g_to_mg(accel[1]) } C.iis2iclx_temp_flag_data_ready_get(d.ctx, ®) if reg != 0 { /* Read temperature data */ tmp := int16(0) C.iis2iclx_temperature_raw_get(d.ctx, &tmp) d.temperature = (float32(tmp) / 256.0) + 25.0 //C.iis2iclx_from_lsb_to_celsius(tmp) } return nil } func (d *Device) Accel() (x, y float32) { return d.x, d.y } func (d *Device) Temperature() float32 { return d.temperature }
アプリケーションの実装
ここまで用意できたら、mainの実装は以下のように書けばOKです。
package main import ( "fmt" "machine" "time" ) func main() { dev := Setup() fmt.Println("Setup Completed") fmt.Printf("WhoAmI: %X\n", dev.GetWhoAmI()) machine.UART0.Configure(machine.UARTConfig{BaudRate: 115200}) for { dev.Update() x, y := dev.Accel() fmt.Printf("Accel: %6.1f, %6.1f ", x, y) fmt.Printf("Temp.: %5.2f\n", dev.Temperature()) time.Sleep(300 * time.Millisecond) } }
TinyGoでのターゲット指定方法
TinyGoはfeather-nrf52840(nRF52840開発ボード)をサポートしているのでそれを継承して以下のようなconfig.jsonを書きます。
{ "inherits": ["feather-nrf52840"], "flash-method": "msd", "msd-volume-name": "NRF52BOOT", "msd-firmware-name": "firmware.uf2" }
ファイル一覧とビルド方法
以上でファイル一覧は以下のようになります。
> ls -1 config.json go.mod iis2iclx_go.c iis2iclx_go.h iis2iclx.go iis2iclx_reg.c iis2iclx_reg.h main.go
以下のコマンドでビルドできます。
tinygo build -target config.json -o app.uf2 .
書き込みには以下のコマンドで。
tinygo flash -target config.json .
pip3 install pyserial
するとpyserial-miniterm
というツールが使えます。
pyserial-miniterm ポート名 115200
するとターゲットのログ出力が見れます。
冒頭の元記事とほぼ同じ出力が得られます。
まとめ
- TinyGoによる組み込み開発はだいぶ実用レベルになってきています。
- USBまわりやBluetoothも利用可能になっています。
- 未サポートのボードでもCPUがコンパチならこの記事のようにすぐに対応実装を書き始めることができます。
- CGOという機能を経由してCの資産を取り込んで利用することができます。
- ただし、本家GoのCGOに比べると細かいいくつかの機能はまだありません。
- Cの資産を取り込めるという点でVMによる組み込み開発よりは実用に向いています。
- TinyGoはllvmを利用するので出力がコンパクトです。今回の事例でも成果物バイナリは70KiB弱で済みます。
- ArduinoはTinyGoと連携していくことが公表済みです。