概要
M5Stackシリーズ共通ライブラリであるM5Unifiedについてちゃんと使い方を調べてみたいと思います。まずはボードが持っている機能をどの程度カバーしているのかを確認してきたいと思います。
M5Unifiedとは?
これまでM5Stack社はボード別に開発ライブラリを作成しており、似たようなライブラリがたくさんあり、そして微妙に仕様が異なるという状況になっていました。グラフィックライブラリであるLovyanGFXが登場したことで搭載している液晶画面などの判定や、標準ライブラリより高速描画が可能になりました。
その後、LovyanGFXを元にM5Stack公式ライブラリとしてM5GFXが作成され、各種ボードの制御コードが追加されたものがM5Unifiedとなります。
気をつけないといけないことは、もともとの個別ライブラリとM5Unifiedは完全コンパチではありません。ほぼそのまま動かすことができるのですが、ボードごとに異なっていたライブラリの関数などを整理する段階で微妙にクラスや関数名などが変わっていることがありますし、M5Unifiedでかなり強化された機能もある一方、まだサポートされていない機能もあります。
上記がリポジトリになります。ライブラリ自体はライブラリマネージャに登録してあるのでM5Unifiedで検索して、すぐに導入することが可能です。
公式ドキュメント
上記にあるドキュメントが公式ドキュメントとなります。いろいろ準備中のようですので、今後充実していくはずです。
先程のページからリンクされている「M5Unifiedへの移植のポイント」のページが使い方がまとまっていて参考になると思います。
上記がPlatformIOを利用したM5Unifiedの導入方法です。
また、らびやんさんの紹介スライドがありますのでこちらで概要が把握できると思います。
複数ボードでのプログラム共通化
M5UnifiedはM5Stackの共通ライブラリであり、ボード自動判定がありますのでまったく同じプログラムを複数のボードで使い回すことが可能です。
条件があえばボード別にビルドする必要もなく、同じバイナリファイルを複数のボードに転送するだけで動作することも可能ですが、最初は機種別にビルドしたほうが安全だと思います。
共通化をする場合には以下の違いを注意してみてください。
MCU(ESP32/ESP32-C3/ESP32-S3)の違い
M5Stackに搭載されているESP32には無印ESP32、廉価版のESP32-C3、高性能版のESP32-S3の3シリーズあります。M5Unifiedはすべてに対応していますが、ビルドするオプションや出来上がったバイナリがシリーズごとに違うので対応ボードを選び直してビルドし直す必要があります。
ただし同じMCUシリーズを採用している場合には、同一バイナリをそのまま動かすことも可能です。
フラッシュメモリ容量の違い
同じ無印ESP32シリーズを採用しているボードでもCore2はフラッシュが16MB、M5StickC Plusは4MBと搭載しているフラッシュ容量が違います。4MBでビルドしたものは16MBのボードにも転送して動かすことは可能ですが、逆はできませんのでビルドし直しが必要になります。
PSRAMの違い
Core2のようにPSRAMを搭載しているボードと、M5StickC Plusのように搭載していないボードで共通プログラム化をする場合にはPSRAMを無効にしたほうが好ましいです。
PSRAMを有効にしてビルドするとArduinoのバージョンによってはPSRAMの初期化エラーで起動失敗する場合があるのでPSRAM非搭載ボードで動かすプログラムについてはPSRAMを無効化にするか、PSRAM搭載機種のみサポートするプログラムにするのかが好ましいです。
機種判定
#include <Arduino.h>
#include <M5Unified.h>
void setup(void) {
auto cfg = M5.config();
M5.begin(cfg);
const char* name;
switch (M5.getBoard()) {
case m5::board_t::board_M5StackCoreS3: name = "StackS3"; break;
case m5::board_t::board_M5AtomS3Lite: name = "ATOMS3Lite"; break;
case m5::board_t::board_M5AtomS3: name = "ATOMS3"; break;
case m5::board_t::board_M5StampC3: name = "StampC3"; break;
case m5::board_t::board_M5StampC3U: name = "StampC3U"; break;
case m5::board_t::board_M5Stack: name = "Stack"; break;
case m5::board_t::board_M5StackCore2: name = "StackCore2"; break;
case m5::board_t::board_M5StickC: name = "StickC"; break;
case m5::board_t::board_M5StickCPlus: name = "StickCPlus"; break;
case m5::board_t::board_M5StackCoreInk: name = "CoreInk"; break;
case m5::board_t::board_M5Paper: name = "Paper"; break;
case m5::board_t::board_M5Tough: name = "Tough"; break;
case m5::board_t::board_M5Station: name = "Station"; break;
case m5::board_t::board_M5Atom: name = "ATOM"; break;
case m5::board_t::board_M5AtomPsram: name = "ATOM PSRAM"; break;
case m5::board_t::board_M5AtomU: name = "ATOM U"; break;
case m5::board_t::board_M5TimerCam: name = "TimerCamera"; break;
case m5::board_t::board_M5StampPico: name = "StampPico"; break;
default: name = "Who am I ?"; break;
}
M5.Display.println(name);
}
void loop(void) {
delay(1);
}
M5.getBoard()にてボードの種類が取得可能です。
上記に対応しているボード一覧があります。
Coreシリーズ | Stickシリーズ | ATOMシリーズ | STAMPシリーズ | その他シリーズ | |
---|---|---|---|---|---|
ESP32 搭載ボード | BASIC GRAY FIRE M5GO CORE2 CORE2 FOR AWS | M5Stick M5StickC M5StickC PLUS M5StickT M5StickT2 | ATOM LITE ATOM MATRIX ATOM ECHO ATOM U | STAMP PICO | CORE.INK M5PAPER TOUGH STATION 485 STATION BAT |
ESP32-C3 搭載ボード | STAMP C3 STAMP C3U | ||||
ESP32-S3 搭載ボード | CORES3 | AtomS3 AtomS3 Lite | StampS3 |
かなり昔に販売終了し日本未発売のM5Stickと、サーモグラフィーカメラを搭載した8万円ぐらいするM5StickT系が未対応になります。

M5StickTは非常に高いので、サーマルカメラユニットやハットを使うことが推奨されていると思います。
また、ボード以外にも外付けLCDやAtom Displayなどにも対応しています。
画面周り
LovyanGFXと使い方はほぼ変わりません。ただしM5.Displayクラスに集約されていますので、ボードによってM5.LcdからM5.Displayクラスに修正する必要があります。
#include <Arduino.h>
#include <M5Unified.h>
void setup(void) {
auto cfg = M5.config();
M5.begin(cfg);
}
void loop(void) {
M5.Display.startWrite();
M5.Display.setCursor(0, 0);
M5.Display.print(millis());
M5.Display.endWrite();
delay(1);
}
特徴的なのが初期化方法です。初期化オプションが増えたのでM5.config()でデフォルトの設定を取得してから初期化しています。
初期化オプションについては上記のスケッチ例が参考になると思います。
描画周りはM5.Displayに集約されているのと、LovyanGFXのテクニックとしてstartWrite()とendWrite()で描画まわりを囲ってあげることで描画速度があがります。ただし、E-INK系は特殊なんですが、基本はこの囲いはなくても動きますし、標準ライブラリよりは描画が早いです。
タッチパネル
#include <Arduino.h>
#include <M5Unified.h>
void setup(void) {
auto cfg = M5.config();
M5.begin(cfg);
}
void loop(void) {
M5.update();
M5.Display.startWrite();
if (M5.Touch.isEnabled()) {
auto t = M5.Touch.getDetail();
auto x = t.distanceX();
auto y = t.distanceY();
auto p = t.isPressed();
M5.Display.setCursor(0, 0);
M5.Display.printf("x = %4d, y = %4d, press = %d", x, y, p);
}
M5.Display.endWrite();
delay(1);
}
タッチパネルはM5.update()を呼び出すことで情報が更新されるので注意してください。タッチパネルもボードにより仕様が異なりますが、M5Unifiedであればとくに違いを意識する必要がありません。
マルチタッチなど詳しい使い方は上記のスケッチ例を参考にしてください。
LED
#include <Arduino.h>
#include <M5Unified.h>
void setup(void) {
auto cfg = M5.config();
M5.begin(cfg);
}
uint8_t led = 0;
void loop(void) {
led++;
M5.Power.setLed(led);
delay(1);
}
Core2、Core3、CoreInk、M5StickC、M5StickC Plusには赤もしくは緑のLEDがあります。ボードによりアクセス方法が本来異なるのですがM5.Power.setLed()に0から255までの明るさを入れることで制御が可能です。
RGB LED
#include <Arduino.h>
#include <M5Unified.h>
#include <EspEasyLED.h>
EspEasyLED *rgbled;
void setup(void) {
auto cfg = M5.config();
M5.begin(cfg);
switch (M5.getBoard()) {
case m5::board_t::board_M5StampS3:
rgbled = new EspEasyLED(GPIO_NUM_21, 1, 20);
break;
default:
rgbled = NULL;
break;
}
}
void loop(void) {
if (rgbled) {
rgbled->showColor(255, 0, 0); // RED
delay(1000);
rgbled->showColor(EspEasyLEDColor::GREEN);
delay(1000);
rgbled->showColor(EspEasyLEDColor::BLUE);
delay(1000);
rgbled->showColor(EspEasyLEDColor::WHITE);
delay(1000);
}
}
ATOM系、Fire、Core2 for AWS、Stamp系などにはRGB LEDが搭載されています。現在のところRGB LEDはM5Unifiedではサポートしていません。何らかのライブラリなどを利用して、自分で制御する必要があります。
上記の例では自作のEspEasyUtilsライブラリの中のEspEasyLEDを利用したサンプルです。1つだけ搭載している場合にはライブラリを利用せずにneopixelWrite(GPIO_NUM_21, 20, 0, 0)などのように直接制御することも可能です。
ライブラリとしてはFastLEDがよく使われていると思います。
スピーカー
上記にスケッチ例があります。M5StackのボードだとDACと呼ばれるアナログ方式の他にI2Sのデジタル方式、そして音のオンオフだけ可能なブザーの3種類があります。
M5Unifiedではその方式でも同じ方法で音を鳴らすことが可能です。音質的にはI2Sがよく、DACが若干ノイズがまじり、ブザーがなんとか効果音が鳴らせるぐらいのものになります。
取り扱いは結構難しいですのでここでは深く取り上げません。
マイク
マイクもI2Sのデジタル方式と、ADCのアナログ方式がありますがM5Unifiedでは同じように取り扱うことができます。詳しくは上記のスケッチ例を見てください。
ボタン
#include <M5Unified.h>
void setup(void) {
auto cfg = M5.config();
M5.begin(cfg);
}
void loop(void) {
M5.update();
M5.Display.startWrite();
M5.Display.setCursor(0, 0);
int state = M5.BtnA.wasHold() ? 1
: M5.BtnA.wasClicked() ? 2
: M5.BtnA.wasPressed() ? 3
: M5.BtnA.wasReleased() ? 4
: M5.BtnA.wasDecideClickCount() ? 5
: 0;
M5.Display.print("BtnA :");
if (state) {
M5.Display.print(state);
}
M5.Display.println();
state = M5.BtnB.wasHold() ? 1
: M5.BtnB.wasClicked() ? 2
: M5.BtnB.wasPressed() ? 3
: M5.BtnB.wasReleased() ? 4
: M5.BtnB.wasDecideClickCount() ? 5
: 0;
M5.Display.print("BtnB :");
if (state) {
M5.Display.print(state);
}
M5.Display.println();
state = M5.BtnC.wasHold() ? 1
: M5.BtnC.wasClicked() ? 2
: M5.BtnC.wasPressed() ? 3
: M5.BtnC.wasReleased() ? 4
: M5.BtnC.wasDecideClickCount() ? 5
: 0;
M5.Display.print("BtnC :");
if (state) {
M5.Display.print(state);
}
M5.Display.println();
state = M5.BtnEXT.wasHold() ? 1
: M5.BtnEXT.wasClicked() ? 2
: M5.BtnEXT.wasPressed() ? 3
: M5.BtnEXT.wasReleased() ? 4
: M5.BtnEXT.wasDecideClickCount() ? 5
: 0;
M5.Display.print("BtnEXT :");
if (state) {
M5.Display.print(state);
}
M5.Display.println();
state = M5.BtnPWR.wasHold() ? 1
: M5.BtnPWR.wasClicked() ? 2
: M5.BtnPWR.wasPressed() ? 3
: M5.BtnPWR.wasReleased() ? 4
: M5.BtnPWR.wasDecideClickCount() ? 5
: 0;
M5.Display.print("BtnPWR :");
if (state) {
M5.Display.print(state);
}
M5.Display.println();
M5.Display.endWrite();
delay(1);
}
M5UnifiedではBtnA、BtnB、BtnC、BtnEXT、BtnPWRに対応しています。Core2などのタッチパネル系のボタンも同じように取得可能です。ボタン系はM5.update()を呼び出すことで状態を更新するので、判定をするまえに必ず呼び出すようにしてください。あと電源ボタンは押すとリセットするボードや長押しが取得できない場合があるので、なるべく使わないほうが安全です。
また、ToughやCoreS3などのタッチパネルであり、Core2のようにボタン用エリアが定義されていないものはボタンも利用できないので注意してください。この場合には自前でタッチパネルの座標などを処理してボタン的な制御をする必要があります。
上記が公式のスケッチ例になります。
IMU
#include <M5Unified.h>
void setup(void) {
auto cfg = M5.config();
M5.begin(cfg);
}
void loop(void) {
M5.Display.startWrite();
M5.Display.setCursor(0, 0);
float ax, ay, az;
float gx, gy, gz;
M5.Imu.getAccel(&ax, &ay, &az);
M5.Imu.getGyro(&gx, &gy, &gz);
M5.Display.printf("Accel X = %12.6f\n", ax);
M5.Display.printf("Accel Y = %12.6f\n", ay);
M5.Display.printf("Accel Z = %12.6f\n", az);
M5.Display.printf("Gyro X = %12.6f\n", gx);
M5.Display.printf("Gyro Y = %12.6f\n", gy);
M5.Display.printf("Gyro Z = %12.6f\n", gz);
M5.Display.endWrite();
delay(1);
}
IMUも世代やボードで複数の種類がありますが、M5Unifiedでは自動判定してくれ同じ処理でデータの取得が可能です。
RTC
#include <M5Unified.h>
void setup(void) {
auto cfg = M5.config();
M5.begin(cfg);
}
void loop(void) {
M5.Display.startWrite();
M5.Display.setCursor(0, 0);
static constexpr const char* const wd[7] = { "Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat" };
auto dt = M5.Rtc.getDateTime();
M5.Display.printf("RTC : %04d/%02d/%02d (%s) %02d:%02d:%02d", dt.date.year, dt.date.month, dt.date.date, wd[dt.date.weekDay], dt.time.hours, dt.time.minutes, dt.time.seconds);
M5.Display.endWrite();
delay(500);
}
M5StackのRTCはほぼ同じRTCチップを採用していますがM5Unifiedでも中身を意識することなく取得可能です。ただし、事前にWi-Fiなどに接続してNTPから時間合わせをしておかないとRTCの意味はありません。
そしてWi-Fiが使える環境の場合にはRTCを使わずに直接時間合わせをすればよいので、スリープなどを使う環境以外ではあまり利用機会はないかもしれません。
赤外線送信(IR)
#include <M5Unified.h>
#include <IRsend.h>
IRsend *irsend;
void setup(void) {
auto cfg = M5.config();
M5.begin(cfg);
switch (M5.getBoard()) {
case m5::board_t::board_M5StickC:
case m5::board_t::board_M5StickCPlus:
irsend = new IRsend(GPIO_NUM_9);
break;
case m5::board_t::board_M5Atom:
irsend = new IRsend(GPIO_NUM_12);
break;
default:
irsend = NULL;
break;
}
}
void loop(void) {
if (irsend) {
uint64_t send = irsend->encodeNEC(0x0000, 22);
irsend->sendNEC(send);
}
delay(500);
}
ATOM系やM5StickC系はIR送信が可能です。標準的に利用されているライブラリがあるため、M5Unifiedでは直接サポートをしていません。自分でライブラリを組み込んで利用することになります。
細かい使い方はライブラリの説明か、上記の記事などを参考にしてください。
バイブ
M5.Power.Axp192.setLDO3(3000); // On 1800-3300
delay(1000);
M5.Power.Axp192.setLDO3(0); // Off
Core2とCore2 for AWSには振動モーターと搭載しており、バイブレーションをすることが可能です。ただし呼び出し方がちょっと面倒です。Core2以外のM5StickCなどはこのLDO3には画面の回路が接続されているので、間違えて呼び出すと画面が消えたりします。
SD(TF)
#include "SD.h"
#include <M5Unified.h>
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
while (false == SD.begin(GPIO_NUM_4, SPI, 25000000)) {
M5.Display.println("SD Wait...");
delay(500);
}
M5.Display.setCursor(0, 0);
File root = SD.open("/");
if (root) {
File file = root.openNextFile();
while (file) {
if (file.isDirectory()) {
// Dir skip
} else {
// File
String filename = file.name();
M5.Display.println(filename);
}
file = root.openNextFile();
}
}
}
void loop() {
delay(1);
}
M5UnifiedはArduino環境だけではなく、広い環境で使えるように作られているライブラリです。そのため非常に移植性が高いのですが、Arduino用の便利な関数やライブラリが標準では利用しないようになっています。
特にSDを利用する場合にはM5Unified.hよりも前にSD.hを読み込むようにしてください。また、SDカードの自動マウントも行われませんので、利用する場合には明示的にマウントをする必要があります。
I2C(PortA)
#include <Wire.h>
#include <M5Unified.h>
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
Wire.begin(M5.Ex_I2C.getSDA(), M5.Ex_I2C.getSCL());
}
void loop() {
M5.Display.startWrite();
M5.Display.fillScreen(BLACK);
M5.Display.setCursor(0, 0);
M5.Display.printf("I2C Scan\n");
M5.Display.printf("SDA:%2d SCL:%2d\n", M5.Ex_I2C.getSDA(), M5.Ex_I2C.getSCL());
for (byte address = 0; address <= 127; address++) {
Wire.beginTransmission(address);
byte error = Wire.endTransmission();
if (error == 0) {
M5.Display.printf("%02X ", address);
} else {
M5.Display.print(".. ");
if (address % 8 == 7) {
M5.Display.println();
}
}
delay(1);
}
M5.Display.endWrite();
delay(3000);
}
I2CもM5UnifiedはArduinoのWireクラスをサポートしていません。内部に独自のI2C_Classがあるので、一般的なArduinoライブラリを利用する場合にはWireクラスで初期化しなおす必要があります。
まず最初にWire.hを読み込んで、M5.begin()後にWire.begin()で初期化してください。PortAのI2Cを利用する場合にはM5.Ex_I2Cがすでに初期化済みですのでgetSDA()とgetSCL()でピン番号をもらって初期化することでPortAのピン番号をボード別に設定する必要がなくなります。
開発ボード
ボード | arduino-esp32 2.0.9 | M5Stack 2.0.7 | PlatformIO |
---|---|---|---|
ATOM LITE | M5Stack-ATOM | M5Stack-ATOM | m5stack-atom |
ATOM MATRIX | M5Stack-ATOM | M5Stack-ATOM | m5stack-atom |
ATOM ECHO | M5Stack-ATOM | M5Stack-ATOM | m5stack-atom |
ATOM U | M5Stack-ATOM | M5Stack-ATOM | m5stack-atom |
BASIC | M5Stack-Core-ESP32 | M5Stack-Core-ESP32 | m5stack-core-esp32 |
GRAY | M5Stack-Core-ESP32 | M5Stack-Core-ESP32 | m5stack-grey |
M5GO | M5Stack-Core-ESP32 | M5Stack-Core-ESP32 | m5stack-core-esp32 |
FIRE | M5Stack-FIRE | M5Stack-FIRE | m5stack-fire |
CORE2 | M5Stack-Core2 | M5Stack-Core2 | m5stack-core2 |
CORE2 for AWS | M5Stack-Core2 | M5Stack-Core2 | m5stack-core2 |
CORE.INK | M5Stack-CoreInk | M5Stack-CoreInk | m5stack-coreink |
M5PAPER | (M5Stick-C) | M5Stack-Paper | (m5stick-c) |
M5StickC | M5Stick-C | M5Stick-C | m5stick-c |
M5StickC PLUS | (M5Stick-C) | M5Stick-C-Plus | (m5stick-c) |
TOUGH | (M5Stack-Core2) | M5Stack-Tough | (m5stack-core2) |
STATION 485 | M5Stack-Station | M5Stack-Station | m5stack-station |
STATION BAT | M5Stack-Station | M5Stack-Station | m5stack-station |
AtomS3 | M5Stack-ATOMS3 | M5Stack-ATOMS3 | m5stack-atoms3 |
AtomS3 Lite | M5Stack-ATOMS3 | M5Stack-ATOMS3 | m5stack-atoms3 |
CORES3 | (ESP32S3 Dev Module) | M5Stack-CoreS3 | (esp32-s3-devkitc-1) |
STAMP PICO | (M5Stick-C) | STAMP-PICO | (m5stick-c) |
STAMP C3 | (ESP32C3 Dev Module) | STAMP-C3 | (esp32-c3-devkitc-02) |
STAMP C3U | (ESP32C3 Dev Module) | STAMP-C3 | (esp32-c3-devkitc-02) |
StampS3 | STAMP-S3 | STAMP-S3 | m5stack-stamps3 |
Atom Display | M5Stack-ATOM | M5Stack-ATOM | m5stack-atom |
Atom Display Lite | M5Stack-ATOM | M5Stack-ATOM | m5stack-atom |
Timer CAM | M5Stack-Timer-CAM | M5Stack-Timer-CAM | (m5stick-c) |
Unit CAM | (M5Stick-C) | M5Stack-Unit-CAM | (m5stick-c) |
PoE CAM | (M5Stack-Timer-CAM) | M5Stack-PoE-CAM | (m5stick-c) |
開発環境ごとにどのボードを選択するかの表になります。カッコに入っているものは専用のものがありませんが、なるべく近いものを書いてあります。
arduino-esp32は次回のバージョンでM5Stack-CoreS3が利用できるはずです。
まとめ
全般的に知っておいたほうがよいことは説明できたと思います。これだけだとちょっと情報が足りないので、ボード別に使い方の説明なども今後記事を書いてみる予定です。
コメント
M5Unifiedに乗り換えをしたのですがM5.Axpが使えなくなってしまいました。まだ情報が少なく対応方法
がみつかりません。
ご教授頂けると幸いです
M5.Power.Axp192
に移動しているはずです
どこかに書いてないか確認中ですが、M5Stackの公式ドキュメントがエラーで見えない。。。
M5.Power.Apx192.~が使用できました、ありがとうございます。
M5Unifiedがリリースされて使いやすい環境になりつつも昔のライブラリを使用している情報が多いため新規ではじめる初心者やライトユーザーには理解しずらい状況になってしまっていると感じています。
公式ドキュメントが早く整理され充実することを願っています。
動いてよかったです
日本人のドキュメント整備メンバーが増えたみたいなので、今後に期待ですね!