ArduinoでI2C通信

ArduinoでI2C通信

はじめに

Arduinoには他の機器と通信するための様々な規格が存在しています。一番わかりやすいのはUSBを介したパソコンとのシリアル通信でこの規格をUART通信といいます。
その他、LCD(液晶ディスプレイ)や各種センサーなどと通信する場合に使える規格としてI2C (Inter-Integrated Circuit / アイスクウェアシー)通信や、SPI (Serial Peripheral Interface / エスピーアイ)通信があります。

それぞれの通信にはメリットデメリットがありますが、使いやすい通信規格で他の機械と接続してやればArduinoから各種センサへの値の送信、センサーから値の受信などができます。

今回は、Arduinoを使ってI2C通信に挑戦します。

I2C通信とは

Master / Slave について

I2C通信おいて、前提として接続された機械にはMaster / Slaveと呼ばれる機器の状態があります。Masterは読んで字のごとく通信の主導権を握る機器のことでSlaveは通信を受ける側の機器のことです。Slaveはペリフェラルと読み替えても概ね問題はないと思います。
Master側からSlaveに対して様々な要求(Slaveへの値の書き込みやSlaveからの値の読み込み)を飛ばしてからSlaveがその要件を実行します。そのため、Slave側からMasterへの要求はできませんし、Slave同士の通信も出来ません。必ずMasterを1台だけ用意して、MasterとSlaveという主従関係のもと通信が行われます。
I2C通信を使ってSlave同士で値をやりとりしたい場合は一度Masterを介する必要があるというわけですね。

I2Cの通信

I2Cの通信には、SDA(Serial Data / データ通信用の線)とSCL(Serial Clock / 機器同期用の線) の2つの線で通信できます。ここにVcc(5V / 3.3V線)とGND線を加えてもたった4つの線をつなぐだけで様々な通信ができるというわけですね。

特にI2C機器にはアドレスと呼ばれる機器管理番号が振られており、SDAからの通信で機器番号を特定することができるのでデバイス並列でつなげることができます。つまり機器ごとにSDAやSCL線を用意する必要はなくMasterから出ているSDAやSCLの線にあらやるSlaveのSDAやSCL線をつないでしまえばいいというわけですね。

ここでSlaveデバイスにはレジスタアドレスと呼ばれるアドレスが存在して複数の値を格納している場合があります。例えばxyz3軸センサーの場合には3つのレジスタを持っていたり、RGBフルカラーLEDが備わっていればそれも3つのレジスタを持っていたりします。I2C通信する場合は、I2Cアドレスを指定するだけでになくレジスタアドレスを指定する必要が出てくる場合があります。

I2Cの通信速度は100kbpsまたは400kbpsほどと低速で、シリアル通信で行なっているので基本的には同じ基板内での機器通信などの短い距離でしか通信できません。

ArduinoにおけるI2C通信

通常I2CはSCLのHigh, Lowの切り替えのタイミングでSDAでシリアルデータを通信するという手順で様々な通信を行なっています。
しかしArduinoとI2Cデバイスを通信させたい場合、ArduinoのスケッチにはWireライブラリというものがあり、Wireライブラリを使うことで適切にSCLやSDAのHigh, Lowを決めてくれるので実際にどのような動作が行われているかを知る必要はありません。

ArduinoでのWireライブラリを使った通信においては、WireライブラリでArduinoをMasterとして使うだけでなく、アドレスを割り振ってSlaveにさせることができます。つまり複数台のArduinoを結線させて通信させることができるというわけですね。
Arduinoボード同士をつないでデータをやりとりするということはなかなか機会がありませんが、ATMELのIC (Arduinoボードに乗っているチップ)を使って何かやりたいという時にはSlaveにできるということを覚えておけばいいかもしれません。

ArduinoでI2C通信

Arduino Wire Library

ArduinoでI2C通信をする場合には、Wireライブラリを使用するのが一般的です。Wireライブラリの使い方を簡単に記載します。

Wire.begin(address)

引数
adress : スレーブアドレス

Wireライブラリを使用するための初期設定を行う関数です。
通常Setup()内に記載します。

引数を取らない場合(Wire.begin())はマスターとして働き、引数にアドレスを記載した場合はスレーブとして働きます。通常スレーブアドレスは7bitで指定し、0は使用できないので、 1から127の値を取ります。また基本的に16進数で記載するのが一般的なので、「0x_」のプレフィクスをつけて16進数で記載します。たとえばアドレス番号1番の場合は「Wire.begin(0x01)」123番の場合は「Wire.begin(0x7B)」です。

Wire.requestFrom(address, count, [stop])

引数
adress : スレーブアドレス
count : 要求するバイト数
[stop] : 要求停止(通常は省略)

戻り値
受け取ったバイト数

マスターからスレーブに対してデータを要求する際に使用します。受け取ったデータはバッファに保存されます。
例えば、スレーブ番号1番に対して2バイト要求する場合は「Wire.requestFrom(0x01, 2)」と記載します。
戻り値には実際に受け取ったバイト数が返ります。

Wire.available()

引数
なし

戻り値
読み取りできるバイト数

スレーブからの受信データの有無を確認します。関数の戻り値は読み取り可能な受信データの数となります。

Wire.read()

引数
なし
戻り値
バッファ中の受信データ

マスターがスレーブに対してWire.requestFrom()要求し受け取ったデータに対して、バッファ内に保存されているデータをバイト単位で読み出します。
同様にスレーブとして作動しているときにマスターから受け取ったデータを読むときにも使用します。

Wire.beginTransmission(address)

引数
スレーブアドレス
戻り値
なし

指定したアドレスのスレーブと通信を開始するときに使用します。

Wire.write(value)

引数
バイトデータ
戻り値
実際に送ったデータ数

Wire.beginTransmission(address)で通信開始を指定したスレーブに対してデータを送ります。1byteのデータを送るWire.write(value)の他に文字データを送るWire.write(string)やバイト数を指定して送るWire.write(data, length)が使用できます。

Wire.endTransmission([stop])

引数
stop (省略可) : ストップメッセージ
戻り値
送信結果 (byte) 
0: 成功 
1: 送ろうとしたデータが送信バッファのサイズを超えた 
2: スレーブ・アドレスを送信し、NACKを受信した 
3: データ・バイトを送信し、NACKを受信した 
4: その他のエラー 

スレーブに対しての送信を終了します。
引数の[stop]は省略可能で、デフォルトではtrueになっています。true(または引数を取らない場合)はストップメッセージが送信されてI2Cの接続が解放されます。falseの場合はrestartメッセージをリクエストのあと送信しコネクションを維持します。 

Wire.onReceive(handler), Wire.onRequest(handler)

スレーブとして使用しているときに、I2C通信が行われた際に割り込みで実行させる処理を指定します。

ArduinoでのI2C通信の実際の処理

Wireライブラリの関数処理が一通りわかったところで、ArduinoでのI2C通信の仕方を記載します。

ArduinoからI2Cデバイスに書き込みを行うとき

Setup()内に Wire.begin(); を記載してArduinoをマスターデバイスとして初期化したのち、

Wire.beginTransmission(address);  //接続するIC2デバイスを選択
Wire.write(register);  //I2Cデバイスのレジスタを指定
Wire.write(value);  //レジスタにデータを書き込み
Wire.endTransmission();  //送信を完了

ArduinoへI2Cデバイスから読み込みを行うとき

Setup()内に Wire.begin(); を記載してArduinoをマスターデバイスとして初期化したのち、

Wire.beginTransmission(address) //接続するIC2デバイスを選択
Wire.write(register) //I2Cデバイスのレジスタを指定
Wire.endTransmission(false) //送信完了するが接続は維持
Wire.requestFrom(adress, count) //IC2デバイスに指定したレジスタアドレスから指定サイズ分のデータ取得を要求
Wire.read() //データを取得 (受け取ったバイト数繰り返す)

ArduinoでI2C通信してみる

はじめに

今回は基本的な使い方を知るために、Arduino 2台を使ってマスターとスレーブを作ります。
スレーブのArduinoには2つのレジスタアドレスを決めて、それぞれのレジスタに対して値を決めるようにしています。

必要なもの

Arduino本体 (今回はArduino Uno互換機)
Akizuki : http://akizukidenshi.com/catalog/g/gM-07385/

タクタイルスイッチ
Akizuki : http://akizukidenshi.com/catalog/g/gP-03647/

抵抗(10kΩ)
Akizuki : http://akizukidenshi.com/catalog/g/gR-25103/

ブレッドボードとケーブル
Akizuki : http://akizukidenshi.com/catalog/g/gP-00315/
Akizuki : http://akizukidenshi.com/catalog/g/gC-05371/

Arduinoボード

Arduinoボードは上の通り。Arduino Uno Rev.3の場合、I2C通信専用に「SDA」「SCL」の2線がピンヘッダにあるので、それを繋げます。
SDA、SCLは通常プルアップさせる必要があるので、Vcc(5V)線と抵抗でプルアップさせます。

Arduinoにおける電源供給は通常はUSB端子やDCプラグ、Vin端子を使用して電源供給を行いますが、5V線に5Vを供給することによっても起動します。このとき5V線はUSB端子と回路が直結しているので、5V線に5Vより大きい電圧を供給しないように注意します。また、この場合はSlaveデバイスは5V線から電源を供給することになるので、Slaveデバイスにスケッチを書き込んだ後は、USBはマスター側だけ刺せば良いことになります。

Arduino IDE

マスター側のコード

まずマスター側のコードです。Arduino同士をつないだ場合以外にも使用できるように汎用性の高いコードにしています。

#include <Wire.h>

#define SLAVE_ADDRESS 0x01
#define SLAVE_REGISTER_1 0x00
#define SLAVE_REGISTER_2 0x01

int val;

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

void loop() {

  Wire.beginTransmission(SLAVE_ADDRESS);
  Wire.write(SLAVE_REGISTER_1);
  Wire.endTransmission(false);
  Wire.requestFrom(SLAVE_ADDRESS, 1);
  Serial.print("SLAVE_REGISTER_1 ");
  while (Wire.available() > 0){
     val = Wire.read();
     Serial.print(val);
     Serial.print(" ");         
  }
  Serial.println(" "); 


  Wire.beginTransmission(SLAVE_ADDRESS);
  Wire.write(SLAVE_REGISTER_2);
  Wire.endTransmission(false);
  Wire.requestFrom(SLAVE_ADDRESS, 1);
  Serial.print("SLAVE_REGISTER_2 ");
  while (Wire.available() > 0){
     val = Wire.read();
     Serial.print(val);
     Serial.print(" ");         
  }
  Serial.println(" "); 

delay(1000);
}

コードの全体です。
1つずつ解説。

#include <Wire.h>

はじめにWireライブラリを使用するために、Wire.hをインクルードします。

#define SLAVE_ADDRESS 0x01
#define SLAVE_REGISTER_1 0x00
#define SLAVE_REGISTER_2 0x01

後述するSlave側の設定です。今回Slaveデバイスとしてアドレス番号0x01、レジスタ番号を0x00と0x01にしています。

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

setup()関数にWire.begin()でWireライブラリを初期化します。引数を取らない場合マスターデバイスになります。

  Wire.beginTransmission(SLAVE_ADDRESS); //接続するIC2デバイスを選択
  Wire.write(SLAVE_REGISTER_1); //I2Cデバイスのレジスタを指定
  Wire.endTransmission(false); //送信完了するが接続は維持
  Wire.requestFrom(SLAVE_ADDRESS, 1); //IC2デバイスに指定したレジスタアドレスから指定サイズ分のデータ取得を要求
  Serial.print("SLAVE_REGISTER_1 ");
  while (Wire.available() > 0){ //バッファにデータがある限り実行
     val = Wire.read(); //データを読み取り
     Serial.print(val);
     Serial.print(" ");         
  }
  Serial.println(" "); 

スレーブ側のコード

次にスレーブ側のコードです。

#include <Wire.h>

#define SLAVE_ADDRESS 0x01
#define SLAVE_REGISTER_1 0x00
#define SLAVE_REGISTER_2 0x01

uint8_t registerIndex = 0;
uint8_t data[2] = {0, 1};

void receiveEvent() {
  while(Wire.available() > 0){
    registerIndex = Wire.read();
  }
}

void requestEvent() {
  Wire.write(data[registerIndex]);
}

void setup() {
Wire.begin(0x01);
Wire.onReceive(receiveEvent);
Wire.onRequest(requestEvent);
}

void loop() {
// delay(100);
}

スレーブ側のコードです。
マスター側と見比べながら解説します。
マスターでのI2C通信のコードは

  Wire.beginTransmission(SLAVE_ADDRESS); //通信開始
  Wire.write(SLAVE_REGISTER_1); //0x00の送信
  Wire.endTransmission(false); //データ送信を終了 (接続は継続)
  Wire.requestFrom(SLAVE_ADDRESS, 1); //データ通信を要求(レジスタ0x00のデータ)

という流れになっていますので、スレーブ側では、「SLAVE_REGISTER_1 ( =0x00)」の受信を受けて、「SLAVE_REGISTER_1 ( =0x00)の中のデータを送信」という順番でI2C通信をする必要があります。

では、スレーブ側でのコードですが、まずはじめI2C通信でマスターからデータ(0x00)が送信され「Wire.onReceive(receiveEvent)」が割り込み実行されます。

void receiveEvent() {
  while(Wire.available() > 0){
    registerIndex = Wire.read();
  }
}

receiveEvent()の中身は、「Wireライブラリのバッファ内にデータがある限り (Wire.available() > 0)」「registerIndexに受信データ(0x00)を格納する」というコードになっています。
もちろん、0x00以外の場合はregisterIndexの値は送られてくるデータ(レジスタアドレス)が格納されます。

次に、マスターからの「Wire.requestFrom(SLAVE_ADDRESS, 1)」を受けて、今度は「Wire.onRequest(requestEvent);」が割り込み実行されます。

void requestEvent() {
  Wire.write(data[registerIndex]);
}

requestEvent()の中身は、「registerIndexdataを送信する (Wire.write(data[registerIndex]))」というもののになっています。
もちろん、0x00以外の場合は配列番号が変わるので別のデータが送られることになります。

uint8_t data[2] = {0, 1};

今回はdata[]の要素は{0,1}なので、SLAVE_REGISTER_1 ( =0x00)を受けてdata[0] = 0 を返し、SLAVE_REGISTER_2 ( =0x01)を受けてdata[1] = 1 を返すという単純なI2Cデバイスになります。

ついでながらregisterIndexは2つの要素しか持たないので、0x02より大きい数字を受けてしまうとエラーを吐くので、実際には場合分け処理などでエラーを吐かないようにする必要が出てきます。

ArduinoでI2C通信まとめ

今回はただ0と1を返すだけのI2Cデバイスですが、ここにボタンやエンコーダーなどをはじめ各種センサを取り付けてやれば簡単にI2Cデバイス化させることができますね。

実際、ロータリーエンコーダーのデータをI2Cで送ることでロータリーエンコーダーデコーダーを作ってやったりしている人がいたりします。
ATTiny85 でロータリーエンコーダーデコーダー : https://qiita.com/ELIXIR/items/69e34b2d068d7ffba9e3

まとめ

今回はArduinoを2台使ってI2C通信を行いました。
I2Cは低速ながら多数のデバイスを同時に扱えるようになるのでマスターしたいですね。

Arduinoカテゴリの最新記事