小嶋秀樹 | 研究室
日本語 | English
ESP に6軸センサを i2c 接続する

ESP-WROOM-02(ESP8266 コア)はアナログ入力(計測範囲 0.0〜1.0V が 1ch のみ)が弱いので,i2c 接続でセンシングすることを考えます.ここでは安価な6軸モーションセンサ(加速度3軸+ジャイロ3軸)として MPU-6050(を使った GY-521 という中華ボード)を対象とします.調達時は Amazon で 210円でした.

GY-521 付属のピンポストを4極分だけ使います.ブレッドボードに差せるように,VCC, GND, SCL, SDA の4極に基板裏側から下向きピンポストをハンダ付けしてください.

【i2c による接続】

i2c は通信方式のひとつです.基準となる GND が共有できていれば,クロック(SCL)とデータ(SDA)の2線だけで,1台のマスター(多くの場合 ESP-WROOM-02 や Arduino など)と,最大 100 台以上のスレーブ(センサやディスプレイなど)の双方向通信が可能です.

ここでは上述の GY-521(MPU-6050)で捉えたモーション情報(加速度・角加速度)を ESP-WROOM-02 で読み出す方法を解説します.まず,ESP-WROOM-02 と GY-521 を次のように接続します.このうち,IO4〜SDA のデータ線と IO5〜SCL のクロック線は,GY-521 の内部でプルアップ(2.2kΩ)されているようですので,外付けのプルアップ抵抗は不要です.

すでに他の i2c デバイスが接続されている場合は,GY-521 を 3V3, GND, IO4, IO5 に並列に接続してください.その場合,プルアップ抵抗は SDA, SCL に1本ずつ入っていればよいので,別段追加する必要はないでしょう.

【データを読み出す】

Arduino 開発環境を立ち上げ,つぎのサンプルプログラムを打ち込んでください.6軸センサから加速度や角速度を読み出し,Arduino 開発環境のシリアルモニタ(115200 baud に設定)に表示するものです.

//
//  MPU6050 on ESP8266

#include <Wire.h>

#define MPU6050_ADDR 0x68
#define MPU6050_AX  0x3B
#define MPU6050_AY  0x3D
#define MPU6050_AZ  0x3F
#define MPU6050_TP  0x41    //  data not used
#define MPU6050_GX  0x43
#define MPU6050_GY  0x45
#define MPU6050_GZ  0x47

short int AccX, AccY, AccZ;
short int Temp;
short int GyroX, GyroY, GyroZ;

void setup() {
  //  serial for debugging
  Serial.begin(115200);
  Serial.println("*** started");
  //  i2c as a master
  Wire.begin();
  //  wake it up
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(0x6B);
  Wire.write(0);
  Wire.endTransmission();
}

void loop() {
  //  send start address
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(MPU6050_AX);
  Wire.endTransmission();  
  //  request 14bytes (int16 x 7)
  Wire.requestFrom(MPU6050_ADDR, 14);
  //  get 14bytes
  AccX = Wire.read() << 8;  AccX |= Wire.read();
  AccY = Wire.read() << 8;  AccY |= Wire.read();
  AccZ = Wire.read() << 8;  AccZ |= Wire.read();
  Temp = Wire.read() << 8;  Temp |= Wire.read();  //  (Temp-12421)/340.0 [degC]
  GyroX = Wire.read() << 8; GyroX |= Wire.read();
  GyroY = Wire.read() << 8; GyroY |= Wire.read();
  GyroZ = Wire.read() << 8; GyroZ |= Wire.read();
  //  debug monitor
  Serial.print("  "); Serial.print(AccX);
  Serial.print("  "); Serial.print(AccY);
  Serial.print("  "); Serial.print(AccZ);
  Serial.print("  "); Serial.print(GyroX);
  Serial.print("  "); Serial.print(GyroY);
  Serial.print("  "); Serial.print(GyroZ); 
  Serial.println("");
  delay(20);
}

IO4(SDA)と IO5(SCL)には複数の i2c デバイスを接続することができますが,アドレス 0x68 によって MPU-6050 を選択しています.MPU-6050 には多数のレジスタがあり,番号で区別してアクセスすることができます.たとえば 0x3B, 0x3C の2バイトにはX軸加速度が格納されていて,この番号を指定して読み出しています.0x6B はセンサの電源を制御するレジスタです.初期値はオフなので,0 を書き込むことでセンサを動作開始させています.(加えて,温度もゲットできるんですが,あくまでデータ校正用にセンサ温度を測定しているようなので,ここでは扱いません.)

このプログラムを動作させると,シリアルモニタ(115200 baud)につぎのような表示が出力されます.最初の3つの数値は,各軸方向の加速度で,符号つき 16 ビット整数で表現されています.フルスケール +32767〜−32768 が,およそ +2g〜−2g に対応します.それに続く3つの数値が角速度で,フルスケール +32767〜−32768 が,およそ +250°/s〜−250°/s に対応するようです.

  2128  -592  16500  23  5  -9
  2136  -588  16472  -13  -6  1
  2108  -656  16388  -15  22  6
  1388  -112  16580  -207  5621  808
  584  -124  16544  75  4142  3171
  1408  -952  16360  -3  6079  3341
  2704  -1512  15456  -280  11240  2731
  680  -964  16964  -482  13041  1024
  -11452  -1224  29504  -45  -2055  -331
  7076  336  9324  -75  1084  381
  188  128  15832  8  479  732
  1208  -2724  16228  23  -37  1126
  332  -1652  17564  463  -11399  -72
  -5932  -3852  21836  890  -21182  -2844
  -3060  -576  22152  -3005  -3359  950
  5308  2780  16656  -3070  2343  2086
  4520  4700  17972  -4190  5058  -1351
  4004  -1208  12012  -2934  22060  -3944

最初の数行は,センサチップの刻印面を上向きに静止させた状態に対応します.刻印面に垂直上方向がZ軸となるため,AccZにのみ約 1g が観測されています.直観的には −1g が観測されるはずですが,逆符号になっているようです.刻印文字の右(東)方向がX軸,上(北)方向がY軸となり,それぞれの軸方向の加速度(やはり逆符号)と,それぞれの軸を右ネジ方向に回転した場合の角速度(こちらは直観どおりの符号)が得られています.

【データを安定化させる】

シリアルモニタ上の数値をよく見ると,静止状態でも多少のオフセット(定常偏差)とノイズが含まれていることがわかります.加速度オフセットをキャンセルするには,ある軸を鉛直上方向したときの値と鉛直下方向にしたときの中間値をゼロ点とするか,あるいは重力加速度と測定軸を直交させたときの値をゼロ点とするなど,いろいろ工夫してみてください.角速度オフセットのキャンセルは,静止状態の値をそのまま引いてやればよいはずです.ノイズの除去には,適切な時定数のローパスフィルタを適用すればよいでしょう.つぎにあげる loop() を参考にしてください.なお,ACC_X_OFS などは,オフセット補正値(整数定数)です.

float AccX_f = 0.0f, AccY_f = 0.0f, AccZ_f = 0.0f;
float GyroX_f = 0.0f, GyroY_f = 0.0f, GyroZ_f = 0.0f;

void loop() {
  //  send start address
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(MPU6050_AX);
  Wire.endTransmission();  
  //  request 14bytes (int16 x 7)
  Wire.requestFrom(MPU6050_ADDR, 14);
  //  get 14bytes
  AccX = Wire.read() << 8;  AccX |= Wire.read();
  AccY = Wire.read() << 8;  AccY |= Wire.read();
  AccZ = Wire.read() << 8;  AccZ |= Wire.read();
  Temp = Wire.read() << 8;  Temp |= Wire.read();
  GyroX = Wire.read() << 8; GyroX |= Wire.read();
  GyroY = Wire.read() << 8; GyroY |= Wire.read();
  GyroZ = Wire.read() << 8; GyroZ |= Wire.read();
  //  filtering
  AccX_f = 0.9 * AccX_f + 0.1 * (AccX - ACC_X_OFS);
  AccY_f = 0.9 * AccY_f + 0.1 * (AccY - ACC_Y_OFS);
  AccZ_f = 0.9 * AccZ_f + 0.1 * (AccZ - ACC_Z_OFS);
  GyroX_f = 0.9 * GyroX_f + 0.1 * (GyroX - GYRO_X_OFS);
  GyroY_f = 0.9 * GyroY_f + 0.1 * (GyroY - GYRO_Y_OFS);
  GyroZ_f = 0.9 * GyroZ_f + 0.1 * (GyroZ - GYRO_Z_OFS);
  //  debug monitor
  Serial.print("  "); Serial.print(AccX_f, 5);
  Serial.print("  "); Serial.print(AccY_f, 5);
  Serial.print("  "); Serial.print(AccZ_f, 5);
  Serial.print("  "); Serial.print(GyroX_f, 5);
  Serial.print("  "); Serial.print(GyroY_f, 5);
  Serial.print("  "); Serial.print(GyroZ_f, 5); 
  Serial.println("");
  delay(20);
}

最新の Arduino 開発環境には「シリアルプロッタ機能」が組み込まれています.Tools から Serial Plotter を開くと,データフィールドごとに色分けされたグラフが表示されます.赤がZ軸加速度です.横1マスは約 2s です.おおよそ安定したデータが取れていることがわかります.

【測定範囲を広げるには / センサを2個使うには】

デフォルトの状態では,加速度は ±2g,角速度は ±250°/s がフルスケールとなっています.より広い測定範囲を得るには,setup() をつぎのように改変してください.

void setup() {
  //  serial for debugging
  Serial.begin(115200);
  Serial.println("*** started");
  //  i2c as a master
  Wire.begin();
  //  wake it up
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(0x6B);
  Wire.write(0);
  Wire.endTransmission();
  //  range of accel
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(0x1C);
  Wire.write(0x18);  //  0x00:2g, 0x08:4g, 0x10:8g, 0x18:16g
  Wire.endTransmission();
  //  range of gyro
  Wire.beginTransmission(MPU6050_ADDR);
  Wire.write(0x1B);
  Wire.write(0x18);  //  0x00:250, 0x08:500, 0x10:1000, 0x18:2000deg/s
  Wire.endTransmission();
}

2つの GY-521 を i2c 接続するには,一方の GY-521 の AD0 端子を VCC に接続してください.これで i2c アドレスが 0x69 になり,もとのアドレス 0x68 の GY-521 と区別して通信することができます.3個以上は... ムリっぽいです.

このセンサチップについてのより詳しい情報については,メーカーのウェブページをご覧ください.また,プログラミングについてのより詳しい情報は Arduino Playground が参考になると思います.

ESP にタッチパネルを SPI 接続する

タッチパネルは,ふつう液晶ディスプレイの表面に置かれ,ディスプレイに表示された画像コンテンツへのタッチ(クリック)を捉えることで,さまざまなインタラクションを実現するためのデバイスです.ここでは抵抗膜式タッチパネル(Aitendo で調達したもの)を SPI 通信によって Dongbeino に接続することを解説します.(TFT 液晶ディスプレイに付属したタッチパネルの読み取りについても参考になると思います.)

【抵抗膜式タッチパネルの原理】

抵抗膜式タッチパネルは,下図に示すように,平行な2枚の抵抗膜(ほぼ透明)からなり,上側の抵抗膜の両端には電極 X+ / X− が,下側の抵抗膜にはそれと直交する軸の両端に電極 Y+ / Y− が取り付けられていて,これらが4つの線として引き出されています.

X+ / X− 間に電圧(V)をかけると,上側の抵抗膜に電圧の勾配が現れます.このとき,外力(タッチ)により上下の抵抗膜がある場所(X 方向の位置 x)で接触すると,その位置における上側抵抗膜の電位 V × x/d を,下側抵抗膜の Y+ から読み出せます.下側抵抗膜には電流はほとんど流れないので,接触点の電位がほぼそのまま Y+ から読み出せ,位置 x を求めることができます.上下の役割を入れ替えれば,Y 方向の位置 y も求められます.

この原理からわかるように,基本的に同時に取得できるタッチは1点のみです.複数の接触点がある場合は,それらの中央付近の「1点」として認識されます.ゆえに複雑なジェスチャを読み取ることはできませんが,簡単なインタフェースで安価にタッチパネルを実現できるので,自作派には重宝するデバイスです.

【SPI 接続によるデータの読み出し】

タッチパネルと Dongbeino(ESP-WROOM-02)との接続には,ADS7843(または XPT2046)という専用 IC を使います.抵抗膜での電圧勾配の生成,接触点の電圧計測と ADC,上下抵抗膜の役割入れ替え,通信の制御などをすべて自動的に行ってくれます.

Dongbeino へのデータ取り込みは SPI(Serial Pheripheral Interface)というシリアル通信を使います.SPI はマスター(ここでは Dongbeino)とスレーブ(ここでは ADS7843)の間をつなぐ4線(CS, CLK, MOSI, MISO)で双方向の通信をするものです.スレーブが複数となる場合もあり,共通の CLK, MOSI, MISO に「ぶら下がる」ように接続します.ESP-WROOM-02 では,CLK は IO14,MOSI は IO13,MISO は IO12 のピンに配置されています.CS(Chip Select;または CE: Chip Enable, SS: Slave Select)は,マスターから各スレーブに1本ずつ接続します.CS には余っているディジタル出力端子を使えばよいでしょう.CS はアクティブ LOW です.

マスターは,CS を LOW にしたスレーブにバイト列を送り出し,それと同時に同じ長さのバイト列をそのスレーブから受け取ります.つまり,同じ長さのバイト列をマスターとスレーブが同時交換するわけです.交換されるデータの長さ(ビット数)は CS が LOW の間のクロック数と同じになります.ひとかたまりのバイト列(パケットなど)の送受信が終われば,CS を HIGH に戻します.

ここで使用する ADS7843 は,下図のような SPI 通信をマスター(Dongbeino)との間で行います.詳しい説明は ADS7843 のデータシートに譲りますが,ここではマスターから A2:0=B001 (X 位置),A2:0=B101 (Y 位置),M=0 (12bit モード),S/D=1 (single-ended),PD1:2=B00 (power down with PEN) からなる 3 バイトデータを送ると,ADS7843 からは B000001110 に続いて 12 bit のデータとそれに後続する B000 がマスターに返されます.つまり,X 位置を調べるには {0x94, 0x00, 0x00} を,Y 位置を調べるには {0xd4, 0x00, 0x00} を送り,ADS7843 からの返答(3 バイト)のうち,下位 2byte を 3bitだけ右シフトすれば,12bit の整数値(0〜4095)が得られるわけです.

【タッチ位置を読み取る回路】

Dongbeino と ADS7843 および抵抗膜式タッチパネルの接続は,およそ下図のようになります.PENIRQ はタッチされているときに GND に接続されます.10kΩ でプルアップし,IO5 から読み取るようにしています.なお,MOSI (IO13) には LED が接続されていますが,SPI 通信には影響ありません.SPI 通信中は LED がやや暗く点灯するはずです.また,IO15 は 10kΩ でプルダウンされていますが,こちらも SPI 通信には影響ありません.

実際の配線は,横30穴のブレッドボードの場合,かなりキチキチです.下図を参考にしてください,ADS7843 のピン配列はデータシートを参照してください.ADS7833 は 0.65mm ピッチの SSOP16 というパッケージになっているため,ここでは 2.54mm ピッチへの変換基板(緑色の部品)を使っています.細かいハンダづけになりますね.また,タッチパネルからのフィルムケーブル(コネクタ部分は 1mm ピッチ)を,この変換基板の裏側(1.27mm ピッチの SOP16 用のパターン)にハンダづけしています.(ちょっと無理やり感ありです.)

【タッチ位置を読み取るプログラム】

タッチ位置を読み取りシリアルモニタに表示するプログラム例をつぎにあげます.ここでは 1000 サンプリングごとに 12bit 整数(0〜4095)を成分とする X-Y 座標を表示します.シリアルプロッタも利用できます.

#include <SPI.h>
#define CS      15
#define PEN     5
int Count = 0;

void setup() {
  //  debug output
  Serial.begin(115200); delay(100);
  Serial.println("");
  Serial.println("*** esp_touthPanel ***");
  //  SPI settings (SCK, MISO, MOSI, CS=IO15)
  pinMode(CS, OUTPUT);
  digitalWrite(CS, HIGH);
  SPI.begin();
  SPI.setBitOrder(MSBFIRST);
  SPI.setDataMode(SPI_MODE0);
  SPI.setFrequency(1000000ULL);  //  max 2MHz
  //  D5 for PEN
  pinMode(PEN, INPUT);
}

void loop() {
  int x, y;
  if (touchPanel(x, y)) {
    if (Count++ % 1000 == 0) {
      Serial.print(x), Serial.print(", "), Serial.println(y);
    }
  }
}

bool touchPanel(int &x, int &y) {  
  if (digitalRead(PEN) == HIGH) {
    //  if not pen-down, return false
    return false;
  }
  else {
    //  get data from ADS7843
    unsigned char dataOut[3], dataIn[3];
    //  X-axis (X+)
    dataOut[0] = 0x94; dataOut[1] = 0x00; dataOut[2] = 0x00;
    digitalWrite(CS, LOW);
    dataIn[0] = SPI.transfer(dataOut[0]);
    dataIn[1] = SPI.transfer(dataOut[1]);
    dataIn[2] = SPI.transfer(dataOut[2]);
    digitalWrite(CS, HIGH);
    x = (dataIn[1] * 256 + dataIn[2]) >> 3;
    //  Y-axis (Y+)
    dataOut[0] = 0xD4; dataOut[1] = 0x00; dataOut[2] = 0x00;
    digitalWrite(CS, LOW);
    dataIn[0] = SPI.transfer(dataOut[0]);
    dataIn[1] = SPI.transfer(dataOut[1]);
    dataIn[2] = SPI.transfer(dataOut[2]);
    digitalWrite(CS, HIGH);
    y = (dataIn[1] * 256 + dataIn[2]) >> 3;
    //  return true
    return true;
  }
}

ここでは SPI のクロックを 1MHz にしています."MSBFIRST" や "SPI_MODE0" はオマジナイと思ってください.SPI.transfer() はスレーブとのバイト交換を実行するメソッドで,引数にスレーブへの送信バイトを指定し,実行後の返値がスレーブからの受信バイトとなります.ここでは3バイトを交換しています.このバイト列交換の前後で CS(アクティブ LOW)を操作しています.これが SPI の基本的な使い方です.