2学期開始!体験授業 受付中 →
ROBO SCIENCE

ずれを差し引く ―
BNO055 のキャリブレーション

2026.10.31
R3 PE(Pre-expert) ES(Expert-soccer) ER(Expert-rescue)
ブレッドボードに載せた Raspberry Pi Pico と、青い基板の9軸センサー BNO055。赤・黒・黄・青の4本のジャンパー線でつながっている
校正は、この基板を45度ずつ転がすだけ。基板にある X / Y / Z の印が、どの向きに置いたかの目印になる。

ジャイロは角速度を足して角度にします。足すから、わずかな偏りが消えずに積み上がっていきます。どのくらいずれるのかを測り、センサー自身に差し引かせるまでをまとめます。

この記事は、ジャイロセンサー BNO055 の続きです。あちらで角度が読めるようになったところから始めます。

読めた角度は、そのままでは信用できません。置いたまま放っておいても、少しずつずれていきます。まず、その理由から見ます。

なぜずれるのか ― 0.1度の偏りが、1分で6度になる

ジャイロを机に置いて、まったく動かさずに値を読んでみます。きれいに 0 が並ぶとはかぎりません。0.05 とか −0.12 とか、小さな値が出続けることがあります。

この「止まっているのに出てしまう値」をバイアス(偏り)といいます。製造上のばらつきや温度で生じるもので、大きさに差はあっても、たいていのジャイロが持っています。

BNO055 のように、これを自分で測って引いてくれるセンサーもあります。その場合、机に置いているあいだはほとんど動きません。とはいえ、引ききれなかったぶんは残ります。

問題は、さきほどの足し算がこれを足し続けることです。

バイアス1秒後1分後10分後
0.1 度/秒0.1度6度60度
0.01 度/秒0.01度0.6度6度

センサーとしては 0.1 度/秒はごく小さな誤差です。しかし積分すると、10分で60度になります。ロボットは「まっすぐ進んでいる」と信じたまま、違う方向を向いていきます。

これがドリフトです。ジャイロの弱点は精度の低さではなく、誤差が消えずに積み上がることにあります。

測ってみた ― 静止で 0.03度/分

2026年10月30日、ブレッドボードに組んだ BNO055 を IMUPLUS で毎秒50回読み、静止している区間を抜き出しました。

測った長さ始め → 終わりずれ1分あたり
109.9秒10.56度 → 10.63度+0.07度+0.04度/分
150.3秒18.56度 → 18.62度+0.06度+0.02度/分

1分で 0.05度未満。50cm先の的で 0.4mm です。ほかに測れたのは次の2つです。

項目結果
回転を止めた直後最初の2秒で1度ほど戻り、そのあとは平ら
読める回数毎秒132〜135回(I2C 100kHz)

ずれていく様子を目で見たいときは、角度を針で表すと一目でわかります。作り方は Processing のはじめ方 にあります。

ずれへの対応

ずれを小さくする方法は、いくつか知られています。どれを使うかはチームによって違います。

方法効きかた
止まっているあいだに
バイアスを測って引く
よく効く。BNO055 は、これを自分で行っている(次の項)
加速度センサーと混ぜる前後左右の傾きには効くが、ヨーには効かない。加速度では水平回転が見えないため
地磁気を使う(NDOF)絶対の向きが分かるが、モーターのそばでは狂いやすい

サッカーでは、地磁気を使わない IMUPLUS を選ぶチームが多くあります。モーターがセンサーのすぐ近くにあるからです。

そして、ずれは完全には消せません。得点のあとのリスタートでロボットを置き直せば、そこで 0 に取り直せます。けれども試合が流れているあいだは、向きを外から教える手段がありません。

ハーフは10分です。「その10分でどれだけずれるか」が、使えるかどうかを決めます。

キャリブレーション ― センサーのずれを測って差し引く

キャリブレーションは、さきほどのバイアスを測って差し引く作業です。バイアスの大きさは1個ずつ違ううえ、温度でも変わります。だから、工場であらかじめ決めておくことができません。使う場所で、そのつど測ります。

BNO055 は、この測定を動かしているあいだ、ずっと自分で続けています。データシートには「自動キャリブレーションは背景で動いており、止めることはできない」と書かれています。前の項の「止まっているあいだにバイアスを測って引く」を、センサーが自分でやっているということです。

いまどこまで合ったかは、センサー自身が教えてくれます。空の変数を4つ渡すと、そこに書き込んで返してきます。

uint8_t sys, gyro, accel, mag;
bno.getCalibration(&sys, &gyro, &accel, &mag);

センサーが入れてくるのは、0 から 3 の4段階です。0 はまだ合っておらず、3 で合った状態。ジャイロ・加速度・地磁気それぞれと、全体の目安で4つです。

返ってくる値3 にする方法
gyro ジャイロ数秒、じっと止める
accel 加速度1辺を軸に45度ずつ転がす。それを2本の軸で(下の図)
mag 地磁気空中に「8」を書くように動かす
sys 全体上の3つの状況に応じて上がる、全体の目安
1辺を軸に45度ずつ一周 ― それを2本の軸で 45度ずつ この辺が軸 ① 1本目の軸 基板の1辺を机の縁に当てて転がす 45度ずつ 別の辺が軸 ② 2本目の軸 90度持ち替えて、別の辺を軸に 6方向(上下左右前後)だけでは上がらない。斜めが要る 1姿勢につき5秒。手で持ったままでよい
きっちり45度でなくてよい。大事なのは、斜めを含めて色々な角度をとること。

IMUPLUS では mag は 0 のままです。地磁気を使わないモードなので、合っていなくて構いません。故障ではありません。

ここで区別しておくことがあります。モーターの磁気の影響は、キャリブレーションでは直りません。あれはセンサーのずれではなく、まわりの環境だからです。そちらは前の項のとおり、地磁気を使わない IMUPLUS を選んで避けます。

つまずいた話 ― 6方向を正確に置くと、かえって上がらない

最初、私たちは上下左右前後の6方向をきちんと置いていました。それでは accel は一度も動きませんでした。

疑わしいものを順に潰しました。

疑ったもの調べ方結果
電源・USB15秒あたりの切断回数を数えた0回。問題なし
センサーの故障セルフテストとエラーのレジスタを読んだ全項目合格・エラー0
モードIMUPLUS/NDOF/ACCONLY を試したどれも同じ
振動静止中の読みのばらつきを計算した十分に静か
置き方の雑さ各姿勢を7〜31秒ずつ保持した十分に長い

そのあいだ gyro は数秒で 3 になり、mag も 0 から 3 まで上がりきりました。センサーの計算そのものは動いています。加速度の欄だけが反応しません。

答えはログの中にありました。accel が初めて 1 に上がった瞬間の読みを調べると、それは6方向のどれでもありませんでした。

X-6.49  Y0.07  Z-7.26   ← ここで A0 → A1

X と Z の両方に重力が乗った、約42度の斜めの姿勢です。うまくいった日は、本に立てかけて手探りだったために、斜めの姿勢が偶然入っていました。きちんと6方向に置くようになってから、失敗するようになったのです。

メーカーの解説動画にも、「少なくとも1軸を中心に、45度ずつ回す」と書かれていました。6方向だけ、という手順はどこにもありません。

何軸まわせばよいか ― 測って決めた

1軸で足りるのか、2軸要るのか。実際に比べました。良し悪しは、重力の大きさで測れます。正しく合っていれば、どの向きに置いても 9.807 になるはずです。

やり方accel重力の大きさのずれ
合わせない03.5%
6方向だけ正確に0 のまま合わせられない
1軸を45度ずつ一周30.88%
2軸を45度ずつ一周30.42%

1軸でも accel は 3 になります。ただし、そこには穴があります。

1本の軸を中心に回すと、その軸自身には重力が一度も乗りません。私たちの試験では Y 軸が回転軸になり、Y の値は −1.05 〜 1.62 の範囲にしか振れませんでした。それでも 3 になったのは、この基板の Y のくせがもともと小さかったからです。くせの大きい基板なら、見逃します。

2軸やれば、3つの軸すべてに重力が乗ります。かかる時間は1分増えるだけです。

もうひとつ。取れる数字は、やり直すたびにかなり変わります。

軸3回やった結果
X−2 / −4 / −15
Y−2 / −5 / +6
Z23 / 19 / 28

幅にすると 0.1 m/s² ほど。数字そのものを見ても、合っているかは分かりません。重力の大きさが 9.807 にどれだけ近いかで見るほうが確実です。

合わせるプログラム ― 全文

集まった向きの数を数えるプログラムです。そのまま書き込めます。配線は ジャイロセンサーの記事のとおりで、ライブラリも同じ2つです。

6姿勢のチェックリストは持っていません。前に集めた向きから30度以上離れていれば「別の向き」として数えます。12通り集めるのを目安にしています。

キーできること
dいまの値を出す。A が 3 でなければ、足りない項目を教える
r集めた向きを捨てて、やり直す

A も G も 3 になると、貼り付け用の数字が自動で出ます。

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>

// アドレスは基板によって違う。2つ用意して、見つかったほうを使う
Adafruit_BNO055 bno29(55, 0x29, &Wire);
Adafruit_BNO055 bno28(55, 0x28, &Wire);
Adafruit_BNO055* bno = nullptr;

const int   TARGET_DIRS = 12;    // これだけ集めれば、たいてい A は 3 になる
const float MIN_SEP_DEG = 30.0;  // 前に集めた向きから、これだけ離れていれば別の向き
const float STILL_TOL   = 0.30;  // 前回の読みとの差がこれ未満なら静止
const unsigned long STILL_MS = 3000;   // 3秒静止で1つぶん

const int MAX_DIRS = 24;
float dirs[MAX_DIRS][3];         // 集めた向き(長さ1にそろえたもの)
int   nDirs = 0;

unsigned long stillStart = 0;
float prevX = 0, prevY = 0, prevZ = 0;
bool  counted = false;           // いまの静止で、もう数えたか
bool  dumped = false;
bool  paused = false;            // 数字を出したあとは表示を止める

// この向きは、まだ集めていない向きか
int whichSlot(float x, float y, float z) {
  float n = sqrtf(x * x + y * y + z * z);
  if (n < 5.0) return -1;                   // 重力が乗っていない。落下中など
  float u[3] = { x / n, y / n, z / n };

  const float limit = cosf(MIN_SEP_DEG * 3.14159265f / 180.0f);
  for (int i = 0; i < nDirs; i++) {
    float dot = u[0] * dirs[i][0] + u[1] * dirs[i][1] + u[2] * dirs[i][2];
    if (dot > limit) return -1;             // 前のどれかと近すぎる
  }
  if (nDirs >= MAX_DIRS) return -1;

  dirs[nDirs][0] = u[0];
  dirs[nDirs][1] = u[1];
  dirs[nDirs][2] = u[2];
  return nDirs++;
}

// 合わせ終わった値を、貼り付けられる形で出す
// 出せたら true。出したあとは表示を止める(でないと流れて読めない)
bool dumpOffsets() {
  uint8_t sys, gy, ac, mg;
  bno->getCalibration(&sys, &gy, &ac, &mg);

  adafruit_bno055_offsets_t o;
  if (!bno->getSensorOffsets(o)) {
    Serial.print("\nまだ読み出せません。いま A");
    Serial.print(ac); Serial.print(" G"); Serial.println(gy);
    Serial.println("A と G の両方が 3 になる必要があります。");
    return false;
  }

  Serial.println("\n---- ここから下を bno_offsets.h に貼る ----");
  Serial.print("// 温度 "); Serial.print(bno->getTemp()); Serial.println("C");
  Serial.println("static const adafruit_bno055_offsets_t BNO_OFFSETS = {");
  Serial.print("  ");
  Serial.print(o.accel_offset_x); Serial.print(", ");
  Serial.print(o.accel_offset_y); Serial.print(", ");
  Serial.print(o.accel_offset_z); Serial.println(",   // 加速度 X Y Z");
  Serial.print("  ");
  Serial.print(o.mag_offset_x); Serial.print(", ");
  Serial.print(o.mag_offset_y); Serial.print(", ");
  Serial.print(o.mag_offset_z); Serial.println(",   // 磁気 X Y Z");
  Serial.print("  ");
  Serial.print(o.gyro_offset_x); Serial.print(", ");
  Serial.print(o.gyro_offset_y); Serial.print(", ");
  Serial.print(o.gyro_offset_z); Serial.println(",   // ジャイロ X Y Z");
  Serial.print("  "); Serial.print(o.accel_radius); Serial.println(",");
  Serial.print("  "); Serial.print(o.mag_radius);   Serial.println("");
  Serial.println("};");
  Serial.println("------------------------------------------");
  Serial.println("表示を止めました。続けるときは r を送ってください。");
  return true;
}

void setup() {
  Serial.begin(115200);
  Wire.setSDA(4);
  Wire.setSCL(5);
  Wire.begin();
  delay(2000);

  if      (bno29.begin(OPERATION_MODE_IMUPLUS)) bno = &bno29;
  else if (bno28.begin(OPERATION_MODE_IMUPLUS)) bno = &bno28;

  // 見つからないときは、黙らずに言い続ける
  if (bno == nullptr) {
    while (true) {
      Serial.println("BNO055 not found. check 3V3 / GND / SDA / SCL");
      delay(1000);
    }
  }

  delay(1000);
  Serial.println("\n基板の1辺を軸にして、45度ずつ転がしてください。");
  Serial.println("1姿勢5秒。一周したら90度持ち替えて、別の辺を軸にもう一周。");
}

void loop() {
  if (Serial.available()) {
    char c = Serial.read();
    if (c == 'd') paused = dumpOffsets();   // 出せたら表示を止める
    if (c == 'r') {
      nDirs = 0;
      dumped = false;
      paused = false;
      Serial.println("\n>>> 集めた向きを捨てて、やり直します");
    }
  }

  imu::Vector<3> a = bno->getVector(Adafruit_BNO055::VECTOR_ACCELEROMETER);
  float x = a.x(), y = a.y(), z = a.z();

  // 止まっているか。前回の読みとの差で見る
  bool still = (fabs(x - prevX) < STILL_TOL &&
                fabs(y - prevY) < STILL_TOL &&
                fabs(z - prevZ) < STILL_TOL);
  prevX = x; prevY = y; prevZ = z;

  if (!still) {
    stillStart = millis();
    counted = false;
  } else if (!counted && millis() - stillStart >= STILL_MS) {
    counted = true;                          // この静止では一度だけ数える
    if (whichSlot(x, y, z) >= 0) {
      Serial.print("\n>>> ");
      Serial.print(nDirs);
      Serial.print(" / ");
      Serial.print(TARGET_DIRS);
      Serial.println(" 通りめ の向きを数えました");
    }
  }

  // --- 表示 ---
  // ふだんは1行だけ。8行も出すと、出た数字がすぐ上へ流れて読めなくなる。
  static unsigned long lastPrint = 0;
  if (!paused && millis() - lastPrint >= 500) {
    lastPrint = millis();

    uint8_t sys, gy, ac, mg;
    bno->getCalibration(&sys, &gy, &ac, &mg);

    Serial.print("X");    Serial.print(x, 2);
    Serial.print(" Y");   Serial.print(y, 2);
    Serial.print(" Z");   Serial.print(z, 2);
    Serial.print("  A");  Serial.print(ac);
    Serial.print(" G");   Serial.print(gy);
    Serial.print("  向き "); Serial.print(nDirs);
    Serial.print("/");       Serial.print(TARGET_DIRS);
    if (still && !counted) {
      Serial.print("   静止 ");
      Serial.print((millis() - stillStart) / 1000.0, 1);
      Serial.print(" / 3.0 秒");
    }
    Serial.println();

    // A と G がそろったら、一度だけ自動で出す
    if (!dumped && ac >= 3 && gy >= 3) {
      dumped = true;
      paused = dumpOffsets();
    }

    // 向きは十分なのに A が上がらないときは、10秒ごとに助言する
    static unsigned long lastHint = 0;
    if (nDirs >= TARGET_DIRS && ac < 3 && millis() - lastHint >= 10000) {
      lastHint = millis();
      Serial.println();
      Serial.print("向きは ");
      Serial.print(nDirs);
      Serial.println(" 通り集まりましたが、A がまだ上がりません。");
      Serial.println("  もう1軸ぶん、持ち替えて転がしてみてください。");
      Serial.println("  肘を机につけて持つと、揺れが収まります。");
      Serial.println();
    }
  }

  delay(20);
}

読むところは3つです。

場所していること
whichSlot()この向きが、もう集めた向きと近すぎないかを見る。読みを長さ1にそろえ、前のどれかと30度以内なら数えない
dirs の配列集めた向きをためておく場所。長さ1にそろえた x・y・z を、最大24個まで
still前回の読みとの差で、止まっているかを見る。0.30 未満が3秒続けば合格

手で持ったままで構いません。実測すると、手持ちの揺れは半分の時間が 0.04 m/s² 以下でした。STILL_TOL の 0.30 には十分おさまります。肘を机につけて持つと、さらに安定します。

なぜ3秒も止めるのか。センサーの中の計算が、その姿勢を1点として受け入れるまでに時間がかかるためです。

なぜ30度で区切るのか。45度ずつ回すつもりでも、手でやれば30度や60度になります。30度にしておけば、45度のつもりの姿勢はほぼ別物として数えられ、同じ向きへの置き直しは数えられません。

画面の見かた ― 数字が出るまで

書き込んだら、シリアルモニタを 115200bps で開きます。0.5秒ごとに、いまの状態が流れ続けます。

X-2.52 Y-0.04 Z9.48  A1 G3  向き 4/12
X-2.54 Y-0.02 Z9.50  A1 G3  向き 4/12   静止 1.5 / 3.0 秒
X-2.53 Y-0.03 Z9.49  A1 G3  向き 4/12   静止 2.0 / 3.0 秒

ふだんは1行だけです。何行も出すと、せっかく出た数字がすぐ上へ流れてしまいます。

向きが1つ増えるたびに、こう出ます。

>>> 5 / 12 通りめ の向きを数えました
見る場所意味
X Y Zいまの読み[m/s²]。重力が乗っている軸が 9.8 前後になる
Aここが本番。加速度の合い具合。0 → 1 → 2 → 3 と上がる
Gジャイロ。机に置いて数秒で 3 になる
向き 4/12集まった向きの数。12 が目安
静止 1.5 / 3.0 秒数えている最中。0.0 に戻ったら動かしすぎ

6つそろって A も G も 3 になった瞬間、貼り付け用の数字が流れます。こちらから何もしなくても出ます。

---- ここから下を bno_offsets.h に貼る ----
// 温度 27C
static const adafruit_bno055_offsets_t BNO_OFFSETS = {
  -15, 6, 28,   // 加速度 X Y Z
  0, 0, 0,   // 磁気 X Y Z
  -2, 1, -2,   // ジャイロ X Y Z
  1000,
  480
};
------------------------------------------

数字を出したら、0.5秒ごとの表示は止まります。止めないと、せっかく出た数字がすぐ上へ流れて読めません。

表示を止めました。続けるときは r を送ってください。

この数字をコピーして、次の項のとおりソースコードに貼ります。

向きが12通り集まっても A が上がらないことがあります。そのときは10秒ごとにこう出ます。

向きは 12 通り集まりましたが、A がまだ上がりません。
  もう1軸ぶん、持ち替えて転がしてみてください。
  肘を机につけて持つと、揺れが収まります。

d と r は、シリアルモニタの送信欄に打って送ります。Arduino IDE なら上の入力欄、PlatformIO ならそのままキーを押します。自動で出るのを待たずに d を送ると、いま何が足りないかを教えてくれます。

まだ読み出せません。いま A1 G3
A と G の両方が 3 になる必要があります。

合わせた値を、保存して使い回す

測った値は、電源を切ると消えます。試合前に毎回ぐるぐる回して合わせ直す方法もありますが、うまく合ったかどうかが、そのときによって変わります。

そこで、一度しっかり合わせた値をソースコードに書いておきます。Pico のフラッシュは電源を切っても消えないので、ソースコードそのものが保存場所になります。なお、データシートやライブラリでは、このバイアスのことを オフセット(offset)と呼びます。

作業は3つに分かれます。①と③は別のプログラムです。変数は電源を切れば消えるので、1本のプログラムではつながりません。

① 合わせる用のプログラムで、値を画面に出す。全部 3 になってから bno.getSensorOffsets(offsets); を呼び、Serial.print で表示します。

② 出た数字を、ソースコードに貼る。私たちは bno_offsets.h というファイルを作って、ここに置いています。

// この基板の実測値(2026-11-01・2軸スイープ・温度 26度)
static const adafruit_bno055_offsets_t BNO_OFFSETS = {
  -15, 6, 28,     // 加速度 X Y Z
  0, 0, 0,        // 磁気 X Y Z    IMUPLUS では未使用
  -2, 1, -2,      // ジャイロ X Y Z
  1000,           // accel_radius
  480             // mag_radius
};

この数字は、この1枚の基板だけのものです。別の基板に書いても合いません。基板を替えたら取り直します。基板に番号を書いておくと取り違えません。

③ ロボットのプログラムで、起動時に書き戻す。bno.setSensorOffsets(BNO_OFFSETS); の1行です。これで電源を入れた直後から合った状態で始められます。

書き戻すときは、いったん設定モードに入る必要があります。Adafruit のライブラリは中でやってくれますが、自分でレジスタを書く場合は忘れないようにします。

ただし、ジャイロのぶんは当てにしません。ジャイロの偏りは温度で変わります。起動のたびに、gyro が 3 になるまで置いたまま待ちます。実測で3〜12秒です。焼き込んだ値は、そこまでの出発点にすぎません。

動かないときに見るところ ― 症状から原因を引く

この表の症状は、ひととおり実際に出たものです。

症状原因対処
起動した直後だけ大きくずれるジャイロの校正がまだ済んでいない置いたまま数秒待つ。gyro が 3 になってから走り出す
accel が 0 から動かない電源か USB が不安定。切れるたびにセンサーが再起動し、積み上げが消えるUSB ケーブルとアダプタを替える。gyro だけ 3 になるのが目印
6方向に置いても accel が 06方向だけでは上がらない。斜めの姿勢が要る1辺を軸に45度ずつ転がす。それを2本の軸で
accel が 1 や 2 で止まる角度の種類が足りないもう1軸ぶん転がす。肘を机につけて持つと安定する
数字は出るが、すぐ流れて読めない0.5秒ごとの表示が上書きしているこの記事のプログラムは出したあと止まる。古い版なら Autoscroll を切る
mag が 0 のままIMUPLUS は地磁気を使わない正常。gyro と accel だけを見る
sys が 0 のまま同じ理由。IMUPLUS では上がらない正常。
書き戻しても値が変わっていく自動キャリブレーションは止められない仕様。焼き込んだ値は出発点として扱う

配線やアドレスにかかわる症状は、ジャイロセンサーの記事にまとめてあります。

ロボサイエンスについて

ロボカップジュニア2027 に向けて、レスキュー(災害救助)とサッカーの2種目に取り組みます。プログラミングと電子工作の経験は問いません。

ロボサイエンスのコース紹介 →
これまでの大会成績 →
体験授業・お問い合わせ →

← ロボサイエンスの記録一覧へ戻る