BNO055をRaspberry Pi Picoで使う方法!9軸IMUで姿勢・角度を簡単取得

  • URLをコピーしました!

皆さんこんにちは。yossyです。

電子工作で「角度を知りたい」「姿勢を安定して取得したい」と思ったことはありませんか?
加速度センサーやジャイロセンサーを個別に扱うと、補正や計算が意外と大変ですよね。

そこで今回は、9軸センサー+姿勢演算まで全部入りの便利IC
BNO055 の使い方を解説します。

初心者の方でも簡単に試せるように、配線やコード、使い方まで丁寧に解説します!

目次

BNO055とは?

BNO055 は、Bosch社が開発した 9軸IMU(慣性計測ユニット) です。

  • 加速度センサー(3軸)
  • ジャイロセンサー(3軸)
  • 地磁気センサー(3軸)

これらに加えて、内部で姿勢推定(センサーフュージョン)まで行ってくれるのが最大の特徴です。

何ができる?:角度・向き・姿勢がわかる!

BNO055は

  • 📐 オイラー角(roll / pitch / yaw)
  • 🧭 方位角(コンパス)
  • 🧊 クォータニオン(姿勢表現)
  • 🚀 加速度・角速度・磁気データ

といったデータをそのまま入手することができます。これらの値を使うことで、

  • 姿勢を検知するロボット
  • 傾きで操作するコントローラー
  • 電子コンパス
  • 姿勢・運動データの可視化

のような装置を作ることができます。

ICにマイコンが搭載されている

通常、IMUで姿勢を出すには以下が必要です。

  • ジャイロの積分
  • 加速度による補正
  • 磁気センサーによる方位補正
  • フィルタ(Complementary / Kalman など)

しかしBNO055では、

  • これらを IC内部のマイコンが自動処理
  • I2Cで 「姿勢の結果」だけ取得

という構造になっています。

そのため、
ラズピコ側は値を読むだけでOKです。

yossy

難しい計算プログラムが不要になりますね!

Raspberry Pi PicoでBNO055を使う方法

ここからは、ラズピコでBNO055を使う方法について解説します!

必要なもの

必要なものはこちら。

  • Raspberry Pi Pico
  • BNO055モジュール
  • 抵抗(1KΩ~4.7KΩ 2本)
  • ブレッドボード
  • ジャンパーワイヤ
  • MicroPython環境(Thonnyなど)

Amazonにはあまり売られていないので、秋月電子で入手することをおすすめします!

商品リンク
BNO055使用 9軸センサーフュージョンモジュールキット

秋月電子のモジュールはプルアップ必須!!!

このセンサの通信方式はI2Cです。

そのため、信号線はプルアップ(3.3Vと抵抗で接続)されている必要があります。しかし、秋月電子で販売されているモジュールにはこのプルアップ抵抗が搭載されていません。

正しくプルアップ抵抗を挿入しないと、動作が不安定になるのでご注意ください。

yossy

商品のQ&Aに書かれているので確認しましょう!

配線

配線はこのようになります。

ラズピコのピン接続先説明
3V3VIN / VCC電源
GNDGNDグランド
GP4SDAI2Cデータ
GP5SCLI2Cクロック

秋月電子のモジュールではSDAとSCLの線を、1KΩ~4.7KΩの抵抗を使って3V3にも接続してください。

ライブラリのインストール

ライブラリのページ(https://github.com/micropython-IMU/micropython-bno055)にアクセスして、

  • bno055_base.py
  • bno055.py

この2つをラズピコに保存します。

プログラム

BNO055から9軸+各種姿勢データとキャリブレーションデータを取得して表示するプログラムはこちら。

import time
from machine import I2C, Pin
from bno055 import BNO055

# I2C初期化
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)

imu = BNO055(i2c)

while True:
    # --- 姿勢データ ---
    euler = imu.euler()          # (heading, roll, pitch)
    quat  = imu.quaternion()     # (w, x, y, z)

    # --- 9軸データ ---
    accel = imu.accel()          # 加速度 (x, y, z) [m/s^2]
    gyro  = imu.gyro()           # 角速度 (x, y, z) [deg/s]
    mag   = imu.mag()            # 地磁気 (x, y, z)

    # --- キャリブレーション状態 ---
    calib = imu.cal_status()
    # (system, gyro, accel, mag) 各0〜3

    if euler and accel and gyro and mag:
        print("Euler   :", euler)
        print("Quat    :", quat)
        print("Accel   :", accel)
        print("Gyro    :", gyro)
        print("Mag     :", mag)
        print("Calib   :", calib)
        print("--------")

    time.sleep(0.5)

プログラムの解説

① 9軸データの取得

  • imu.accel()
    → 加速度(X/Y/Z)
  • imu.gyro()
    → 角速度(回転の速さ)
  • imu.mag()
    → 地磁気(方位に使用)

これらを個別に取れるため、
「生データ解析」や「自作フィルタ」にも使えます。


② 各種角度データ

  • imu.euler()
    → 人が直感的に分かりやすい角度
    (表示・UI向け)
  • imu.quaternion()
    → 数学的に安定した姿勢表現
    (制御・演算向け)

なので、

  • 表示・ログ用途 → オイラー角
  • 制御・演算用途 → クォータニオン

という使い分けがおすすめです。


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

system, gyro, accel, mag = imu.cal_status()

各値は 0〜3 で、

  • 0:未キャリブレーション
  • 3:完全にキャリブレーション済み

を表します。

キャリブレーションが不十分だと、

  • 角度がフラフラする
  • 方位がズレる

といった症状が出ます。なので、

  • 8の字に回す(地磁気)
  • 6方向に数秒置く(加速度)
  • 金属・磁石の近くを避ける

という方法でキャリブレーションしましょう!


実際に使ってみた

プログラムを動作させると、このようにログが表示されます。

yossy

静止させると、オイラー角とクォータニオンの値も止まりますね!

また、クオータニオンを使ってPCに表示された立方体を動かしてみました。

yossy

ChatGPTが作ってくれたプログラムを載せておきます!

Pico側

import time
from machine import I2C
from bno055 import BNO055

i2c = I2C(0)
imu = BNO055(i2c)

while True:
    q = imu.quaternion()  # (w, x, y, z)
    if q:
        w, x, y, z = q
        print("{:.6f},{:.6f},{:.6f},{:.6f}".format(w, x, y, z))
    time.sleep_ms(20)
    

PC側

import re
import time
import threading
from dataclasses import dataclass, field

import numpy as np
import serial


from PyQt5 import QtWidgets, QtCore
import pyqtgraph as pg
import pyqtgraph.opengl as gl


# ===== 設定 =====
PORT = "COM11" 
BAUD = 115200

# 描画更新(fps)
UI_HZ = 60

# クォータニオン平滑化(0〜1、大きいほど追従が速い)
SMOOTH = 0.35

STATIC_THRESH_DEG = 0.3   # ← 0.2〜0.5 くらいで調整


# パース(数値を4つ拾う)
num_re = re.compile(r"[-+]?\d+(?:\.\d+)?")

def parse_four_floats(line: str):
    vals = num_re.findall(line)
    if len(vals) < 4:
        return None
    return float(vals[0]), float(vals[1]), float(vals[2]), float(vals[3])


def quat_normalize(q):
    q = np.array(q, dtype=float)
    n = np.linalg.norm(q)
    if n == 0:
        return np.array([1.0, 0.0, 0.0, 0.0], dtype=float)
    return q / n


def quat_fix_sign(prev_q, q):
    # q と -q は同じ姿勢。符号が反転すると「切り替わった」感じが出るので補正。
    if prev_q is None:
        return q
    if float(np.dot(prev_q, q)) < 0.0:
        return -q
    return q


def quat_nlerp(q_prev, q_new, alpha):
    # 軽い平滑化(SLERPより簡単で十分)
    q = (1.0 - alpha) * q_prev + alpha * q_new
    return quat_normalize(q)

def quat_angle_diff_deg(q1, q2):
    # q1, q2 は正規化済み
    dot = np.clip(np.dot(q1, q2), -1.0, 1.0)
    # 回転角 = 2 * acos(|dot|)
    return np.degrees(2.0 * np.arccos(abs(dot)))


def quat_to_rotmat(q):
    # q = [w, x, y, z]
    w, x, y, z = q
    return np.array([
        [1 - 2*(y*y + z*z),     2*(x*y - z*w),     2*(x*z + y*w)],
        [    2*(x*y + z*w), 1 - 2*(x*x + z*z),     2*(y*z - x*w)],
        [    2*(x*z - y*w),     2*(y*z + x*w), 1 - 2*(x*x + y*y)],
    ], dtype=float)


@dataclass
class Shared:
    q: np.ndarray = field(default_factory=lambda: np.array([1.0, 0.0, 0.0, 0.0], dtype=float))
    t: float = 0.0


shared = Shared()
lock = threading.Lock()
stop_flag = False


def serial_thread():
    global stop_flag
    try:
        ser = serial.Serial(PORT, BAUD, timeout=0.1)
    except Exception as e:
        print(f"[ERR] シリアルを開けません: {e}")
        stop_flag = True
        return

    time.sleep(0.5)
    ser.reset_input_buffer()  # 溜まってる古いデータを捨てる

    prev_q = None

    while not stop_flag:
        try:
            line = ser.readline()
            if not line:
                continue
            s = line.decode("utf-8", errors="ignore").strip()
            parsed = parse_four_floats(s)
            if not parsed:
                continue

            q = quat_normalize(parsed)          # w,x,y,z
            q = quat_fix_sign(prev_q, q)
            prev_q = q

            with lock:
                shared.q = q
                shared.t = time.time()

        except Exception:
            # ゴミ行/一時的エラーは無視して継続
            continue

    try:
        ser.close()
    except Exception:
        pass


class Viewer(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Pico BNO055 Quaternion Viewer")

        layout = QtWidgets.QVBoxLayout(self)

        self.view = gl.GLViewWidget()
        self.view.setCameraPosition(distance=4)
        layout.addWidget(self.view)

        # ワールド座標軸
        self.view.addItem(gl.GLAxisItem(size=pg.Vector(1.5, 1.5, 1.5)))

        # 立方体(線で描く)
        self.verts0, self.edges = self.make_cube(size=1.5)
        self.lines = []
        for a, b in self.edges:
            pts = np.vstack([self.verts0[a], self.verts0[b]])
            item = gl.GLLinePlotItem(pos=pts, width=2, antialias=True, mode='lines')
            self.view.addItem(item)
            self.lines.append(item)

        self.info = QtWidgets.QLabel("q = (w,x,y,z)")
        layout.addWidget(self.info)

        self.q_state = np.array([1.0, 0.0, 0.0, 0.0], dtype=float)

        self.timer = QtCore.QTimer(self)
        self.timer.timeout.connect(self.tick)
        self.timer.start(int(1000 / UI_HZ))

    def make_cube(self, size=1.0):
        s = size / 2.0
        V = np.array([
            [-s, -s, -s],
            [ s, -s, -s],
            [ s,  s, -s],
            [-s,  s, -s],
            [-s, -s,  s],
            [ s, -s,  s],
            [ s,  s,  s],
            [-s,  s,  s],
        ], dtype=float)
        edges = [
            (0, 1), (1, 2), (2, 3), (3, 0),
            (4, 5), (5, 6), (6, 7), (7, 4),
            (0, 4), (1, 5), (2, 6), (3, 7),
        ]
        return V, edges

    def tick(self):
        with lock:
            q_latest = shared.q.copy()
            t_latest = shared.t

        # 符号補正
        q_latest = quat_fix_sign(self.q_state, q_latest)

        # ★ 静止判定
        angle_diff = quat_angle_diff_deg(self.q_state, q_latest)

        if angle_diff < STATIC_THRESH_DEG:
            # 微振動 → 完全に止める
            q_use = self.q_state
        else:
            # 動いている → 平滑化して追従
            q_use = quat_nlerp(self.q_state, q_latest, SMOOTH)

        self.q_state = q_use

        R = quat_to_rotmat(self.q_state)
        V = (R @ self.verts0.T).T

        for i, (a, b) in enumerate(self.edges):
            pts = np.vstack([V[a], V[b]])
            self.lines[i].setData(pos=pts)

        age_ms = (time.time() - t_latest) * 1000.0 if t_latest else 9999.0
        w, x, y, z = self.q_state
        self.info.setText(
            f"q=(w={w:+.4f}, x={x:+.4f}, y={y:+.4f}, z={z:+.4f}) "
            f"Δθ={angle_diff:.3f}° age={age_ms:.0f}ms"
        )



def main():
    global stop_flag
    th = threading.Thread(target=serial_thread, daemon=True)
    th.start()

    app = QtWidgets.QApplication([])
    w = Viewer()
    w.resize(900, 700)
    w.show()

    def on_exit():
        global stop_flag
        stop_flag = True

    app.aboutToQuit.connect(on_exit)
    app.exec_()


if __name__ == "__main__":
    main()

まとめ

今回は、Raspberry Pi PicoでBNO055を使う方法を紹介しました!

BNO055はセンサフュージョン込みで姿勢取得がとても簡単にでき、初心者にもおすすめの9軸センサーでした。

ロボット、姿勢制御、トラッキングなど角度や姿勢を使う工作にピッタリのセンサーですので、ぜひ試してみてください!

この記事が気に入ったら
いいねしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次