ジャイロは角速度を足して角度にします。足すから、わずかな偏りが消えずに積み上がっていきます。どのくらいずれるのかを測り、センサー自身に差し引かせるまでをまとめます。
この記事は、ジャイロセンサー 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つの状況に応じて上がる、全体の目安 |
IMUPLUS では mag は 0 のままです。地磁気を使わないモードなので、合っていなくて構いません。故障ではありません。
ここで区別しておくことがあります。モーターの磁気の影響は、キャリブレーションでは直りません。あれはセンサーのずれではなく、まわりの環境だからです。そちらは前の項のとおり、地磁気を使わない IMUPLUS を選んで避けます。
つまずいた話 ― 6方向を正確に置くと、かえって上がらない
最初、私たちは上下左右前後の6方向をきちんと置いていました。それでは accel は一度も動きませんでした。
疑わしいものを順に潰しました。
| 疑ったもの | 調べ方 | 結果 |
|---|---|---|
| 電源・USB | 15秒あたりの切断回数を数えた | 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 | 重力の大きさのずれ |
|---|---|---|
| 合わせない | 0 | 3.5% |
| 6方向だけ正確に | 0 のまま | 合わせられない |
| 1軸を45度ずつ一周 | 3 | 0.88% |
| 2軸を45度ずつ一周 | 3 | 0.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 |
| Z | 23 / 19 / 28 |
幅にすると 0.1 m/s² ほど。数字そのものを見ても、合っているかは分かりません。重力の大きさが 9.807 にどれだけ近いかで見るほうが確実です。
BNO055 をつくっている Bosch 自身の解説です。合わせるときの動かし方が、映像でわかります。なお動画に出てくるPCソフト(Development Desktop 2.0)は Bosch 専用の基板か USB スティックが必要で、Pico につないだ基板では使えません。
How to calibrate the absolute orientation sensor BNO055(Bosch Sensortec・YouTube・英語)合わせるプログラム ― 全文
集まった向きの数を数えるプログラムです。そのまま書き込めます。配線は ジャイロセンサーの記事のとおりで、ライブラリも同じ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秒です。焼き込んだ値は、そこまでの出発点にすぎません。
BNO055 を載せたロボットが試合中におかしな方向を向く原因と、その直し方がまとまっています。モードの選び方と、オフセットの保存の手順が具体的です。
BNO055を載せたロボットが試合中に変な方向向く人へ(しろくま胡瓜の自由帳)動かないときに見るところ ― 症状から原因を引く
この表の症状は、ひととおり実際に出たものです。
| 症状 | 原因 | 対処 |
|---|---|---|
| 起動した直後だけ大きくずれる | ジャイロの校正がまだ済んでいない | 置いたまま数秒待つ。gyro が 3 になってから走り出す |
accel が 0 から動かない | 電源か USB が不安定。切れるたびにセンサーが再起動し、積み上げが消える | USB ケーブルとアダプタを替える。gyro だけ 3 になるのが目印 |
6方向に置いても accel が 0 | 6方向だけでは上がらない。斜めの姿勢が要る | 1辺を軸に45度ずつ転がす。それを2本の軸で |
accel が 1 や 2 で止まる | 角度の種類が足りない | もう1軸ぶん転がす。肘を机につけて持つと安定する |
| 数字は出るが、すぐ流れて読めない | 0.5秒ごとの表示が上書きしている | この記事のプログラムは出したあと止まる。古い版なら Autoscroll を切る |
mag が 0 のまま | IMUPLUS は地磁気を使わない | 正常。gyro と accel だけを見る |
sys が 0 のまま | 同じ理由。IMUPLUS では上がらない | 正常。 |
| 書き戻しても値が変わっていく | 自動キャリブレーションは止められない | 仕様。焼き込んだ値は出発点として扱う |
配線やアドレスにかかわる症状は、ジャイロセンサーの記事にまとめてあります。
BNO055 の公式の資料です。117ページあり、モードの一覧・レジスタの詳細・軸の向きの定義まで載っています。なお資料の中では、記事で使った IMUPLUS が IMU という名前で書かれています。IMUPLUS は Adafruit のライブラリでの呼び方です。
ロボカップジュニア2027 に向けて、レスキュー(災害救助)とサッカーの2種目に取り組みます。プログラミングと電子工作の経験は問いません。