こんにちは。BNO085というセンサーをArduio言語で使用する方法について紹介します。
この記事は Tuton Advent Calender 2025 の9日目の記事です。
BNO085とは
CEVA Technologies社が開発した9軸センサーフュージョンモジュールです。
今回はAdafruit社のこちらのモジュールを使用します。
通信方式はI²C・UART・SPIが利用できます。今回はI²Cを使用します。
配線
以下のように配線してください。
I²CではINTピンやRSTピンを繋ぐ必要はありません。
- 5V系マイコンの場合
| BNO085のピン | マイコンのピン |
|---|---|
| VDD | 5V |
| GND | GND |
| SCL | SCL |
| SDA | SDA |
- 3.3V系マイコンの場合
| BNO085のピン | マイコンのピン |
|---|---|
| VDD | 3.3V |
| GND | GND |
| SCL | SCL |
| SDA | SDA |
4.7kΩ~10kΩのプルアップ抵抗をSCL・SDAとVDDの間にそれぞれつなげてください。
プログラム
ここではheading・pitch・rollを連続的に取得するプログラムを作成します。
今回はPlatformIOで開発します。
こちらのAdafruitさんのAdafruit_BNO08xライブラリを使用します。
PlatformIOからライブラリをインポートしてください。
インポートできたら、こちらのサンプルをmain.cppに貼り付けて、SerialやI2Cなどを適宜マイコンに合わせて書き換えたうえで書き込んでみてください。
上手く出力されれば成功です。
こちらのコードは、上のサンプルを参考に私が作成したラッパーライブラリです。STM32マイコンでの使用を想定していますが少し書き換えれば他のマイコンでも使えると思います。
ご自由にお使いください。
自作BNO085ラッパーライブラリ
#ifndef BNO085_H
#define BNO085_H
#include <Arduino.h>
#include <Adafruit_BNO08x.h>
#include <Wire.h>
// レポートタイプと間隔の定義
#define FAST_MODE
#ifdef FAST_MODE
// Top frequency is reported to be 1000Hz (but freq is somewhat variable)
#define REPORT_TYPE SH2_GYRO_INTEGRATED_RV
#define REPORT_INTERVAL_US 2000
#else
// Top frequency is about 250Hz but this report is more accurate
#define REPORT_TYPE SH2_ARVR_STABILIZED_RV // AR/VR Stabilization Rotation Vector (より正確)
#define REPORT_INTERVAL_US 5000 // 5000 us = 200 Hz
#endif
class BNO085
{
public:
// コンストラクタ: I2C通信を想定
BNO085( TwoWire *wire = &Wire,HardwareSerial *debugSerial= nullptr);
bool begin();
// キャリブレーション状態を確認 (0:unreliable to 3:fully calibrated)
bool isCalibrated();
void setZero();
// センサー値を読み取り、成功すればtrueを返す
bool read();
// 読み取り値(オイラー角)
float direction; // headingからオフセットを引いた値
float heading, pitch, roll;
void print();
private:
Adafruit_BNO08x _bno;
float direction_offset;
uint8_t _calibration_status;
HardwareSerial *_debugSerial;
// クォータニオンからオイラー角への変換
void quaternionToEuler(float qr, float qi, float qj, float qk, float* yaw, float* pitch, float* roll, bool degrees = true);
void setReports(sh2_SensorId_t reportType, long report_interval);
};
#endif
#include "BNO085.h"
#ifndef PI
#define PI 3.14159265358979323846
#endif
#ifndef RAD_TO_DEG
#define RAD_TO_DEG (180.0 / PI)
#endif
#ifndef DEG_TO_RAD
#define DEG_TO_RAD (PI / 180.0)
#endif
// コンストラクタの実装
BNO085::BNO085( TwoWire *wire, HardwareSerial *debugSerial)
{
_bno = Adafruit_BNO08x(-1);
_debugSerial = debugSerial;
}
// BNO085の初期化
bool BNO085::begin()
{
direction_offset = 0.0;
_calibration_status = 0;
// I2C初期化 (begin_I2Cの呼び出しは、コンストラクタで設定したパラメータを使用)
if (!_bno.begin_I2C())
{
return false;
}
// レポートの有効化
setReports(REPORT_TYPE, REPORT_INTERVAL_US);
_debugSerial->println("BNO085 initialized");
return true;
}
// キャリブレーション状態の確認
// BNO085のキャリブレーションは 0 (unreliable) から 3 (fully calibrated)
bool BNO085::isCalibrated()
{
// キャリブレーションステータスが3(完全キャリブレーション)であればtrue
return (_calibration_status == 3);
}
// センサー値を読み取り、オイラー角を更新
bool BNO085::read()
{
sh2_SensorValue_t sensorValue;
if (_bno.wasReset())
{
_debugSerial->print("sensor was reset ");
setReports(REPORT_TYPE, REPORT_INTERVAL_US);
}
// センサーイベントの取得
if (!_bno.getSensorEvent(&sensorValue))
{
return false;
}
// ARVR Stabilized Rotation Vector レポートであるかを確認
if (sensorValue.sensorId != REPORT_TYPE)
{
return false;
}
// キャリブレーションステータスを更新
_calibration_status = sensorValue.status;
// クォータニオンデータへのポインタを取得
sh2_RotationVectorWAcc_t *rotational_vector = &sensorValue.un.arvrStabilizedRV;
// クォータニオンからオイラー角に変換
quaternionToEuler(rotational_vector->real,
rotational_vector->i,
rotational_vector->j,
rotational_vector->k,
&heading, &pitch, &roll, true); // trueで度(degrees)に変換
// directionの計算 (0-360度の範囲に収める)
direction = heading - direction_offset;
if (direction < 0.0)
{
direction += 360.0;
}
else if (direction >= 360.0)
{
direction -= 360.0;
}
return true;
}
// directionを現在のheadingにリセット
void BNO085::setZero()
{
read(); // 最新のheadingを取得
direction_offset = heading;
}
// センサー値のシリアル出力
void BNO085::print()
{
_debugSerial->print(F("Calib: "));
_debugSerial->print(_calibration_status);
_debugSerial->print(F(" Heading: "));
_debugSerial->print(heading, 2);
_debugSerial->print(F(" Pitch: "));
_debugSerial->print(pitch, 2);
_debugSerial->print(F(" Roll: "));
_debugSerial->print(roll, 2);
_debugSerial->print(F(" Direction: "));
_debugSerial->println(direction, 2);
}
// --- プライベート関数 ---
/**
* @brief クォータニオンからオイラー角 (Yaw, Pitch, Roll) への変換
* @param qr クォータニオンの実部 (w)
* @param qi クォータニオンの i 成分 (x)
* @param qj クォータニオンの j 成分 (y)
* @param qk クォータニオンの k 成分 (z)
* @param yaw 計算されたヨー角 (Heading) の格納先ポインタ
* @param pitch 計算されたピッチ角 の格納先ポインタ
* @param roll 計算されたロール角 の格納先ポインタ
* @param degrees 結果を度数 (true) またはラジアン (false) で返すか
*/
void BNO085::quaternionToEuler(float qr, float qi, float qj, float qk, float *yaw, float *pitch, float *roll, bool degrees)
{
float sqr = sq(qr);
float sqi = sq(qi);
float sqj = sq(qj);
float sqk = sq(qk);
float t;
// Yaw (Z軸回転)
// atan2(2.0 * (qi * qj + qk * qr), (sqi - sqj - sqk + sqr));
t = 2.0 * (qi * qj + qk * qr);
t /= (sqi - sqj - sqk + sqr);
*yaw = atan2(2.0 * (qi * qj + qk * qr), (sqi - sqj - sqk + sqr));
// Pitch (Y軸回転)
// asin(-2.0 * (qi * qk - qj * qr) / (sqi + sqj + sqk + sqr));
*pitch = asin(-2.0 * (qi * qk - qj * qr) / (sqi + sqj + sqk + sqr));
// Roll (X軸回転)
// atan2(2.0 * (qj * qk + qi * qr), (-sqi - sqj + sqk + sqr));
*roll = atan2(2.0 * (qj * qk + qi * qr), (-sqi - sqj + sqk + sqr));
if (degrees)
{
*yaw *= RAD_TO_DEG;
*pitch *= RAD_TO_DEG;
*roll *= RAD_TO_DEG;
// Yaw (Heading) を 0 から 360 度の範囲に調整
if (*yaw < 0.0)
{
*yaw += 360.0;
}
}
}
void BNO085::setReports(sh2_SensorId_t reportType, long report_interval) {
Serial.println("Setting desired reports");
if (! _bno.enableReport(reportType, report_interval)) {
Serial.println("Could not enable stabilized remote vector");
}
}
おわりに
BNO055に比べて、BNO085は圧倒的に安定していて精度が高い印象があります。ぜひ試してみてください。