💡

Raspberry Pi で BNO055を動かしてみる(modeの切り替えとキャリブレーション方法)

に公開

はじめに

今回は Raspberry Pi で BNO055(9軸センサ)を使ったときに遭遇した
「Yaw(ヨー角)だけがドリフトしていく」問題と、
その原因である 動作モードの扱い・キャリブレーション処理の正しい方法
についてまとめます。

なぜ Yaw だけがドリフトするのか?

BNO055 を何も考えずにchatGPTで作ってもらったコードで起動したところ、
IMUPLUS(磁気なし) で動作してしまいました。

IMUPLUS は以下のような性質があります:

  • 加速度+ジャイロのみを使用
  • 地磁気(コンパス)を使わない
  • Yaw はジャイロの積分で求めるため、時間と共に必ずずれる

その結果、机に置いている間は静かですが、
少しゆすると Yaw だけがじわじわ回転していく という挙動になります。

Yaw を安定させるには NDOF モード(磁気・加速度・ジャイロ全部使用) が必須です。

Raspberry Pi でモードが反映されない?

setMode() が正しく機能していない罠

最初に作ったコードは、setMode() が CONFIG モードを経由しない実装になってしまっていました

BNO055 はデータシート上、

  • OPR_MODE を書き換えるときは 必ず CONFIG モードで行う
  • 書き換え後 最小 19~30ms の wait が必要

と定義されています。

正しいモード切り替え方法

そこで、BNO055 の仕様通り

  1. CONFIG モードへ移行
  2. 30ms 以上待機
  3. OPR_MODE を書き換え
  4. 30ms 以上待機

という処理を行う safeSetMode を作成しました。

    def safeSetMode(self, mode):
        self.setMode(BNO055.OPERATION_MODE_CONFIG)
        time.sleep(0.03)
        self.setMode(mode)
        time.sleep(0.03)

この SafeMode 経由でモード変更を行うことで、

  • MAGONLY
  • IMUPLUS
  • NDOF

いずれのモードも Raspberry Pi で安定して切り替えられるようになりました。

キャリブレーションについて

BNO055 を NDOF のようなフュージョンモードで使用する場合、
正確なキャリブレーションがとても重要です。
BNO055 は出荷時にもある程度のキャリブレーションは行われていますが、
地磁気環境・重力方向・温度・取り付け姿勢が変われば内部補正値は常に変化します。
そのため、初回の使用時やセンサーの取り付け環境が変わったときには必ずキャリブレーションを行う必要があります。
また BNO055 は特徴的で、キャリブレーションが完了(各ステータスが 3/3)しても、その後も裏で自動的に補正を続けます。

  • 起動直後 → 補正値がゼロで不安定になりがち
  • しばらく動かすと → 内部アルゴリズムが勝手に補正を進める
  • sys(システム)のキャリブ状態が 3→0 に戻ることもある(異常ではなく再調整)

という動作が仕様です。
そのため今回は、

  • calibrator.py で一度キャリブレーションを実行し、
  • そのときのオフセット値を JSON に保存し、
  • 起動時にその値を読み込んで「安定した初期状態」を作る

という方法を採用しています。
これはあくまで 起動直後の不安定な時間を短くするためであり、
その後の細かな補正は BNO055 が内部で自動的に進めてくれます。

キャリブレーション用スクリプト

BNO055 のキャリブレーションは、

  • GYRO(静止)
  • ACCEL(6方向)
  • MAG(8の字軌道)

を行い、SYS/GYRO/ACC/MAG がすべて 3/3 になると完了です。

特にポイントなのは、

  • キャリブレーションが完了した瞬間、オフセット値は自動的に内部レジスタへ書き込まれる
  • 補正値を読み込むときは、CONFIGモードに入れる

という点です。

キャリブレーション結果は 0x55〜0x6A の 22 バイトに保存されます。

ただし、電源を切ると値は消えるので、
このオフセット値を JSON として保存し、
次回起動時に CONFIG モード中に書き戻す運用にしました。
下記にキャリブレーション用のスクリプトを示します

import BNO055
import time
import json
import os

# キャリブレーションデータを保存するファイル名
CALIBRATION_FILE = 'bno055_calibration.json'


def get_bno_calibration_status(bno):
    """BNO055からキャリブレーションステータスを取得し、表示する"""
    # System, Gyro, Accel, Mag の順に 0〜3 の値を取得
    sys, gyro, acc, mag = bno.getCalibration()

    # 3が最高レベルのキャリブレーション完了を示す
    status_str = (
        f"SYS:{sys}/3 | GYRO:{gyro}/3 | ACCEL:{acc}/3 | MAG:{mag}/3"
    )
    return sys, gyro, acc, mag, status_str

def read_calibration_offsets(bno):
    """
    現在のオフセットレジスタ(0x55〜0x6A)を22バイト読み込んでリストで返す
    """
    # 今のモードを退避
    mode_back = bno._mode

    # CONFIG モードに一時的に切り替え
    bno.setMode(bno.OPERATION_MODE_CONFIG)
    time.sleep(0.03)  # 20〜30ms くらい待つ
    data = bno.readBytes(bno.ACCEL_OFFSET_X_LSB_ADDR, 22)
    # 元のモードに戻す
    bno.setMode(mode_back)
    time.sleep(0.03)
    return list(data)

def print_offset_diff(before, after):
    """
    キャリブ前後のオフセットを比較して表示する
    """
    print("\n=== キャリブレーション結果 (オフセット比較) ===")
    print(f"  バイト数: {len(before)}")

    print("\n  [0] 初期オフセット:")
    print(f"    {before}")

    print("\n  [1] キャリブ後オフセット:")
    print(f"    {after}")

    print("\n  [2] 変更があったバイトのみ差分表示:")
    for i, (b, a) in enumerate(zip(before, after)):
        if b != a:
            print(f"    index {i:2d}: {b:6d} -> {a:6d}")
    print("=== 比較終了 ===\n")

def save_calibration_data(calibration_data):
    """キャリブレーションデータをファイルに保存する"""
    print(f"-> キャリブレーションデータを {CALIBRATION_FILE} に保存中...")

    # BNO055のオフセットレジスタからデータを読み出す
    data = calibration_data

    # Pythonのリストに変換してJSONファイルに保存
    with open(CALIBRATION_FILE, 'w') as f:
        json.dump(list(data), f)

    print("-> 保存完了。")


def main():
    # BNO055インスタンスの作成 (NDOFモードで開始)
    bno = BNO055.BNO055()

    if not bno.begin(mode=bno.OPERATION_MODE_NDOF, calib=True):
    #if not bno.begin(mode=bno.OPERATION_MODE_MAGONLY):
        raise RuntimeError('BNO055の初期化に失敗しました')

    bno.start()

    print("=== 起動直後のオフセットを読み込みます ===")
    initial_offsets = read_calibration_offsets(bno)
    print(f"初期オフセット(raw 22バイト):\n  {initial_offsets}\n")

    print("--- BNO055 キャリブレーション開始 ---")
    print("目標: 全てのステータスを 3/3 にする (特に MAG)")
    print("GYROは機体を完全に停止、ACCELは6つの面(前後、上下、左右)を下にして数秒停止、MAGは八の字を描くようにすべての向きを変えることによりステータスがあがります")

    is_calibrated = False
    # --- 地磁気監視用の変数 (新規) ---
    mag_min = [float('inf'), float('inf'), float('inf')]
    mag_max = [float('-inf'), float('-inf'), float('-inf')]

    try:
        while not is_calibrated:
            # BNO055インスタンスの current_data から直接読み取る
            data = bno.current_data
            print(data)
            if data is not None and 'mag' in data:
                raw_mag = data['mag']
                #print(raw_mag)

                # 地磁気の最小値と最大値を更新
                for i in range(3):
                    mag_min[i] = min(mag_min[i], raw_mag[i])
                    mag_max[i] = max(mag_max[i], raw_mag[i])

                # 監視情報を表示
                mag_range_str = (
                    f" | Range X:{mag_max[0] - mag_min[0]:.0f}"
                    f" Y:{mag_max[1] - mag_min[1]:.0f}"
                    f" Z:{mag_max[2] - mag_min[2]:.0f}"
                )
            else:
                mag_range_str = ""
            # センサーの状態を取得し表示
            sys, gyro, acc, mag, status_str = get_bno_calibration_status(bno)
            print(sys, gyro, acc, mag)


            # コンソールに状態を表示
            print(f"Magのステータス: {mag_range_str}")
            print(f"現在のステータス: {status_str}")

            if sys == 3 and mag == 3 and acc == 3:  # 最重要: システムと地磁気が完了したら終了
                is_calibrated = True
                print("\n キャリブレーション完了!")

            # 地磁気センサーの校正方法のガイダンス
            if mag < 3:
                print("MAG (地磁気) が未完了です。機体を空中で『8の字』にゆっくり動かしてください。")

            time.sleep(0.5)

        final_offsets = read_calibration_offsets(bno)
        print_offset_diff(initial_offsets, final_offsets)
        # 完了後、キャリブレーションデータを保存
        save_calibration_data(final_offsets)

    except KeyboardInterrupt:
        print("\nキャリブレーションを中断します。")
    finally:
        bno.stop()


if __name__ == '__main__':
    main()

これを起動させて、センサーを静止させておくと、GYRO:3/3になります
次に、前後、上下、左右の6面を上に向けて静止させたり、くるくるいろんな面を回します
そうすると、SYSとACCELとMAGが3/3になります
少し時間がかかったり、突然SYSが0に戻ったりしてしまいます
どうしてもうまく進まなければ、再起動したりして試してみます

キャリブレーション比較の結果

キャリブレーション前後でレジスタを読み出して比較したところ、

Magのステータス:  | Range X:238 Y:223 Z:149
現在のステータス: SYS:3/3 | GYRO:3/3 | ACCEL:3/3 | MAG:3/3

 キャリブレーション完了!

=== キャリブレーション結果 (オフセット比較) ===
  バイト数: 22

  [0] 初期オフセット:
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 232, 3, 0, 0]

  [1] キャリブ後オフセット:
    [247, 255, 47, 0, 48, 0, 127, 0, 197, 255, 31, 0, 255, 255, 254, 255, 0, 0, 232, 3, 207, 7]

  [2] 変更があったバイトのみ差分表示:
    index  0:      0 ->    247
    index  1:      0 ->    255
    index  2:      0 ->     47
    index  4:      0 ->     48
    index  6:      0 ->    127
    index  8:      0 ->    197
    index  9:      0 ->    255
    index 10:      0 ->     31
    index 12:      0 ->    255
    index 13:      0 ->    255
    index 14:      0 ->    254
    index 15:      0 ->    255
    index 20:      0 ->    207
    index 21:      0 ->      7
=== 比較終了 ===

-> キャリブレーションデータを bno055_calibration.json に保存中...
-> 保存完了。

  • ACCEL
  • MAG
  • GYRO

のオフセット値が大きく更新されており、
BNO055 内でキャリブレーション処理が正しく行われていることを確認できました。

特に MAG の値は ±数百 LSB 程度の変化があり、
地磁気センサの初期誤差が大きく補正されています。

  • MAG の “範囲(max-min)” が大きいほど、現場の磁場環境を十分に走査できている
  • 実測では Range X:238, Y:190, Z:149 → 妥当な磁場振れ幅

という結果になりました。

BNO055の動作確認

それではキャリブレーションも完了したので、動作確認をしてみます。
下記にスクリプト全文を示します。

import smbus
import time
import struct
import threading
import json

class BNO055:
    BNO055_ADDRESS_A = 0x28
    BNO055_ADDRESS_B = 0x29
    BNO055_ID = 0xA0

    # Power mode settings
    POWER_MODE_NORMAL = 0X00
    POWER_MODE_LOWPOWER = 0X01
    POWER_MODE_SUSPEND = 0X02

    # Operation mode settings
    OPERATION_MODE_CONFIG = 0X00
    OPERATION_MODE_ACCONLY = 0X01
    OPERATION_MODE_MAGONLY = 0X02
    OPERATION_MODE_GYRONLY = 0X03
    OPERATION_MODE_ACCMAG = 0X04
    OPERATION_MODE_ACCGYRO = 0X05
    OPERATION_MODE_MAGGYRO = 0X06
    OPERATION_MODE_AMG = 0X07
    OPERATION_MODE_IMUPLUS = 0X08
    OPERATION_MODE_COMPASS = 0X09
    OPERATION_MODE_M4G = 0X0A
    OPERATION_MODE_NDOF_FMC_OFF = 0X0B
    OPERATION_MODE_NDOF = 0X0C

    # Output vector type
    VECTOR_ACCELEROMETER = 0x08
    VECTOR_MAGNETOMETER = 0x0E
    VECTOR_GYROSCOPE = 0x14
    VECTOR_EULER = 0x1A
    VECTOR_LINEARACCEL = 0x28
    VECTOR_GRAVITY = 0x2E

    # REGISTER DEFINITION START
    BNO055_PAGE_ID_ADDR = 0X07

    BNO055_CHIP_ID_ADDR = 0x00
    BNO055_ACCEL_REV_ID_ADDR = 0x01
    BNO055_MAG_REV_ID_ADDR = 0x02
    BNO055_GYRO_REV_ID_ADDR = 0x03
    BNO055_SW_REV_ID_LSB_ADDR = 0x04
    BNO055_SW_REV_ID_MSB_ADDR = 0x05
    BNO055_BL_REV_ID_ADDR = 0X06

    # Accel data register
    BNO055_ACCEL_DATA_X_LSB_ADDR = 0X08
    BNO055_ACCEL_DATA_X_MSB_ADDR = 0X09
    BNO055_ACCEL_DATA_Y_LSB_ADDR = 0X0A
    BNO055_ACCEL_DATA_Y_MSB_ADDR = 0X0B
    BNO055_ACCEL_DATA_Z_LSB_ADDR = 0X0C
    BNO055_ACCEL_DATA_Z_MSB_ADDR = 0X0D

    # Mag data register
    BNO055_MAG_DATA_X_LSB_ADDR = 0X0E
    BNO055_MAG_DATA_X_MSB_ADDR = 0X0F
    BNO055_MAG_DATA_Y_LSB_ADDR = 0X10
    BNO055_MAG_DATA_Y_MSB_ADDR = 0X11
    BNO055_MAG_DATA_Z_LSB_ADDR = 0X12
    BNO055_MAG_DATA_Z_MSB_ADDR = 0X13

    # Gyro data registers
    BNO055_GYRO_DATA_X_LSB_ADDR = 0X14
    BNO055_GYRO_DATA_X_MSB_ADDR = 0X15
    BNO055_GYRO_DATA_Y_LSB_ADDR = 0X16
    BNO055_GYRO_DATA_Y_MSB_ADDR = 0X17
    BNO055_GYRO_DATA_Z_LSB_ADDR = 0X18
    BNO055_GYRO_DATA_Z_MSB_ADDR = 0X19

    # Euler data registers
    BNO055_EULER_H_LSB_ADDR = 0X1A
    BNO055_EULER_H_MSB_ADDR = 0X1B
    BNO055_EULER_R_LSB_ADDR = 0X1C
    BNO055_EULER_R_MSB_ADDR = 0X1D
    BNO055_EULER_P_LSB_ADDR = 0X1E
    BNO055_EULER_P_MSB_ADDR = 0X1F

    # Quaternion data registers
    BNO055_QUATERNION_DATA_W_LSB_ADDR = 0X20
    BNO055_QUATERNION_DATA_W_MSB_ADDR = 0X21
    BNO055_QUATERNION_DATA_X_LSB_ADDR = 0X22
    BNO055_QUATERNION_DATA_X_MSB_ADDR = 0X23
    BNO055_QUATERNION_DATA_Y_LSB_ADDR = 0X24
    BNO055_QUATERNION_DATA_Y_MSB_ADDR = 0X25
    BNO055_QUATERNION_DATA_Z_LSB_ADDR = 0X26
    BNO055_QUATERNION_DATA_Z_MSB_ADDR = 0X27

    # Linear acceleration data registers
    BNO055_LINEAR_ACCEL_DATA_X_LSB_ADDR = 0X28
    BNO055_LINEAR_ACCEL_DATA_X_MSB_ADDR = 0X29
    BNO055_LINEAR_ACCEL_DATA_Y_LSB_ADDR = 0X2A
    BNO055_LINEAR_ACCEL_DATA_Y_MSB_ADDR = 0X2B
    BNO055_LINEAR_ACCEL_DATA_Z_LSB_ADDR = 0X2C
    BNO055_LINEAR_ACCEL_DATA_Z_MSB_ADDR = 0X2D

    # Gravity data registers
    BNO055_GRAVITY_DATA_X_LSB_ADDR = 0X2E
    BNO055_GRAVITY_DATA_X_MSB_ADDR = 0X2F
    BNO055_GRAVITY_DATA_Y_LSB_ADDR = 0X30
    BNO055_GRAVITY_DATA_Y_MSB_ADDR = 0X31
    BNO055_GRAVITY_DATA_Z_LSB_ADDR = 0X32
    BNO055_GRAVITY_DATA_Z_MSB_ADDR = 0X33

    # Temperature data register
    BNO055_TEMP_ADDR = 0X34

    # Status registers
    BNO055_CALIB_STAT_ADDR = 0X35
    BNO055_SELFTEST_RESULT_ADDR = 0X36
    BNO055_INTR_STAT_ADDR = 0X37

    BNO055_SYS_CLK_STAT_ADDR = 0X38
    BNO055_SYS_STAT_ADDR = 0X39
    BNO055_SYS_ERR_ADDR = 0X3A

    # Unit selection register
    BNO055_UNIT_SEL_ADDR = 0X3B
    BNO055_DATA_SELECT_ADDR = 0X3C

    # Mode registers
    BNO055_OPR_MODE_ADDR = 0X3D
    BNO055_PWR_MODE_ADDR = 0X3E

    BNO055_SYS_TRIGGER_ADDR = 0X3F
    BNO055_TEMP_SOURCE_ADDR = 0X40

    # Axis remap registers
    BNO055_AXIS_MAP_CONFIG_ADDR = 0X41
    BNO055_AXIS_MAP_SIGN_ADDR = 0X42

    # SIC registers
    BNO055_SIC_MATRIX_0_LSB_ADDR = 0X43
    BNO055_SIC_MATRIX_0_MSB_ADDR = 0X44
    BNO055_SIC_MATRIX_1_LSB_ADDR = 0X45
    BNO055_SIC_MATRIX_1_MSB_ADDR = 0X46
    BNO055_SIC_MATRIX_2_LSB_ADDR = 0X47
    BNO055_SIC_MATRIX_2_MSB_ADDR = 0X48
    BNO055_SIC_MATRIX_3_LSB_ADDR = 0X49
    BNO055_SIC_MATRIX_3_MSB_ADDR = 0X4A
    BNO055_SIC_MATRIX_4_LSB_ADDR = 0X4B
    BNO055_SIC_MATRIX_4_MSB_ADDR = 0X4C
    BNO055_SIC_MATRIX_5_LSB_ADDR = 0X4D
    BNO055_SIC_MATRIX_5_MSB_ADDR = 0X4E
    BNO055_SIC_MATRIX_6_LSB_ADDR = 0X4F
    BNO055_SIC_MATRIX_6_MSB_ADDR = 0X50
    BNO055_SIC_MATRIX_7_LSB_ADDR = 0X51
    BNO055_SIC_MATRIX_7_MSB_ADDR = 0X52
    BNO055_SIC_MATRIX_8_LSB_ADDR = 0X53
    BNO055_SIC_MATRIX_8_MSB_ADDR = 0X54

    # Accelerometer Offset registers
    ACCEL_OFFSET_X_LSB_ADDR = 0X55
    ACCEL_OFFSET_X_MSB_ADDR = 0X56
    ACCEL_OFFSET_Y_LSB_ADDR = 0X57
    ACCEL_OFFSET_Y_MSB_ADDR = 0X58
    ACCEL_OFFSET_Z_LSB_ADDR = 0X59
    ACCEL_OFFSET_Z_MSB_ADDR = 0X5A

    # Magnetometer Offset registers
    MAG_OFFSET_X_LSB_ADDR = 0X5B
    MAG_OFFSET_X_MSB_ADDR = 0X5C
    MAG_OFFSET_Y_LSB_ADDR = 0X5D
    MAG_OFFSET_Y_MSB_ADDR = 0X5E
    MAG_OFFSET_Z_LSB_ADDR = 0X5F
    MAG_OFFSET_Z_MSB_ADDR = 0X60

    # Gyroscope Offset registers
    GYRO_OFFSET_X_LSB_ADDR = 0X61
    GYRO_OFFSET_X_MSB_ADDR = 0X62
    GYRO_OFFSET_Y_LSB_ADDR = 0X63
    GYRO_OFFSET_Y_MSB_ADDR = 0X64
    GYRO_OFFSET_Z_LSB_ADDR = 0X65
    GYRO_OFFSET_Z_MSB_ADDR = 0X66

    # Radius registers
    ACCEL_RADIUS_LSB_ADDR = 0X67
    ACCEL_RADIUS_MSB_ADDR = 0X68
    MAG_RADIUS_LSB_ADDR = 0X69
    MAG_RADIUS_MSB_ADDR = 0X6A

    # REGISTER DEFINITION END

    _AXIS_MAP = {
        'X': 0x00, 'Y': 0x01, 'Z': 0x02,
        '-X': 0x04, '-Y': 0x08, '-Z': 0x10  # 符号付きのビットフラグを仮定義
    }
    # 軸マッピング値 (レジスタ値)
    _AXIS_MAP_CONFIG = {
        'X': 0x00, 'Y': 0x01, 'Z': 0x02
    }
    # 軸符号反転ビット (レジスタ値)
    _AXIS_MAP_SIGN = {
        'X': 0x00, '-X': 0x01,
        'Y': 0x00, '-Y': 0x02,
        'Z': 0x00, '-Z': 0x04
    }

    def __init__(self, address=0x28, mount_axis_x='X', mount_axis_y='Y'):
        self._address = address
        self._bus = smbus.SMBus(1)
        self.running = False
        self.current_data = None
        self._mount_x = mount_axis_x
        self._mount_y = mount_axis_y

    def start(self):
        if not hasattr(self, "_mode"):
            raise RuntimeError("start() を呼ぶ前に begin() を呼んでください")
        self.running = True
        # threading.Thread(target=self.run).start()
        self.thread = threading.Thread(target=self.run)
        self.thread.daemon = True  # この行を追加
        self.thread.start()

    def run(self):
        while self.running:
            self.current_data = self.get_all_data()
            time.sleep(0.01)

    def stop(self):
        self.running = False
        self.thread.join()  # スレッドが終了するまで待機

    def load_calibration_data(self, data):
        """保存されたキャリブレーションデータをBNO055のレジスタに書き込む"""
        # 0x55 (ACCEL_OFFSET_X_LSB_ADDR) から 22バイトのデータを書き込む
        self.writeBytes(BNO055.ACCEL_OFFSET_X_LSB_ADDR, data)
        print("BNO055: キャリブレーションデータがロードされました。")

    def get_all_data(self):
        euler = self.getVector(BNO055.VECTOR_EULER)
        accel = self.getVector(BNO055.VECTOR_ACCELEROMETER)
        gyro = self.getVector(BNO055.VECTOR_GYROSCOPE)
        mag = self.getVector(BNO055.VECTOR_MAGNETOMETER)
        quat = self.getQuat()
        return {'euler': euler, 'accel': accel, 'gyro': gyro, 'mag': mag, 'quat': quat}

    def _get_remap_config(self):
        """インスタンス設定 (_mount_x, _mount_y) からレジスタ値を計算する"""

        # マッピング設定 (X, Y, Z の順番を定義)
        # BNO055のAXIS_MAP_CONFIGは、X-Y-Zの順に2ビットずつ、合計6ビットで定義されます。
        # Z軸は残りの軸に自動的に割り当てられます。

        # 符号を除いた軸名を取得
        x_axis_name = self._mount_x.replace('-', '')
        y_axis_name = self._mount_y.replace('-', '')

        # Z軸を決定 (X, Yが使用されていない軸)
        used_axes = {x_axis_name, y_axis_name}
        all_axes = {'X', 'Y', 'Z'}
        z_axis_name = list(all_axes - used_axes)[0]

        # マッピング値を計算 (Z << 4 | Y << 2 | X)
        map_config = (
                self._AXIS_MAP_CONFIG[z_axis_name] << 4 |
                self._AXIS_MAP_CONFIG[y_axis_name] << 2 |
                self._AXIS_MAP_CONFIG[x_axis_name]
        )

        # 符号設定 (符号ビットをOR演算で組み合わせる)
        map_sign = 0
        if self._mount_x.startswith('-'): map_sign |= self._AXIS_MAP_SIGN['-X']
        if self._mount_y.startswith('-'): map_sign |= self._AXIS_MAP_SIGN['-Y']
        if self._mount_x.startswith('Z'):  # Z軸の符号反転が必要な場合も考慮
            if self._mount_x.startswith('-'): map_sign |= self._AXIS_MAP_SIGN['-Z']
        # (Z軸の符号は、X, Yで使われなかった軸の符号として計算する必要があるため複雑ですが、
        #  ここではX, Y軸の符号反転のみを扱います。)

        return (map_config, map_sign)

    def begin(self, mode=None, cal_file=None, calib=False):
        if mode is None: mode = BNO055.OPERATION_MODE_IMUPLUS
        self._mode = mode
        if cal_file is None: cal_file = 'bno055_calibration.json'
        # Open I2C bus
        self._bus = smbus.SMBus(1)

        # Make sure we have the right device
        if self.readBytes(BNO055.BNO055_CHIP_ID_ADDR)[0] != BNO055.BNO055_ID:
            time.sleep(1)  # Wait for the device to boot up
            if self.readBytes(BNO055.BNO055_CHIP_ID_ADDR)[0] != BNO055.BNO055_ID:
                return False

        # Switch to config mode
        self.setMode(BNO055.OPERATION_MODE_CONFIG)
        time.sleep(0.03)

        # Trigger a reset and wait for the device to boot up again
        self.writeBytes(BNO055.BNO055_SYS_TRIGGER_ADDR, [0x20])
        time.sleep(1)
        while self.readBytes(BNO055.BNO055_CHIP_ID_ADDR)[0] != BNO055.BNO055_ID:
            time.sleep(0.01)
        time.sleep(0.05)

        # Set to normal power mode
        self.writeBytes(BNO055.BNO055_PWR_MODE_ADDR, [BNO055.POWER_MODE_NORMAL])
        time.sleep(0.01)

        self.writeBytes(BNO055.BNO055_PAGE_ID_ADDR, [0])
        self.writeBytes(BNO055.BNO055_SYS_TRIGGER_ADDR, [0])
        time.sleep(0.01)

        # キャリブレーションモード以外での起動時に、補正値を読み込む
        if not calib:
            try:
                # ファイルの存在を確認し、読み込み
                with open(cal_file, 'r') as f:
                    cal_data = json.load(f)

                # 外部に定義されたメソッドを使ってデータを書き込む
                self.load_calibration_data(cal_data)

                print(f"[BNO055] キャリブレーションデータ '{cal_file}' を適用しました。")

            except FileNotFoundError:
                print(
                    f"[BNO055] Warning: キャリブレーションファイル '{cal_file}' が見つかりません。補正なしで起動します。")
            except Exception as e:
                # ファイル形式の破損など
                print(f"[BNO055] Error: キャリブレーションデータのロード中にエラーが発生しました: {e}")
        # --- BNO055 Axis Remap 設定の適用 ---
        # 1. Axis Remap の設定値を計算
        map_config, map_sign = self._get_remap_config()
        # 2. レジスタに書き込む
        self.writeBytes(BNO055.BNO055_AXIS_MAP_CONFIG_ADDR, [map_config])
        self.writeBytes(BNO055.BNO055_AXIS_MAP_SIGN_ADDR, [map_sign])
        time.sleep(0.01)
        # Set the requested mode
        self.setMode(mode)
        time.sleep(0.02)

        return True

    def setMode(self, mode):
        self._mode = mode
        self.writeBytes(BNO055.BNO055_OPR_MODE_ADDR, [self._mode])
        time.sleep(0.03)


    def getMode(self):
        """現在のOPR_MODEを取得(0x00〜0x0C)"""
        data = self.readBytes(BNO055.BNO055_OPR_MODE_ADDR, 1)[0]
        time.sleep(0.02)
        return data

    def safeSetMode(self, mode):
        self.setMode(BNO055.OPERATION_MODE_CONFIG)
        time.sleep(0.03)
        self.setMode(mode)
        time.sleep(0.03)


    def setExternalCrystalUse(self, useExternalCrystal=True):
        prevMode = self._mode
        self.setMode(BNO055.OPERATION_MODE_CONFIG)
        time.sleep(0.025)
        self.writeBytes(BNO055.BNO055_PAGE_ID_ADDR, [0])
        self.writeBytes(BNO055.BNO055_SYS_TRIGGER_ADDR, [0x80] if useExternalCrystal else [0])
        time.sleep(0.01)
        self.setMode(prevMode)
        time.sleep(0.02)

    def getSystemStatus(self):
        self.writeBytes(BNO055.BNO055_PAGE_ID_ADDR, [0])
        (sys_stat, sys_err) = self.readBytes(BNO055.BNO055_SYS_STAT_ADDR, 2)
        self_test = self.readBytes(BNO055.BNO055_SELFTEST_RESULT_ADDR)[0]
        return (sys_stat, self_test, sys_err)

    def getRevInfo(self):
        (accel_rev, mag_rev, gyro_rev) = self.readBytes(BNO055.BNO055_ACCEL_REV_ID_ADDR, 3)
        sw_rev = self.readBytes(BNO055.BNO055_SW_REV_ID_LSB_ADDR, 2)
        sw_rev = sw_rev[0] | sw_rev[1] << 8
        bl_rev = self.readBytes(BNO055.BNO055_BL_REV_ID_ADDR)[0]
        return (accel_rev, mag_rev, gyro_rev, sw_rev, bl_rev)

    def getCalibration(self):
        calData = self.readBytes(BNO055.BNO055_CALIB_STAT_ADDR)[0]
        return (calData >> 6 & 0x03, calData >> 4 & 0x03, calData >> 2 & 0x03, calData & 0x03)

    def getTemp(self):
        return self.readBytes(BNO055.BNO055_TEMP_ADDR)[0]

    def getVector(self, vectorType):
        buf = self.readBytes(vectorType, 6)
        xyz = struct.unpack('hhh', struct.pack('BBBBBB', buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]))
        if vectorType == BNO055.VECTOR_MAGNETOMETER:
            scalingFactor = 16.0
        elif vectorType == BNO055.VECTOR_GYROSCOPE:
            scalingFactor = 900.0
        elif vectorType == BNO055.VECTOR_EULER:
            scalingFactor = 16.0
        elif vectorType == BNO055.VECTOR_GRAVITY:
            scalingFactor = 100.0
        else:
            scalingFactor = 1.0
        return tuple([i / scalingFactor for i in xyz])

    def getQuat(self):
        buf = self.readBytes(BNO055.BNO055_QUATERNION_DATA_W_LSB_ADDR, 8)
        wxyz = struct.unpack('hhhh',
                             struct.pack('BBBBBBBB', buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7]))
        return tuple([i * (1.0 / (1 << 14)) for i in wxyz])

    def readBytes(self, register, numBytes=1):
        return self._bus.read_i2c_block_data(self._address, register, numBytes)

    def writeBytes(self, register, byteVals):
        return self._bus.write_i2c_block_data(self._address, register, byteVals)


# 使用例
def demo():
    bno = BNO055()
    # beginでモードを指定して起動してもいいし、あとからsafeSetModeで切り替えてもよい
    if not bno.begin():
    # if not bno.begin(mode=bno.OPERATION_MODE_NDOF):
    # if not bno.begin(mode=bno.OPERATION_MODE_MAGONLY):
        raise RuntimeError('BNO055の初期化に失敗しました')

    bno.start()
    bno.safeSetMode(bno.OPERATION_MODE_NDOF)
    # bno.safeSetMode(bno.OPERATION_MODE_MAGONLY)

    try:
        while True:
            data = bno.current_data
            mode = bno.getMode()
            if data is None:
                print("No data yet...")
                time.sleep(0.5)
                continue

            e = data['euler']  # (heading, roll, pitch)
            a = data['accel']  # (ax, ay, az)
            g = data['gyro']   # (gx, gy, gz)
            m = data['mag']    # (mx, my, mz)

            # 小数点2桁フォーマット
            e_str = f"{e[0]:6.2f}, {e[1]:6.2f}, {e[2]:6.2f}"
            a_str = f"{a[0]:6.2f}, {a[1]:6.2f}, {a[2]:6.2f}"
            g_str = f"{g[0]:6.2f}, {g[1]:6.2f}, {g[2]:6.2f}"
            m_str = f"{m[0]:6.2f}, {m[1]:6.2f}, {m[2]:6.2f}"

            print(
                f"Euler(Y,R,P)= {e_str} | "
                f"Acc= {a_str} | "
                f"Gyro= {g_str} | "
                f"Mag= {m_str} | "
                f"Mode=0x{mode:02X}"
            )
            time.sleep(0.5)

    except KeyboardInterrupt:
        print("Stop demo.")
    finally:
        bno.stop()

if __name__ == "__main__":
    demo()

補足

本当はこのクラスでリマップという機能を使ってみたかったのです
センサーの向きを逆さまで取り付けたりしても、リマップの機能で補正できると思っていました。
しかし、この機能を使ったところ、よくわからないのですが、NDOFで動かしているはずなのに、IMUPLUSっぽい挙動になりました。
原因はよくわかっていませんが、リマップの機能は使わず、出てきた値に補正をかけてあげるほうがいいと思います。

最後に(まとめ)

今回 Raspberry Pi で BNO055 を扱ってわかったことを整理すると:

  • Yaw がドリフトするのは IMUPLUS(地磁気なし)で動いていたため
  • 正しい NDOF へ切り替えるには CONFIG モード経由が必須
  • NDOFを使う場合、キャリブレーション推奨
  • キャリブレーションは完了時に自動でオフセットが保存される
  • キャリブレーションの補正データを読み込んだり書き込んだりするときはCONFIGモード経由が必須
  • 電源断で消えるため JSON に保存して復元するのが実運用向け
  • リマップの機能はあるみたいだが、使わない方がよい

これで Raspberry Pi でも BNO055 のフュージョン機能を正しく・安定して使える環境が整いました。

Discussion