2軸加速度センサーIIS2ICLXブレイクアウトをTinyGo(nRF52840)で動かす

144Labの入江田です。

2軸加速度センサーIIS2ICLXブレイクアウトをArduino(nRF52840)で動かすの内容をArduinoではなくTinyGoで実装するときの方法を紹介します。

f:id:irieda:20210325173843p:plain

あらかじめ必要なもの

あとターゲットによっては追加インストールが必要になったりしますが、今回の事例では不要です。

プロジェクトの構成

プロジェクト名フォルダを作成し、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, &reg)
    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, &reg)
    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と連携していくことが公表済みです。