Raspberry PiとArduinoをI2C通信で連携させる

th_rapiro_i2c_1

前回で一応Arduino側のハードの準備はしたので、ここからはソフトの準備です。

Raspberry PiとArduinoを連携させるにあたって、連携方法は色々あると思います。素直にRaspberry PiにArduino IDEを入れてUSBで繋ぐ方法もあるのでしょうが、個人的にラピロの目にLEDマトリックス基板を入れたときにI2Cを使う準備を整えていたことと、あとRaspberry PiにArduino IDEを入れるのは何か嫌だな、Raspberry PiからはArduinoというより単なるI2Cデバイスとして扱う方が使い勝手良さそうだな、と思ったことから、I2Cでやってみることにしました。

。。。まあ、結論から言うと、結構ハマってしまいましたが。一応、できることはできました。

では、早速。

以下では、ちょっと珍しいかもしれませんが、ArduinoにはArduino Fioを使います。

理由は、Fioのピンの入出力は3.3Vなので、Raspberry Piと接続する際にレベルコンバーター云々を気にしなくよいこと。それから、Arduino Pro Mini (3.3V, 8MHz)では動かなかった、自作の赤外線リモコンプログラムが、Fioだとちゃんと動いてくれること。それから、電源とか通信方法の選択の自由度が高そうだから、等々です。ただ、やっぱりArduino Pro Miniに比べると大きいので、これは好みに応じて選べばよいと思います。

ArduinoとRaspberry Piの接続ですが、Raspberry PiのSDAをArduinoのA4ピン、SCLをA5ピンにそれぞれ接続します。電源はとりあえず、Raspberry PiのUSBソケットからUSBケーブル&FTDI USBシリアル変換アダプター経由で供給します。この接続は、単なる電源供給のためのものであり、データ通信は行いません。

ちなみに、これを一個持っておくと、Arduino Pro Mini (5V, 16MHz)のソフトアップロードとかにも使えるので便利です。

th_rapiro_i2c_2

実際はこんな感じです。ビローンと長く伸びている青と黄色の線が、I2C通信用の線です。

 

I2C通信をするにあたって、Raspberry Pi側の方のPythonプログラムはsmbusモジュール、Arduino側の方はWireライブラリを使えば、それぞれ簡単にマスター/スレーブのプログラムを書けます。以下がとても参考になります。

が、しかし、です。

上記のサイトを含め、ほとんどのサイトでは、1バイトのデータのやりとりをサンプルとして載せています。自分の場合は、これだけだとちょっと困るのです。

使い方のイメージとしては、Arduinoでセンシングしているデータを、Raspberry Piが定期的or必要に応じて取得しにいく、という形です。Arduinoの方で取得しているセンサの値は、例えばこんな感じです。

  • 温度 … 20.25
  • 湿度 … 49.8
  • 照度 … 130.56
  • マイク … 1.60 (<- 電圧値)
  • 人感 … 1

さて、このデータをRaspberry PiにI2Cで渡すにはどうすればよいかと。

とりあえず小数をそのまま送るのは難しそうなので、小数点以下切捨てて送るか、Arduino側で100倍して送ってRaspberry Piで1/100するか、で対応することにします。

小数点以下切捨てにすると、温度と湿度は1バイトで値の範囲も分解能も十分だと思います。照度については、切り捨てること自体は問題ないのですが、そもそも値が700ぐらいになったりするので、1バイト(0〜255)では表せません。また、マイクについては小数点以下を切り捨ててしまうと”1″,”2″,”3″の値しか取らなくなるので、これについては100倍->1/100倍で対応したいところです。

ということで、なんとかしてint型(Arduinoだと2バイト)の整数をI2CでRaspberry Piに送る必要があります。

最初は、「Arduino側でbyte配列作って、そこに全センサの値をまるっとコピーしてRaspberry Piに送れんかな」という方針でやっていました。

ところが、Python側で、それが全然受信できず。read_word_dataとかread_block_dataとかいうメソッドは用意されているのですが、これらを使ってみてもダメです。サンプルも全然見つかりません。smbusのリファンレンスが、ネットを探しまわってもこれとかこれとかこれぐらいしか見つけられず、しかもパラメータの説明がほとんどないから意味がわからず。。。cmdって何よ。。。I2Cについて理解していれば、意味もわかるかもしれないのですが。

そんなわけで、どうにか1バイトのやりとりでなんとかできんか、という試行錯誤を半日ほどやった結果がこちら。まずはスレーブ側のArduinoのソース(抜粋)です。

(3/21追記:こちらで修正版のソースを公開しています)

#include <Wire.h>
#define SLAVE_ADDRESS 0x21

const float SUPPLY_VOLT = 3.3;

int TEMP_PIN   = A3;
int HUMID_PIN  = A2;
int ILL_PIN    = A1;
int MIC_PIN    = A0;
int MOTION_PIN = 10;
int LED_PIN    = 11;

float temp_c   = 0;
float humid_c  = 0;
float ill_c    = 0;
float mic_c    = 0;
int   motion_c = 0;
bool  led_c    = false;

const int I2C_COMMAND_LED_ON  = 0;
const int I2C_COMMAND_LED_OFF = 1;
byte sendByte;

float getTemperature(){
  int LM35DZ_Value = analogRead(TEMP_PIN);
  return ((SUPPLY_VOLT * LM35DZ_Value) / 1024) * 100;
}

float getHumidity(float temp){
  int HIH4030_Value = analogRead(HUMID_PIN);
  float voltage  = HIH4030_Value/1024.0 * SUPPLY_VOLT;
  float sensorRH = 161.0 * voltage / SUPPLY_VOLT - 35;
  float trueRH   = sensorRH / (1.0546 - 0.00216 * temp);
  return trueRH;
}

float getIlluminance(){
  int AMS302_Value = analogRead(ILL_PIN);
  float lx = AMS302_Value*(3300.0/1024.0)/1000.0/(0.26/100);
  return lx;
}

float getMic(){
  float ADMP401_Value = analogRead(MIC_PIN);
  return 3.3*ADMP401_Value/1024.0;
}

void receiveData(int byteCount){
  int i2c_command = -1;
  while(Wire.available()){
    i2c_command = Wire.read();
    Serial.print("i2c data received: ");
    Serial.println(i2c_command);
    switch(i2c_command){
      case 0x00: digitalWrite(LED_PIN, HIGH); led_c = !led_c; break;
      case 0x01: digitalWrite(LED_PIN, LOW);  led_c = !led_c; break;
      case 0xF0: sendByte = (uint8_t)(int(temp_c*100)); break;
      case 0xF1: sendByte = (uint8_t)(int(temp_c*100) >> 8); break;
      case 0xF2: sendByte = (uint8_t)(int(humid_c*100)); break;
      case 0xF3: sendByte = (uint8_t)(int(humid_c*100) >> 8); break;
      case 0xF4: sendByte = (uint8_t)(int(ill_c)); break;
      case 0xF5: sendByte = (uint8_t)(int(ill_c) >> 8); break;
      case 0xF6: sendByte = (uint8_t)(int(mic_c*100)); break;
      case 0xF7: sendByte = (uint8_t)(int(mic_c*100) >> 8); break;
      case 0xF8: sendByte = (uint8_t)motion_c; break;
      case 0xF9: sendByte = (uint8_t)(motion_c >> 8); break;      
    }
  }
}

void sendData(){
  Wire.write(sendByte);
}

void setup() {
  Serial.begin(57600);

  pinMode(TEMP_PIN, INPUT);
  pinMode(HUMID_PIN, INPUT);
  pinMode(ILL_PIN, INPUT);
  pinMode(MIC_PIN, INPUT);
  pinMode(MOTION_PIN, INPUT);
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  Wire.begin(SLAVE_ADDRESS);
  Wire.onReceive(receiveData);
  Wire.onRequest(sendData);

  Serial.println("Ready.");
}

void loop() {  
  temp_c   = getTemperature();
  humid_c  = getHumidity(temp_c);
  ill_c    = getIlluminance();
  motion_c = digitalRead(MOTION_PIN);
  mic_c    = getMic();

  Serial.print(temp_c); Serial.print(" / "); 
  Serial.print(humid_c); Serial.print(" / ");
  Serial.print(ill_c); Serial.print(" / ");
  Serial.print(motion_c); Serial.print(" / ");
  Serial.println(mic_c);

  delay(1000);
}

続いて、マスター側のRaspberry Piのソース(抜粋)。

import time
import smbus

bus = smbus.SMBus(1)
ARDUINO_ADDRESS = 0x21

registerMap = {
  "temperature":[0xF0,0xF1],
  "humidity":   [0xF2,0xF3],
  "illuminance":[0xF4,0xF5],
  "mic":        [0xF6,0xF7],
  "motion":     [0xF8,0xF9]
}

def read_value(sensor):
  time.sleep(0.005)
  bus.write_byte(ARDUINO_ADDRESS,registerMap[sensor][0])
  time.sleep(0.001)
  data_1 = bus.read_byte(ARDUINO_ADDRESS)
  bus.write_byte(ARDUINO_ADDRESS,registerMap[sensor][1])
  time.sleep(0.001)
  data_2 = bus.read_byte(ARDUINO_ADDRESS)
  #print data_1
  #print data_2
  data = ((data_2 << 8) | data_1)
  if sensor in ['temperature','humidity','mic']:
    data = data*1.0/100
  return data 

def control_arduino_on():
  bus.write_byte(ARDUINO_ADDRESS,0)
  return "OK, Arduino LED ON."

def control_arduino_off():
  bus.write_byte(ARDUINO_ADDRESS,1)
  return "OK, Arduino LED OFF."

def control_arduino_read():
  print read_value('temperature')
  print read_value('humidity')
  print read_value('illuminance')
  print read_value('mic')
  print read_value('motion')
  return "OK, Arduino READ."

手順としては、

  1. Raspberry Pi側から、Arduinoに対して「この後、このセンサの値の下位1バイトを取りにいくから準備しといて!」というメッセージ(1バイト)を送信する(write)
  2. メッセージを受信したArduinoは、メッセージに応じて、センサの値の下位1バイトを、送信用のbyte型変数に入れておく
  3. Raspberry Pi側から、Arduinoに対して「今お前が用意している1バイトのデータをくれ!」と要求する(read)
  4. 要求を受信したArduinoは、用意しておいたbyte型の値を返す
  5. Raspberry Pi側から、Arduinoに対して「この後、このセンサの値の上位1バイトを取りにいくから準備しといて!」というメッセージ(1バイト)を送信する(write)
  6. メッセージを受信したArduinoは、メッセージに応じて、センサの値の上位1バイトを、送信用のbyte型変数に入れておく
  7. Raspberry Pi側から、Arduinoに対して「今お前が用意している1バイトのデータをくれ!」と要求する(read)
  8. 要求を受信したArduinoは、用意しておいたbyte型の値を返す
  9. Raspberry Pi側で、4.と8.で送られてきた計2バイトの値から、元々のint型の整数を復元する(必要に応じて、1/100もする)
  10. 1.〜9.の手順を、センサの数だけ繰り返す

となります。。。。わー、煩雑。。。もっとスマートにやりたかった。。。

しかも、1.(5.)と3.(7.)のwrite/readを連続して記述するとreadで値の取得に失敗して、間に0.001秒でもsleepを入れるとちゃんと動いてくれるというおまけつき。。。すみません、pythonに慣れた方ならもっとスマートに実現できるのでしょうが、今の自分にはこれが精一杯です。。。が、まあなんとか、これで一応、Raspberry Piから、Arduinoのセンシングデータを取得できるようになりました。

 

これでハードとソフト的には一通りワンパスは通っているワケですが、この記事のトップにある写真が示すように、現状ではとても実用的な配線になっていません。ラピロの頭がフルオープンです。

次のステップとしては、このあたりの配線をいかに綺麗にまとめあげるか。。。ですが、これはちょっと時間がかかりそうです。