はじめに
本記事では、Arduino を用いた I²C 通信について理解を深めつつ、MPU-6500 を用いた姿勢(ピッチ・ロール)の取得方法を紹介します。
MPU-6500 固有の話題にとどまらず、他のデバイスでも応用できるよう、できる限り一般的な手順で解説します。
この記事では、
I²Cバスを制御する側を、コントローラー(Controller)
コントローラーから呼ばれる側を、ペリフェラル (Peripheral) と表記します。
元々は master/slave と表記されていたものであり、一昔前の記事ではこの表記になっていることもあるので、適宜読み変えると良いでしょう。
免責事項
本記事の内容を参考・利用された結果として発生した機器の破損、データの消失、その他いかなる損害や不利益に対しても、筆者は一切の責任を負いません。すべて自己責任にてご活用ください。
I²Cとは?
I²C は Inter-Integrated Circuit の略で、I-squared-C(アイ・スクエアド・シー)と発音します。
このプロトコル(通信ルール)では、2つの線を用います。
名称 (略称) | 用途 |
---|---|
Serial CLock (SCL) | クロック用(データ転送のタイミングを同期するための時計) |
Serial DAta (SDA) | データ送信用(2つのデバイス間でデータをやり取りする) |
以後、SCLやSDAといった略称で説明を進めます。
通信方式
SCLとSDAはそれぞれ、High (1) か Low (0, つまりGND) の2つの状態を取ります。
SCL は定期的(決まった時間ごと)に、Highとなり、この際に、SDA の値が読み取られます。
言い換えると、SCLは「メトロノーム」みたいな存在ですね。
逆に、SDAは SCLがLowの間に変化して次のビットを準備します。
▲ 例えば、100110というデータ列を送信する場合。横軸は時間。
接続方法
基本的には、使用したいデバイスの SCL ・ SDA と、Arduino側の SCL ・ SDA を接続するのみです。
「基本的には」なので、完全ではありません。早まらないでください。
プルアップ抵抗
上の図には謎の抵抗が書かれていますが、これをプルアップ抵抗(Pull-up resistor)と言います。
抵抗を通してSCL・SDAラインをVDDに引っ張る (Pull up) ことで、デバイスが信号を出していないときはラインが High になります。逆にデバイスがLowを出すときは、そのラインをGNDに落とします。このとき電流は抵抗を通じて流れるため、短絡せず安全にLowが作られます。
MPU-6500を使う
今回は Amazon で詐欺商品ARCELI MPU-9250 9DOFモジュールを買ってきました。MPU-9250と書いてあるものの、MPU-6500が入っているという代物です。
動作電圧の確認
データシートを見ます。
a VDD operating range of 1.71 to 3.6V,
とあるので、Arduinoの場合は3.3Vで動作させるのが良いでしょう。
逆に言えば、5Vで動かしてはいけないということですね。
Who am I ? (型番確認)
Register-Mapによると、メモリ番地:0x75が「WHO_AM_I」レジスタであると記載されています。このレジスタに格納されている値を読み取ることで、どの型番のMPUが搭載されているかを確認することができます。
格納されている値(Hex) | 型番 | 参照元 |
---|---|---|
0x68 | MPU-6000/6050 | MPU-6000 and MPU-6050 Register Map |
0x70 | MPU-6500 | MPU-6500 Register Map |
0x71 | MPU-9250 | MPU-9250 Register Map |
型番確認プログラム
Wireライブラリにについてはの後ほど解説します。
#include <Wire.h>
#define MPU_ADDR 0x68 // スレーブアドレスの定義
void setup() {
Serial.begin(115200);
Wire.begin();
delay(1000);
Wire.beginTransmission(MPU_ADDR);
// WHO_AM_I Register
Wire.write(0x75);
Wire.endTransmission(false);
Wire.requestFrom(MPU_ADDR, 1, true);
if (Wire.available()) {
byte c = Wire.read();
Serial.print("WHO_AM_I = 0x");
Serial.println(c, HEX);
} else {
Serial.println("Failed to read WHO_AM_I");
}
}
void loop() {
// Nothing to do
}
WHO_AM_I = 0x70 が返ってきたので、残念ながらこのモジュールにはMPU-6500が搭載されていることが確認できました。
I²Cバス用双方向電圧レベル変換モジュール(PCA9306) の導入
Arduino は 3.3V を High と認識してくれるので、必須ではないものの(たぶん...)、安定性のため、電圧レベル変換モジュールを導入します。このモジュールの良いところはプルアップ抵抗を内蔵してくれていることです。(つまり、別にプルアップ抵抗を用意する必要がない)
Wire ライブラリ
Arduino と I²Cデバイスの通信には Wire ライブラリを使用すると便利です。
Functions
Wire.begin()
public void begin();
public void begin(uint8_t address);
Wireライブラリを初期化し、I²Cバスに参加します。
引数を指定しない場合は、コントローラーとして、引数を指定した場合は、その7ビットのI²Cアドレスでペリフェラルとしてバスに参加します。
この関数は基本的に開始時に1度だけ(つまり、setup()の中のみで)呼び出します。
Wire.beginTransmission()
public void beginTransmission(uint8_t address);
指定デバイスへの送信トランザクションを開始する宣言をします。
アドレスの記憶および、送信用バッファ(配列)の書き込み位置と長さのリセットを行います。
Wire.write()
public virtual size_t write(uint8_t data)
public virtual size_t write(const uint8_t *data, size_t quantity);
指定したデータを送信用バッファ(配列)に溜めていきます。
返り値:書き込んだバイト数。(破棄可能)
郵便ポストにはがきを投函するイメージです
Wire.endTransmission()
public uint8_t endTransmission();
public uint8_t endTransmission(uint8_t sendStop);
write()
によってバッファに蓄えられたバイトを送信し、送信トランザクションを終了します。
引数を true とした場合、送信後に stop メッセージを送信し、I²C バスを解放します。false の場合、送信後に restart メッセージを送信します(バスは解放されない)。デフォルト値は true です。
返り値:エラーコード
返り値 | 説明 |
---|---|
0 | 正常終了 |
1 | データが送信バッファに収まっていない なお、 #define BUFFER_LENGTH 32 と定義されているように、最長では32 |
2 | アドレスの送信時にNACKを受信した |
3 | データ送信時にNACKを受信した |
4 | その他のエラー |
5 | タイムアウト |
配達員さんが郵便ポストにたまったはがきをまとめて運んでくれるイメージです
Wire.requestFrom()
public uint8_t requestFrom(uint8_t address, uint8_t quantity);
public uint8_t requestFrom(uint8_t address, uint8_t quantity, uint8_t sendStop);
ペリフェラルからの+所定バイト数の読み出しを要求します。
引数 | 説明 |
---|---|
address | ペリフェラルのI²Cアドレス(7ビット) |
quantity | 要求するバイト数 |
sendStop | true の場合、送信後に stop メッセージを送信し、I²C バスを解放する。false の場合、送信後に restart メッセージを送信し、バスは解放されない。デフォルト値は true 。 |
「おーい、〇〇さん。3バイト出してくれや?なあ?そこにいるのはわかってるんだよ?(こわいお兄さんによる借金の取り立て)」
Wire.available()
public virtual int available();
read() で取得可能なバイト数(つまり。まだ読んでいないバッファの残りバイト数)を返します。
Wire.read()
public virtual int read();
requestFrom()
の呼び出し後に周辺デバイスからコントローラデバイスに送信されたバイト、またはコントローラデバイスから周辺デバイスに送信されたバイトを読み出します。
I²Cスレーブアドレスの確認
データシートを見ます。
このデータシートによると、AD0をGNDにしている場合のアドレスは 1101000 (つまり0x68)です。よって、バイトの送信要求をする際には、
Wire.requestFrom(0x68, 1);
のようにすれば良いというわけです。
欲しいデータの格納先を知る
ここでは、ジャイロセンサーの値を取得することを目標とします。
MPU-6500 の Register Map を見て、その値が格納されているレジスタを探します。
▲ MPU-6500 の Register Mapより抜粋。
ジャイロセンサーの(生)データは、0x43から0x48に格納されていることが分かります。
ここではついでに0x41から読んで温度も取得してみましょう。
#include <Wire.h>
const uint8_t MPU_ADDR = 0x68;
const float TEMP_SENS = 333.87f; // LSB/°C (データシートより)
void setup() {
Serial.begin(115200);
Wire.begin();
}
void loop() {
int16_t temp, gx, gy, gz;
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x41); // デバイス内部の読み出し開始レジスタ番地を指定
Wire.endTransmission(false);
// 0x41からの8バイトをまとめて読みたい
Wire.requestFrom(MPU_ADDR, (uint8_t)8, (uint8_t)true);
if (Wire.available() == 8) {
temp = (Wire.read() << 8) | Wire.read();
temp = (temp / TEMP_SENS) + 21.0f; // データシートより
gx = (Wire.read() << 8) | Wire.read(); // GYRO_XOUT_H, GYRO_XOUT_L
gy = (Wire.read() << 8) | Wire.read(); // GYRO_YOUT_H, GYRO_YOUT_L
gz = (Wire.read() << 8) | Wire.read(); // GYRO_ZOUT_H, GYRO_ZOUT_L
}
Serial.print("TEMP : "); Serial.print(temp);
Serial.print(" GX : "); Serial.print(gx);
Serial.print(" GY : "); Serial.print(gy);
Serial.print(" GZ : "); Serial.println(gz);
delay(50);
}
シリアルモニタを開いて、適当にセンサーを揺らすと、ジャイロの値(角速度)が変化していることが確認できます。
外部ライブラリの使用
とはいえ、先人によって作成されたライブラリが存在するので、ありがたく使用させていただきます。
今回は、Ewan Leng McCairm氏によって作成されたmpu6050ライブラリを使用します。
サンプルプログラムをそのまま使用するだけでも、ピッチ&ローの値を取得できています。ありがたいことです。
おわりに
Arduino を用いた I²C 通信について学習しつつ、MPU-6500 を用いて姿勢(ピッチ・ロール)の取得を行いました。最終的には外部ライブラリを利用しましたが、仕組みを学んだ上で用いることで、ライブラリを単なるブラックボックスとしてではなく、内部でどのようにレジスタにアクセスし、どの値を読み出しているのかを理解した上で活用できるようになります。
ご意見やご指摘がありましたら、ぜひコメントをいただければ幸いです。