概要
上記で紹介しているM5Stack社の製品をワンコード、ワンバイナリで開発できるライブラリですが、I2Cまわりがちょっと面倒になるので、どのような処理にすればいいかを考えてみました。
ついでにI2Cスキャナも作ってみました。
シリーズ別状況
M5Stack | Core2 | M5StickC | ATOM | |
内蔵I2C GPIO | 21, 22 | 21, 22 | 21, 22 | 21, 25 |
内蔵I2C クラス | Wire | Wire1 | Wire1 | Wire1 |
Wireクラス 初期GPIO | 21, 22 | 32, 33 | 32, 33 | 26, 32 |
GROVEポートGPIO | 21, 22 | 32, 33 | 32, 33 | 26, 32 |
HAT GPIO | 0, 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として処理をしています。
M5Stack | Core2 | M5StickC | ATOM | |
内蔵I2C GPIO | 21, 22 | 21, 22 | 21, 22 | 21, 25 |
GROVEポートGPIO | 32, 33 | 32, 33 | 26, 32 | |
HAT GPIO | 0, 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クラスは初期化していないためです。
M5Stack | Core2 | M5StickC | ATOM | |
Wireクラス(外部拡張) | 未初期化 | 未初期化 | 未初期化 | 未初期化 |
Wire1クラス(内蔵I2C) | 21, 22 | 21, 22 | 21, 22 | 21, 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文を書かなくても初期化できるようにはしたいと思っています。
コメント