Arduinoでi2c通信でセンサから値をとってみる

こちらはSRA Advent Calendar の10日目の記事です。

皆さんこんにちわ
hurusuです。中部でプログラム書く仕事してます。

Advent Calendar登録しものの、何書けばいいかよくわからん...orz
ということで色々考えた結果、巷で話題のArduinoを触ってみた体験記を書いて見ることにしました。
ちなみにマイコン超初心者による初心者向けの記事です。
それでも読んでもいいよって人は読んでください。

Arduinoとは...!!!
-
・組み込み用途のマイコン
・オープンソースハードウェアなのでハードの設計と実装(回路や部品配置など)が公開されている(互換機多々あり)
・ブートローダ付きなので、簡単に実装したプログラムを書き込める
・デジタルIOピン/アナログIOピンがある (本数等はモデルによって異なる)
・5v/3.3v の出力ピンがある(モデルによって異なる)
・開発言語はC++のようなにからしい。Arduino言語？
・プログラムのことをSketch(スケッチ)という風習らしい。
・センサのライブラリ等がそこそこあるので、初心者にもやさしい。
・シールドとよばれる、Arduinoをはんだづけ不要で拡張できる部品が多々ある(WifiとかLCDとか色々)
・創立者同士が揉めて分離していてArduino.orgとArduino.ccと2つ公式サイトがある。長い間裁判したり争ってたけど、10月に和解して協力体制に移行したらしい

Arduino色々種類ありますけど、
今回触ってみたのは、こちらのエントリーモデルのArduino UNOです。 
![IMG_2592.JPG](https://qiita-image-store.s3.amazonaws.com/0/149669/3a5c2a39-fa91-ebb0-d3b7-e3972698886c.jpeg)

タバコのパッケージと並べて見ました。けっこう小さいですね。
気になるお値段ですが、Arduino unoがだいたい3500円くらいです。
個人で勉強するには十分ですね。
ちなみに、本格的な組み込みをする場合、Arduino Pro mini とかのほうが小さくて安いです。

さて、折角記事を書くのでArduino使って何かやらないと駄目ですね。。。
ということで、今回のテーマは、「Arduinoでi2c通信でセンサから値をとってみる」としました!! 

今回使用したもの
-
- Arduino UNO 
- ACアダプター 9V/1.3A
- USB2.0ケーブル(A-Bタイプ)
- MMA8451(ジャイロセンサ)  http://amzn.asia/6expAX1
- ADT7410(温度センサ)      http://akizukidenshi.com/catalog/g/gM-06675/
- ジャンプワイアケーブル(オス*オス)
- ピンソケット
- はんだごて
- はんだ

開発環境&配線図作成ツール
-
Arduino 1.6.9  https://www.arduino.cc/en/Main/Software
Fritzing  http://fritzing.org/download/

配線する前に
-
買った時点でのセンサ(ジャイロ・温度)はピンソケットささってないので、
自分ではんだづけします。
こんな感じですね。
![IMG_2594.JPG](https://qiita-image-store.s3.amazonaws.com/0/149669/ead1c502-da8c-e534-afae-adabe9e81e74.jpeg)

配線
-
以下のように配線します。
※FritzingでMMA8451とADT7410がなかったので、
それっぽい似た部品での配線図になっています。
図だとArduinoのGNDがジャイロのSDAと繋がってたり色々おかしいですが
実際のMMA8451ではこの辺のピン配置だと思ってください。


![adventcalender_ブレッドボード.png](https://qiita-image-store.s3.amazonaws.com/0/149669/2e6dea4f-b423-35cc-78fb-31a3516358c0.png)


今回I2Cの通信としては以下の関係となっています。
Arduino...マスター
ジャイロセンサ...スレーブ
温度センサ...スレーブ


ArduinoのSCLピン(クロック出力)が黄色です。
ArduinoのSDAピン(データ出力)が緑です。
温度センサとジャイロセンサのSCLピン/SDAピンにArduinoから伸ばしてます。


実際に配線した図はこちら
![IMG_kessen.JPG](http://i.imgur.com/NeLBKZp.jpg)

アドレス確認
-
配線したら、まずはI2Cのモジュールのアドレスを確認します。
Arduino.ccに、i2cscannerというツールのソースコードがあります。
これを使うと、見つかったi2cのモジュールのアドレスが分かります。
http://playground.arduino.cc/Main/I2cScanner

I2cScannerを使って見ると、各センサのアドレスが取れます。
![IMG_i2cscanner.JPG](https://i.imgur.com/N2OgyGY.png)

部品のデータシート見た感じでは、
ジャイロセンサ:0x1D
温度センサ:0x48
のアドレスのようです。

実際にはこれらのアドレスを使ってArduinoのWireクラスを使用してデータを取ってきます。
で、さっそくコードを書き始めたのはいいのですが、時間が足りなくて、温度センサのコードまでは書けなかった。すいませぬorz

Wireの使い方とソース
-
通常、こういうのには大抵ライブラリがあったりするものなのですが、
ここはあえてお勉強のために、ライブラリを使わず、1つのコードで完結させていいます。
※とはいっても、Adafruit MMA8451のライブラリ(BSD)をから拝借して手を加えてます。

とりあえずWireの使い方のお作法は以下のようにわたくしは解釈しました。
* * *
<b> i2cのスレーブに書き込むときのWireのお作法 <b>

 ①Wire.beginTransmission(addr)  ⇒接続するIC2のモジュールを選択
 ②Wire.write(reg)              ⇒I2Cのモジュールの書き込みレジスタを指定
 ③Wire.write(value)             ⇒②のレジスタにデータを書き込む
 ④Wire.endTransmission()       ⇒送信を完了しI2Cバスを開放

<b> i2cのスレーブから読み込むときのWireのお作法 <b>

 ①Wire.beginTransmission(addr)  ⇒接続するIC2のモジュールを選択
 ②Wire.write(reg)              ⇒I2Cのモジュールの読み込みレジスタを指定
 ③Wire.endTransmission(false)   ⇒送信完了するがコネクションは維持させる
 ④Wire.requestFrom(adr, size)   ⇒IC2のモジュールから②で指定したアドレスから指定サイズ分のデータ取得を要求
 ⑤Wire.read()                   ⇒②のレジスタからのデータを取得する。④で指定したサイズ分繰り返しで取得可
* * *
実際のソースは下記になります



```cpp:i2c_communication.ino
#include <Wire.h>

// MMA8451 レジスタアドレス定義
#define AD_MMA8451 0x1D //gyro
#define MMA8451_REG_OUT_X_MSB     0x01   // 14 bit X Data R 
#define MMA8451_REG_SYSMOD        0x0B   // System Mode R
#define MMA8451_REG_WHOAMI        0x0D   // ID Register R
#define MMA8451_REG_XYZ_DATA_CFG  0x0E   // Data Config R/W
#define MMA8451_REG_PL_STATUS     0x10   // PL Status R
#define MMA8451_REG_PL_CFG        0x11   // PL Configuration R/W
#define MMA8451_REG_CTRL_REG1     0x2A   // Control Reg1 R/W
#define MMA8451_REG_CTRL_REG2     0x2B   // Control Reg2 R/W
#define MMA8451_REG_CTRL_REG4     0x2D   // Control Reg4
#define MMA8451_REG_CTRL_REG5     0x2E   // Control Reg5

//標準重力加速度
#define SENSORS_GRAVITY_EARTH             (9.80665F)              /**< Earth's gravity in m/s^2 */
#define SENSORS_GRAVITY_STANDARD          (SENSORS_GRAVITY_EARTH)


// MMA8451 スケールレンジ定義
typedef enum
{
  MMA8451_RANGE_8_G           = 0b10,   // +/- 8g
  MMA8451_RANGE_4_G           = 0b01,   // +/- 4g
  MMA8451_RANGE_2_G           = 0b00    // +/- 2g (default value)
} mma8451_range_t;

// MMA8451 XYZセンサ生データ(14bitデータを格納)
int16_t x, y, z;

//  MMA8451 XYZ角加速度(スケールレンジ向けに割った値を格納 標準重力加速度は未考慮)
float x_g, y_g, z_g;

//　MMA8451のアドレスを設定する
uint8_t mma8451_adr = 0;


// ADT7410 温度センサのアドレス
#define ADT7410 0x48    //temperature



/**********************************************
 * MMA8451(スレーブ)からArduino(マスタ)に対して送られたデータを受信する
 * requestFrom()でMMA8451に対して応答データの要求を行ったあとに実行すること
 *********************************************/
static inline uint8_t i2cread(void) {
  return Wire.read();
}

/**********************************************
 * Arduino(マスタ)からMMA8451(スレーブ)に対してデータ送信する
 * beginTransmission()とendTransmission()の間で実行すること
 *********************************************/
static inline void i2cwrite(uint8_t x) {
  Wire.write((uint8_t)x);
}

/******************************************
 * MMA8451の指定レジスタにデータを書き込む
 *****************************************/
void writeMMA8451_Register(uint8_t reg, uint8_t value) {
  // 指定したI2Cのスレーブ(MMA8451)に対して送信処理を始める
  Wire.beginTransmission(mma8451_adr);
  
  //書き込みレジスタを指定する
  i2cwrite((uint8_t)reg);
  
  //データを書き込む
  i2cwrite((uint8_t)(value));
  
  // 送信完了
  Wire.endTransmission();
}

/******************************************
 * MMA8451の指定レジスタからデータを読み出す
 *****************************************/
uint8_t readMMA8451_Register(uint8_t reg) {
    // 指定したI2Cのスレーブ(MMA8451)に対して送信処理を始める
    Wire.beginTransmission(mma8451_adr);
    
    // 読み込みたいレジスタを送信
    i2cwrite(reg);

    // 送信完了(コネクションは維持する)
    Wire.endTransmission(false);

    // MMA8451に対して1byteのデータを応答データに要求
    Wire.requestFrom(mma8451_adr, 1);
    
    // readで読み取れるバイト数がなければエラー
    if (! Wire.available()) return -1;

    // MMA8451から1byteのデータを取得して返却
    return (i2cread());
}

/******************************************
 * MMA8451とi2c通信を開始する(初期化処理)
 *****************************************/
bool MMA8451_begin(uint8_t i2caddr)
{
  //  Wireライブラリの初期化 I2Cバスにマスタかスレーブとして接続
  //  マスタとしてバスに接続する。
  Wire.begin();
  mma8451_adr = i2caddr;

  //デバイスリセット有効
  writeMMA8451_Register(MMA8451_REG_CTRL_REG2, 0x40); // reset
    
  //デバイスリセット有効になるまで待ち
  while (readMMA8451_Register(MMA8451_REG_CTRL_REG2) & 0x40);

  // フルスケール 4gに設定
  writeMMA8451_Register(MMA8451_REG_XYZ_DATA_CFG, MMA8451_RANGE_4_G);
  
  // Powerモード
  // 0x00 normal
  // 0x01 Low Noise Low Power
  // 0x02 High Resolution
  // 0x03 Low Power
  writeMMA8451_Register(MMA8451_REG_CTRL_REG2, 0x02);
  
  // DRDY on INT1
  writeMMA8451_Register(MMA8451_REG_CTRL_REG4, 0x01);
  writeMMA8451_Register(MMA8451_REG_CTRL_REG5, 0x01);

  // Turn on orientation config
  writeMMA8451_Register(MMA8451_REG_PL_CFG, 0x40);

  // Activate at max rate, low noise mode
  writeMMA8451_Register(MMA8451_REG_CTRL_REG1, 0x01 | 0x04);

  return true;   
}

/******************************************
 * スケールレンジを取得する
 * 
 *  0x00 2g
 *  0x01 4g
 *  0x02 8g
 *  0x03 reserved
 *****************************************/
mma8451_range_t MMA8451_getRange(void)
{
  return (mma8451_range_t)(readMMA8451_Register(MMA8451_REG_XYZ_DATA_CFG) & 0x03);
}

/******************************************
 * スケールレンジを設定する
 * 
 *  0x00 2g
 *  0x01 4g
 *  0x02 8g
 *  0x03 reserved
 *****************************************/
void MMA8451_setRange(mma8451_range_t range)
{
  uint8_t reg1 = readMMA8451_Register(MMA8451_REG_CTRL_REG1);
  writeMMA8451_Register(MMA8451_REG_CTRL_REG1, 0x00);            // deactivate
  writeMMA8451_Register(MMA8451_REG_XYZ_DATA_CFG, range & 0x3);
  writeMMA8451_Register(MMA8451_REG_CTRL_REG1, reg1 | 0x01);     // activate
}


/******************************************
 * MMA8451から、XYZ軸のデータを取得する
 *****************************************/
void MMA8451_read(void) {
  // 指定したI2Cのスレーブ(MMA8451)に対して送信処理を始める
  Wire.beginTransmission(mma8451_adr);

  // データ取得開始アドレスはX軸上位バイトのレジスタ
  i2cwrite(MMA8451_REG_OUT_X_MSB);
  
  // 送信完了(コネクションは維持する)
  Wire.endTransmission(false); // MMA8451 + friends uses repeated start!!

  // MMA8451に対して6byteのデータを応答データに要求
  Wire.requestFrom(mma8451_adr, 6);

  // X軸レジスタのアドレスら計6byte取得する(X軸:14bit, Y軸:14bit, Z軸:14bit)
  /* MMA8451_REG_OUT_X_MSB: 0x01  X軸上位バイト 
   *                        0x02  X軸下位バイト
   *                        0x03  Y軸上位バイト
   *                        0x04  Y軸下位バイト
   *                        0x05  Z軸上位バイト
   *                        0x06  Z軸下位バイト
   */
  x = Wire.read(); x <<= 8; x |= Wire.read(); x >>= 2;
  y = Wire.read(); y <<= 8; y |= Wire.read(); y >>= 2;
  z = Wire.read(); z <<= 8; z |= Wire.read(); z >>= 2;

  // スケールレンジを取得
  uint8_t range = MMA8451_getRange();
  uint16_t divider = 1;

  /* 角加速度はスケールレンジによって計算方法が異なる
   *     14bitデータ      Range±2g    Range±4g  Range±8g
   *  01 1111 1111 1111   1.99975g     +3.9995g   +7.999g
   *  データシートのTable75の表を参照
   */
  if (range == MMA8451_RANGE_8_G) divider = 1024;
  if (range == MMA8451_RANGE_4_G) divider = 2048;
  if (range == MMA8451_RANGE_2_G) divider = 4096;

  x_g = (float)x / divider;
  y_g = (float)y / divider;
  z_g = (float)z / divider;
}

void setup() {
    Serial.begin(9600);

    /* i2c通信初期化(MMA8451と通信するものとする */
    if(!MMA8451_begin(AD_MMA8451))  {
      Serial.println("Couldnt start");
       while (1);
    }
    /* スケールレンジは Range±2g とする*/
    MMA8451_setRange(MMA8451_RANGE_2_G);
}

void loop() {
  // MMA8451からXYZ軸の生データを読み込む
  MMA8451_read();
 
  // XYZセンサ生値を出力
  Serial.print("X:\t"); Serial.print(x); Serial.print("\n"); 
  Serial.print("Y:\t"); Serial.print(y); Serial.print("\n"); 
  Serial.print("Z:\t"); Serial.print(z); Serial.print("\n");
  Serial.println();

  // 角加速度データを出力(m/s^2)
  Serial.print("X m/s^2:\t"); Serial.print(x_g * SENSORS_GRAVITY_STANDARD); Serial.print("\n"); 
  Serial.print("Y m/s^2:\t"); Serial.print(y_g * SENSORS_GRAVITY_STANDARD); Serial.print("\n");
  Serial.print("Z m/s^2:\t"); Serial.print(z_g * SENSORS_GRAVITY_STANDARD); Serial.print("\n");
  Serial.println();
  
  delay(500);
}
```
動作を確認してみる
-
ブレッドボードを傾けたりしてると値が変わることが確認できます。
今回は、センサから取得した生データと、3軸の角加速度の両方を出してます。

![Arduinolog.png](https://i.imgur.com/qFZBWTq.png)


最後に
-
今回の記事では、Arduinoとセンサを配線して繋ぎ、i2c通信でデータを取ってくるところまでやりました。いざマイコンやってみると電気電子の知識が必要になる場面があり、面倒くさいことは多々ありますが、たくさんの有志の人たちがwebにArduinoやセンサを使ったノウハウを残してくださっていますので、簡単な工作ぐらいなら、手軽にできちゃいます。皆さんもArduino。挑戦してみませんか？