M5Dialの開発メモ

About
M5Dialこと、M5Stack Dial ESP32S3の開発で詰まったところのメモ。
ベースはM5Stack StampS3になっており、ESP32S3ベースになっている。

PlatformIOを使うのは避けた方がいいかも?
最初は PlatformIO (+CLion) を使用していたが、どうやらPlatformIOとespressifの対立があり、現在公式ライブラリとしてはESP32は更新されていないらしい。途中まで使っていたが、やめてVSCode拡張を使うことにした。
以下、情報としては残しておく。
以下、PlatformIOを使っていた際のメモ
PlatformIO + CLionの開発環境構築
VSCodeで使う場合は簡単なのだが、CLionで環境構築するのは少し手間がかかる。
予め PlatformIO Core を導入する必要があり、CLionのためにパスを通しておく。
CLion向けPlatformIOプラグインはJetbrains公式になってるので安心して使用できる。公式Docを見ればまず迷うことはないはず
USBでシリアル通信が受信できない
ビルドフラグで ARDUINO_USB_CDC_ON_BOOT=1
を立てておく必要アリ。
ESP32S3の仕様に起因する問題。
[env:usb_serial]
src_dir = src/examples/usb_serial
build_flags =
-D PIO_USB_SERIAL
-D ARDUINO_USB_CDC_ON_BOOT=1
参考
PlatformIOで複数のエントリポイントを共存させたい
src/main.cpp
はメイン開発用、サンプルコードを src/examples/usb_serial.cpp
に置きたい…といったときの対処法。
platformio.ini
の設定を特に何もしていないと、ビルド時に setup()
や loop()
の定義が複数存在した状態で全体をコンパイルするので、重複定義でコンパイルがコケる。
対策として platform.ini
で複数envを切っておき、それぞれのビルドフラグで任意のフラグを立てておく。その後コード側の #ifdef
で条件付きコンパイル。
[env]
では共通設定を書いておけるので、ここに platform
や board
設定を書いておくとよい。
例:
[platformio]
env_default = main
[env]
platform = espressif32
board = m5stack-stamps3
monitor_speed = 115200
framework = arduino
lib_deps =
m5stack/M5Dial@^1.0.2
m5stack/M5Unified@^0.2.4
m5stack/M5GFX@^0.2.6
[env:main]
build_flags = -D PIO_MAIN
#ifdef PIO_MAIN
void setup()
{
// ...
}
void loop()
{
// ...
}
#endif
BluetoothのSerial Port Protocol (SPP) は使えない
Bluetoothでのシリアル通信は、SPP(Serial Port Protocol)というプロトコルがあり、これを使うことで容易に実装できる。M5Stack関連のコードを見るとよく以下を見かける。
#include "BluetoothSerial.h"
PlatformIOの arduino-espressif32
ライブラリは C:\Users\<username>\.platformio\packages\framework-arduinoespressif32\libraries
にあるのだが、ここのREADME.mdを読むと以下の記載がある。
BluetoothSerial
Serial to Bluetooth redirection server
Note: This library depends on Bluetooth Classic which is only available for ESP32
(Bluetoothserial is not available for ESP32-S2, ESP32-C3, ESP32-S3).
M5Dial は StampS3 ベースなので、つまり ESP32-S3
が乗っている。よくよく調べてみると、ESP32-S3はそもそもSPPプロファイルがサポートされていないことに起因しているらしい。

esp-idfのVSCode Extension
上記のPlatformIOのもろもろの理由でCLionが使いづらいのと、かといってArduino IDEでは開発環境としてパワー不足なところがあり、Arduino core for esp32 を使った開発は断念。
そもそもArduino環境に慣れていたためなんとなく使い続けていたぐらいの感覚だったため、特に拘りもなかった。本来esp32はesp-idfで開発したほうがいいだろうし、こちらを採用することにした。
幸いM5Stackの関連ライブラリである M5GFX
や M5Unified
はesp-idf対応で、Arduino coreを使っていた時と同様のライブラリが使用可能なので、フレームワークを変えてもそんなに問題にはならなそう。M5Dialにプリインストールされているデモプログラムもesp-idfベースのコードになっている。
esp-idfは公式サポートでVSCodeの拡張機能が提供されているため、こちらを使用。ツールチェインは拡張機能インストール後に画面に沿って行けば自動でセットアップしてくれるので楽。ちなみにCLionは開発ができなくもないが、マニュアルでセットアップする項目が多く面倒そうなので見送り。

C++を使いたい
esp-idfのサンプルコードを見ると拡張子が .c
になっている。ただC++はサポートされているので使用可能。一部制限がある。
特に esp-idf のエントリポイントになる app_main
関数の定義には注意が必要で、 .cpp
ファイル内にある場合は extern "C"
としてCリンケージで定義されている必要がある。
extern "C" void app_main()
{
}


外部 Component の読み込み
プロジェクトルートに components
フォルダを作成し、その中に入れていけばよい。
例えば git submodule add
で追加していくなら以下の通り。
cd components
git submodule add https://github.com/M5Stack/M5GFX
git submodule add https://github.com/m5stack/M5Unified
次にmainの CMakeLists.txt
に依存関係を定義。複数読み込むなら横並びにすればよい。これで main.cpp
内で参照できるようになる。
idf_component_register(SRCS "main.cpp"
PRIV_REQUIRES M5GFX M5Unified
INCLUDE_DIRS "")
参考

M5GFXがM5DialをM5PaperS3として誤認識してしまう?
M5GFXがM5PaperS3として認識されてしまい、正常に動かない場合がある。この時ログをモニタリングしていると、以下のようなログが出ている。
I (302) M5GFX: [Autodetect] board_M5PaperS3
ところが、M5DialのWakeボタンを押しながら起動すると、正常なデバイス判定となり表示もうまくいく。回路図からWakeボタンのGPIOの番号は 42
らしい 。
M5GFX::autodetect
のM5PaperS3の判定に関する箇所を覗いてみると、確かにG42ピンのチェックを行っている様子。ただ自分はそこまでM5Stackシリーズに詳しくないので、ここが直接的な原因なのかどうかは断言できません…。
不思議な点としてArduino coreの環境では同様の問題が発生しなかった。Arduino IDEで下記のサンプルコードをbuild & uploadし、 Core Debug Level: Verbose
にして検証。
#include <M5GFX.h>
#include <M5Unified.h>
void setup() {
auto cfg = M5.config();
M5.begin();
M5GFX* gfx = &M5.Display;
int fontSize = gfx->height() / 50;
if (fontSize == 0) fontSize = 1;
gfx->setTextSize(fontSize);
int x = gfx->width() / 2;
int y = gfx->height() / 2;
gfx->setCursor(x, y);
gfx->print("hello world");
}
void loop() {
}
すると以下のログを確認。
[ 1000][I][M5GFX.cpp:732] init_impl(): [M5GFX] [Autodetect] load from NVS : board:12
[ 1003][D][M5GFX.cpp:694] _read_panel_id(): [M5GFX] [Autodetect] read cmd:04 = ff019a00
[ 1005][I][M5GFX.cpp:1603] autodetect(): [M5GFX] [Autodetect] board_M5Dia
Arduino環境ではNVSを見てデバイスの特定を行っている様子。暫定的に直接NVSにM5Dialのボード番号 12
を書き込めばひとまず解決できそう。 以下のような関数を用意し、 M5.config()
より前に呼び出すようにした。
結果は成功。
I (313) M5GFX: [Autodetect] load from NVS : board:12
I (323) M5GFX: [Autodetect] board_M5Dial
esp-idfはバリバリ初心者なので、そもそもビルドに関する設定が誤ってこのような現象が発生してる可能性もありそう。ライブラリ側ではなく自分の問題な気がしないでもないので、もう少し調べてみる…
WIP

再起動後にESP-IDFのFrameworkのインストール場所を見失う
インストールの翌日、VSCodeがESP-IDF frameworkを見つけられなくなり、 No ESP-IDF frameworks found のエラーになった。
<project root>/.vscode/settings.json
でframeworkのフォルダが指定されており、これを参照している模様[1]。しかし正常な値が入っていた。念のため IDF_PATH
や IDF_TOOLS_PATH
も手動で設定したが、これも効果なし。
GithubのReleaseページを見てみると、ちょうど14時間前に拡張機能が 1.9.1
にアップデートされていた[2]。これが問題になってコンフィグ回りがおかしくなっていた可能性が高い。
案の定VSCodeのクイックバーから > ESP-IDF: COnfigure ESP-IDF Extension
で再設定 したところ、上記の現象は収まった。はっきりと検証したわけではないが、bugfixesの中に関連しそうなトピックが幾つか見当たるので、たぶん大丈夫だと思いたい。

ESP-IDF環境で、JTAGでシリアル通信
ESP-IDFでシリアル通信をするのが結構難関で、Arduinoじゃないので当然 Serial.println()
みたいなものは存在しない。いろいろ試してみたが、たぶんJTAGを使うのが一番楽。
#include <driver/usb_serial_jtag.h>
// ...
void read_serial(void *pvParameters) {
ESP_LOGI("READ_SERIAL", "Starting read_serial Task...");
// USB Serial JTAG
usb_serial_jtag_driver_config_t jtag_config = {
.tx_buffer_size = SERIAL_BUFFER_SIZE,
.rx_buffer_size = SERIAL_BUFFER_SIZE,
};
ESP_ERROR_CHECK(usb_serial_jtag_driver_install(&jtag_config));
ESP_LOGI("JTAG", "USB_SERIAL_JTAG init done");
uint8_t* data = (uint8_t *) malloc(SERIAL_BUFFER_SIZE);
if (data == NULL) {
ESP_LOGE("JTAG", "out of memory");
vTaskDelete(NULL);
}
while(1) {
int len = usb_serial_jtag_read_bytes(data, (SERIAL_BUFFER_SIZE - 1), 20 / portTICK_PERIOD_MS);
if (len) {
usb_serial_jtag_write_bytes((const char *) data, len, 20 / portTICK_PERIOD_MS);
data[len] = '\0';
}
}
}
ちなみにUARTでできないこともないのだが、猛烈にコードが異様にデカくなったり、line_endings
を正しく設定しても fgets()
後に \r\n
送出後に謎の空行が出てきたりと頭を抱えることが多かったためやめました。
このexampleが参考になります。というかほとんどそのまんま。ただ下記コードではコンフィグの構造体定義だけ out-of-order initializers are nonstandard in C++
になるのと、Taskはreturnしてはならない[1]ので vTaskDelete(NULL)
してます。

シリアル通信をcloseするとESP32-S3が落ちる?
モニターソフトウェアのせいです。
VSCodeのシリアルモニタもそうですが、デフォルトでクローズ時に DTR
と RTS
を自動的に飛ばします。ESP32では DTR = high
RTS = low
になると電源が落ちるようで、これが悪さをします。PuTTYも同じことが起きるらしい。
VSCodeのシリアルモニタを使っている場合は、歯車マークをクリックして詳細設定(Data bitsとかStop bitsの設定が出せるところ)を押して、DTRとRTSのチェックを外す と回避できます。
参考

ESP_LOGI
の行でエラーになる
フォルダを切るとIntellisenseが 例えば <project root>/main/common/hoge.cpp
のように、mainフォルダ内にディレクトリを切ってその中でコーディングを行っていると、 ESP_LOGI
等がエラーになってしまう。これはあくまでIntellisenseが勝手にエラーを出しているだけで、ビルドは問題なく通る。
identifier "CONFIG_LOG_MAXIMUM_LEVEL" is undefined
CONFIG_LOG_MAXIMUM_LEVEL
は build/config/sdkconfig.h
で定義されており、Intellisenseがこれを読みにいかなくなる。この現象は main直下のファイル main/main.cpp
では発生しない。
不思議なことに、 .vscode/c_cpp_properties.json
のincludePath
は ${workspaceFolder}/**
で再帰になっているので問題がなさそうに見える。
"includePath": [
"${config:idf.espIdfPath}/components/**",
"${config:idf.espIdfPathWin}/components/**",
"${workspaceFolder}/**"
],
解決法はこの下に直指定で ${workspaceFolder}/build/config
を指定すること。なぜかこれで解決する…
"includePath": [
"${config:idf.espIdfPath}/components/**",
"${config:idf.espIdfPathWin}/components/**",
"${workspaceFolder}/**",
"${workspaceFolder}/build/config"
],

M5Dialのダイヤルを使う
esp-bspの M5Dialのページ を見ると、 espressif/knob
コンポーネントが使えることが記載されている。これを使うと楽。
エンコーダのAがGPIO_41
、BがGPIO_40
なので[1]それをもとにコードを書く。
#ifndef LVS_M5_DIAL
#define LVS_M5_DIAL
#include "iot_knob.h"
#include "esp_log.h"
#define TAG_M5DIAL "M5DIAL"
#define M5DIAL_KNOB_A 41
#define M5DIAL_KNOB_B 40
class M5DialUtils {
private:
knob_config_t knob_config;
knob_handle_t knob_handler;
static void knob_left_callback(void* arg, void* data);
static void knob_right_callback(void* arg, void* data);
public:
void init();
};
extern M5DialUtils M5Dial;
#endif
#include "M5Dial.h"
void M5DialUtils::init() {
knob_config = {
.default_direction = 1, // 1にしないと逆回転になるので注意
.gpio_encoder_a = M5DIAL_KNOB_A,
.gpio_encoder_b = M5DIAL_KNOB_B
};
knob_handler = iot_knob_create(&knob_config);
iot_knob_register_cb(knob_handler, KNOB_LEFT, knob_left_callback, NULL);
iot_knob_register_cb(knob_handler, KNOB_RIGHT, knob_right_callback, NULL);
}
void M5DialUtils::knob_left_callback(void* arg, void* data) {
ESP_LOGI(TAG_M5DIAL, "LEFT");
}
void M5DialUtils::knob_right_callback(void* arg, void* data) {
ESP_LOGI(TAG_M5DIAL, "RIGHT");
}
M5DialUtils M5Dial;

CLion + ESP-IDFのセットアップ
JetbrainsヘビーユーザーなのでやはりVSCodeだと補完周りが心もとなく、CLionを使うことにした…
VSCode拡張からインストールしたツールや、Windows向けインストーラでセットアップしてしまうと .espressif\python_env
内に activate.bat
が入っていなかったりなど、微妙に構成が違ってそのまま使えない。Githubのリポジトリから落としてきて全部手動でやった方がよい。
git clone https://github.com/espressif/esp-idf
cd esp-idf
install.bat
export.bat
CLionのドキュメントを参考に、CMake用ツールチェイン設定のカスタムスクリプトを用意。
以上で終了。動画やドキュメントを読んでも旧verの情報だったりしてちょっと苦労しました。

CLionでESP-IDFプロジェクトを開いたときの注意点
幾つか気になる点があったためメモ。随時更新。
TerminalのStart Directoryがおかしい
Tools > Terminal > Project Settings > Start directoryが ESP-IDFのインストールパス になることがある。プロジェクトルートに変更推奨。
Terminalで idf.py を使えるようにしたい
コマンドプロンプトを使っている場合は、前述のCMakeツールチェイン設定で作った .bat
ファイルが使えるのですが、powershellだとそうもいきません。新しく作ります。パスは自分のインストール場所で置き換えてください。
D:\Install\.espressif\python_env\idf5.5_py3.12_env\Scripts\Activate.ps1
D:\Install\esp-idf\export.ps1
起動時引数でこれを読み込むように設定しておきます。
menuconfigの表示が乱れる
Jetbrains IDEのTerminalと相性が悪そうです。こればかりは諦めて、Windowsターミナルで動かした方が賢明だと思います。一応動く組み合わせはあるので紹介しておきます。
-
標準Terminalの場合
⭕cmd
,powershell
ともに行間に隙間ができて見づらいですが、動きはする。 -
New Terminalの場合
⭕cmd
は行間に隙間ができて見づらいですが、動きはする。
❌powershell
は表示が乱れ、Escが反応せず厳しい。

作ってる最中のプログラムが完成したので、ひとまずクローズ。
esp-idf絡みというより、freertosやC++周りが分かっていない点が多く詰まることが多かった…
ごちゃごちゃしてるのでリファクタ後に記事にちゃんとまとめる予定。