公開日:2019/10/26
title.png

8th Season


#8-1 Pi ZeroWのI2C通信

 「#7-7 Pi ZeroWの非同期シリアル通信」では、Pi ZeroWとArduinoをUARTで接続しました。UARTは、手軽に使えて、非常に便利なのですが、双方対称な通信システムであること、最大通信速度が115,200baudであること、から、もう少し違ったソリューションを用意しておくのも、良いかな?と、思っています。
 そこで、今回は、Pi ZeroWのI2C通信を掘り下げてみたいと思います。今回の通信相手も、Arduinoです。Pi ZeroWがI2Cマスタ、ArduinoがI2Cスレーブとなって、通信することを試してみます。双方、Fastモードの通信に対応しているので、最大通信速度は400kHzと、UARTより高速な通信が期待できます。

Pi ZeroWのI2C通信の準備

 まず、RaspbianでI2Cを使えるように設定されている必要があります。「#7-2 デザートにラズベリーパイはいかが?」で説明した通りにしてあれば、既に有効化されているはずです(以下図)。

ar77_rpiset.png

 もう一つ、I2Cを使う場合の通信速度を設定しておく必要があります。こちらは、Raspbianの設定ファイルを編集して、追加(または変更)する必要があります。

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【sudo vi /boot/config.txt】 …… 設定ファイルをroot権限で編集する
  dtparam=i2c_baudrate=400000
    【ZZ】 …… セーブしてエディタを抜ける

 「dtparam=i2c_baudrate=」の設定は、デフォルト状態では、/boot/config.txtファイルに存在しません。この場合、I2C standardモードの最高周波数である、100kHzになっているらしいです。今後、何らかのI2CスレーブモジュールをPi ZeroWに接続しようとした時、そのモジュールの最高通信周波数に合わせて変更する必要がありますので、これを機会に、「dtparam=i2c_baudrate=」の行を追加しておいてください。
 設定値はHz単位の周波数なので、デフォルトの100kHzで使いたい場合には、「dtparam=i2c_baudrate=100000」としておけば、OKです。なお、Pi ZeroWのSOC(BCM2835)搭載I2Cユニットは、Fastモード(@400kHz)が最速のユニットなので、一旦、400kHzに設定しておきます。

  【sudo reboot】 …… 設定を有効にするために再起動が必要です

 ArduinoのWireライブラリでは、ダイナミック(使用時)に周波数を設定できるのですが、Raspbianでは、再起動が必要です。I2Cは、プラグ&プレイができるような仕様では無いので、仕方ないとは言え、面倒ですね……。

※色々調べている時にぶつかったのですが、/boot/config.txtに「core_freq=250」の設定を追加しておかないと、所望の設定にならない機体があるみたいです。どうやら、この設定、GPUのクロックを250MHzに制限する設定らしいです。Pi ZeroWのGPUは元々250MHz動作なので、恐らく、この設定は不要なはずです。……GPUのクロックドメインに、I2Cユニットも一緒に属しているのが問題らしい(←設計ミスだよね?)。

C言語でのアクセス方法

 C言語でI2Cのアクセスを行うには:

 こんな感じだそうです。まあ、上の方法でも、特に難しいことはありませんが、本HP(Arduino DIY)では、wiringPiライブラリの使用を前提としているので、そちらを使います。

  // initialize
  if (wiringPiSetupPhys ()==-1)
  {
    // ERROR:
    perror (NULL);
    return (-1);
  }


 wiringPiライブラリを初期化して、使用可能な状態に整えます。

  // I2C
  fi2c=wiringPiI2CSetup (i2c_slv_adr);
  if (fi2c<0)
  {
    // ERROR:
    perror (NULL);
    return (-1);
  }


 wiringPiのI2Cライブラリをセットアップします。i2c_slv_adrは、アクセス対象のスレーブアドレスですが、どうやら、この時点でスレーブアドレスが存在しなくても(バス上に見つからなくても)、エラーは返ってこないみたいです。wiringPiI2CSetup()関数の返り値fi2cは、open()関数でデバイスを開いた時に返ってくるファイルディスクリプタと同じものですので、この後、write()関数やread()関数を使ってアクセスすることができます。

※I2C関連の関数を使うときには、#include <wiringPiI2C.h>が必要となります。また、write()関数やread()関数を使って、複数バイトを転送する場合、Arduino Wireライブラリのバッファサイズ(=32バイト、デフォルト)以内に収まるようにする必要があります。

 open()関数で開いたのと同じなので、I2Cの接続を終了する時は、普通に「close (fi2c);」で閉じます。
 ハイ、これだけ。ですが、wiringPi HPのドキュメントを眺めていると、以下の様な関数があることに気づきます。

  wiringPiI2CWriteReg8()
  wiringPiI2CWriteReg16()
  wiringPiI2CReadReg8()
  wiringPiI2CReadReg16()

 どうやら、これらの関数は、SMBusというプロトコルに従って、I2Cデバイスにアクセスするための関数みたいです。もう少し詳しく掘り下げてみましょう。

SMBusプロトコル

 SMBus(System Management Bus)と言うのは、I2Cで接続されたスレーブデバイスと通信して、制御するためのプロトコルみたいです。

  SMBusの仕様について調べる(別ウインドウが開きます)

 ざっと図にしてみました。

ar81_smbus.png

 ちょっと大きめの箱は、8ビットのデータ転送を表しています。普通のI2Cと違うのは、SMBusスレーブに対して、何らかのコマンド(CMD)を渡せて、それに関係するデータ(DAT)をやり取りできる、と言うことです。通信自体は、I2Cの通信プロトコルに則って実行されるので、I2C通信として見て矛盾がありません。
 ただし、コマンドCMDやデータDATと言った、通信データの「使い道」に関しては、SMBusの仕様となりますので、相手がSMBus対応のモジュールでない限り、正しく通信することはできません。

※SMBusには、CMDの存在しないプロトコルや、PEC(Packet Error Code)付きのプロトコル等も存在します。詳しくは、上で検索したSMBusの仕様書をご覧ください。

 SMBus ver.2.0では、動作周波数範囲が10〜100[kHz]と規定されていたようですが、ver.3.0では、400kHzと1MHzクラスが追加されているみたいです。ここで、もう一度、前節のwiringPiの関数仕様を詳しく見てみましょう。

  wiringPiI2CWriteReg8 (int fi2c, int CMD, int DAT)
  wiringPiI2CWriteReg16 (int fi2c, int CMD, int DATHL)
  DAT=wiringPiI2CReadReg8 (int fi2c, int CMD)
  DATHL=wiringPiI2CReadReg16 (int fi2c, int CMD)

 こんな感じで、引数に渡されたCMDとDATを使って、SMBusトランザクションを生成してアクセスしてくれます。実は、書き込み(Write Byte/Word)の方は、write()関数でも作れそうなのですが、読み出し(Read Byte/Word)の方は、write()関数とread()関数を組み合わせても、作り出せません。理由は、上の図に示した反復STARTコンディション(R)が作り出せないからです。
 その辺りをうまく処理してくれる関数と言うことで、便利に使うことができます。

 さて、SMBusの使い道ですが、I2CはUARTと異なり、非対称の通信で、マスタとスレーブが存在します。UARTの様に、お互いにデータを垂れ流して通信するのではなく、明らかに、マスタが何らかの意思を持ってスレーブにアクセスすることが目的な訳です。
 そのようなケースで、マスタが単に「2バイトのデータを送れ」と言って、スレーブにリードリクエストを送っても、スレーブ側としては、「『何の?』データを2バイト送ればいいの?」ってことです。
 この点、SMBusでは、リードする時にもコマンド(CMD)をスレーブに対して送信可能なので、このCMDで、アクセスする対象のデータは何なのか、スレーブに命じることができます。

 例えば、スレーブに16本のI/Oレジスタがあって、0〜15の番号が付いていたとします。「CMDには、I/Oレジスタ番号を送る」という取り決めをしておくと、マスタは、3番レジスタを読み出したい時、CMD=3として、SMBusリードトランザクションを発行すれば良いことになります。

サンプルアプリケーション

 Arduinoで、SMBus対応のスレーブとして応答するプログラムを走らせておき、Pi ZeroWがSMBusマスタになって、Arduino内に設置したI/Oレジスタにアクセスしてみたいと思います。

 と言って、現時点では、特定の用途を想定できないので、I/Oレジスタアクセス型のアプリケーションをイメージした上で、単なる256バイトのRAMとして機能するスレーブモジュールを作ります。要は、「フリーでアクセス(リード/ライト)可能なI/Oレジスタが256バイトある」感じですね。
 アクセスサイズは、1バイトのみとします。よって、2バイトデータにアクセスする、Write Word/Read Wordトランザクションには対応しません。また、CMDには、256バイトのレジスタアドレス(0〜255)を、そのまま指定することに決めておきます。

Arduino側のプログラム

 ArduinoをI2Cスレーブとして使うアプリケーションについては、「#3-2 Arduino間I2C通信を試す」で解説済みですので、詳しくはそちらをご覧ください。今回は、単なるI2Cスレーブではなく、SMBusスレーブとして動作する必要があるので、この点にフォーカスします。

 以下、ArduinoをI2Cスレーブとしてセットアップするためのコードです。

  // i2c slave
  Wire.begin (i2c_slv_adr);
  Wire.onReceive (i2c_receive);
  Wire.onRequest (i2c_request);


 では、I2Cマスタから、データを受信した時に呼び出される、onReceive()ハンドラのコードを示しながら、その下に解説を追加していきます。

  void i2c_receive (
  int bcnt)
  {
    // receive from master device
    int cmd;

    switch (bcnt)
    {


 マスタからデータを受信した時に呼び出されるハンドラです。マスタからの受信データ数bcntに従って、場合分けして処理します。

      case 1: // smbus read command
        i2c_read_cmd=Wire.read ();
        break;


 マスタから受け取ったデータ数が1バイトだった時の処理です。
 スレーブアドレスを指定する先頭バイトは、データ数bcntにカウントされていないので、受信データが1バイトのケースは、上で示したSMBusプロトコルの図で、「Read Byte」または「Read Word」の、CMDを指定するI2Cライト(WR)トランザクションの場合だけです。
 従って、この場所でWire.read()したデータは、SMBus ReadのコマンドCMDです。後でonRequest()処理に使うので、グローバル変数i2c_read_cmdに保持しておきます。

      case 2: // smbus write byte
        cmd=Wire.read ();
        while (cmd>=i2c_max_reg)
          cmd -= i2c_max_reg;
        i2c_reg[cmd]=Wire.read ();
        break;


 マスタから受け取ったデータが2バイトだった時の処理です。このケースは、「Write Byte」を受け取った場合ですので、第一バイトがCMD、第二バイトが書き込みデータDATです。
 実際にレジスタi2c_reg[ ]への書き込みを実行します。

      case 3: // smbus write word
        // ERROR:
        Serial.println ("ERR: SMBus Write Word is not implemented.");
        break;


 マスタから受け取ったデータが3バイトだった時の処理です。このケースは、「Write Word」を受け取った場合ですが、今回は未対応ですので、エラーとします。

      default: // unknown
        // ERROR:
        Serial.print ("ERR: i2c data received: ");
        Serial.print (bcnt);
        Serial.println (" bytes");
        break;
    }


 それ以外のデータ数だった場合も、未対応なので、エラーとします。ただし、Arduino側のシリアルモニタに、エラーメッセージを出力するだけで、Pi ZeroWにはエラー状態が通知されないので、発生したエラーは「無視」したことになります。

    // discard extra data
    while (Wire.available ())
      Wire.read ();
  }


 エラーが発生した場合など、受信バッファに残ったデータを読み捨てて、ハンドラを終了します。

 続いて、マスタから送信要求を受け取った時に呼び出される、onRequest()ハンドラのコードを示しながら、その下に解説を追加していきます。

  void i2c_request (void)
  {
    // request from master device
    // SMBus.Read.Word is not implemented.
    int cmd;


 マスタからデータ要求があった時に呼び出されるハンドラです。

    // send byte data
    cmd=i2c_read_cmd;
    while (cmd>=i2c_max_reg)
      cmd -= i2c_max_reg;
    Wire.write (i2c_reg[cmd]);
  }


 Read Byteだけに対応しているので、onReceive()ハンドラで受け取ったコマンドi2c_read_cmdに従って、対応するレジスタi2c_reg[ ]から読み出した1バイトのデータを返信します。

 もし、Read Wordに対応する場合、困ることが1つあります。それは、このハンドラ内では、マスタが何バイトのデータを要求しているかわからないことです。少なくとも、Read Byteと、Read Wordが存在するので、要求データ数が1なのか2なのか、知る必要があります。
 一つの解法は、「常に2バイトアクセスだと思って、2バイトのデータを返す」ことです。SMBusのプロトコルでは、2バイトデータの下位バイト→上位バイトの順で送信する決まりなので、常に2バイト返信しておくと、マスタが1バイト受信で止めた場合も、2バイト受信した場合も、正常に動作します。
 この場合、もし、「読んだ」ことで状態が変化するような仕様のレジスタを作った場合、破綻しますので注意してください。

 もう一つの解法は、Read時に渡されるCMDに、データサイズを指定するビットを設けておくことです。CMDを見て、返信するデータ数を知ることができれば、解決です。

Pi ZeroW側のプログラム

 Pi ZeroWがI2C(SMBus)マスタとなり、Arduinoのレジスタにアクセスします。
 Arduino側で動作させるプログラムは、あたかも、I2C接続の256バイトRAMであるかのような動作をするので、まずは、全256バイトをダンプして表示(16×16バイト+ASCII)する機能を持たせます。相手はRAMなので、任意のアドレスへバイトアクセス(リード/ライト)できる機能も必要でしょう。
 アクセスは、wiringPiライブラリのSMBusアクセス関数(wiringPiI2CReadReg8()関数/wiringPiI2CWriteReg8()関数)を使って実装します。
 また、アクセスの正しさを検証するため、メモリテストアルゴリズムの一つである、マーチングテストを実行して、アクセスの整合性を検証可能としておきます。

 マーチングテストのアルゴリズムは以下の通りとします。

 本当のメモリテストとしては、これだけでは不十分で、他にも、ギャロッピング/ピンポン/物理ストライプ/物理チェッカ/リテンション/……等々、心配になってテストし始めると、キリが無いほどのアルゴリズムが存在します。
 今回は、「メモリセル」のテストが目的ではなくて、ArduinoとのI2C通信が検証できれば良いので、マーチング(しかも昇順のみ)だけで十分だと思われます。

Pi ZeroWとArduinoのI2C通信

 では、いよいよ、Pi ZeroWとArduino UNOをI2C接続してみましょう。

Pi ZeroW 接続 L/S 接続 Arduino UNO
3.3V(phys#1等) LV  
SDA(phys#3) LV1
SCL(phys#5) LV2
GND(phys#9等) GND GND
  HV 5V
HV1 SDA
HV2 SCL

 I/Oレベルが異なるので、レベルシフタ(L/S、下図)が必要です。

ar47_lvshft.jpg

 このレベルシフタは、双方向、オープンドレイン&プルアップタイプなので、I2C信号線にプルアップ抵抗を別途接続する必要はありません。

ar81_expr.jpg

 Arduinoで実行するプログラムをリンクしておきます。

  完成プログラム:test_smbus.ino

 コンパイルしてUNOに書き込んでおいてください。シリアルモニタを開くと、以下のコマンドを受け付けるようになっています。

 続いて、Pi ZeroW側で実行するプログラムをリンクしておきます。

  完成プログラム:test_smbus.cc

  コンパイル:【g++ -lwiringPi test_smbus.cc -o test_smbus】

  使い方:【test_smbus -h】で表示されるメッセージをご覧ください。

 準備が整いましたので、Pi ZeroWのshellウインドウを開いて、実験を進めて行きます。

  実験サンプル:【 】内はshellウインドウでのコマンド入力
  【test_smbus -c】 …… 現在の/boot/config.txt設定値を表示
  dtparam=i2c_arm=on
  dtparam=i2c_baudrate=400000
   …… I2C 400kHzの設定になっています。

  【gpio i2cdetect】 …… I2Cに接続されたスレーブアドレスをスキャン
    0 1 2 3 4 5 6 7 8 9 a b c d e f
  00:     -- -- -- -- -- 08 -- -- -- -- -- -- --
  10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  70: -- -- -- -- -- -- -- --


 Arduinoのスレーブアドレスは0x08で検出されました。test_smbus.ccの定数も、i2c_slv_adr=0x08の設定となっていますので、これで互いに通信可能です。もし、他のI2Cモジュールとアドレスが重なっている場合には、Arduino側、Pi ZeroW側両プログラムのi2c_slv_adr定数を、セットで変更してください。

  【test_smbus】 …… ArduinoのI/Oレジスタをダンプ
  addr: +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +a +b +c +d +e +f : 0123456789abcdef
  0000: ff ff -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  0010: ff -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  0020: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  0030: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  0040: -- -- -- -- -- -- -- -- -- -- -- -- ff -- -- -- : ................
  0050: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  0060: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  0070: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  0080: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  0090: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  00a0: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  00b0: -- -- -- -- ff -- -- -- -- -- -- -- -- -- -- -- : ................
  00c0: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  00d0: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  00e0: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ff : ................
  00f0: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- : ................
  DEBUG: access time=203000[us]/256[byte] freq=10.089[kHz]
  ERROR: read error count=250
  Remote I/O error


 本来であれば、読み出した2桁の16進数数値が表示されるべきところですが、リードエラーが発生したことを示す「--」で埋まっています……。うまく通信できていない様です。

  【test_smbus -m】 …… マーチングテストを実行
  Start marching test, wait a moment...
  ERROR: marching test failed -4

 マーチングテストを実行してみましたが、当然、失敗します。マーチングのエラーコードErrMarch=−4は、読み出した期待値が合わなかったことを示しています。つまり、最初の「1)write all "0"」は、エラー無く、実行できているみたいです(∵書き込みで失敗した場合は、ErrMarchW=−5が返るコードになっている)。

 マーチングテスト失敗直後、Arduino側のシリアルモニタに移って【dump】コマンドでメモリの値を確認したところ、全アドレスへの0x00書き込みは成功していました。

 明らかに、読み出しで失敗しています。実は、SMBusプロトコルの図を見た時に、ちょっと心配していました。以下図で解説します。……心配が現実になったようです。

ar81_smbrd.png

 SMBusのRead Byteプロトコルだと、上図の様にI2Cライト(WR)の直後に、連続してI2Cリード(RD)が発生します。Arduino側の処理として読み替えてみると、I2Cライトを受け取った時点で、onReceive()処理が始まります。一方、直後に飛んでくるI2Cリードを受け取ると、すぐさまonRequest()処理を開始しなくてはなりません。
 そう、onReceive()処理と、onRequest()処理は同時実行できないので、Arduino側の処理が間に合わずに、通信が破綻しているのではないだろうか?と、推定してみました。

 そこで、Pi ZeroW側のプログラムで、wiringPiI2CReadReg8()関数をオーバライドして、I2Cのwrite()とread()トランザクションを分離してみます。分離した上で、間に適切なウェイトを挟むと、onReceive()処理とonRequest()処理の競合が無くなって、正常動作するのではないでしょうか?

  int ovr_wiringPiI2CReadReg8 (
  int fi2c,
  int cmd)
  {
  #ifdef DEBUG_SMBR
    int cnt;
    uint8 buf;

    buf=cmd;
    write (fi2c,&buf,1);
    cnt=read (fi2c,&buf,1);
    if (cnt<=0)
      return (-1);

    return (buf);
  #else // DEBUG_SMBR
    return (wiringPiI2CReadReg8 (fi2c,cmd));
  #endif // DEBUG_SMBR
  }


 DEBUG_SMBRを有効にした手順だと、write()とread()の間にSTOPコンディション(P)が挟まって、改めてSTARTコンディション(S)が発行されるため、SMBusプロトコルではなくなってしまいますが、Arduino側の実装では問題なく(※)動作可能です。まず、ウェイトは挟まずに、この状態で試してみます。

※I2Cバスをマルチバスマスタで使っている場合には、write()とread()の間でバス権が他のバスマスタに移ってしまい、他のバスマスタがArduinoにアクセスしてしまうと、アウトです。この実験では、バスマスタがPi ZeroWしか居ないので、「問題ない」ということです。
※反復STARTコンディション(R)を使うSMBusプロトコルの場合、このタイミングでバス権の移動が発生しないため、SMBusリードトランザクションがアトミックに実行されることが保証されます。

  【g++ -lwiringPi -DDEBUG_SMBR test_smbus.cc -o test_smbus】 …… 再コンパイル

  【test_smbus -h】
  test_smbus ver.1.00 (c) 2019 1YEN Toru
  attention: not a SMBus version, DEBUG_SMBR enabled.
  usage: test_smbus [<options>]
    : 以下省略

  【test_smbus】 …… ArduinoのI/Oレジスタをダンプ
  addr: +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +a +b +c +d +e +f : 0123456789abcdef
  0000: 68 65 6c 6c 6f 2c 20 72 61 73 70 62 69 61 6e 00 : hello, raspbian
  0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  DEBUG: access time=67090[us]/256[byte] freq=30.526[kHz]


 最後の行は、256バイトの読み込みに要した時間と、実効通信速度freqを表示しています。実効通信速度freq=転送したデータ(DAT)ビット数/転送に要した時間と、定義しました。

  【test_smbus -m】 …… マーチングテストを実行
  Start marching test, wait a moment...
  OK: marching test passed (358105[us])

 マーチングテストも、パスしました。正しく動作しています。
 ovr_wiringPiI2CReadReg8()関数で、write()とread()の間に適切なウェイト時間を設定しないとダメかな?と思ったのですが、明示的なウェイト無しで動作してしまいました。

※Raspbianが、write()関数とread()関数の間で、CPUタイムを剥奪した結果、良い感じに時間が空いて、たまたま、動いているのかもしれません。失敗するようなら、write()関数とread()関数の間に適切なウェイト時間を設けてください。
※あるいは、I2Cのデバイスドライバが、write()とread()の間に、適切なウェイトを挟むような仕様になっているのかも??

 これだけ見ると、SMBusプロトコル図の中で、反復STARTコンディション(R)が悪さしているんじゃないか?と言う疑いもありますね。
 ただ、ArduinoのI2Cユニットは、反復STARTコンディション(R)自体は、受信可能な仕様になっている(ATmega328のデータシートで確認済み)ので、I2Cのwrite()とread()トランザクションを分離したこと自体が効いているとは思えません。別角度から検証してみましょう。

 DEBUG_SMBRを無効にして、再々コンパイルします。

  【g++ -lwiringPi test_smbus.cc -o test_smbus】

  【test_smbus -h】 …… 「attention:」が表示されない
  test_smbus ver.1.00 (c) 2019 1YEN Toru
  usage: test_smbus [<options>]
    : 以下省略

 更に、「dtparam=i2c_baudrate=」の設定を変更して、通信周波数を落とします。

  【sudo vi /boot/config.txt】
  dtparam=i2c_baudrate=75000
    【ZZ】 …… セーブしてエディタを抜ける

  【sudo reboot】 …… 設定を有効化するため再起動

  …… 以下、再起動後に実行
  【test_smbus -c】 …… 現在の/boot/config.txt設定値を表示
  dtparam=i2c_arm=on
  dtparam=i2c_baudrate=75000

  【test_smbus】 …… ArduinoのI/Oレジスタをダンプ
  addr: +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +a +b +c +d +e +f : 0123456789abcdef
  0000: 68 65 6c 6c 6f 2c 20 72 61 73 70 62 69 61 6e 00 : hello, raspbian
  0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  0090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  00f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 :
  DEBUG: access time=156302[us]/256[byte] freq=13.103[kHz]


  【test_smbus -m】 …… マーチングテストを実行
  Start marching test, wait a moment...
  OK: marching test passed (850311[us])

 I2Cの通信速度を75kHzまで落としたところ、SMBusプロトコルのままでも、Arduinoとの通信が成功しました。やはり、SMBusプロトコル自体は、Arduinoでも受けられる(受信できる)みたいです。
 ただし、通信クロック周波数を落としたことで、ダンプの最後に表示される実効通信速度freqが13kHz程度まで低下してしまいました。

結論

LCDサブシステムがあったね

 本ページ最初の方で参照した「#3-2 Arduino間I2C通信を試す」と、言えば、次の記事(「#3-3 I2C接続 LCD表示モジュール」)で、Arduinoを使ったLCDサブシステムを構築しました。これをそのまま、Pi ZeroWに接続してみます。
 LCDサブシステムは、普通にI2Cのデータ受信しか実行していないので、400kHzで動作可能なはずです。

 Pi ZeroW側の処理に目を移すと、UARTの時の様に、/dev/serial0みたいなデバイスに対して、直接データを転送するような使い方は、I2Cではできません。送信先のスレーブアドレスを指定する必要があるためです。そこで、標準入力で受け取ったデータを、そのままI2Cの指定したスレーブアドレスへ、リダイレクトするプログラムをPi ZeroW用に作成しました。

  完成プログラム:redir_lcd.cc

  コンパイル:【g++ -lwiringPi redir_lcd.cc -o redir_lcd】

  使い方:【redir_lcd -h】で表示されるメッセージをご覧ください

 解説するほどの大したことはしていないので、中身については、上にリンクしたプログラム(redir_lcd.cc)を参照してください。

 Arduinoの方は、「#3-3 I2C接続 LCD表示モジュール」に従って、LCDサブシステム回路の接続と、プログラム(lcd_subsys.ino)のコンパイル&書き込みをしておいてください(筆者は、Arduino Microを使いました)。

 Pi ZeroWとArduino Microを接続します。

Pi ZeroW 接続 L/S 接続 Arduino Micro
3.3V(phys#1等) LV  
SDA(phys#3) LV1
SCL(phys#5) LV2
GND(phys#9等) GND GND
  HV 5V
HV1 SDA
HV2 SCL

 以下、実験中の様子です。

ar81_expr2.jpg

 上写真では、Pi ZeroWの5V端子から、Arduino Micro+LCDサブシステムへ電源を供給しています。ただし、この場合は、ArduinoのUSB端子は絶対に接続しないでください。Pi ZeroWのACアダプタ(5V)は、1.2A以上の物を用意した(「#7-2 デザートにラズベリーパイはいかが?」参照)ので、電流容量的には全く問題ないはずです。

※念のため、USB電圧・電流計で、消費電流値を測ったところ、Pi ZeroW+Micro(400kHz I2C)+LCDサブシステム(バックライトON)=210〜240[mA]程度を観測しました。

  【gpio i2cdetect】 …… I2Cに接続されたスレーブアドレスをスキャン
    0 1 2 3 4 5 6 7 8 9 a b c d e f
  00:     -- -- -- -- -- 08 -- -- -- -- -- -- --
  10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  70: -- -- -- -- -- -- -- --


 Arduino(LCDサブシステム)のスレーブアドレスが0x08であると確認できました。別なアドレスに変更したい場合には、lcd_subsys.inoの定数i2c_slv_adrを変更してコンパイル&書き込んでください。

  【ls | redir_lcd -s 08】 …… 0x08がデフォルトなので、-sオプション無しでもOK

 lsコマンドの出力がLCDに表示されましたか?バックログ機能付きなので、ロータリエンコーダ(U/I)でスクロールさせて、前後のファイル名を閲覧できるはずです。



 昔作ったLCDサブシステムがそのまま、使えてしまいました(ニンマリ(1^u^))。

 ちょっと気分が良いので、Pi ZeroW+LCDで時計を作ってみました。

  完成プログラム:lcd_clock.csh
  #!
  echo "\f" | redir_lcd
  while (1)
    set cur=`date +%S`
    date +"%Y/%b/%d(%a)%n%H:%M:%S [%p]%Z" | redir_lcd
    if ( $status )\
      break
    while ( $cur == `date +%S` )
      sleep 0.1
    end
  end


 cshスクリプトで、「ちょちょい!」って感じです。
 LCDサブシステムのプログラム(lcd_subsys.ino)では、FF(フォームフィード、0x0c、'\f')コードを受信すると、バックログと画面を消去して、最初からデータを表示し直す機能が実装されているので、スクリプトの先頭で、その機能を使っています。



 Pi ZeroWがNTPで時計を同期しているので、いつでも正しい時刻を表示してくれます。WiFiとNTPを使った、これも一種の「電波時計」ですね。

I2C通信総括

 UARTに比べて、通信クロック周波数が最大400kHzと、高速な通信方式なため、ちょっと期待していました。しかし、Arduinoがスレーブとなって通信する前提で、かつSMBusとなってくると、結局、通信速度が制限される結果となりました。「高速通信」と言う目的では、結構厳しいかもしれません。

※UARTだと、8ビットのデータを転送するのに、スタートビット+ストップビットを加え、合計10ビットの転送が必要なので、実効通信速度は×8/10倍に低下します。115,200baudなら、115,200×8/10≒92k[bps](Bits Per Second)です。SMBus 75kHzで送信した時の実効通信速度は、上の実験結果より13kHz(bps)でした。SMBusを無視して400kHzで通信した時も、30kHz(bps)と、いずれもUARTの通信速度を下回ります。

 I2Cの場合、第一バイトでスレーブアドレスを通知しなければならなかったり、データ受信後にACKをやり取りする仕様なので、データ通信外のオーバヘッドも、考慮する必要があります(∵実効通信速度が低下する)。
 ArduinoのWireライブラリだと、送・受信バッファサイズが32バイトなので、オーバヘッドを軽減する目的で連続転送しようにも、最大32バイト毎でしか転送できません。
 使い方次第でしょうけど……。Pi ZeroWからArduinoへのコマンド/命令の類は、I2C(SMBus)の方が使い易いので、それを使い、その実行結果となる大量のデータを受信する時には、UARTで転送する、とか、特徴に応じて使い分けるのが良さそうです。


 ArduinoをPi ZeroWのコプロセッサとして使う時、ArduinoをI/Oレジスタ形式で制御できると便利かな?と思って、SMBusスレーブ機能を実装してみたのですが、またしてもArduinoの処理能力不足で、通信速度を制限する必要が生じてしまいました。うまく行かないものです……。結局、UARTで通信するのが、一番速くて、しかも使い易い、って事かな?


btn_arduino.png

btn_prev.png btn_next.png

home