Arduinoを使って何かを制御するときに,『Arduino同士で制御したい!』,『複数のArduinoを束ねて大きなシステムとして構築したい!』なんてことはありませんか?
今回は,複数のArduinoを使ったI2C通信の基礎を解説します.
目次
I2Cとは
I2C(アイ・スクエアド・シー)とは,I2C(以後I2Cとする)は通信方式の1つです.
フィリップス社によって開発された通信規格になります[1].
当ブログではI2Cの詳しい仕様に関して解説するのではなく,Arduinoによる具体的な使い方に焦点をあてていきます.
通信方式
I2C通信はMaster,Slaveと呼ばれる2つの役割に分類されます.
I2C 通信 は1台のMasterと複数台のSlaveで構築する通信方式です.1台のMasterが常に指示を出し,複数のSlaveはMasterの指示の下データを送受信します.
SlaveからMasterに対して何かしら要求することはできません.

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

2本の信号線はSDAとSCLで構成され,SDAはシリアルデータと呼ばれデータのやり取りに使用します.他方のSCLはシリアルクロックと呼ばれ,I2C通信に必要なクロック信号(動作の基準の信号)を伝えるのに使用します.
MasterはI2Cに基本的に1台のみ存在し,絶対的な権限を持ちます.
同じI2Cに接続されたSlaveはMasterの指示に従うことになります.
Masterになる部品はマイコンなど,「センサ情報を受け取る」,「アクチュエータに指令を出す」といった情報の処理や指令を担います.
SlaveはI2C上に複数台構成することができます.
Slaveは自身がMasterからの指示があったときのみ通信できます.なので,Masterからの指示があって初めてデータの送信や受信が可能になります.
Slaveになる部品は主に,「センサから情報を取得する」,「アクチュエータで何かを動作させる」といった情報の取得や動作そのものを担います.
ArduinoによるI2C通信
始めに2台のArduinoを使用し,I2Cを使った簡単な通信を試します.
1台のArduinoをMaster,もう1台のArduinoをSlaveに設定します.本記事ではI2CでArduino間で送受信を実現するところまでを説明します.
今回は2台のArduinoを直接接続します.回路構成を以下の図に示します.

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

まず,2台のArduino間の電源(5VとGND)を接続して共通化します.図中の赤と黒の配線です.
プログラムを動作させるときはMasterのArduinoまたはSlaveのArduinoをPCとUSBケーブルで接続します.
次にI2C通信に使用する信号線を接続します.図中の緑色と黄色です.
MasterのSDLとSlaveのSDL,MasterのSDAとSlaveのSDAをそれぞれ接続します.

データの取得では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増加させるプログラムになります.
実行結果
上記のプログラムを実行した結果について示します.MasterのArduinoにMasterのプログラムを書き込み,SlaveのArduinoにSlaveのプログラムを書き込みます.

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

次に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に対してデータを要求すると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へのデータ受信を実現するプログラムを記述しています.
受信と送信時にシリアルモニタにデータを表示するようにしています.
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日