2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

STM32でBNO055をI2Cで読む——アドレスのシフト・LSB/MSB変換・プルアップまで

2
Posted at

STM32でBNO055をI2C接続——ポインタとアドレスのシフトでつまずいた初心者が、姿勢角を読むまで

I2Cでセンサーを読むのは、配線2本とコード数行で済む——はずなのに、最初の1個目はそう簡単にはいきませんでした。

自分がつまずいたのは、I2Cそのものよりも、その手前にある C言語のポインタデータシートの読み方 でした。最初はチップIDのレジスタ(0x00)を読もうとして値が返らず、「HAL_..._Transmit で書き込みできてないから失敗してるのか?」と何時間も別の所を疑っていました。本当の原因は、「レジスタ番号を書き込む → そこから値を読む」という2段階の流れ と、関数に渡す「ポインタ」 を理解していなかったこと。結局、きちんと仕様書(データシート)を読んで、ようやく腑に落ちました。

この記事では、自分がハマったその「2段階の読み取り」「ポインタ」「データシートの読み方」も含めて、STM32L432KC と Bosch の9軸センサー BNO055 を I2C でつなぎ、姿勢角(向き)を読むところまでをやります。

環境:STM32CubeIDE 1.19.0 / HALドライバ / Nucleo-L432KC(STM32L432KCUx)。I2C1(SCL=PA9 / SDA=PA10)、センサーは BNO055。


1. I2C通信は、ざっくりこういうもの

まず仕組みだけ押さえておくと、つまずいたときに切り分けやすくなります。

I2Cは2本の線で複数のデバイスとやり取りする、クロック同期式のシリアル通信です。

  • SDA(Serial Data)… データを送受信する線。マスターとスレーブの双方向
  • SCL(Serial Clock)… タイミングを合わせるクロックの線。マスターからスレーブへの一方向

ここでの「マスター」はマイコン(STM32)、「スレーブ」はセンサー(BNO055)です。同じ2本の線に複数のセンサーをぶら下げられるので、各スレーブにはアドレス(住所)があり、マスターは「このアドレスの子としゃべりたい」と宣言してから通信します。

勉強しながら、自分はこんなふうに手で整理しました(接続のしかたと、SDA/SCLの波形)。

IMG_5394.JPG

I2Cの1回の通信は、おおまかにこの順で進みます。

  1. スタートコンディション:通信開始の合図(SCLがHighのときにSDAをLowにする)
  2. スレーブアドレス送信:相手を指定する
  3. 書き込み / 読み出しビット:これからどっちをするか(書き込み=0 / 読み出し=1)
  4. ACK / NACK:スレーブからの返事(通信OK=0 / 不可=1)
  5. データ:8ビット単位でやり取り
  6. ACK / NACK:1バイトごとに返事
  7. ストップコンディション:通信終了の合図

他のシリアル通信と並べると、立ち位置が見えてきます。

方式 通信速度の目安 クロック線 信号線 構成
SPI 〜50Mbps データ・クロック・チップセレクト マスター1 ↔ 複数スレーブ
I2C 〜1Mbps データ・クロック 複数マスター ↔ 複数スレーブ
UART 〜500kbps データ デバイス1 ↔ デバイス1

この「アドレス」と「2段階のやり取り」が、後で出てくる関門になります。


2. センサーを読むのは「書いてから、読む」の2段階

I2Cでセンサーの値を読むときに、自分が最初につまずいた最大のポイントがここです。

「読む」のに、いきなり受信するのではありません。まず「どのレジスタを読みたいか」をスレーブに書き込んで(送信して)から、改めてそのデータを読み出す、という2段階になります。

IMG_5396.JPG

HALだと、この2段階はそのまま2つの関数に対応します。

// ① 読みたいレジスタの番号を「書き込む」
tx[0] = 0x1A;  // 例:オイラー角データの先頭レジスタ
HAL_I2C_Master_Transmit(&hi2c1, DevAddress_BNO055, tx, 1, 100);

// ② そこからデータを「読む」
HAL_I2C_Master_Receive(&hi2c1, DevAddress_BNO055, rx, 6, 100);

自分は最初、Master_Transmit を「書き込み(=値をセンサーに書く)」だと思い込んでいて、チップIDが読めないのを「書き込みが失敗してる?」と延々疑っていました。実際は、Master_Transmit読みたいレジスタの番号を伝えMaster_Receiveその中身を受け取る、という役割分担でした。ここが分かると一気に進みます。


3. BNO055とは(なぜ初心者にやさしいか)

BNO055は、加速度センサー・ジャイロ・地磁気センサーを1つに統合した9軸センサーです。最大の特徴は、チップの中で計算(センサーフュージョン)まで済ませてくれること。生データを自分で積分・フィルタする必要がなく、「向き(オイラー角)」を読むだけで済みます。

今回使う値を、データシートから拾って先に並べておきます。これらはコードを眺めても出てこない、仕様書を読んで初めて分かる情報です。

  • I2Cアドレス0x28(ADDRピンをL/GNDにしたとき。Hにすると 0x29)。2進数だと 0b0101000
  • チップID:レジスタ 0x00 を読むと 0xA0 が返る(生きているBNO055の確認に使う)
  • 動作モード:起動直後は設定モード。OPR_MODE0x3D)に値を書くと動き出す。今回は姿勢制御向けの IMUモード(0x08 を使用(地磁気を使わない相対姿勢)
  • オイラー角データ:レジスタ 0x1A から。1度 = 16 LSB / 1ラジアン = 900 LSB
  • 起動待ち:電源投入からセンサーが立ち上がるまで時間がかかるので、最初に十分なウェイト(数百ms〜)を入れる

4. CubeMXの設定

  1. Connectivity から I2C1 を選び、Mode を「I2C」にする
  2. ピンは SCL=PA9 / SDA=PA10 に割り当てた(CubeMXが自動で選ぶピンと違う場合は手動で変更)
  3. I2C Speed は 100kHz か 400kHz。BNO055は400kHzまで対応

スクリーンショット 2026-06-29 222800.png

設定すると、生成コードに hi2c1 というハンドルと MX_I2C1_Init() が作られます。

配線とプルアップ:GPIOの設定で有効にする

I2CでつまずきやすいのがSDA/SCLのプルアップです。I2Cはオープンドレインという仕組みで、線を「H」に引き上げる役目がないと信号がうまく出ません。

外付けの抵抗(4.7kΩ程度)で引き上げる方法もありますが、今回は CubeMXのGPIO設定で内蔵プルアップを有効化しました。I2C1を設定したあと、GPIO Settings で SCL / SDA の GPIO Pull-up/Pull-down を「Pull-up」にします。

これを設定するまで IsDeviceReady も通らず、ここでもしばらく悩みました。「配線は合ってるのに反応しない」ときは、プルアップを真っ先に疑うのがよさそうです。

補足:内蔵プルアップは抵抗値がやや弱めなので、通信速度が速いときや配線が長いときは、外付けの抵抗のほうが安定することもあります。まず内蔵で試して、不安定なら外付けを検討、という順でよいです。


5. コード:アドレスを作る → 確認する → 起こす → 読む

コードの前に:uint8_t / uint16_t って何?

コードに出てくる uint8_tuint16_t は、サイズが決まった整数の型です。組み込みではレジスタやバイト単位を直接扱うので、「何ビットの入れ物か」をはっきりさせるためによく使います。

  • uint8_t … 符号なし 8ビット(1バイト、0〜255)。I2Cでやり取りする1バイトのデータはこれ
  • uint16_t … 符号なし 16ビット(2バイト、0〜65535)。I2Cアドレスなどに使う
  • int16_t符号あり 16ビット(−32768〜32767)。角度のようにマイナスもとる値はこれ

u が付くと符号なし(プラスだけ)、付かないと符号あり(マイナスもOK)、数字がビット数、と読むと分かりやすいです。センサーの角度は負の値もとるので、後で int16_t で受けています。

アドレスは「7bitを1bit左シフト」する

I2C + HAL でいちばん多いつまずきです。データシートのアドレスは 7bitの 0x28 ですが、HALの関数は8bitのアドレスを要求します。なので1bit左にシフトします。

uint16_t DevAddress_BNO055 = 0b0101000 << 1;  // 0x28 << 1 = 0x50

0x28 のまま渡すと、いつまでも応答が返らず「配線は合ってるのに動かない」と悩むことになります。

① チップIDを読んで「本物のBNO055」か確認する

いきなり姿勢角を読む前に、答えが決まっているレジスタで試すのがコツです。チップID(0x00)は仕様書に「必ず 0xA0 が返る」と書いてあるので、これが読めれば「アドレスもI2Cの読み取りも正しい」と確定できます。値が毎回変わるセンサーでいきなり試すと、配線が悪いのかコードが悪いのか切り分けられません。

uint8_t reg[1];
uint8_t value[1];

reg[0] = 0x00;  // CHIP_ID レジスタ
HAL_I2C_Master_Transmit(&hi2c1, DevAddress_BNO055, reg, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, DevAddress_BNO055, value, 1, 100);

printf("BNO055 Chip ID : 0x%X\r\n", value[0]);  // 0xA0 が出れば成功

② IMUモードにする(書き込み = 2バイト送信)

起動直後は設定モードで、姿勢角はまだ出てきません。レジスタに値を書き込んで動作させます。書き込みは「[レジスタ番号, 書き込む値] の2バイトを送信する」だけです。

uint8_t opr_mode[2];
opr_mode[0] = 0x3D;  // OPR_MODE レジスタ
opr_mode[1] = 0x08;  // IMU モード
HAL_I2C_Master_Transmit(&hi2c1, DevAddress_BNO055, opr_mode, 2, 100);
HAL_Delay(100);      // モード切替の待ち

③ オイラー角(向き)を読む

データは 0x1A から、下位バイト→上位バイトの順で並んでいます。6バイトまとめて読み、2バイトずつ組み立てます。

uint8_t tx[1];
uint8_t rx[6];

tx[0] = 0x1A;
HAL_I2C_Master_Transmit(&hi2c1, DevAddress_BNO055, tx, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, DevAddress_BNO055, rx, 6, 100);

int16_t heading = ((int16_t)rx[1] << 8) | (int16_t)rx[0];
int16_t roll    = ((int16_t)rx[3] << 8) | (int16_t)rx[2];
int16_t pitch   = ((int16_t)rx[5] << 8) | (int16_t)rx[4];

// 1度 = 16 LSB なので16で割ると「度」になる
float heading_deg = heading / 16.0f;
float roll_deg    = roll    / 16.0f;
float pitch_deg   = pitch   / 16.0f;

これで姿勢角が度で取れます。

バイナリデータの変換:LSBとMSBを1つにまとめる

ここで初心者がつまずくのが、センサーの値が2つのバイトに分かれて返ってくることです。16ビットの値は1バイト(8ビット)には収まらないので、BNO055は 下位8ビット(LSB)と上位8ビット(MSB)の2バイトに分けて、しかも LSBを先に送ってきます。

  • rx[0] … LSB(下位8ビット)
  • rx[1] … MSB(上位8ビット)

これを1つの数値に戻すのが、コード中の ((int16_t)rx[1] << 8) | (int16_t)rx[0] です。MSBを8ビット左にずらして「上の桁」に持ち上げ(<< 8)、LSBと |(OR)で合体させています。この変換をやらないと、値がおかしくなります。なぜLSBが先なのか、何ビットずらすのかは、データシートのレジスタ表を読んで初めて分かるところでした。

ちなみに、オイラー角・クォータニオン・線形加速度・重力ベクトルはレジスタが連続して並んでいるので、rx を大きめ(26バイトなど)にして一度の Receive でまとめて読むと効率的です。

④ ループで読み続ける

姿勢角は刻々と変わるので、③の「読む部分(Transmit → Receive)」を while(1) のループの中に置いて、繰り返し読み続けます。チップID確認とモード設定は一度きりでいい初期化なのでループの前に、データの読み取りはループの中に、と分けるのがポイントです。

// --- ループの前(初期化)---
// チップID確認、IMUモード設定 …(前述)

while (1) {
    // 「レジスタを書く → データを読む」を毎回くり返す
    tx[0] = 0x1A;
    HAL_I2C_Master_Transmit(&hi2c1, DevAddress_BNO055, tx, 1, 100);
    HAL_I2C_Master_Receive(&hi2c1, DevAddress_BNO055, rx, 6, 100);

    // …ここで rx を角度に変換して使う…

    HAL_Delay(20);  // 読みすぎないよう少し待つ
}

Transmit(レジスタ指定)と Receive(読み取り)はセットです。毎周このペアを回すことで、最新の姿勢角が取れ続けます。


6. つまずきポイント(自分がハマった所)

★ ポインタ:rx&rx[0] のこと

最大の壁でした。HAL_I2C_Master_Receive の引数には rx とだけ書きますが、これは「配列名がそのまま先頭要素のアドレスを表す」からです(rx&rx[0] は同じ意味)。関数側は「ここから何バイト書き込んでいいよ」という置き場所の先頭住所を受け取って、そこに順番にデータを入れていきます。

整理すると、

  • 配列rxtx)→ そのまま渡せる(配列名 = 先頭アドレス)
  • 1個の変数& を付けてアドレスにして渡す(例:&value
  • ポインタ型は宣言時に * を付ける(例:uint8_t *p;

「ポインタは変数のアドレスを格納する変数」「配列名は先頭要素を指すポインタ」——この2つが腑に落ちると、HALの関数の引数が急に読めるようになりました。

★ 自分のコードは USER CODE BEGIN の中に書く

地味だけど重要。CubeMXでピンや設定を変えてコードを再生成すると、/* USER CODE BEGIN *//* USER CODE END */ の外に書いたコードは消えます。自分で足したコードは、必ずこのコメントブロックの中に書きます。これを知らずに、再生成で自分のコードが消えて「あれ、動かなくなった」となるのは初心者あるある(自分もやりました)。

printfが出ない / 表示が崩れる

printf でマイコンのデータをPCに出すとき、出力がすぐ反映されないことがあります。setbuf(stdout, NULL); でバッファリングを切ると、その場で出るようになります。ターミナル(Teratermなど)側でバッファをクリアすると表示もきれいになります。

いちばんの教訓:データシートから逃げない

アドレスが7bitか8bitか、データはLSB/MSBどちらが先か、1度が何LSBか、モードはどう設定するか——全部データシートに書いてあり、コードを眺めても出てきません。最初は英語の仕様書が面倒ですが、必要なのは「レジスタ表」と該当する数ページだけ。そこを読む習慣がついてから、つまずきの大半が消えました。

ソフトだけでなく、ハードも疑う

最後にもうひとつ、大事な実体験を。コードをいくら見直しても動かず、何時間も「自分のプログラムのどこが悪いんだ」と悩んでいたのに、実は原因がハードウェア側だったことがありました。自分の場合は、基板そのものの接続がうまくいっておらず、センサーを別の個体に交換したらあっさり成功しました。

ソフトを書く人ほど「バグはコードにある」と思い込みがちですが、組み込みでは配線・半田・接触・センサーの個体不良といったハード側の原因も同じくらいあります。チップID(0xA0)すら読めないときは、コードを疑う前に、ジャンパ線を挿し直す・別のセンサーや配線で試す、といったハードの切り分けも並行してやると、無駄に時間を溶かさずに済みます。


まとめ

  • I2Cは SDA / SCL の2本。マスター(STM32)がアドレスで相手を指定する
  • センサーの読み取りは 「レジスタ番号を書く(Transmit)→ データを読む(Receive)」の2段階。これを ループの中でくり返す
  • HALのI2Cアドレスは 7bitを1bit左シフト0x28 << 1
  • 値は LSB/MSBの2バイトに分かれて返るので、(MSB << 8) | LSB で1つに戻す
  • uint8_t(1バイト)/ int16_t(符号あり16ビット)など、型のビット数を意識する
  • SDA/SCLには プルアップが要る(CubeMXのGPIO設定で内蔵Pull-upを有効化。弱ければ外付け)
  • 流れは「チップID(0xA0)で確認 → OPR_MODEを設定 → データを読む
  • ポインタは「配列名 = 先頭アドレス」「1個の変数は & を付ける」
  • 自分のコードは USER CODE BEGIN の中に書く(再生成で消えるのを防ぐ)
  • 倍率やレジスタは データシートにすべて書いてある
  • 動かないときは ソフトだけでなくハード(配線・接触・センサーの個体)も疑う(自分は別のセンサーに替えたら動いた)

ポインタや2段階の読み取りでつまずいたのは、たぶん自分だけじゃないはずです。同じ所で止まっている人に、この記事が地図になればうれしいです。

次回は、ここで読んだ姿勢角を使って、前回作ったサーボを PD制御 で動かすところまでやります。


※コード・レジスタ値・倍率は BNO055 + 標準的なHALドライバを前提にしています。ボードやライブラリのバージョンによって細部が異なる場合があるので、ご自分の環境で動作を確認してから使ってください。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?