概要
上記で紹介している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文を書かなくても初期化できるようにはしたいと思っています。




コメント