公開日:2018/12/10
title.png

3rd Season


#3-2 Arduino間I2C通信を試す

 前回(「#3-1 LCD1602に表示する」)試したLCD表示器ですが、売られているモジュールの中には、I2Cで通信できるようになっているモジュールがあります。I2C接続できれば、使用するI/Oポートは2本で済みます。
 今回は、I2Cについて少し掘り下げたいと思っています。

I2C付きのLCDモジュール

 前回のバス接続でLCD表示器を制御する方法では、7本のI/Oポートが必要なため、他にモジュールを使いたい時にI/Oポートが足りなくなる恐れがあります。I2Cなら、I/Oポート2本で通信可能な上に、I2Cはバスと言う形をとっていて、バス上に複数のI2Cモジュールを接続可能なため、他のモジュールを接続する場合にも邪魔にならない利点があります。

 市販されている、I2Cで通信可能なLCDモジュールですが、写真や仕様を見ていると、筆者の手元にあるLCDモジュールに、I2C接続のI/Oエクスパンダモジュールが接続されているだけの様です。I/OエクスパンダICの型番からデータシートを探したところ、NXPのPCF8574Aなどが見つかりました。似たようなチップが各社から出ているようなので、別なモジュールには他社のチップが載っているかもしれませんが、要は、I2Cで受けた8ビットデータを、そのまま8ビットの拡張I/Oポートに出力するだけのチップみたいです。
 LCDのコントローラを直接制御してくれるチップではないので、前回筆者が実行したような制御を、I/Oエクスパンダ経由でやっているだけ、ですね。当然、1回のアクセスを実行するだけで数バイトのデータをI2C経由で送る必要があり、アクセスにはそれなりの時間がかかるのではないでしょうか?
 このチップ(PCF8574A)は、入力ポートとしても、出力ポートとしても使用可能な優れものです。拡張I/Oポートとしては非常に有用であるとは思います。

 ただ、Arduinoを使ってI/Oエクスパンダのクローンを作っても仕方ないので、以下図のようなシステムを目指したいと思っています。

ar32_lcdsub.png

 LCDの表示にArduino 1個使うなんて、もったいないと思われるかもしれませんが、U/Iも付けて、Arduino 1個使う価値を創造したいと思います。

Arduino間のI2C通信

 前節のサブシステム構築を目指すにあたり、まずやるべきことは、Arduino間でのI2C通信です。
 I2Cについて、筆者はそれほど詳しくないので、基本的な知識は、以下で検索を……。

  I2C+仕様で検索する(別ウインドウが開いて検索します)

 元々はPhilipsと言う会社が提唱したインターフェイスなのですが、2線で通信できる、1本のバス(信号線数2本)に複数の機器が接続可能、複数のバスマスタが存在し得る、等々の利点が相当魅力的だったのか、様々な機器で採用されています。
 I2Cバスには、データを積極的(能動的)に送信したり受信したりするマスタデバイスと、マスタからの指示に従ってデータを受け取ったり、返したりするスレーブデバイスの2種類が存在します。
 Arduino(というよりATmega)に搭載されているTWI(Two Wire serial Interface)は、呼び名こそ違いますが、PhilipsのI2C仕様と互換性がある(Compatible with Philips’ I2C protocol)と謳われており、マスタにもスレーブにもなることが可能です。

 前節で示したサブシステムの図において、Arduino#1をI2Cマスタ、Arduino#2をI2Cスレーブとして通信できることを試してみます。

 Arduino HP ⇒ REFERENCEと進んで、ライブラリ(Standard libraries)の中から、Wireに進みます。これが、ATmegaデータシートで言うところのTWI、一般にはI2Cの通信ライブラリ(Wireクラス)です。いつもの通り、REFERENCEには書いていませんが、Arduino IDEから参照可能なサンプルコード(ファイルメニュー ⇒ スケッチ例 ⇒ Wire)を参考にして、#include <Wire.h>をプログラムの先頭で読み込んでおきます。後はsetup()関数の中で、「Wire.begin();」で準備完了なのですが、今回は、もう少しオプションがあります。

  #include <Wire.h>
  void setup (void)
  {
    // i2c
    if (mst0_slv1==i2c_MASTER)
    {
      // master
      Wire.begin ();
    }
    else
    {
      // slave
      Wire.begin (i2c_slv_adr);
      Wire.onReceive (i2c_receive);
      Wire.onRequest (i2c_request);
    }
  }


 ArduinoをI2Cマスタとして使用する場合は、「Wire.begin();」で準備完了です。ただし、スレーブとして使用する場合は、もう少し手順を踏む必要があります。

 I2Cバスには、複数のスレーブが接続可能なので、マスタがどのスレーブと通信するのか指定できるよう、全てのスレーブには、ユニークな「アドレス」が割り当てられています。Wire.begin()にスレーブとしてのアドレスを渡すと、それ以降、そのArduinoはI2CのスレーブとしてI2Cバスにぶら下がることになります。

      Wire.begin (i2c_slv_adr);

 スレーブアドレス(i2c_slv_adr)は、Arduino(ATmega)の場合、7ビットです。0b1111xxxと、0b0000xxxの16アドレスは、I2Cの規格書にて予約されているので、使えません。よって、アドレスとして使用可能な最小値は8です。プログラムの先頭付近で定義してあります。I2Cバスに他のスレーブが接続されない場合は、このままでOKです。他のスレーブが接続される場合、全てのアドレスがユニークになる必要があるので、他のスレーブと重複しないアドレスになるように変更してください。

  const int i2c_slv_adr=0x08;  // i2c slave address (7 bits)

 I2Cスレーブは、自分から積極的(能動的)にデータをやり取りすることはできません。マスタがデータを送信してきたとき(Wire.onReceive())に受け取ることと、マスタがデータを要求してきたとき(Wire.onRequest())に渡すことが可能です。

  Wire.onReceive (i2c_receive);
  Wire.onRequest (i2c_request);


 データを受信しないモジュールはWire.onReceive()を登録する必要は無く、同様に、データを返さないモジュールはWire.onRequest()を登録する必要がありません。今回はテストなので、両方登録しておきます。

 登録するi2c_receiveは、「void i2c_receive (int)」で宣言される関数です。マスタからデータを受信したときに、関数i2c_receive()がコールされますので、関数内でデータを受信(Wire.read())します。i2c_receive()は、「I2Cでデータを受信した」時に発生する、割り込み処理内から呼び出されているため、ISR(Interrupt Service Routine、「#2-2 スイッチを検出する」を参照)と同じ制約を受けますので、注意してプログラムを組む必要があります。

 i2c_requestの方は、「void i2c_request (void)」で宣言される関数です。「マスタから要求があった」時に発生する、割り込み処理内から呼び出されます。同様にISRとしての制約の中で実行してください。i2c_request()内で、要求されたデータを送信します(Wire.write())。

 I2Cの物理層では、結構複雑なプロトコルが流れていて、I2Cの通信回路を設計しなさいと言われたら裸足で逃げ出してしまいます(1^^;)が、その辺りは、ATmega328のTWI回路とArduinoのWireクラスが隠ぺいしてくれているので、以上で準備完了です。これまた簡単❤

スレーブ側の動作

 スレーブ側は、基本的にはloop()関数内で処理すべきことはありません。スレーブは、マスタから通信が来ない限り、何もすることがありません。
 活躍するのが、setup()で登録した2つの関数(ハンドラ)です。まずは、マスタからデータを受信したときに呼び出されるハンドラi2c_receive()から見ていきましょう。

  static char i2c_buf[max_lbuf];  // i2c receive data buffer

  void i2c_receive (
  int bcnt)
  {
    // for slave, receive from master device
    char chr;
    int idx;

    idx=0;
    while (Wire.available ()>0)
    {
      chr=Wire.read ();
      if (idx<max_lbuf - 1)
      {
        i2c_buf[idx]=chr;
        idx++;
      }
    }
    i2c_buf[idx]='\0';
  }


 i2c_receive()が呼び出されると、データ数が引数として渡されます(bcnt)。しかし、結局、スレーブは全てのデータを受信するしかないので、wire.available()で受信可能なデータが有る限り、データを受信(Wire.read())します。
 最終的には、引数で受け取ったbcntバイトのデータを受信するはずです。WireクラスのREFERENCEには、「I2Cのデータバッファは32バイト」であり、「この制限を超えたデータは取りこぼす(dropped)」と記載があります。これは、I2Cの仕様ではなく、Wireクラスの仕様です。よって、Wireクラスを使う限り、bcnt≦32が保証されます。受け取るバッファ(i2c_buf[ ])も、32バイト以上を確保しておけば、当面の問題は無いはずです。

 実際のアプリケーションでは、データをバッファに受け取ると同時に、データを受信したことを示すフラグを立てておき、loop()関数内でフラグをポーリングしてデータの処理を実行するような形になるはずです。先ほども説明した通り、i2c_receive()は、ISR内で実行されているので、他の割り込みがすべて禁止されています。i2c_receive()の中で時間のかかる処理をすべきではありません。

 続いて、マスタからデータ要求があった時に呼び出されるハンドラi2c_request()を見てみましょう。

  void i2c_request (void)
  {
    // for slave, request from master device
    const char *str_c;

    // send all string
    for (str_c=i2c_buf; (*str_c)!='\0'; str_c++)
    {
      Wire.write (*str_c);
    }
    Wire.write ('\0');
  }


 こちらのハンドラには何も引数が渡されません。「マスタから要求があった」という状況だけが既知です。マスタが、どんな情報を、どのくらいの分量必要としているのか?全くわかりません。そうした情報をあらかじめ取り決めるための会話(communication)または事前の取り決めが別途必要だと思われます。例えば、マスタがデータ要求を出す前に、マスタから事前に、要求するデータの詳細をコマンドとして送信しておくとか、です。

 ともあれ、今回はI2Cの通信テストですので、さっきio_receive()が受信したデータ(i2c_buf[ ])を、そのままエコーバック(Wire.write())しています。本プログラムにおいて、送信データはC string('\0'終端文字列)を前提としているので、最後に'\0'を送信していますが、これはI2CやWireクラスの規定ではありません。

マスタ側の動作

 マスタ側は、データを送りたい時に送り、要求したい時に要求しますので、スレーブの時のようなハンドラはありません。まずは、送信から見てみましょう。

  Wire.clearWriteError ();
  Wire.beginTransmission (i2c_slv_adr);
  Wire.write (&lbuf[4]);
  Wire.write ('\0');
  errcod=Wire.getWriteError ();
  if (errcod)
  {
    // ERROR:
    // エラー処理#1
  }
  errcod=Wire.endTransmission ();
  if (errcod)
  {
    // ERROR:
    // エラー処理#2
  }


 まず、書き込みエラーステータスをクリアします。

  Wire.clearWriteError ();

 送信データの準備を開始します。スレーブはI2Cバスに複数存在して良いので、どのスレーブと通信するか、アドレスを指定する必要があります。この例ではアドレスをi2c_slv_adr(=8)に設定してあります。

  Wire.beginTransmission (i2c_slv_adr);

 Wire.beginTransmission()と、Wire.endTransmission()の間で、Wire.write()を使って、送信したいデータを準備します。

  Wire.write (&lbuf[4]);
  Wire.write ('\0');


 本プログラムにおいて、送信データはC string('\0'終端文字列)を前提としているので、最後に'\0'を送信していますが、これはI2CやWireクラスの規定ではありません。
 ここで、Wire.write()がエラーになっていないかチェックしておきます。

  errcod=Wire.getWriteError ();
  if (errcod)
  {
    // ERROR:
    // エラー処理#1
  }


 この時点では、まだI2Cのトランザクションが発生していないため、発生するエラーは、Wireクラスの仕様である、「バッファサイズ32バイト」を超えたデータをWire.write()した場合です。このエラーが発生しても、32バイト以下のデータは送信準備が整っていますので、次のWire.endTransmission()に進んで、データを送信することは可能です。ただし、32バイトを超えた部分は送信されないため、必要に応じて対処が必要です。

 データの準備が整ったら、実際にI2Cのトランザクションを発生させて、データを送信します。

  errcod=Wire.endTransmission ();
  if (errcod)
  {
    // ERROR:
    // エラー処理#2
  }


 ここで改めて、I2C通信で発生したエラーがレポートされますので、エラーのチェックと対処が必要です。
 バッファサイズ(32バイト)の制約があるので、大量のデータを送信する場合には、データを32バイト毎に分割して、上述転送プログラムを実行するような手順が必要になると思います。今回はテストなので、32バイト以上のデータを送信しようとした時は、エラーまたはウォーニング(警告)をシリアルモニタに送出して終わりにします。

 次は受信ですね。まずは全体の流れから見てみましょう。

  Wire.requestFrom (i2c_slv_adr, BUFFER_LENGTH);
  // response
  Serial.print ("OK: ");
  while (Wire.available())
  {
    char chr;
    chr=Wire.read();
    Serial.print (chr);
  }
  Serial.println ("");


 まず、スレーブに対して、データを要求します。スレーブはI2Cバスに複数存在して良いので、どのスレーブと通信するか、アドレスを指定する必要があります。この例ではアドレスをi2c_slv_adr(=8)に設定してあります。
 BUFFER_LENGTHは、Wire.hの中で定義されているマクロで、Wireクラスのバッファサイズ32が設定されています。Wire.requestFrom()で受信すべきデータサイズの指定に使用しています。もし、必要なデータ数が4バイトなら、第二引数に4を指定すればOKです。

  Wire.requestFrom (i2c_slv_adr, BUFFER_LENGTH);

 実はこの関数が、既にスレーブと通信して、データを受け取っています。Wire.requestFrom()関数は、受信したデータサイズを返しています。ただ、「そういう仕様」なのか、スレーブが実際に送信したデータ数とは無関係に、Wire.requestFrom()の第二引数で指定したデータサイズがそのまま返ってきます。意味が無いので受け取っていません。

 受信可能なデータをWire.read()で読み込みます。

  while (Wire.available())
  {
    char chr;
    chr=Wire.read();
    Serial.print (chr);
  }


 テスト用に、そのままSerial(シリアルモニタ)に流していますが、実際に使うときには、何かしらのバッファに受信して使うことになるでしょう。

完成プログラムと実験回路

 プログラムが完成したので、完成版プログラムをリンクしておきます。

  完成プログラム:test_i2c.ino

 マスタ用とスレーブ用にプログラムが2本になるのも面倒なので、1本にまとめてあります。マスタとスレーブの選択用に、デジタルピンのD4を使っています。setup()関数内で、D4を読み取って、D4=LOWの時はマスタとして、D4=HIGHの場合はスレーブとして動作するようにしてあります。

 この先の実験には、2機のArduinoが必要です。2機のArduinoを以下の様に接続してください。

Arduino#1(マスタ) 接続 Arduino#2(スレーブ)
SCL(Microの場合はD3)
(プルアップ)
SCL(UNOの場合はA5)
SDA(Microの場合はD2)
(プルアップ)
SDA(UNOの場合はA6)
GND GND
D4 GND  
  × D4

 うまく表現できずにスミマセン。筆者の環境では、Arduino#1にMicro、Arduino#2にUNOを使用したため、ピンアサインが異なっており、上表のような表現になってしまいました。ポイントは、UARTの様に、TX→RX、RX→TXのようなクロス接続ではなく、SDA⇔SDA、SCL⇔SCLと、ストレートに接続することです。

 以下、実験中のボード写真です。

ar32_circt.jpg

 I2Cバスの規定で、SCLとSDA端子は、プルアップする規定になっています。上の表では括弧付き「(プルアップ)」となっていて、写真でもプルアップせずに直結していることにお気づきでしょうか(オレンジのジャンパ線)?

 Wire.begin()した時に、ArduinoのSDAとSCL端子の内蔵プルアップ抵抗が有効になっているため、それをそのまま使っています。
 でも、これって、Wireクラスの仕様として合っているの??スレーブの場合もマスタの場合も、有無を言わさずに内蔵プルアップ抵抗を有効にしているので、上写真のテスト環境では、マスタとスレーブの2つとも、プルアップしてますよ。しかも、ATmega328のデータシートによると、内蔵プルアップ抵抗は、20kΩ(MIN)〜50kΩ(MAX)で、I2Cのプルアップ抵抗値としては、ちょっと大きい。
 I2Cの規定では、バスの容量負荷を最大400pFと規定しています。負荷容量C(=400pF(MAX))と、プルアップ抵抗R(=50kΩ(MAX))が接続された場合、時定数C×Rを計算してみると、CR=20μsとなります。時定数は、0Vだった配線を5Vまで引き上げようとした時に5Vの63.2%まで上昇するのにかかる時間ですので、それが20μsかかるということです。
 I2Cの通信速度は、標準モードで最大100kHz、高速モードで最大400kHzと規定されています。Wireクラスのデフォルト通信速度は100kHzですので、I2Cのデータ転送用クロックSCLの1パルスがどのくらいの時間か?と言うと、1÷100kHz=10μs、SCLのHIGH幅がこの1/2と考えると5μsとなります。さっきの20μsと比べてみると、ちょっと厳しいですよね?
 つまり、内蔵プルアップ抵抗で済ませようというのは、今回の様に2つの機器を直結して、負荷容量が小さいことや、通信速度が遅いことなどの条件が重なって可能になるので、通常は外付けに適切なプルアップ抵抗をつけるべきです。
 その際、内蔵プルアップ抵抗が邪魔だとお考えなら、以下の方法で無効にすることが可能です。

  // to disable internal pull-up resistance
  pinMode (PIN_WIRE_SDA, INPUT);
  pinMode (PIN_WIRE_SCL, INPUT);
  //  or
  digitalWrite (PIN_WIRE_SDA, LOW);
  digitalWrite (PIN_WIRE_SCL, LOW);


 上のコードを、Wire.begin()の後で実行してください。

通信してみる

 2機のArduinoに同じプログラムを転送したら、双方、シリアルモニタを開いて、以下の表示を確認してください。

  RDY: master
    または、
  RDY: slave

 D4端子の設定によって、マスタとスレーブが正しく設定されていることを確認します。一方がマスタ、他方がスレーブになっていればOKです。

 この先は、マスタ側のシリアルモニタで実験を進めます。マスタ側のシリアルモニタでは、以下のコマンドを受け付けるようになっています。

 I2C通信周波数を【clk】コマンドで設定できるようにしておきました。スレーブモジュールによっては、遅い速度でしか通信できないものもあるので、その場合は、相手に合わせて通信速度を落とす必要が生じます。Wire.setClock()メソッドで設定します。
 I2Cの規格では、標準(standard)モードで最大100kHz、高速(fast)モードで最大400kHzが規定されており、これより遅い速度で通信して良いことになっています。この上に、ハイスピード(fast plus)モードなども新たに規定されており、最大3.4MHzで通信可能ですが、ATmega328では400kHzの高速モードが上限です。

  実行サンプル:【 】内はシリアルモニタからの送信データ
  【snd,Is this love?】
  OK: 1468[us] sent: Is this love?

  【rcv】
  OK: 3860[us] recv: Is this love?・・・・・・・・・・・・・・・・・・・

 「OK:」の後の数値は、通信に要した時間μsです。後で通信周波数を変更できたことを観測するために時間を計測しています。【snd】コマンドで文字列13文字を送信しました。【rcv】コマンドで受け取ったのは、スレーブが直前に受け取った文字列"Is this love?"なのですが、ごみがついています。

 実際のアプリケーションでは、マスタとスレーブ間でやり取りするデータに関して、事前の取り決めが必要だと思われます。あらかじめスレーブからの応答バイト数を固定しておくとか、最初の要求でスレーブがデータ数を返し、そのデータ数に応じて、マスタがデータを受信するとか、方法はいろいろとあると思います。

  実行サンプル:【 】内はシリアルモニタからの送信データ
  【clk,400】
  OK: f_scl=400[kHz]

  【snd,Is this love?】
  OK: 456[us] sent: Is this love?

  【rcv】
  OK: 1696[us] recv: Is this love?・・・・・・・・・・・・・・・・・・・

 通信周波数を100→400[kHz]に設定したので、通信にかかった時間が短縮しています。オシロスコープとかロジアナを持っていないので、通信波形で確認することはできませんが、通信にかかった時間の計測によって、間接的に「周波数が変わったな」とわかる仕組みです。
 シリアルモニタに使用しているUARTの最大通信速度が115,200baud→115.2kHzに相当、ですので、400kHzの高速モードを使うと、×4倍速でデータを送ることができます。……実効レートで考えると、もう少し控えめに考えておいた方が良いですが……。

※受信"rcv"の結果:「32バイトで1,696μs」を使うと、1ビット当たり1,696μs/32バイト/8ビット≒6.6μsで受信していますので、実効レートで約152k[baud]です。UARTの方は、8ビットの転送に10ビットのフレームを送るので、実効レート=115,200baud/10×8≒92k[baud]ですから、65%程、I2Cの方が速いですね。


 マスタとスレーブ間でデータのやり取りに関しての取り決めが必要なことなど、使用するにあたっての課題もはっきりしましたし、なにより、問題なく2機のArduino間でI2C通信ができたことは収穫でした。


btn_arduino.png

btn_prev.png btn_next.png

home