M5StickCのButtonクラスを調べてみた

Homeボタンと右ボタン、そして電源ボタンの制御について調べてみました。

※現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスを確認してください。

ボタンについて

ざっくりと上記で取得できます。

ホームボタンと右ボタンについて

M5.update()

ボタンの状態を更新する関数です。Buttonクラスを利用する場合にはloop()の先頭に近い場所で必ず実行するようにしましょう。

この関数を呼ばないとボタンの状態は更新されません。

isPressed()

今現在ボタンを押しているかを返却します。ボタンを押している間は常にTRUEが戻ってきます。

isReleased()

今現在ボタンを離しているかを返却します。ボタンを押していない間は常にTRUEが戻ってきます。

wasPressed()

ボタンを押してから最初に呼び出した時だけTRUEを返却します。1度しか状態を取得できないので注意して呼び出しましょう。

wasReleased()

ボタンを押して、離してから最初に呼び出した時だけTRUEを返却します。1度しか状態を取得できないので注意して呼び出しましょう。一般的にボタンを押した判定はこの関数の戻り値で判定したほうが自然だと思います。

pressedFor(ms)

ボタンをms以上押している場合にTRUEが返却されます。次のループでもTRUEを返却するので長押し処理で数値をカウントアップする場合などに利用すると良いと思います。

releasedFor(ms)

ボタンを離してからms以上経過している場合にTRUEが返却されます。ボタン連打防止などでこの関数で一定以上時間経過後にフラグ更新して、次の入力を受け付ける処理などに利用できると思います。

wasReleasefor(ms)

ms以上ボタンを押して、離してから最初に呼び出した時だけTRUEを返却します。1度しか状態を取得できないので注意して呼び出しましょう。

長押しを戻るボタンに設定した場合などに利用すると、便利だと思います。

lastChange()

最後にボタンの状態が変更された時の millis() の値が返却されます。現在のmillis()からの差分が経過時間になります。

電源ボタン

M5.Axp.GetBtnPress()

  • 戻り値1:電源ボタンを1秒以上押した場合
  • 戻り値2:電源ボタンを1秒未満押してから離した場合
  • 戻り値0:上記以外

この関数は0以外の数値は1度しか取得できないので注意してください。

また6秒以上電源ボタンを押すと、電源が切れるので長押しの操作はあまり適していません。

サンプルスケッチ

#include <M5StickC.h>

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3); // 画面に入らないので横向きにする
}

void loop() {
  // Buttonクラスを利用するときには必ずUpdateを呼んで状態を更新する
  M5.update();

  // カーソル初期化
  M5.Lcd.setCursor(0, 0);

  // ホームボタンが現在押されているか?
  M5.Lcd.print("BtnA.isPressed():");
  M5.Lcd.println( M5.BtnA.isPressed() );

  // ホームボタンが現在離しているか?
  M5.Lcd.print("BtnA.isReleased():");
  M5.Lcd.println( M5.BtnA.isReleased() );

  // ホームボタンを押したか?(1度だけ取得可能)
  if ( M5.BtnA.wasPressed() ) {
    Serial.println("BtnA.wasPressed() == TRUE");
  }

  // ホームボタンを離したか?(1度だけ取得可能)
  if ( M5.BtnA.wasReleased() ) {
    Serial.println("BtnA.wasReleased() == TRUE");
  }

  // ホームボタンを現在ms以上押しているか?
  if ( M5.BtnA.pressedFor(1000) ) {
    Serial.println("BtnA.pressedFor(1000) == TRUE");
  }

  // ホームボタンを離してからms以上経過しているか?
  M5.Lcd.print("BtnA.releasedFor(1000):");
  M5.Lcd.println( M5.BtnA.releasedFor(1000) );

  // ホームボタンをms以上押してから離したか?(1度だけ取得可能)
  if ( M5.BtnA.wasReleasefor(1000) ) {
    Serial.println("BtnA.wasReleasefor(1000) == TRUE");
  }

  // ホームボタンが最後に更新した起動経過時間 millis()
  M5.Lcd.print("BtnA.lastChange():");
  M5.Lcd.println( M5.BtnA.lastChange() );

  // 空行を追加
  M5.Lcd.println();

  // 右ボタンが現在押されているか?
  M5.Lcd.print("BtnB.isPressed():");
  M5.Lcd.println( M5.BtnB.isPressed() );

  // 右ボタンが現在離しているか?
  M5.Lcd.print("BtnB.isReleased():");
  M5.Lcd.println( M5.BtnB.isReleased() );

  // 右ボタンを押したか?(1度だけ取得可能)
  if ( M5.BtnB.wasPressed() ) {
    Serial.println("BtnB.wasPressed() == TRUE");
  }

  // 右ボタンを離したか?(1度だけ取得可能)
  if ( M5.BtnB.wasReleased() ) {
    Serial.println("BtnB.wasReleased() == TRUE");
  }

  // 右ボタンを現在ms以上押しているか?
  if ( M5.BtnB.pressedFor(1000) ) {
    Serial.println("BtnB.pressedFor(1000) == TRUE");
  }

  // 右ボタンを離してからms以上経過しているか?
  M5.Lcd.print("BtnB.releasedFor(1000):");
  M5.Lcd.println( M5.BtnB.releasedFor(1000) );

  // 右ボタンをms以上押してから離したか?(1度だけ取得可能)
  if ( M5.BtnB.wasReleasefor(1000) ) {
    Serial.println("BtnB.wasReleasefor(1000) == TRUE");
  }

  // 右ボタンが最後に更新した起動経過時間 millis()
  M5.Lcd.print("BtnB.lastChange():");
  M5.Lcd.println( M5.BtnB.lastChange() );

  // 電源ボタンの状態取得(一度しか0以外のステータスは取得できない)
  int axpButton = M5.Axp.GetBtnPress();
  if ( axpButton == 1 ) {
    // 1秒以上電源ボタンを押している
    Serial.println("M5.Axp.GetBtnPress() == 1");
  }
  if ( axpButton == 2 ) {
    // 1秒未満電源ボタンを押して離した
    Serial.println("M5.Axp.GetBtnPress() == 2");
  }
}

まとめ

電源ボタンも利用できるようになったので、ボタンの利用用途が広がりますね。ただ取得できるタイミングが違うので、完全に同じように使うことはできないみたいです。

普通のボタンクリックは wasReleased() と axpButton == 2 が同じような動きになりますが、電源の長押しは1秒以上押したら1度だけ1が返却されるので、押しっぱなしかどうかがわからないんですよね。

6秒以上電源ボタン押していると電源切れるので、長押しは電源ボタンにはあまり割り振らないほうがいいですね。

M5StickC 0.0.7のAXP192追加関数を調べてみた

Githubのソースは確認していましたが、リリースされたSDKで動作検証もしてみました。

最新版の詳細はM5StickC日本語リファレンスで確認してください。

追加関数

内部動作電圧取得 GetVapsData()

内部の動作している電圧を取得する関数です。あまり使うことはないと思いますが、この電圧が下がってくると、バッテリー残量少ないです。

電源ボタン取得 GetBtnPress()

電源ボタンの状態が取れるようになりました!
これで3ボタン使えるので、リモコン系を作るときには便利ですね。

ただし、ボタンの状態じゃなくって、1秒以下のクリックか、1秒以上ボタンを押していたってイベントだけ取得できます。

低電圧状態チェック GetWarningLeve()

APS電圧が約3.4V以下になっていると1が戻ってきます。低電圧になったら警告を出すなどの場合に利用します。

スリープ状態に移行 SetSleep()

CPUのスリープじゃなくって、画面とか外部装置のスリープです。電源ボタンを1秒以下でクリックすると復帰します。

未操作が続いたら画面を消すとかに使えるのかな?

ライトスリープ状態に移行 LightSleep()

内部でSetSleep()で外部装置をスリープしてからESP32のライトスリープ状態に入ります。タイマーをセットするとその秒数経過すると復帰します。

ただし、SetSleep()で電源ボタンを押すと画面だけ復帰して、CPUはスリープ状態のままなので、操作する人が混乱しないように設計したほうがいいと思います。

ディープスリープ状態に移行 DeepSleep()

こちらも、内部でSetSleep()で外部装置をスリープしてからESP32のディープスリープ状態に移行します。

こちらも電源ボタンを押すと画面が復帰しますが、ディープスリープタイマーが来ると再起動するので、使い方に気をつけましょう。

まとめ

使えるボタンが増えたのが非常に嬉しいです。日本語リファレンスはちょっとずつ更新していますが、まだまだ足りない項目がたくさん残っています。

M5StickCでBLEデバイスを検索する

BLEを使ってみようとしましたが、まずはデバイスのUUIDを調べないといけないので、調べるツールを作ってみました。

※現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスを確認してください。

BLEとは?

Bluetooth 4.0で規定されているBluetooth Low Energyです。Bluetooth LEとも表現されることがあるみたいです。

3.0まではClassicとかSimpleとかでよばれていることが多いみたいです。

BLEデバイスの構造

デバイスアドレス

6バイトで前半3バイトが製造会社のIDで、後半3バイトが任意に使えて製造番号的なものが入っています。 11:22:33:44:55:66みたいな形で表現されていることが多いです。

この番号は重複しないので、同じデバイスを複数持っていてもすべて違う番号になっています。

デバイス名

こちらの文字列でもデバイスの識別ができます。ただしすべてのデバイスでデバイス名が定義されているわけではないようです。

サービスUUID

マウスだったり、キーボードだったり特定の機能を表すサービスのIDです。複数の機能がある場合には、複数のサービスが登録されています。

サービスを識別するのがサービスUUIDになります。一般的には有名なサービスは定義済みになっており、この中のサービスを提供していることとなります。

定義済みサービスUUID

https://www.bluetooth.com/ja-jp/specifications/gatt/services/

キャラクタリスティックUUID

サービスの中にさらにキャラクタリスティックがあります。こちらは実際の値を保存しているもので、書き込みや読み込みなどの実際の操作を行うことができる機能のUUIDです。

バッテリー残量だったり、キーボードなどの入力値などの機能です。こちらも有名なものは定義済みになっています。

定義済みキャラクタリスティックUUID

https://www.bluetooth.com/ja-jp/specifications/gatt/characteristics/

サンプルスケッチ

#include "BLEDevice.h"

// 検索するBLEデバイス。serviceUUIDを調べる場合には空にする(例はHuman Interface Device"00001812-0000-1000-8000-00805f9b34fb")
static BLEUUID serviceUUID("1812");

static BLEAdvertisedDevice* myDevice;

// 接続してCharacteristic一覧を取得
bool connectToServer() {
  Serial.print("接続先 : ");
  Serial.println(myDevice->getAddress().toString().c_str());
  BLEClient*  pClient  = BLEDevice::createClient();
  pClient->connect(myDevice);

  // サービス取得
  BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
  if (pRemoteService == nullptr) {
    pClient->disconnect();
    return false;
  }

  // Characteristic一覧
  Serial.println("characteristic一覧");
  std::map<std::string, BLERemoteCharacteristic*>* mapCharacteristics = pRemoteService->getCharacteristics();
  for (std::map<std::string, BLERemoteCharacteristic*>::iterator i = mapCharacteristics->begin(); i != mapCharacteristics->end(); ++i) {
    Serial.print(" - characteristic UUID : ");
    Serial.print(i->first.c_str());
    Serial.print(" Broadcast:");
    Serial.print(i->second->canBroadcast()?'O':'X');
    Serial.print(" Read:");
    Serial.print(i->second->canRead()?'O':'X');
    Serial.print(" WriteNoResponse:");
    Serial.print(i->second->canWriteNoResponse()?'O':'X');
    Serial.print(" Write:");
    Serial.print(i->second->canWrite()?'O':'X');
    Serial.print(" Notify:");
    Serial.print(i->second->canNotify()?'O':'X');
    Serial.print(" Indicate:");
    Serial.print(i->second->canIndicate()?'O':'X');
    Serial.println();
  }

  // stop
  Serial.println("プログラム停止!");
  while (1) delay(1000);
}

// 検索したデバイスを受信するコールバック関数
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
    void onResult(BLEAdvertisedDevice advertisedDevice) {
      Serial.print("BLE デバイス発見 : ");
      Serial.println(advertisedDevice.toString().c_str());

      if (advertisedDevice.haveServiceUUID() &amp;&amp; advertisedDevice.isAdvertisingService(serviceUUID)) {
        // 指定デバイスだったら接続する
        BLEDevice::getScan()->stop();
        myDevice = new BLEAdvertisedDevice(advertisedDevice);
      }
    }
};

void setup() {
  Serial.begin(115200);
  Serial.println("BLEデバイス検索開始...");
  BLEDevice::init("");

  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setInterval(1349);
  pBLEScan->setWindow(449);
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5, false);
}

void loop() {
  if (myDevice != NULL) {
    connectToServer();
  }

  delay(1000);
}

最初はserviceUUIDを空にして実行すると、待受中のBLEデバイスすべてを列挙することができます。

その後にCharacteristicを取得したいserviceUUIDを指定することで、そのserviceUUIDが持つCharacteristicを列挙します。

ただし、同じ種類のデバイスが複数待受中になっている場合は、serviceUUIDだけで接続してしまうと、違うデバイスに接続しにいってしまう場合があるので、その場合にはアドレスやデバイス名などを条件に加えてください。

実行結果例

BLEデバイス検索開始...
BLE デバイス発見 : Name: AB Shutter3       , Address: ff:ff:c1:??:??:??, appearance: 961, serviceUUID: 00001812-0000-1000-8000-00805f9b34fb
接続先 : ff:ff:c1:??:??:??
characteristic一覧
[E][BLERemoteCharacteristic.cpp:273] retrieveDescriptors(): esp_ble_gattc_get_all_descr: Unknown
[E][BLERemoteCharacteristic.cpp:273] retrieveDescriptors(): esp_ble_gattc_get_all_descr: Unknown
[E][BLERemoteCharacteristic.cpp:273] retrieveDescriptors(): esp_ble_gattc_get_all_descr: Unknown
[E][BLERemoteCharacteristic.cpp:273] retrieveDescriptors(): esp_ble_gattc_get_all_descr: Unknown
 - characteristic UUID : 00002a4a-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:X Write:X Notify:X Indicate:X
 - characteristic UUID : 00002a4b-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:X Write:X Notify:X Indicate:X
 - characteristic UUID : 00002a4c-0000-1000-8000-00805f9b34fb Broadcast:X Read:X WriteNoResponse:O Write:X Notify:X Indicate:X
 - characteristic UUID : 00002a4d-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:X Write:X Notify:O Indicate:X
 - characteristic UUID : 00002a4e-0000-1000-8000-00805f9b34fb Broadcast:X Read:O WriteNoResponse:O Write:X Notify:X Indicate:X
プログラム停止!

ダイソーのスマホ用リモートシャッターを調べたものです。コンパイルオプションでエラーを表示するようにしているので、エラーが4行見えます。

この結果を見ると定義済みのcharacteristicが5つ見えます。

  • 2a4a : HID Information
  • 2a4b : Report Map
  • 2a4c : HID Control Point
  • 2a4d : Report
  • 2a4e : Protocol Mode

ただ、おそらくこのデバイスはcharacteristicの構造がおかしいようでして、ただしく利用することができませんでした。

iPhoneでcharacteristicを調べるアプリ「GATTBrowser」を使っても「180a : PnP ID」と「180f : Battery Level」の2つしか見えませんでした。

検索して、このデバイスに接続してcharacteristicを取得すると、端末にリセットがかかるので、標準ライブラリだと利用することができないと思います。

検索しても同じようにリセットがかかっている人が多いので、接続先デバイスによってESP32からBLE接続をすると本体のリセットがかかって、接続できないようです。

他のデバイスで接続する実験を行ってみたいと思います。

ESP32-WROOM-32Uの技適更新?

更新通知が来たけれどなんだろう?

該当の技適

https://lang-ship.com/giteki/detail.php?number=211-171103

んー、写真が無かったのが追加されているのと社名が「Espressif Systems (Shanghai) PTE Ltd.」から「Espressif Systems (Shanghai) Co., Ltd.」になっている?

まあ、該当モジュールは外部アンテナバージョンで、申請内容に違いはないのできにしないようにします。

M5StickCでSPI通信をする

本体に内蔵しているのと同じSPI接続のST7735Sに160×80ピクセルの0.96インチOLEDを接続してみました。

※現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスを確認してください。

SPIとは?

3線+αの信号線を使って通信する通信方式です。I2Cに比べて高速通信が可能で、M5StickCでは画面表示などに利用しています。

ArduinoMaster側Slave側別名用途
SCKSCKSCKSCLK, SCLデータ送信のクロックをマスターが送信する
MISOSDISDODC, D/Cマスターの受信、スレーブの送信をする
MOSISDOSDISDAマスターの送信、スレーブの受信をする
SSSSCS特定スレーブのCSを0Vにすることで通信先を選択する

基本は3線で通信を行い、複数の通信先(スレーブ)がいる場合には、マスター側が複数のSS用PINを操作して、通信先のCSを0Vにすることで通信先を選択する。

相手先が1つだけの場合には、配線の段階でCSをGNDに接続することで常に選択されている状態となる。

名称は接続先のデバイスによって命名が様々なので、接続先に注意する必要がある。またSPI以外の通信線も接続する必要があるデバイスもあるので混乱しやすい。

SPIサポート

M5StickCでは最大4系統のSPI通信をサポートしています。

  • SPI0 : 内部Flashへの接続に利用済み
  • SPI1 : 内部Flashへの接続に利用済み
  • HSPI : 未使用
  • VSPI : 内蔵画面への接続に利用済み

M5StickCで追加で利用できるSPIは1系統のみになります。ただし、そもそも4線しか使えるピンが無いので外部に2系統使うことはできません。

接続デバイス

手元にあったST7735Sと接続してみました。M5StickCに内蔵しているのと同じスペックの画面ですが、内蔵クラスはPIN番号などが固定化されているので、そのまま使えませんでした。

ST7735Sのピン配置

ST7735M5StickC内容
GNDGND共通GND
VCC3V3電源
SCL0SCK
SDA32MOSI
RES33リセット
DC26MISO
CSGNDこのデバイスを選択
BLK未接続バックライト制御

上記の接続にしました。CSはGNDに直結したので3線でSPI自体は通信できますが、画面のリセット用の1線必要になっています。

接続例

Groveはメス端子は販売していますが、ケーブルは自作できないので、この手の接続をするときにはきれいに接続するのは難しいかもしれません。

サンプルスケッチ

#include <M5StickC.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>

Adafruit_ST7735 tft = Adafruit_ST7735(-1, 0, 26, 32, 33); // cs, dc, mosi, sclk, rst

uint16_t color = 0;

void setup() {
  M5.begin();

  tft.initR(INITR_MINI160x80);
}

void loop() {
  tft.fillScreen(color);
  M5.Lcd.fillScreen(color);
  color += 0x0100;
}

最低限のコードだけに削っているものになります。AdafruitとM5StickCでcolorの型が違うので、違う色が表示されますが、画面の色が塗り替わっているまでは確認できました。

Adafruit ST7735とその親クラスにあたるAdafruit GFXを利用させていただいています。

まとめ

M5StickCでSPI通信は使えました。しかし自由に使えるPINが4線しかないので、接続とかがいろいろ大変そうですね。

M5StickCでUART(Serial)を使う

2本線での通信ですので、簡単に試せます。

※現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスを確認してください。

UARTとは

送信と受信の通信線をクロスにつないだ通信方式です。2本の通信線で通信ができるので簡単ですが、フロー制御などがないので文字化けとか通信データの欠落などがでてきます。

ESP32では3系統のUARTが利用することができます。

  • Serial : USB接続した場合にPCと通信するシリアル
  • Serial1 : 2系統目のシリアル
  • Serial2 : 3系統目のシリアル

1系統目はPCとの接続に利用しています。2系統目と3系統目はどちらを使ってもいいですし、両方同時に使うことも可能です。

また、Bluetooth経由でワイヤレス接続のBluetoothSerialってのもありますが、こちらは後日紹介します。

ピン配置

入力専用のIO36を除き、外部接続可能な4PINはどんな組み合わせでもI2Cで通信が可能でした。

サンプル配線

M5StickCのみで実験をする場合の配線です。特徴としてはRXとTXを接続することです。RXがReciverで受信、TXはTransmitterで送信を意味しています。

  • Serial1 : RX(0), TX(26)
  • Serial2 : RX(32), TX(33)

上記にアサインしています。Groveは32,33の順で割り当てるのが標準ですが、EXT IOの0と26は逆でも構いませんが、I2Cと同じように若い番号から割り当てています。

サンプルコード

#include <M5StickC.h>

void setup() {
  M5.begin();

  Serial1.begin(115200, SERIAL_8N1, 0, 26); // EXT_IO
  Serial2.begin(115200, SERIAL_8N1, 32, 33);// Grove
}

void loop() {
  if (Serial.available()) {
    // Serial(PC) to Serial1(EXT_IO)
    int inByte = Serial.read();
    Serial1.write(inByte);
  }
  if (Serial1.available()) {
    // Serial1(EXT_IO) to Serial2(Grove)
    int inByte = Serial1.read();
    Serial2.write(inByte);
  }
  if (Serial2.available()) {
    // Serial2(Grove) to Serial(PC)
    int inByte = Serial2.read();
    Serial.write(inByte);
  }
}

Arduinoのシリアルモニタから送信したデータをSerial(USB)で受信して、Serial1(EXT_IO)に出力。Serial1(EXT_IO)で受信したデータをSerial2(Grove)に送信。Serial2(Grove)で受信したデータをSerial(USB)に送信というサンプルです。

通信速度について

サンプルは一番最速の「115200」を指定していますが、速度が早いとエラーになる可能性があがるので、もう少し遅い速度を指定したほうが安定すると思います。

  • 300
  • 1200
  • 2400
  • 4800
  • 9600
  • 14400
  • 19200
  • 28800
  • 38400
  • 57600
  • 115200

あたりが指定できますが、相手と同じ速度を指定する必要があるので、相手が固定値だと選択肢がないですが、速度が必要ない場合には9600あたりか、少し早い19200あたりの方がエラーは少ないと思います。

遅めで通信をして、速度が必要になった場合に早くするほうが苦労が少ないと思います。

外部装置との接続例

ESP32の開発ボードとM5StickCをUARTで接続する実験をしてみました。

M5StickC側サンプルスケッチ

#include <M5StickC.h>

void setup() {
  M5.begin();

  Serial2.begin(115200, SERIAL_8N1, 32, 33);
}

void loop() {
  if (Serial2.available()) {
    int inByte = Serial2.read();
    Serial.write(inByte);
  }

  if (Serial.available()) {
    int inByte = Serial.read();
    Serial2.write(inByte);
  }
}

Grove側の32と33を使って接続しています。ESP32から送信されたデータをSerial2(Grove)で受信して、Serial(USB)に送信しています。また、Serial(USB)から送信されたデータをSerial2(Grove)に送信しています。

ESP32開発ボード側サンプルスケッチ

void setup() {
  Serial.begin(115200);  
  Serial2.begin(115200, SERIAL_8N1, 32, 33);
}

void loop() {
  if (Serial2.available()) {
    int inByte = Serial2.read();
    Serial.write(inByte);
  }

  if (Serial.available()) {
    int inByte = Serial.read();
    Serial2.write(inByte);
  }
}

ほぼ一緒ですが、M5.begin()の中で実行されているSerial.begin()を自分で呼び出す必要があります。

実行の注意点

Arduino IDEのシリアルモニタは1つのシリアルしか開けません。開発ボードを変更すると他の設定も一度に変わってしまうので、別のパソコンとかに繋げて実験するのが一番動かしやすいです。

UARTの注意点

電圧差異

M5StickCは内部は3.3Vで動いていますが、一般的なArduinoボードは5Vで動いています。そのまま接続すると電圧が異なりますので、繋げないほうがいいです。

M5StickCは実際には5V信号を入力しても、壊れない気がしますがデータシート上は5Vは許容していませんので、電圧変換の回路やICを間にいれて接続する必要があります。

物によっては12Vとかを流してくるシリアルもあるので、安易に繋ぐのはやめましょう。

速度差異

お互いに同じ速度を設定しないと通信ができません。複数機材を接続するのであれば、お互いに同じ値を設定するればよいのですが、LTEモジュールなどの通信機器と接続するためには、相手側の初期値で接続してから、変更するなどをする必要があるので注意しましょう。

エラー処理

基本的に1バイト単位で送受信する機能しかありませんので、途中でエラーが発生したり、相手側が受信を失敗しているかを確認する方法が標準では提供されていません。

エラー処理は自分で考慮する必要があるので注意して使ってください。

まとめ

UART自体は簡単に利用することができます。ただし長い距離を通信させることや、高速で通信する場合にはエラー処理などがちょっと不安ですので、注意が必要そうです。

パソコンと接続するのであればBluetoothSerialを使うことで、速度とエラー処理などの心配がほぼ無くなるのでおすすめです。

M5StickCのライブラリ0.0.7がリリース

リリースされていたので、差分を調べてみました。

※現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスを確認してください。

クラス構造

  • Buttonクラス追加
  • MPU6866クラス追加

ボタン管理用クラスと、I2CセンサーのMPU6866が増えましたが、中身はジャイロとかあるから加速度センサーかな?

これI2Cのアドレスが0x68なんで今後増える外付け用ライブラリか、M5Stack系のライブラリが混入したのかな?

もしくは、現行の加速度センサーがなんかおかしいので載せ替えか、追加?

個別変更点

AXP192クラス

  • uint16_t  GetVusbinData (void) : USBからの電圧取得
  • uint16_t  GetIusbinData (void) : USBからの電流取得
  • uint16_t  GetVapsData (void) : 内部動作電圧取得
  • uint8_t  GetBtnPress (void) : 割り込みでのボタン状態取得(IRQステータスレジスタ3)
  • void  SetSleep (void) : スリープ設定
  • void  DeepSleep (uint64_t time_in_us=0) : ディープスリープ設定
  • void  LightSleep (uint64_t time_in_us=0) : ライトスリープ設定
  • uint8_t  GetWarningLeve (void) : 警告レベル取得

上記が追加されています。GetVusbinData()系はプルリクエスト送ったらマージしてくれました。

スリープ系が結構増えていますね。

IMUクラス

  • void getAccelAdc (int16_t *ax, int16_t *ay, int16_t *az)
  • void getGyroAdc (int16_t *gx, int16_t *gy, int16_t *gz)
  • void getTempAdc (int16_t *t)

上記が増えていました。Adcだとint16_tで、既存のDataだとfloatで返却していますね。

スケッチ例

─Advanced
│  ├─*AXP192
│  │  ├─*PowerWake
│  │  └─*sleep
│  ├─Display
│  │  ├─Cellular_Automata
│  │  ├─drawXBitmap
│  │  ├─HZK16
│  │  ├─TFT_Clock
│  │  ├─TFT_Float_Test
│  │  ├─TFT_Pie_Chart
│  │  ├─TFT_Print_Test
│  │  ├─TFT_Spiro
│  │  ├─TFT_Starfield
│  │  ├─TFT_String_Align
│  │  └─TFT_Terminal
│  ├─I2C_Tester
│  └─QRCode
├─Basics
│  ├─AXP192
│  ├─Display
│  ├─FactoryTest
│  ├─HelloWorld
│  ├─Micophone
│  ├─*MPU6866
│  ├─RTC
│  └─SH200I
├─Games
│  ├─*Dices
│  └─FlappyBird
├─*Hat
│  ├─*ENV
│  ├─*MLX90640
│  ├─*NCIR_HAT
│  ├─*PIR
│  └─*SPEAKER
└─*Unit
    ├─*ADC_ADS1100
    ├─*ANGLE
    ├─*BUTTON
    ├─*CardKB
    ├─**Color
    ├─*DAC_MCP4725
    ├─*Dual_Button
    ├─*Earth
    ├─*ENV
    ├─*EXT_IO
    ├─*Fingerprint
    ├─*GPS
    │  └─*GPSRaw
    ├─*HEART
    │  └─*MAX30100_RawData
    ├─*IR
    ├─*Joystick
    ├─*Light
    ├─*Makey
    ├─*MLX90640
    ├─*NCIR
    ├─*NEOFLASH
    ├─*NEOPIXEL
    │  └─*display_rainbow
    ├─*PaHUB
    ├─*PbHUB
    ├─*PIR
    ├─*Relay
    ├─*RFID
    ├─*RGB
    ├─*TOF_VL53L0X
    ├─*TRACE
    └─*WEIGHT

ものすごく増えています!

*が先頭についているものが新規追加です。

まとめ

細かいところは、あとで検証しますがM5StickC.hにコメントが追加されていますが、クラス名がM5Stackになっているので、修正してもわないと。。。

//! Licensed under the MIT license. See LICENSE file in the project root for full license information.
/**
 * \par Copyright (C), 2016-2017, M5Stack
 * \class M5Stack
 * \brief   M5StickC library.
 * @file    M5StickC.h

あとはESP32プラットフォーム以外でもコンパイルできていたのが、エラーになるように修正されていました。

M5StickCでI2C通信をする

I2C通信を実験してみました。

※現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスを確認してください。

概要

ESP32は最大2組のI2Cが可能で、M5StickCは内部で1つ使っています。

入力専用のIO36を除き、外部接続可能な4PINはどんな組み合わせでもI2Cで通信が可能でした。

接続方法

M5StickCは上側に外部接続用のピンソケットと、下側にGrove端子がついています。IO26とIO0のような組み合わせも可能でしたが、通常はピンソケットのIO0とIO26、Grove端子のIO32とIO33の組み合わせで使うと思います。

おすすめピンアサイン

  • ピンソケット SDA:IO0, SCL:26
  • Grove端子 SDA:IO32, SCL:33

GroveのI2C端子が上記のアサインなので、逆に使うと混乱します。ピンソケット側はどちらでも構わないのですが、OfficialのI2Cを使ったHATがこのピンアサインだったので、こちらを使ったほうが無難だと思います。

同時に2系統使えるの?

内部のI2C通信を使わないのであれば、同時に利用が可能でした。

void setup() {
  M5.begin();

  Wire.begin(32, 33);
  Wire1.begin(0, 26);
}

Wireが1系統目のI2C通信で、Wire1が2系統目のI2C通信です。通常Wire1は電源管理などの内部接続用I2Cとして使われています。

M5.begin()で電源管理の初期化をしてしまえば、あとは接続していなくても問題ないかと思いますので、Wire1.begin()で別のピンにアサインしなおします。

3系統同時に使えないの?

#include <M5StickC.h>

void setup() {
  M5.begin();

  Wire.begin(32, 33);
}

void loop() {
  byte error, address;
  int nDevices;

  Serial.println("Scanning... Wire");

  nDevices = 0;
  for (address = 1; address < 127; address++ )
  {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();

    if (error == 0)
    {
      Serial.print("I2C device found at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.print(address, HEX);
      Serial.println("  !");

      nDevices++;
    }
    else if (error == 4)
    {
      Serial.print("Unknown error at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.println(address, HEX);
    }
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("done\n");

  Serial.println("Scanning... Wire1");
  Wire1.begin(0, 26);

  nDevices = 0;
  for (address = 1; address < 127; address++ )
  {
    Wire1.beginTransmission(address);
    error = Wire1.endTransmission();

    if (error == 0)
    {
      Serial.print("I2C device found at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.print(address, HEX);
      Serial.println("  !");

      nDevices++;
    }
    else if (error == 4)
    {
      Serial.print("Unknown error at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.println(address, HEX);
    }
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("done\n");

  Serial.println("Scanning... Wire1-2");
  Wire1.begin(21, 22);

  nDevices = 0;
  for (address = 1; address < 127; address++ )
  {
    Wire1.beginTransmission(address);
    error = Wire1.endTransmission();

    if (error == 0)
    {
      Serial.print("I2C device found at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.print(address, HEX);
      Serial.println("  !");

      nDevices++;
    }
    else if (error == 4)
    {
      Serial.print("Unknown error at address 0x");
      if (address < 16)
        Serial.print("0");
      Serial.println(address, HEX);
    }
  }
  if (nDevices == 0)
    Serial.println("No I2C devices found\n");
  else
    Serial.println("done\n");

  delay(5000);
}

I2Cのアドレススキャナで検証しましたが、使う前にbigin()でアドレス指定をすれば使えそうです。

ただ、複数使うのはトラブルになりそうなので、可能であればピンソケットかGroveのどちらか1系統だけを使ったほうが安全だと思います。

ESP32のPWM出力は255が最大じゃなかった

※現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスを確認してください。

ledcWrite()の指定がちょっとモヤッていたのだが、上記をみてやっぱり255じゃないのがわかりました。

ESP32のPWM出力について

ESP32でPWM出力を行う場合には、事前に周波数と分解能をセットします。

#include <M5StickC.h>
 
int PIN = 26;
int PWMCH = 0;
 
void setup() {
  M5.begin();
 
  pinMode(PIN, OUTPUT);
  ledcSetup(PWMCH, 7812.5, 8); // 7812.5Hz, 8Bit(256段階)
  ledcAttachPin(PIN, PWMCH);
  ledcWrite(PWMCH, 128);  //  50%(1.7V)
}
 
void loop() {
}

上記の場合、ledcSetup(PWMCH, 7815.5, 8)で7812.5Hzの8ビット精度です。

7812.5Hzは1秒間に7812.5回の周波ですので、1回あたり128usになります。

上記がオシロスコープの結果ですが、1マスが10usで、ONとOFFのワンセットで13マス弱ですので128usぐらいですね。

8ビット精度ってことは256段階ですから、128usを更に256分割の0.5us単位で制御していることになります。

上記がledcWrite(PWMCH, 1)の結果です。128のときには256分の128なので、50%が電圧出力でしたが、今回は256分の1の出力になっています。

さらに拡大した図ですが、1マス100nsなので0.1usが5マスの0.5us間のパルスが出力されています。ledcWrite(PWMCH, 2)にすると倍の1us間のパルスが出力されるはずです。

最大値は8ビット精度の場合には256になります。DACは0から255までですが、8ビットの場合PWMは0から256までの範囲になるので注意してください。

分解能別最大周波数

Bit分解能最大周波数
16655361,220.70Hz
15327682,441.41Hz
14163844,882.81Hz
1381929,765.63Hz
12409619,531.25Hz
11204839,062.50Hz
10102478,125.00Hz
9512156,250.00Hz
8256312,500.00Hz
7128625,000.00Hz
6641,250,000.00Hz
5322,500,000.00Hz
4165,000,000.00Hz
3810,000,000.00Hz
2420,000,000.00Hz
1240,000,000.00Hz

最大周波数を利用したい場合にはビット数1で40MHzになります。この場合には分解能が2ですので、ONとOFFが交互にくる50%の矩形波しか出力できません。

一番分解能を上げると16ビットで1220.70Hzまで周波数が下がります。8ビットの場合には312.5KHzですので、この数値以下を指定して利用しましょう。

参考サイト

M5StickCのIOについて調べてみた

とりあえず1PIN単位で動くものだけ調べてみました。

※(2019/08/30)ごめんなさい、嘘書いてありました。アナログのpinMode()設定が違っていたので入力値がおかしかったみたいです。

最新情報はM5StickC非公式日本語リファレンスを確認おねがいします。

PIN配置

GROVEのVOUTは5Vでした。しなしながらIO32とIO33のアナログ入力は3.3Vまでなので、アナログのGROVEセンサーとかをつなげると物によってはちゃんと動かない気がします。

PIN設定

PINIOMapFunction
IO26I/OExtended IO portGPIO26, DAC_2, ADC2_CH9, RTC_GPIO7, EMAC_RXD1
IO36(SENSOR_VP)IExtended IO portGPIO36, ADC1_CH0, RTC_GPIO0
IO0I/OMicrophone SCL
Extended IO port
ADC2_CH1, TOUCH1, RTC_GPIO11, CLK_OUT1, EMAC_TX_CLK
IO32I/OGROVE SDA32K_XP (32.768 kHz crystal oscillator input), ADC1_CH4, TOUCH9, RTC_GPIO9
IO33I/OGROVE SCL32K_XN (32.768 kHz crystal oscillator output), ADC1_CH5, TOUCH8, RTC_GPIO8

外部からアクセスができる上記5PINが調査対象です。

調査結果

PINdigitalRead()analogRead()touchRead()dacWrite()digitalWrite()ledcWrite()
IO26NG
IO36NGNGNGNG
IO0NGNGNG
IO3233NG
IO3332NG

IO26はADC2_CH9に繋がっているのに、Wi-fi使っていない場合でもアナログ入力できませんでした。上側にあるポートからはアナログ入力できなそうですね。

タッチセンサーがまた微妙で、ESP32のデータシートがおそらく間違っていて、内部的に逆に接続されている気がします。

調査方法

digitalRead() デジタル入力 0(1.65V未満) or 1(1.65V以上)

#include <M5StickC.h>

int PIN = 32;

void setup() {
  M5.begin();

  pinMode( PIN, INPUT);
}

void loop() {
  Serial.printf("%04d\n", digitalRead(PIN) );
  delay(500);
}

上記コードのPIN変数を変えていって実験しました。digitalRead()はどのポートでも利用できます。

analogRead() アナログ入力 0(0V)-4095(3.3V)

#include <M5StickC.h>

int PIN = 32;

void setup() {
  M5.begin();

  pinMode(PIN, ANALOG);
}

void loop() {
  Serial.printf("%04d\n", analogRead(PIN) );
  delay(500);
}

アナログ入力はIO0が正しく取得できません。内部的にプルアップされている関係で、0と4095のどちらかの数値になります。

touchRead() 静電容量取得 0に近いほうがタッチ

#include <M5StickC.h>

int PIN = 32;
boolean touched = false;
int threshold = 16;

void gotTouch() {
  touched = true;
}

void setup() {
  M5.begin();

  pinMode( PIN, INPUT);
  touchAttachInterrupt(PIN, gotTouch, threshold);
}

void loop() {
  if (touched) {
    Serial.println("touch!");
    touched = false;
  }
  Serial.printf("%4d\n", touchRead(PIN) );
  delay(500);
}

touchRead()は触ると数値が小さくなるので、触らないときの数字と、触って下がったときの数字の中間か、やや低い値をthresholdに設定してください。

タッチは初めて実験してみましたが、混乱しました。IO32とIO33の結果が逆でした。

コードを見ても、データシートも見ても問題ないはずだったので、他のESP32 Devボードで実験してみたら、やっぱり逆です。

たぶんデータシートが間違っていますね。

typedef struct {
    uint8_t reg;      /*!< GPIO register offset from DR_REG_IO_MUX_BASE */
    int8_t rtc;       /*!< RTC GPIO number (-1 if not RTC GPIO pin) */
    int8_t adc;       /*!< ADC Channel number (-1 if not ADC pin) */
    int8_t touch;     /*!< Touch Channel number (-1 if not Touch pin) */
} esp32_gpioMux_t;

こんな構造体があって、ArduinoではGPIOを管理していました。

PINregrtcadctouch
00x4411111
10x88-1-1-1
20x4012122
30x84-1-1-1
40x4810100
50x6c-1-1-1
60x60-1-1-1
70x64-1-1-1
80x68-1-1-1
90x54-1-1-1
100x58-1-1-1
110x5c-1-1-1
120x3415155
130x3814144
140x3016166
150x3c13133
160x4c-1-1-1
170x50-1-1-1
180x70-1-1-1
190x74-1-1-1
200x78-1-1-1
210x7c-1-1-1
220x80-1-1-1
230x8c-1-1-1
240-1-1-1
250x24618-1
260x28719-1
270x2c17177
280-1-1-1
290-1-1-1
300-1-1-1
310-1-1-1
320x1c949
330x20858
340x1446-1
350x1857-1
360x0400-1
370x0811-1
380x0c22-1
390x1033-1

こんな感じのマトリクスで、内部レジスタのアドレスとか番号が並んでいます。

PINregrtcadctouch
320x1c949
330x20858

該当部分だけ抜きですと、RTCとTouchとかはPIN番号大きい方から割り当てていますが、ADCは逆です。Touchもデータシート上は上記の記述ですが、実際の実装はIO32がT8でIO33がT9になっていると思われます。

ここ以外のいろんな場所でも逆に定義されているので、ライブラリは修正されない気もします。

GROVE – タッチセンサ

一応商品としてはありますので、使うときにはIO32を取得してください。

dacWrite() アナログ出力 0(0V)-255(3.3V)

#include <M5StickC.h>

int PIN = 26;

void setup() {
  M5.begin();

  pinMode(PIN, OUTPUT);
}

void loop() {
  dacWrite(PIN, 0);
  delay(500);
  dacWrite(PIN, 128);
  delay(500);
  dacWrite(PIN, 255);
  delay(500);
}

dacWrite()はIO26でしか使えません。(内部的にはIO25でも可能)

IO26からの出力にLEDと抵抗に接続してものをオシロスコープで測定した結果です。若干電圧低下していますが、概ね255で3.3V程度、128でその半分の電圧が出力されています。

digitalWrite() デジタル出力 LOW(0V) or HIGH(3.3V)

#include <M5StickC.h>

int PIN = 32;

void setup() {
  M5.begin();

  pinMode(PIN, OUTPUT);
}

void loop() {
  digitalWrite(PIN, HIGH);
  delay(500);
  digitalWrite(PIN, LOW);
  delay(500);
}

IO36はIOがInputのみなので、出力には使えませんが、それ以外のPINでは使えました。

ledcWrite() PWM出力

#include <M5StickC.h>

int PIN = 32;
int PWMCH = 0;

void setup() {
  M5.begin();

  pinMode(PIN, OUTPUT);
  ledcSetup(PWMCH, 12000, 8);
  ledcAttachPin(PIN, PWMCH);
}

void loop() {
  ledcWrite(PWMCH, 0);    //   0%(0.0V)
  delay(500);
  ledcWrite(PWMCH, 128);  //  50%(1.7V)
  delay(500);
  ledcWrite(PWMCH, 256);  // 100%(3.3V)
  delay(500);
}

PWMはdigitalWrite()で出力を定期的にON/OFFさせることで、擬似的なdacWrite()に似た動きになります。PWMは4PIN同時に利用することが可能で、細かい設定はledcSetup()で行っています。サンプルは周波数が 12KHz で分解能が8ビット(0-256)になっています。

8ビットの場合、256分割してそのうち何個を出力するかの指定なので、出力しないの0と全部出力するの256までの257段階で制御が可能です。

上記はキャプチャ用に周波数50Hzに落として実験しましたが、上記のように128(50%)を指定するとONとOFFが交互にきて、平均すると半分の電圧相当になります。

LEDの明るさ調整とかであればdacWrite()でなくて、PWM制御で十分明るさが変わります。

まとめ

GROVEの電源が5Vなんで、ちょっと注意が必要そうですね。あと本家だといろいろな拡張HATが開発されているようですので、楽しみです。

今後はI2Cとか2PIN以上必要な通信を調べていきたいと思います。