BMI270 IMUのSPI通信実装：APIを使わずにPythonで制御を行う

# 背景
BMI270は6軸の加速度・ジャイロセンサをもった慣性測定ユニット（IMU）です。
BMI270の使い方を検索するとライブラリを使った記事がすでにありますが、本記事ではそれらを使用せずにデータシートを読んでイチから実装したので自分の備忘録も兼ねてその方法を記載します。
理由としては、

・ArduinoIDEなどで紹介されている方法ではなく、どのような環境でも構築できるようにしたい
・IMUのパラメータをデータシートを読みながら変更できるようにしたい

あたりが動機です。もともとはある基板にIMUを載せるにあたり最初はBMI160を選定していたのですが、ディスコンで入手不可になったため、代替品として選定しました。
最初はデバイスのパッケージサイズも同じ後発品なので、BMI160とほぼ同じ実装でいけるかと思ったのですが、想像以上に違う実装をする必要があってだいぶ苦戦しました。
BMI270のデータシートはこちらです。これを読みながら実装を進めていきます。

https://www.bosch-sensortec.com/products/motion-sensors/imus/bmi270/

なお、ほかの方がやられているArduino IDEでAPIを使用した方法はこちらの記事です。参考まで。
https://qiita.com/kaz19610303/items/2f394e3e35e1b47be3c8

# ハードウェアの説明
## 構成説明
BMI270単体は手付け困難なLGA-14というパッケージですので、ピンヘッダが出ている評価基板を使用します。
評価基板はSpresense用のものなど、いくつか種類があるようですが、今回はMIKROE-4073という評価基板を使用します。これはDigi-keyなどから購入可能です。
評価基板自体は3000円程度ですが、センサ1個単体で1000円程度ですので、正規品としては割と良心的な価格ではないかと思います。Spresense用の評価基板も3000円程度のようです。

https://www.digikey.jp/ja/products/detail/mikroelektronika/MIKROE-4073/12092511

今回の構成ですが、RaspberryPi 4からSPIインターフェースを使用し、評価基板と接続します。SPIは3線式もありますが、本記事では4線式での方法を紹介します。3線式の場合はアドレス0x6Bのレジスタに設定の書き込みが必要です。
SPIはSPI0を使用し、ピンはSPI0-CE0を使用します。
ただ、SPIを使用する場合、MIKROE-4073評価ボードのチップ抵抗を付けなおさなければなりません。この評価ボードはI2CとSPIどちらを使うかチップ抵抗で接続を変える方式となっており、最初はI2Cに接続されているためです。
写真左側に付いているチップ抵抗4個をすべて右側に付け直します。難しくはないですが、より楽なジャンパヘッダやはんだジャンパにしてほしかったですね。

付け直したらRaspberryPi 4とBMI270をジャンパー線接続します。接続図は次の通りです。電源は3.3VとGNDを接続します。

![bmi270.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181010/6ee4b04a-b8e0-9836-d0c8-e4126f246814.png)

これでハードウェアの準備は完了です。

## BMI270のSPI仕様
次にソフトウェアの実装をしていきますが、その前にまずBMI270が対応しているSPIモードと、読み出しと書き出しの方法を確認しておきます。
まずSPIモードですが、データシートを読むと124ページを読むとモード00(CPOL=0, CPHA＝0)とモード11(CPOL=1, CPHA＝1)に対応していると書いてあります。本記事ではモード0で実装をします。

次に書き込みですが、125ページにモード0における書き込みのときの図がありあす。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181010/5845dfd6-1c6d-d511-81be-e9655306fba2.png)

これを見ると、先頭バイトは1ビット目で書き込みか読み込みかを指定し、2~8ビット目でアドレスを指定するようです。
2バイト目以降はデータ部となり、何を書くかはレジスタごとに異なります。
書き込みの1バイト目の1ビット目は0でよいので、1バイト目にはアドレス値をそのまま書き込めばよいです。

読み出しの場合ですが、126ページに読み出しの図があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181010/3c5daaad-51a9-a5a5-96ba-35843a697eb0.png)

書き込み時と大きく異なるのは読み込みはアドレス部とデータ部の間にダミーバイトが必要なことです。
このダミーバイトはどのような値を入れてもよいです。
読み出し時は1バイト目の1ビット目は1を指定します。あとは3バイト目以降に読み出したいデータ分だけバイト列を指定します。

さて、ここまで正しくできているか、BMI270と疎通するための簡単なプログラムを書いてみます。
データシート17ページに疎通確認のフローチャートがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181010/35b522cc-ee58-3c94-fe39-857ff50abcb7.png)

アドレス0x00のレジスタを読み出し、応答がデータ部が0x24となればOKなようです。
前述のようにSPI0のCE0、モード0を本記事では使用するので、その設定をします。
その後、アドレス0x00のレジスタを読み出すので、コードは次のようになります。
SPI周波数は適当に50kHzとしました。

spixfer2の0x80|0x00は0x80とすることで読み出し指定、0x00を指定という意味です。
2バイト目はダミーバイトなのでなんでもよいですが0x00を指定し、最後に読み出し分だけ0x00を挿入します。今回は応答は0x24になるはずなので1バイト分だけの指定です。

```python:SPI初期化処理
import spidev
import time
import ast

spi = spidev.SpiDev()
spi.open(0, 0) //SPI0, CE0を使用
spi.mode = 0 //SPIモード0を使用
spi.max_speed_hz = 50000

spi.xfer2([0x80|0x00,0x00,0x00])
```

```python:実行結果
[0x00, 0x00, 0x24]
```

これでSPIを使って実装する準備は完了です。

## 初期化手順
SPIを使ってBMI270と通信はできたものの、これですぐにどのレジスタの読み書きでもできるようになったわけではありません。
BMI270から加速度データやジャイロデータを読み出したりするためには初期化処理が必要なためです。
データシート18ページに初期化の手順があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181010/2831fc78-7911-a5a6-edae-bcdcb47953fc.png)

フローチャートを読むと次のような手順となります。

1. アドレス0x7Cに値0x00を書き込む
2. 450us待つ
3. アドレス0x59に値0x00を書き込む
4. 初期化データを読んで配列に格納
5. 読み込んだ配列をアドレス0x5Eに書き込む
6. アドレス0x59に値0x01を書き込む

最後にデータシート19ページにあるチェックをして終了です。
チェックはアドレス0x21のレジスタを読んで1であればOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181010/76f307b3-8958-c75a-55a0-374b184b4aa5.png)

25ページにもほぼ同様の内容が書かれています。しかし、一点重要な異なる部分があります。
それが、最初の任意のレジスタを読め、というものです。先ほどのフローチャートだけ読んで実装してしまうと気づきません。私はこれに気付かずハマりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181010/39ccea12-c86a-ed3a-f9b3-7db3b9bbc538.png)

ということで任意のレジスタを読む必要がありますが、実は読み出しの疎通確認で行った内容をそのまま残して実装すれば問題ありません。
あとは注意が必要なのは上記手順4.と5.の初期化データを配列へ読み込んで書き込む手順でしょう。

初期化データは下記リンクにあると書かれています。
https://github.com/BoschSensortec/BMI270-Sensor-API/blob/master/bmi270.c

なぜか初期化データとして個別ファイルになっているのではなく、bmi270.cのコード内に直接書かれています。
仕方ないのでこの部分だけコピペして別ファイルとして保存する必要がありますが、そのままコピペすると改行コードが含まれるためそのままでは使い物になりません。
pythonとか適当なプログラミング言語で改行コードを半角スペースに置換します。

できたらbmi270_init.jsonとか適当にファイル名を付けて保存します。

バッファ用の配列への書き込みは
```python
    with open('bmi270_init.json', 'r') as f:
        data = f.read()
    data_array = ast.literal_eval(data)
```
そしてアドレス0x5Eにspi.xfer2を使って書き込みます。
```python
    address = 0x5E
    spi.xfer2([address] + data_array)
```

しかし、実行するとここでバッファサイズが4096バイトを超えているというエラーが出てしまします。
```python
OverflowError: Argument list size exceeds 4096 bytes.
```
書き込むファイルが8キロバイトなのでこのままでは初期化を完了することができません。
エラーメッセージを調べてみると、RaspberryPiの/boot/cmdline.txtにspidev.bufsiz= (サイズ数値)という変数を追加することで、SPIの最大バッファサイズを指定することができるようです。

https://qiita.com/kan573/items/b362b6b9fc493ee2223f

記事によるとバッファサイズは2のn乗で指定するようです。
ひとまず8キロバイトあればよいので、spidev.bufsiz=16384としました。

しかし、この変数を指定してもまだ同様のエラーメッセージが出ます。

SpiDevの仕様を調べるとSpiDev Documentationという文書にspi.xfer2は4096バイトの上限があるが、spi.xfer3は上限がないという記述があります。

https://www.sigmdel.ca/michel/ha/rpi/dnld/draft_spidev_doc.pdf

これをxfer2の代わりに使ってみます。

```python
    address = 0x5E
    spi.xfer3([address] + data_array)
```
これでなんとか実行できました。
最後に、アドレス0x59に1を書き込んで初期化完了です。
```python
    spi.xfer3([0x59,0x01])
```

以上が初期化処理の全体の流れですが、気を付けるのはこの初期化は1回のみ実行されるようにする必要があることです（デートシート参照）。
これを踏まえると初期化の処理はif文でアドレス0x21を読み込んだ時に1になっているかどうかで処理を実行させるか判断させます。
```python
    if(spi.xfer2([0x80|0x21,0x00,0x00])[2]!=1):
```
以上から、初期化処理の全体は次のようになります。
```python:BMI270初期化処理
#初期化が終わっていなければ初期化実行
if(spi.xfer2([0x80|0x21,0x00,0x00])[2]!=1):
    #disable PWR_CONF
    spi.xfer2([0x7C,0x00])
    #最低450μs待つ
    time.sleep(0.001)
    #初期化開始
    spi.xfer2([0x59,0x00])
    # JSONファイルから読み込む
    with open('bmi270_init.json', 'r') as f:
        data = f.read()
    # 文字列を配列に変換
    data_array = ast.literal_eval(data)
    # アドレス0x5Eにバースト書き込み
    address = 0x5E
    spi.xfer3([address] + data_array)
    #初期化完了
    spi.xfer2([0x59,0x01])
```
なお、xfer2はすべてxfer3にしても問題ありません。

## データの読み出し
初期化ができたらあとは任意の取得したデータを読み出せばよいです。
以下はZ座標の加速度と角加速度データを読み出すコードです。
X座標、Y座標も読み出すバイトを変えて取得できます。

```python:データ読み出し処理
    #加速度Z座標を0x10から2バイト分読み出し
    accel_z_block = spi.xfer2([0x80|0x10,0x00,0x00,0x00])
    #0x11を8ビットシフトして上位ビットとし、下位ビットの0x10と論理和をとる
    accel_z_data = accel_z_block[2] | accel_z_block[3] << 8
    #データが負の場合、2の補数で符号を反転
    if(accel_z_data & 0x8000):
        accel_z_data = ((~accel_z_data & 0xFFFF) + 1)* -1
    #最大レンジが±8G（初期値）なので変換
    accel_z_value = 8 * 9.8 * accel_z_data / 0x7FFF
    print(accel_x_value,accel_y_value,accel_z_value)
    #角加速度Z座標を0x16から2バイト分読み出し
    gyro_z_block = spi.xfer2([0x80|0x16,0x00,0x00,0x00])
    #0x16を8ビットシフトして上位ビットとし、下位ビットの0x15と論理和をとる
    gyro_z_data = gyro_z_block[2] | gyro_z_block[3] << 8
    #データが負の場合、2の補数で符号を反転
    if(gyro_z_data & 0x8000):
        gyro_z_data = ((~gyro_z_data & 0xFFFF) + 1)* -1
    #最大レンジが±2000（初期値）なので変換
    gyro_z_value = 2000 * gyro_z_data / 0x7FFF
    print(gyro_z_value)
```

これでIMUのデータを取得することができました。
以上、SPIでAPIを使わずにデータを読み出すためのPythonを使った最低限の実装を記述しました。
これ以外にも加速度と角加速度の最大レンジ（精度）を変更したり、温度を読み出したりすることもできます。そのあたりは割愛しますので、必要に応じてデータシートを読んで実装するとよいでしょう。

## ソースコード、初期化ファイル
今回使用したpythonソースコードと初期化データはこちらにアップロードしました。ご参考まで。
https://github.com/exmachic104/BMI270