LoginSignup
13
12

More than 5 years have passed since last update.

Raspberry Pi から Arduino へ I2C でデータを送る

Last updated at Posted at 2018-08-30

[ インスタレーションサバイバルガイド ]

はじめに

Raspberry Pi で処理した情報を Arduino に伝える方法には様々なものがあります。本文書では I2C を使って情報を伝える方法について説明します。使用する Arduino は UNO R3 です。Raspberry Pi は Model 3 を使って確認しています。

I2C の利点

I2C は、Raspberry Pi でも Arduino UNO R3 でも標準装備されており、使用する信号線も 2 本だけです。とはいえ、元々基板上での IC 間の通信のために設計された規格のようですので、あまり長い距離での通信には不向きです(ラズパイと Arduino の距離が 10cm 程度であれば問題なく動いています)。

開発環境

Arduino の開発は Raspberry Pi で行っています。これは、外部回路の設計ミスなどにより、万が一壊れてしまっても経済的ダメージが少なくなるためと、Raspberry Pi には I2C 関連のツールも標準で備わっているからです(NOOBS を用いてインストールした Raspbian で確認)。

Arduino 側のプログラム

Raspberry Pi から見た場合 Arduino は I2C デバイスとして認識される必要があります。まずは、Arduino を I2C デバイス化するプログラム(スケッチ)を作成します(List1)。

また、Arduino が正しく情報を取得できているかどうかを確認する機能も組み込んでおきます。うまく動かない等、問題が発生した時にまずすべきことは「どこが原因なのか」「何が原因なのか」をつきとめることです。そのため、問題を切り分けられるように Arduino 側で取得している情報を確認できるような仕組みが大切です。幸い Arduino ではシリアルコンソールが使えるので、それを活用します(L チカよりも多くの情報が分かりやすい形で確認できるので)。

List1
#include <Wire.h>

const int I2C_SLAVE_ADDRESS=0x04;

void setup() {
  Serial.begin(9600);  // Arduino IDE にあるシリアルモニタのデフォルト通信速度

  // I2C
  Wire.begin(I2C_SLAVE_ADDRESS);
  Wire.onReceive(onRecv);

  Serial.print("STARTUP -- OUT\n");  // 正常に setup できたことを通知
}

void loop() {
  // empty
}

static char gBuf[255];

void onRecv(int inNumOfRecvBytes) {
  sprintf(gBuf,"NumOfBytes=%d : ",inNumOfRecvBytes);
  Serial.print(gBuf);

  while(Wire.available()>0) {
    unsigned char c=Wire.read();
    sprintf(gBuf,"%02X ",c);  // Serial には printf がないので、一度文字列に出力する
    Serial.print(gBuf);
  }
  Serial.print("\n");
}

このプログラムは、Arduino が I2C で何か受信したら、送られてきたバイト数と共に、そのデータを表示するものです。

[ 備考:作品への応用を考えている人へ ]
ここでは、簡単なプログラムにて、I2C 経由でデータが受信できていることを示しています。そのため、Wire.onReceive() 関数にて指定する受信ハンドラ onRecv() 関数中にてシリアル出力しています。しかし、実際のプログラミングでは、受信ハンドラ中のシリアル出力は上手く機能しない場合もあります。実際の作品制作においては、受信ハンドラではデータをグローバル変数に記録するなど、簡単な処理のみを行い、シリアルへの出力などは loop() 関数内で行う方が良いでしょう。

I2C デバイスとして Arduino が動作しているか、動作確認

List1 により Arduino が I2C デバイスとして動作しているかどうかを確認します。まずは、Raspberry Pi と Arduino を I2C にて接続します。その方法ですが、Raspberry Pi 側の SDA を Arduino 側の SDA とつなぎます。同様に Raspberry Pi と Arduino の SCL 同士を接続します(注1)。これで I2C による物理的な Raspberry Pi と Arduino の接続は完了です(Raspberry Pi にて該当 Arduino の開発を行っているという前提なので、Raspberry Pi の USB 端子と Arudino の USB 端子が接続されており、Arduino の電源は Raspberry Pi より供給されているものとします)。

注1:Raspberry Pi3 は 3.3V 駆動。Arduino は 5V 駆動なので、きちんと接続するならば I2C バス用双方向電圧レベル変換 IC ( PCA9306 )などを経由するべきです。単に電線で直結しても通信できる場合もあるので、その場合、動作確認程度はできます。

Raspberry Pi には i2cdetect というツールがあるので、それを用いて確認します。

i2cdetect -y 1

-y はインタラクティブモードをオフにするオプションです。1 はデバイスを指定する番号です。Raspbian では I2C デバイスも /dev/i2c-N として抽象化されています。Raspberry Pi 3 では I2C デバイスは 1 つしかないので、デバイスファイルも /dev/i2c-1 のみとなります。なので 1 を指定します。

i2cdetect を実行すると、以下のように表示されます:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- 04 -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --                         

04 という文字のみが表示されています。これは List1 で I2C_SLAVE_ADDRESS=0x04 と指定しているので、今回作成したプログラムにて Arduino が アドレス 0x04 をもつ I2C デバイスとして機能しているためです。

i2cdetect はバイト数 0 バイトのデータを送信するので、Arduino IDE のシリアルモニタには、

NumOfBytes=0 : 

という表示が出ていると思います。

このような状態になれば、まずは I2C で Raspberry Pi と Arduino が通信できていることとなります。

この状態になっていない場合は、そもそも I2C で通信ができていない状態ですので、Arduino 側のプログラム(List1)に間違いが無いか、I2C で接続できているか(接続しているピンに間違いがないか等)を今一度確認してみて下さい。

標準ツールを用いて Arduino にデータを送る

次は任意のデータを I2C 経由にて Raspberry Pi から Arduino に送ってみます。これも i2cset という標準ツールがあるのでそれを利用します:

i2cset -y 1 0x04 0xDE 0xAD 0xBE 0xEF 0x12 0x34 i

としたら、Arduino IDE のシリアルモニタに

NumOfBytes=6 : DE AD BE EF 12 34

と表示されば成功です。i2cset のパラメータですが、-y と 1 は i2cdetect と同じです。以後続く 0x04 は送信先の I2C デバイスのアドレスです。0xDE 〜 0x34 までが送信するデータとなります。最後の i は I2C の送信モードを示すパラメータで複数のデータを送信する block mode を示しています。

ここまで来たら、Raspberry Pi から Arduino へ I2C 経由で情報が送信できているので、あとは Raspberry Pi 側のプログラムを作成すれば完了です。

WiringPi を用いた Raspberry Pi 側のプログラム

まずは 3 バイトを送るサンプルプログラム

Raspberry Pi から I2C 経由でデータを送信するために、WiringPi を使用します。このライブラリには、wiringPiI2CWriteReg16 関数などがあり、一見するとレジスタ番号を含めて最大 3 バイトしか送信できないの?と思ってしまいます(私も誤解していました)。

実は、wiringPiI2CSetup() の戻り値は標準的な Linux のファイルハンドルです。そのため、これを用いて、read() や write() などの関数を使用し、複数バイトのデータも送受信可能です(任意の複数バイトからなるデータを送信するプログラムを List3 として後ほど示します)。

List 2 では、wiringPiI2CWriteReg16() を使って、ラズパイから Arduino へレジスタ番号を含め、3 バイトのデータを送信してみます。Raspbery Pi 側のプログラムはスレイブとして動作しているターゲットの I2C アドレス(List1 の場合は 0x04)や、WiringPi 側の要請でレジスタ番号を入力するようにしています。もちろん、16 ビットの送信データも入力できるようにしています。入力するデータは全て 16 進数とします。頭に 0x という文字は不要です。例えば、12 と入力したら、それは 0x12 として解釈されます。

List2
// I2C maste test
// g++ i2cMasterTest.cpp -lwiringPi

#include <stdio.h>
#include <errno.h>
#include <string.h>

#include <wiringPi.h>
#include <wiringPiI2C.h>

int main() {
    if(wiringPiSetup()<0) {  // WiringPi が初期化できなかったらエラー表示して終了
        fprintf(stderr,"Error: can not init wiringPi.\n");
        fprintf(stderr,"errno=%d\n",errno);
        fprintf(stderr,"%s\n",strerror(errno));
        return -1;
    }

    // スレーブ側の I2C アドレスの入力
    int addr;
    printf("input target addr: ");
    scanf("%02x",&addr);  // とりあえず…
    printf("TARGET Addr=0x%02X[=%d]\n",addr,addr);

    // 対象の I2C デバイスへの通信路を開く
    int fd=wiringPiI2CSetup(addr);
    if(fd<0) {
        fprintf(stderr,"ERROR: "  // 長いので 2 行に分けた
                       "Can not start to communicate I2C device %0x02X.\n",addr);
        return -1;
    }

    // 対象のレジスタ(List1 にはそんなものは無いが送信のために必要なので…)の入力
    // ここで入力したバイト値も、次に入力する 16 ビット値の前に送信される。
    int reg;
    printf("input target reg: ");
    scanf("%02x",&reg);
    printf("TARGET Reg=0x%02X[=%d]\n",reg,reg);

    // 送信するデータ
    int data;
    printf("input send data (16bit): ");
    scanf("%04x",&data);
    printf("SEND Data=0x%04X[=%d]\n",data,data);
    // エンディアンの問題で、実際に送信してみると上位バイトと下位バイトが入れ替わるので
    // 送信前に入れ替えておく(0x1234 を送ってみると 0x34 0x12 と受信されてしまうので)。
    data=(data&0xFF)*0x100+((data>>8)&0xFF);

    // 送信!
    int result=wiringPiI2CWriteReg16(fd,reg,data);
    if(result<0) {
        fprintf(stderr,"Error: can not write to the target.\n");
        fprintf(stderr,"errno=%d\n",errno);
        fprintf(stderr,"%s\n",strerror(errno));
        return -1;
    }

    return 0;
}

このプログラムを例えば i2cMasterTest.cpp というファイル名で保存したならば、

g++ i2cMasterTest.cpp -lwiringPi

としてコンパイルする。すると a.out ができるので、

sudo ./a.out

として実行する。あとは、ターゲットに 04、レジスタに適当な値―例えば 12 とし、データに beef と入力すると、Arduino IDE のシリアルモニタには

NumOfBytes=3 : 12 BE EF

と表示されるはずです(Arduino を動作させておくのを忘れずに…)。

wiringPi を用いた任意のバイト数からなるデータの送信プログラム

次に、任意のバイト数からなるデータを Arduino に送る wiringPiI2C 版 i2c-write を示します(WiringPi を使わない版は こちら)。このプログラムでは i2cset のように、コマンドラインの引数として送信データを記述します。

i2c-write 送信先I2Cアドレス 送信データ1 送信データ2 … 送信データN

例えば、アドレス 0x04 が割り振られた I2C デバイスに 0xCA 0xFE 0xDE 0xAD 0xBE 0xEF という 6 バイトのデータを送信する場合は、

i2c-write 04 CA FE DE AD BE EF

となります。各数値は 16 進数で指定します。i2cset とは異なり、16 進数を示す接頭詞 0x はつけないで下さい。

注意:Arduino 側は標準では 32 バイトまでしか受信できないようです。

List3
// g++ i2c-write.cpp -lwiringPi  -o i2c-write

#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#include <wiringPi.h>
#include <wiringPiI2C.h>

int main(int argc,char *argv[]) {
    if(wiringPiSetup()<0) {
        fprintf(stderr,"ERROR: can not initialize wiringPi.\n");
        return -1;
    }

    if(argc<5) {
        fprintf(stderr,"--- This is WiringPi I2C version i2c-write. ---\n");
        fprintf(stderr,"Usage: i2c-write address data1 data2 ... dataN\n");
        fprintf(stderr,"ex) send 0xCA 0xFE 0xDE 0xAD 0xBE 0xEF to 0x04.");
        fprintf(stderr,"    i2c-write 04 CA FE DE AD BE EF\n");
        return -1;
    }

    int i2cAddr;
    sscanf(argv[1],"%02x",&i2cAddr);
    printf("address=0x%02X (=%d)\n",i2cAddr,i2cAddr);

    const int numOfBuf=argc-2;
    char buf[numOfBuf];
    printf("data to send: ");
    for(int i=2,j=0; i<argc; i++,j++) {
        int t;
        sscanf(argv[i],"%02x",&t);
        buf[j]=(unsigned char)t;
        printf("%02X ",t);
    }
    printf("\n");

    // Open
    int handle=wiringPiI2CSetup(i2cAddr);
    if(handle<0) {
        fprintf(stderr,"ERROR: can not open I2C device(0x%02X).\n",i2cAddr);
        return -1;
    }

    // Send
    int result=write(handle,buf,numOfBuf);
    if(result!=numOfBuf) {
        fprintf(stderr,"ERROR: can not send data.\n");
        fprintf(stderr,"result=%d\n");
        if(result<0) {
            fprintf(stderr,"%s\n",strerror(errno));
        }
        return -1;
    }

    // Close
    if(close(handle)) {
        fprintf(stderr,"ERROR: can not close I2C.\n");
        fprintf(stderr,"%s\n",strerror(errno));
        return -1;
    }
    return 0;
}

まとめ

この文書では、Raspberry Pi から Arduino へ I2C を用いてデータを送信することに特化して、特定のセンサ IC などを想定することなく説明してみました。構成要素が複数になる場合、問題が発生した時、どこに問題があるのかを特定することが重要です(再掲)。そのため、ひとつひとつ確認するステップについても説明を行いました。

ベテランの皆様へ:本文書は当方の不勉強や勘違いにより、結果として間違った内容となっている可能性もあります。そのようなことを見つけたら(やさしくw)ご指摘いただけると助かります。また誤字脱字の指摘も歓迎いたします。必要に応じて修正し、更新を繰り返すことでよりよい文章になっていけば幸いです。

13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12