M5LiteのI2C周りを考える(+I2Cスキャナ作成)

概要

上記で紹介しているM5Stack社の製品をワンコード、ワンバイナリで開発できるライブラリですが、I2Cまわりがちょっと面倒になるので、どのような処理にすればいいかを考えてみました。

ついでにI2Cスキャナも作ってみました。

シリーズ別状況

M5StackCore2M5StickCATOM
内蔵I2C GPIO21, 2221, 2221, 2221, 25
内蔵I2C クラスWireWire1Wire1Wire1
Wireクラス 初期GPIO21, 2232, 3332, 3326, 32
GROVEポートGPIO21, 2232, 3332, 3326, 32
HAT GPIO0, 26

M5Stack(BASIC, GRAY, FIRE)は内蔵I2CをWireクラスでアクセスしていますが、M5StickC以降は内蔵I2CがWire1クラスで、外部拡張がWireクラスに変更になっています。

これはM5Stackは本体のGROVEポートに内蔵I2CのGPIOが出ているためで、通常Wire1クラスを利用しない構成です。

M5StickC以降のボードは、内蔵I2CのGPIOがGROVEポートには出ていないため、GROVEで接続するI2CデバイスにはWireクラス、内蔵I2CにはWire1クラスと使い分けています。

また、M5StickCはHATが存在しているので、GROVEかHATかで処理が変わってきます。I2Cは2系統までしか同時に利用できないので内蔵I2Cと、GROVEかHATのどちらかという構成になります。

M5Stack Core2はちょっと特殊で、内部I2CはM-BUSには出ていますので、M5Stack Fireのボトムや今後発売されるかもしれないGROVEポート増設用のモジュールなどを使うことで、内蔵I2CをGROVE端子から利用できるようになると思います。通常はGROVEの32と33を使ってWireクラスで接続する構成になります。

M5Stack全シリーズ対応I2Cスキャナを作ってみた

#include <M5Lite.h>

TwoWire *i2cWire;

uint8_t sda = -1;
uint8_t scl = -1;
uint8_t i2cMode = 0;

void updateI2c() {
  if (M5.Ex.board == lgfx::board_M5Stack) {
    i2cMode = 0;

    // Internal
    sda = 21;
    scl = 22;
    i2cWire = &Wire1;
  } else if (M5.Ex.board == lgfx::board_M5StackCore2) {
    // M5Stack Core2
    if (i2cMode == 1) {
      // GROVE
      sda = 32;
      scl = 33;
      Wire.begin(sda, scl);
      i2cWire = &Wire;
    } else {
      i2cMode = 0;

      // Internal
      sda = 21;
      scl = 22;
      i2cWire = &Wire1;
    }
  } else if (M5.Ex.board == lgfx::board_M5StickC || M5.Ex.board == lgfx::board_M5StickCPlus) {
    // M5StickC
    if (i2cMode == 1) {
      // GROVE
      sda = 32;
      scl = 33;
      Wire.begin(sda, scl);
      i2cWire = &Wire;
    } else if (i2cMode == 2) {
      // PIN Header
      sda = 0;
      scl = 26;
      Wire.begin(sda, scl);
      i2cWire = &Wire;
    } else {
      i2cMode = 0;

      // Internal
      sda = 21;
      scl = 22;
      i2cWire = &Wire1;
    }
  } else if (M5.Ex.board == lgfx::board_TTGO_TWatch) {
    i2cMode = 0;

    // Internal
    sda = 21;
    scl = 22;
    i2cWire = &Wire1;
  } else if (M5.Ex.board == lgfx::board_unknown) {
    // ATOM
    if (i2cMode == 1) {
      // GROVE
      sda = 26;
      scl = 32;
      Wire.begin(sda, scl);
      i2cWire = &Wire;
    } else {
      i2cMode = 0;

      // Internal
      sda = 21;
      scl = 25;
      i2cWire = &Wire1;
    }
  }
}

void setup() {
  M5.begin();
  M5.Lcd.fillScreen(BLACK);
}

void loop() {
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setCursor(0, 0);

  M5.Lcd.printf("I2C Scan\n");
  Serial.printf("===================\n");
  Serial.printf("I2C Scan\n");

  i2cMode = 0;
  updateI2c();

  while (1) {
    M5.Lcd.println();
    M5.Lcd.printf("SDA:%2d SCL:%2d\n", sda, scl);
    Serial.println();
    Serial.printf("SDA:%2d SCL:%2d\n", sda, scl);

    for (byte address = 0; address <= 127; address++) {
      i2cWire->beginTransmission(address);
      byte error = i2cWire->endTransmission();
      if (error == 0) {
        M5.Lcd.printf("%02X ", address);
        Serial.printf("%02X ", address);
      } else {
        if (240 <= M5.Lcd.width()) {
          M5.Lcd.print(".. ");
          if (address % 16 == 15) {
            M5.Lcd.println();
          }
        }
      }
      delay(1);
    }

    M5.Lcd.println();
    Serial.println();

    i2cMode++;
    updateI2c();
    if (i2cMode == 0) {
      break;
    }
  }

  delay(3000);
}

M5LiteではLCD制御にらびやんさんのLovyanGFXを利用しているので、LCDの搭載していないATOM以外は機種判定ができます。現状は機種判定できない場合にはATOMとして処理をしています。

M5StackCore2M5StickCATOM
内蔵I2C GPIO21, 2221, 2221, 2221, 25
GROVEポートGPIO32, 3332, 3326, 32
HAT GPIO0, 26

上記のGPIOで機種別にWireクラスを初期化からI2Cスキャンを繰り返すコードになっています。

上記のような感じで同じソースを、複数ボードで共有することができます。ATOMは画面がないのでシリアルモニタだけの出力になっています。画面サイズに応じてM5Stack系は一般的なI2Cスキャン風の表示、M5StickCだと見つかったアドレスだけ表示するようにしています。

M5Liteが入っている「ESP32LitePack」ライブラリのスケッチ例にも追加してあります。

M5Lite Debugにも追加

上記のI2Cスキャナは既存のWireクラスを初期化しながら実行しているため、そのままのコードを既存スケッチに入れると環境を壊してしまうことになります。

そのため、レジスタから初期化済みのI2Cを探し、そこに接続しているI2Cをスキャンするようにしてみました。

    void dispI2c() {
      Serial.printf("===============================================================\n");
      Serial.printf("I2C Scan\n");
      Serial.printf("===============================================================\n");

      // Disp I2C
      int i2c0SDA = -1;
      int i2c0SCL = -1;
      int i2c1SDA = -1;
      int i2c1SCL = -1;
      for (int i = 0; i < 40; i++) {
        uint32_t reg = GPIO_FUNC0_OUT_SEL_CFG_REG + 0x04 * i;
        uint32_t reg_val = ESP_REG(reg);
        int func = reg_val & 0b111111111;
        if (func == I2CEXT0_SDA_IN_IDX) {
          i2c0SDA = i;
        } else if (func == I2CEXT0_SCL_IN_IDX) {
          i2c0SCL = i;
        } else if (func == I2CEXT1_SDA_IN_IDX) {
          i2c1SDA = i;
        } else if (func == I2CEXT1_SCL_IN_IDX) {
          i2c1SCL = i;
        }
      }

      if (i2c0SDA != -1 && i2c0SCL != -1) {
        // Wire
        Serial.printf("Wire(I2C0) SDA:%2d SCL:%2d\n", i2c0SDA, i2c0SCL);
        for (byte address = 0; address <= 127; address++) {
          Wire.beginTransmission(address);
          byte error = Wire.endTransmission();
          if (error == 0) {
            Serial.printf("%02X ", address);
          } else {
            Serial.print(".. ");
            if (address % 16 == 15) {
              Serial.println();
            }
          }
          delay(1);
        }
        Serial.println();
      }
      if (i2c1SDA != -1 && i2c1SCL != -1) {
        // Wire1
        Serial.printf("Wire1(I2C1) SDA:%2d SCL:%2d\n", i2c1SDA, i2c1SCL);
        for (byte address = 0; address <= 127; address++) {
          Wire1.beginTransmission(address);
          byte error = Wire1.endTransmission();
          if (error == 0) {
            Serial.printf("%02X ", address);
          } else {
            Serial.print(".. ");
            if (address % 16 == 15) {
              Serial.println();
            }
          }
          delay(1);
        }
        Serial.println();
      }
    }

呼び出している関数部分の抜粋ですが、レジスタからGPIOマトリクスの情報を取得して、初期化済みI2Cを探しています。その後はI2Cスキャナと同じようにスキャンしています。

この処理はシリアルモニタから「I2C」と打ち込むことで実行されます。

上記のように上のテキストボックスにI2Cと入れて、エンターを押すと画面に初期化済みI2Cに対してスキャンをします。

上記はM5StickCにENV HATと、GROVE経由でACCELユニットを接続しているのですが、内蔵I2CのWire1しかスキャンしていません。これはM5Liteライブラリは内蔵I2CをWire1クラスで初期化し、外部拡張のWireクラスは初期化していないためです。

M5StackCore2M5StickCATOM
Wireクラス(外部拡張)未初期化未初期化未初期化未初期化
Wire1クラス(内蔵I2C)21, 2221, 2221, 2221, 25

つまり、上記の設定で初期化しています。M5Stackが標準ライブラリのWireから、M5LiteではWire1に変更されていますが、シリーズ全体としては統一した利用方法になっています。

HAT端子を利用

#include <M5Lite.h>

void setup() {
  M5.begin();
  Wire.begin(0, 26);
}

void loop() {
  M5.update();
}

上記のようにWireクラスを0と26のHAT端子側で初期化してみました。

Wireを確認してみると、0x10と0x5c、0x76が見つかりました。ENVハットなのでBMM150(0x10)とDHT12(0x5c)、BMP280(0x76)が搭載されているので大丈夫ですね。

GROVE端子を利用

#include <M5Lite.h>

void setup() {
  M5.begin();
  Wire.begin(32, 33);
}

void loop() {
  M5.update();
}

GROVE端子側の32と33で初期化します。

0x53が取得できました。接続したのはACCELユニットなのでADXL345(0x53)で大丈夫そうですね。

まとめ

内蔵I2CはすべてWire1で統一できたのですが、GROVE端子側が機種別対応になってしまっています。本当はこのへんも整理したいのですが、I2C以外の用途でGROVE端子を使うことがあるので、勝手にラッピングすることはできないと思っています。

おそらくはM5.Exクラス以下にGROVE端子のピン番号などを準備して、ボード別のif文を書かなくても初期化できるようにはしたいと思っています。

コメント