ArduinoでI2C相互通信

Arduinoを使って何かを制御するときに,『Arduino同士で制御したい!』,『複数のArduinoを束ねて大きなシステムとして構築したい!』なんてことはありませんか?

今回は,複数のArduinoを使ったI2C通信の基礎を解説します.

I2Cとは

I2C(アイ・スクエアド・シー)とは,I2C(以後I2Cとする)は通信方式の1つです.

フィリップス社によって開発された通信規格になります[1]

当ブログではI2Cの詳しい仕様に関して解説するのではなく,Arduinoによる具体的な使い方に焦点をあてていきます.

通信方式

仕組み

I2C通信はMasterSlaveと呼ばれる2つの役割に分類されます.

I2C 通信 は1台のMasterと複数台のSlaveで構築する通信方式です.1台のMasterが常に指示を出し,複数のSlaveはMasterの指示の下データを送受信します.

SlaveからMasterに対して何かしら要求することはできません.

構成

I2C 通信 は信号線2本とGNDの3本で構成することができます.ここに通常は電源としてVccを加え,計4線で構成されます.

2本の信号線はSDAとSCLで構成され,SDAはシリアルデータと呼ばれデータのやり取りに使用します.他方のSCLはシリアルクロックと呼ばれ,I2C通信に必要なクロック信号(動作の基準の信号)を伝えるのに使用します.

注意点

電子部品には電源電圧や通信線の電圧が5Vや3.3Vのものが混在しています.電圧の異なる部品同士を接続した場合,故障の原因となるので注意しましょう!

電圧の異なる部品同士でI2C通信する場合はレベル変換という,電圧の違う部品同士を接続するための回路が必要となります.

Masterとは

MasterはI2Cに基本的に1台のみ存在し,絶対的な権限を持ちます.

同じI2Cに接続されたSlaveはMasterの指示に従うことになります.

Masterになる部品はマイコンなど,「センサ情報を受け取る」,「アクチュエータに指令を出す」といった情報の処理や指令を担います.

Slaveとは

SlaveはI2C上に複数台構成することができます.

Slaveは自身がMasterからの指示があったときのみ通信できます.なので,Masterからの指示があって初めてデータの送信や受信が可能になります.

Slaveになる部品は主に,「センサから情報を取得する」,「アクチュエータで何かを動作させる」といった情報の取得や動作そのものを担います.


ArduinoによるI2C通信

始めに2台のArduinoを使用し,I2Cを使った簡単な通信を試します.

1台のArduinoをMaster,もう1台のArduinoをSlaveに設定します.本記事ではI2CでArduino間で送受信を実現するところまでを説明します.

Arduinoの接続

今回は2台のArduinoを直接接続します.回路構成を以下の図に示します.

上記構成の回路図を以下に示します.

まず,2台のArduino間の電源(5VとGND)を接続して共通化します.図中の赤と黒の配線です.

プログラムを動作させるときはMasterのArduinoまたはSlaveのArduinoをPCとUSBケーブルで接続します.

次にI2C通信に使用する信号線を接続します.図中の緑色と黄色です.

MasterのSDLとSlaveのSDL,MasterのSDAとSlaveのSDAをそれぞれ接続します.


Slaveからデータを取得

データの取得では2台のArduinoを使用し,I2Cを使った簡単な通信を試します.

1台のArduinoをMaster,もう1台のArduinoをSlaveに設定します.お互いのArduinoをI2Cで接続し,MasterがSlaveに対してデータを要求し,SlaveはMasterにデータを送信します.

プログラム

次にArduinoにプログラムを記述していきます.なお,Arduinoにプログラムを書き込む方法等に関しては別の記事をご覧ください.

Masterのプログラム

/*Masterのプログラム*/
#include <Wire.h> //I2C通信に必要

//初期設定
void setup() {
  Wire.begin();//Masterとして設定
  Serial.begin(9600);//シリアルモニタ用に設定
}

//ループ文
void loop() {
  Wire.requestFrom(8, 1);//IDが8番のSlaveから1byteのデータを要求

  while (Wire.available()) {//受信データがある場合はwhile文を抜けない
    byte data = Wire.read();//byte型の変数dataに受信データを代入
    Serial.println(data);//シリアルモニタに受信データを表示
  }
  delay(500);//500ms停止
}

MasterのプログラムはI2C通信のための処理とシリアルモニタに表示するための処理が記述されています.

今回は500ms経過する毎にMasterからSlaveに対してデータを要求するように記述しています.

Slaveのプログラム

/*Slaveのプログラム*/
#include <Wire.h> //I2C通信に必要
byte data = 0;//byte型の変数dataを初期化
void setup() {
  Wire.begin(8);//IDを8番としてSlaveに設定
  Wire.onRequest(DataRequest);//Masterから要求ががあったときに呼び出す関数
}
void loop() {
}
void DataRequest() {
  Wire.write(data);//データを送信
  data = data + 5;//data変数に5を加算する
}

SlaveのプログラムではWire.onRequest()というArduinoのライブラリに用意されている関数を使用しています.

この関数はMasterからi2c通信の送信要求が来たときにDataRequest()という関数を呼び出しています.

DataRequest()という関数は自分で設置した関数になります.よって関数名は自分で変更可能です.

関数DataRequest()はデータの送信と変数を5増加させるプログラムになります.

注意点

Wire.onRequest() で呼び出される関数はloop処理中でも強制的に割り込んで処理が実行されます.

呼び出される関数は処理が少なくなるように記述する必要があり,今回のプログラム例では関数 DataRequest() が該当します.

この関数内の処理ができるだけ少なくなるように記述します

実行結果

上記のプログラムを実行した結果について示します.MasterのArduinoにMasterのプログラムを書き込み,SlaveのArduinoにSlaveのプログラムを書き込みます.

Master側のシリアルモニタを表示すると表示される値が5増加していることが確認できます.よって,SlaveからMasterにデータが送信されたことを確認できました.


Slaveにデータを送信

次にMasterがSlaveに向けてデータを送信します.Slaveでは受信したデータを表示して確認します.

プログラム

Masterのプログラム

/*Masterのプログラム*/
#include <Wire.h> //I2C通信に必要
/*変数の定義*/
byte data;
//初期設定
void setup() {
  Wire.begin();//Masterとして設定
  /*変数の初期化*/
  data = 0;
}
//ループ文
void loop() {
  Wire.beginTransmission(8);//ID8のSlaveとの通信確立
  Wire.write(data);//送信用データの選択
  Wire.endTransmission();//データの送信と送信終了
  data = data + 10;//data変数に10を加算する
  
  delay(500);//500ms停止
}

MasterのプログラムはI2C通信とデータを送信するための処理が記述されています.

500ms経過する毎にMasterからSlaveに対してデータを送信するように記述しています.

Slaveのプログラム

/*Slaveのプログラム*/
#include <Wire.h> //I2C通信に必要
byte data = 0;//byte型の変数dataを初期化
void setup() {
  Wire.begin(8);//IDを8番としてSlaveに設定
  Serial.begin(9600);//シリアルモニタ用に設定
  
  Wire.onReceive(DataReceive);//Masterからデータを受信したときに呼び出す関数
}
void loop() {
}
void DataReceive(){
  while (Wire.available()) {//受信データがある場合はwhile文を抜けない
    byte data = Wire.read();//byte型の変数dataに受信データを代入
    Serial.println(data);//シリアルモニタに受信データを表示
  }
  
}

SlaveのプログラムではWire.onReceive ()というArduinoのライブラリに用意されている関数を使用しています.

この関数はMasterからデータが送信されたときに DataReceive()という関数を呼び出しています.

DataReceive()という関数は自分で設置した関数になります.よって関数名は自分で変更可能です.関数 DataReceive ()は受信したデータをシリアルモニタに表示するプログラムです.

実行結果

上記のプログラムを実行した結果について示します.MasterのArduinoにMasterのプログラムを書き込み,SlaveのArduinoにSlaveのプログラムを書き込みます.

Slave側のシリアルモニタを表示すると表示される値が10増加していることが確認できます.よって,MasterからSlaveにデータが送信されたことを確認できました.


MasterとSlave間で相互通信

最後はMasterとSlave間で相互通信する方法です.

MasterがSlaveに対してデータを要求するとSlaveはMasterにデータを送信します.Masterはデータ受信後にSlaveに向けてデータを送信します.

MasterとSlaveで送受信したデータをシリアルモニタに表示し,正しく動作したのか確認します.

プログラム

これまでやってきたSlaveからデータを取得する方法とSlaveにデータを送信するプログラムを組み合わせて相互通信するプログラムを実現します.

Masterのプログラム

/*Masterのプログラム*/
#include <Wire.h> //I2C通信に必要
/*変数の定義*/
byte data;
//初期設定
void setup() {
  Wire.begin();//Masterとして設定
  Serial.begin(9600);//シリアルモニタ用に設定
  /*変数の初期化*/
  data = 0;
}
//ループ文
void loop() {
  /*シリアルモニタに表示*/
  Serial.println(" ");//シリアルモニタの表示を改行
  
  /*データ受信処理*/
  Wire.requestFrom(8, 1);//IDが8番のSlaveから1byteのデータを要求
  
  while (Wire.available()) {//受信データがある場合はwhile文を抜けない
    data = Wire.read();//変数dataに受信データを代入
    
    /*シリアルモニタに表示*/
    Serial.print("Get_data << ");//シリアルモニタに表示
    Serial.println(data);//シリアルモニタに受信データを表示
  }
  Wire.endTransmission(false);//データの送信と接続終了かつ接続解除
  /*送信用データ生成*/
  data = data + 5;//data変数に5を加算する
  /*待機時間*/
  delay(500);//500ms停止
  /*データを送信処理*/
  Wire.beginTransmission(8);//ID8のSlaveとの通信確立
  Wire.write(data);//送信用データの選択
  Wire.endTransmission(false);//データの送信と接続終了かつ接続解除
  /*シリアルモニタに表示*/
  Serial.print("Send_data >> ");//シリアルモニタに表示
  Serial.println(data);//シリアルモニタに送信データを表示
  
  /*待機時間*/
  delay(500);//500ms停止
}

MasterのプログラムはSlaveからのデータ受信とSlaveへのデータ受信を実現するプログラムを記述しています.

受信と送信時にシリアルモニタにデータを表示するようにしています.

注意点

送受信する際の注意点として,受信や送信が終了した際に接続を解除する必要があります.接続を解除しない場合はデータの送受信を切り替えることが出来ないので注意が必要です. Wire.endTransmission(false)をfalseにすることで接続が解除されます.

Slaveのプログラム

/*Slaveのプログラム*/
#include <Wire.h> //I2C通信に必要
byte Get_data = 0;//byte型の変数Get_dataを初期化
byte Send_data = 0;//byte型の変数send_dataを初期化
void setup() {
  Wire.begin(8);//IDを8番としてSlaveに設定
  Serial.begin(9600);//シリアルモニタ用に設定
  
  Wire.onRequest(DataRequest);//Masterから要求ががあったときに呼び出す関数
  Wire.onReceive(DataReceive);//Masterからデータを受信したときに呼び出す関数
}
void loop() {
}
void DataRequest() {
  /*データを送信処理*/
  Wire.write(Send_data);//データを送信
  
  /*シリアルモニタに表示*/
  Serial.print("Send_data >> ");//シリアルモニタにを表示
  Serial.println(Send_data);//シリアルモニタに送信データを表示
}
void DataReceive(){
  
  /*データ受信処理*/
  while (Wire.available()) {//受信データがある場合はwhile文を抜けない
    Get_data = Wire.read();//byte型の変数dataに受信データを代入
    
    /*シリアルモニタに表示*/
    Serial.println(" ");//シリアルモニタの表示を改行
    Serial.print("Get_data << ");//シリアルモニタに表示
    Serial.println(Get_data);//シリアルモニタに受信データを表示
    /*送信用データ生成*/
    Send_data = Get_data;//受信したデータを送信データに代入
  }
}

Slaveのプログラムでは Wire.onRequest()とWire.onReceive ()の2つの関数を利用します.この2つは最初に紹介したプログラムで既に使用しています.

関数 Wire.onRequest() はMasterからi2c通信の送信要求が来たときにDataRequest()という関数を呼び出しています.

一方の関数 Wire.onReceive () はMasterからデータが送信されたときに DataReceive()という関数を呼び出しています.

DataRequest()とDataReceive()という関数は自分で設置した関数になります.よって関数名は自分で変更可能です.

DataRequest()はデータを送信し, シリアルモニタに表示するプログラムです.関数 DataReceive ()は受信したデータを送信用変数に代入し,シリアルモニタに表示するプログラムです.

実行結果

上記のプログラムを実行した結果について示します.

また,MasterのArduinoにMasterのプログラムを書き込み,SlaveのArduinoにSlaveのプログラムを書き込みます.

MasterとSlaveのシリアルモニタを表示を確認します.

Master側は受信した値を5増加させて送信しています.一方のSlave側は受信したデータをそのまま送信していることがわかります.よって,MasterとSlave間でデータを送受信することが出来ました.

まとめ

お読みいただきありがとうございました.

今回は簡単にI2Cについて説明し,I2C通信ではMasterとSlaveがあることを説明しました.

その後,実際に2台のArduino接続しI2C通信でデータの送受信するプログラムを順に作成しました.

なにか参考になれば幸いです.それでは〜!

参考文献

  • 本記事中の回路図及びシステム構成図はFritzingを用いて作成しています.https://fritzing.org/
  • [1] Wikipedia,I2C,I2C – Wikipedia,2021年10月7日