前回紹介したシリアル通信はクロック信号がないため、ノイズによる干渉の影響を受けやすいという欠点があります。これを改善するため、フィリップスという会社がI2Cインタフェース(「アイスクエアドシー」または「アイツーシー」と読みます)を開発しました。今回は2つのArduino間でI2Cによるデータ送受信プログラムを紹介します。
I2Cの概要
I2Cは「Inter-Integrated Circuit」の略で、デバイス間でデータ通信を行うための組み込み集積回路を意味します。シリアル通信では常に1対1で通信を行いますが、I2Cはバス接続することでより多くのコンポーネントを接続して使用できます。(発表当初は最大112個まで通信可能で、その後最大1008個まで拡張されました)。
電気的に、バスはすべてのコンポーネントが接続されている2本のラインと、共通のグランドだけで構成されています。ラインの1つはクロック信号線(Clock – CLK)として、もう1つのラインはデータ通信線(Serial Data – SDA)として使用します。
I2C通信では、接続されたコンポーネントが「マスター」か「スレーブ」という役割を持って動作します。コンポーネントのうち1つが常に「マスター」としてふるまいます。マスターはクロック信号を生成し、バス全体の通信を管理します。会議の議長・司会・進行役のような役割です。
他のすべてのコンポーネントは「スレーブ」として動作し、マスターに従います。各スレーブは、マスターが指示を送るために個別のアドレス(数字の並び)を割り当てられていて、マスターはいつでもスレーブにデータを送信できます。
しかし、スレーブの方はいつでもデータ送信できるわけではありません。マスターから通信があった時にデータ送信の許可が与えられ、その場合にのみデータを送信できます。
このしくみにより、1本しかない通信線に複数のコンポーネントが同時に信号を送信して、信号が混線してしまう事態を防いでいます。
ArduinoでI2Cを使う場合、たいていArduinoはマスターとして動作し、センサやディスプレイなどのコンポーネントをスレーブとして接続・通信します。I2Cを利用して複数のArduinoを接続することも可能です。
Arduino間でI2C通信する
1つのArduinoがマスターとして動作し、I2Cバスを介してアナログ入力で測定された可変抵抗の位置を2番目のArduinoに送信し、スレーブ側のLEDの明るさ(デューティ比)を制御します。
以前に紹介したシリアルポートと同様、Arduinoに使われているATmega328Pマイクロコントローラは、I2Cインタフェース用の独立したピンを持ちません。アナログピンのA4(SDA)・A5(SCL)と共通になっているため、I2Cインタフェースを使用する場合、この2つのピンをアナログ入力ピンとして使用できなくなります。 また、SDA・SCLのコネクタは、D13ピンの近くにも設けられています。(Arduino UNO R3以降の新しいArduinoに設けられています。A4・A5ピンと共通なことに変わりはないので、こちらを使用してもA4・A5ピンはアナログ入力ピンとしては使えなくなります。)
マスター側のプログラム
まず、左側のArduinoに以下のスケッチを書き込みます。
#include "Wire.h" //(1) void setup() { Wire.begin(); //(2) } int knob = 0; //(3) void loop() { knob = analogRead(2); //(4) knob /= 4; //(5) Wire.beginTransmission(8); //(6) Wire.write(knob); //(7) Wire.endTransmission(); //(8) delay(500); }
スケッチの内容の解説
(1) includeコマンドを最初に記述します。スケッチがコンパイラに渡される前に、”Wire.h”からプログラムコードをスケッチに埋め込みます。“Wire.h”はI2C通信に必要な機能を提供するライブラリです。このファイルの中身は関数を定義しています。I2C通信などはよく使われますが、使うために本来は複雑な手順を踏む必要があります。ライブラリで関数として利用することで複雑な手順を気にすることなく、スケッチをより簡単に作成できるようになります。例えばI2C通信の制御ではクロック信号を発生させる、送信するデータをビットごとに分けて送信する、正しく送信できたかチェックするなど、多くの作業が必要になります。ライブラリを使うことで、そういった多くの作業を気にすることなく、通信しなさいという簡単な指示だけで通信ができるようになります。他にも様々なライブラリがあり、簡単にインターネットからArduino IDEにダウンロード・インストールして使用できます。
(2) ”Wire.h”ライブラリにはI2Cに関するすべての機能を持つオブジェクト「Wire」が用意されています。「Wire.begin()」関数を呼び出すと、ArduinoのI2Cインタフェースが動作を開始します。この例のように引数を渡さずに実行すると、ArduinoはI2Cマスターとして動作します。「Wire.begin()」はsetupループの中で呼び出し、スケッチ実行時の最初1回だけ動作させます。
(3) 可変抵抗から読み取った値を保存するためのint型変数「knob」を定義します。
(4) アナログ入力ピンを使って電圧を読み取る方法はすでに学習しました。ここでこの読み取り値は10ビット、つまり0~1023の間の数値となることが重要なポイントです。
(5) 一方、I2Cを使ったデータ通信では1バイト(=8ビット)単位でのデータ送信しかできないため、4で割ることで8ビット(0~255)の範囲に収めます。例えば元の読み取り値が1023のとき255、512のとき128を送信します。
(6) 「Wire.beginTransmission()」関数はデータ送信を開始する命令です。カッコの中にはデータを送信したいスレーブのアドレスを入れておきます。通常は8~119の間を使用します。ここでは「8」としました。
(7) 「Wire.write()」関数は送信するデータを用意する命令です。この関数はデフォルトで1バイトのデータ送信しか行えません。今回のanalogRead()関数で読み取った変数knobは1バイトより大きいデータでしたが、(5)の工程で1バイトに収めているので問題なく送信できます。1バイトよりも大きなデータを渡した場合、下位ビット側から8ビットだけが送信されます。1バイトより大きなデータや多くのデータを送信したい場合は、データを1バイト単位でいくつかに分けて転送します。
(8) 「Wire.endTransmission()」関数はWire.write()関数で用意したデータをI2Cバスに送信して、I2C接続を終了する命令です。
上記のスケッチでは、アナログ入力ピンA2で可変抵抗の値を読み取り、その値をI2C経由でスレーブに送信する、という動作を1秒間に2回繰り返しています。
スレーブ側のプログラム
続いて、右側のArduinoに以下のスケッチを書き込みます。
#include "Wire.h" volatile byte receiveValue; //(1) void setup() { Wire.begin(8); //(2) Wire.onReceive(dataReceive); //(3) pinMode(10, OUTPUT); } void loop() { analogWrite(10, receiveValue); } void dataReceive(int Number) { //(4) if(Wire.available()) //(5) receiveValue = Wire.read(); //(6) }
ここでは、I2Cデータの受信を、「割り込み」というプログラミングの技法を使用して実現します。割り込みは、マイクロコントローラが持っている機能のひとつです。特定の動作があった際に、通常のプログラムフローを中断して別の処理を行います。この例では、「I2Cマスターからデータを受信した」時に割り込みが発生し、Arduinoの「loop文の実行」を一時止めて、受信したデータに関する処理を行います。
割り込みを発生させる動作はI2Cのデータ受信以外にもいくつかあります。より身近な例を挙げると、何か作業をしているときに電話がかかってきたとします。どうするでしょうか。それまでやっていた作業を中断して電話を取るでしょう。この電話がかかってくることが「割り込み」です。
スケッチの内容の解説
(1) I2C通信で受け取った数値を代入するための変数receiveValueを定義しますが、ここではキーワード volatile を追加で使用します。これによって、変数receiveValueは通常のプログラムフローの外でも(ここでは割り込みによって)、変更できるようになります。
(2) このArduinoはI2Cのスレーブとして動作させます。Wire.begin()関数にアドレスの「8」を引数として渡して実行します。
(3) 「Wire.onReceive()」関数はI2C通信でデータを受信したときの動作(呼び出す関数)を定義します。データを受信した際に実行する関数の名前を指定します。このような関数を「割り込みサービスルーチン(ISR)」と呼びます。先の例で言うと、「電話がかかってきたとき」が「Wire.onReceive()」関数で、「電話に出る」という動作をISRとして指示します。
(4) (3)で指定したISRを宣言しています。ISRで使用する関数は戻り値を持たないvoid型の関数を使用します。今回のdataReceive関数では、引数として受信したバイト数を表すint型変数Numberを定義しています。今回は定義しただけで使用していません。割り込みは実際のプログラムの動作を止めて別の動作を優先して行わせることから、ISRはプログラムの流れを乱してしまいます。このため、ISRは可能な限り短くする必要があります。また、ISRの実行中には別に割り込みが発生してもそちらには反応しません。このことからも、「受信した値を変数に代入する」などの単純な受信のみ行うことが望ましいのです。
(5) データが実際に受信されたかどうかの確認を行います。I2Cでデータを受信して割り込みでdataReceive関数が呼び出されたとき、ここではif文はtrueを返します。割り込み以外の場所で誤ってdataReceive関数を呼び出してしまうこともできてしまいますが、この時は何かエラーが発生するかもしれません。この確認を行うことで、関数を誤って呼び出してしまった場合には後の処理をパスできます。
(6) 受信したデータを変数receiveValueに代入します。
(7) このままISR内でanalogWrite()関数で出力することもできますが、先述の通りISRは極力短い方が良いので、変数に代入するだけにします。
(8) 実際にanalogWrite()関数で出力するのはメインループloop()内で行います。
割り込みを使わなくても、loop文の中でI2C通信の内容を読み取ることもできます。しかし、その場合にはマスターがデータ送信するタイミングで、スレーブがデータを受け取れる状態になっていないといけません。スレーブがデータを受け取れない状態ならば通信に失敗します。特にスレーブになるコンポーネントをたくさん使用する場合、スレーブ側では割り込みを利用しないと正しく通信を行うことが難しくなります。
TINKERCADシミュレーション
シミュレーションを開始ボタンを押してください。左側Arduinoの可変抵抗を動かすと、オシロスコープに表示されるPWMデューティーに連動して、LEDの明るさが変化するのが分かると思います。もし表示されない場合は、TINKERCADのページに飛んでください。
張江成知 says
I2C通信でTVチュナー2台を同じCHで動作さたいのですが、単純にパラ接続でよいですか。
アドレスはおなじです。よろしくお願いいたします。
STEMSHIP says
TVチュナーがスレーブで、どのCHで動作させるかを指示するコントローラ(?)がマスターでしょうか?
TVチュナー2台が同じアドレスだと、TVチュナー2台からコントローラへの送信データが衝突してしまうのではないでしょうか?