m5stack のソフトウェアを esp-idf 環境で開発して動かす
目的・概要
- esp-idf 環境を構築して m5stack を動かす。
- arduino IDE よりもっと柔軟性の高い環境で書きたいけど、なんもわからん人向け
 - esp-idf の学習を兼ねて書いてます
 
 - esp-idf 環境上で M5GFX を導入してスクリーン描画のデモを動かす。
 - ボタン操作及び音声再生
 - (加速度センサー等はまた別記事で…)
 
環境・バージョン
- Ubuntu 20.04
 - m5stack core
 
(esp-idf)
$ git log -n1
commit 8bf14a9238329954c7c5062eeeda569529aedf75 (HEAD, tag: v4.3.2)
Author: Ivan Grokhotkov <ivan@espressif.com>
Date:   Mon Dec 20 19:02:41 2021 +0100
    versions: Update version to 4.3.2
    
(M5GFX)
$ git log -n1
commit a8e406af056f3b1cb331c0b66126ab828197755e (HEAD -> master, tag: 0.0.15, origin/master, origin/HEAD)
Merge: 41af34c c2370c0
Author: lovyan03 <42724151+lovyan03@users.noreply.github.com>
Date:   Sat Nov 27 12:21:25 2021 +0900
    Merge pull request #24 from m5stack/develop
    
    0.0.15
手順
Install
esp-idf を github から clone する。普通に clone すると master を pull するので、適切なバージョンにチェックアウトする(本記事では v4.3.2 をチェックアウトしています)。esp-idf は submodule を持つため、チェックアウトする時は、git submodule update を忘れずにする[1]。
pyenv や asdf を導入している場合は python の扱いに注意。python 系のエラーが出た時は、pip install した時に何処にある python にインストールされるか・esp-idf はどの python を使うか、などをチェック。
git clone https://github.com/espressif/esp-idf.git -b v4.3.2
cd esp-idf.git
./install.sh
Build hello_world
idf.py を使うため、export.sh から環境変数を読み込む。これで idf.py 等が使えるようになる。
今後ビルドや書き込みは idf.py を使っていく。
source export.sh
お試しで、examples/get-started/hello_world をビルド、書き込んでみる。
cd examples/get-started/hello_world
idf.py build
以下が出れば成功
Project build complete. To flash, run this command:
/home/fai/.espressif/python_env/idf5.0_py3.8_env/bin/python ../../../components/esptool_py/esptool/esptool.py -p (PORT) -b 460800 --before default_reset --after hard_reset --chip esp32  write_flash --flash_mode dio --flash_size detect --flash_freq 40m 0x1000 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0x10000 build/hello_world.bin
or run 'idf.py -p (PORT) flash'
m5stack を PC と接続して、デバイスファイルを確認。ls /dev/*USB* で雑に確認できる。
$ ls /dev/*USB*
/dev/ttyUSB0
ビルド成功時のメッセージ通り flash を実行すると書き込まれる。楽だね
idf.py -p /dev/ttyUSB0 flash
hello_world はシリアルに出力されるので、monitor でシリアルを確認する。シリアルを抜けるには、ctrl + ] を押せ、とメッセージが表示されるので、抜けたいときはそのようにする[2]。
idf.py -p /dev/ttyUSB0 monitor
以下のようなテキストをシリアルから受け取っているはず。
Hello world!
This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, silicon revision 1, 2MB external flash
Minimum free heap size: 294012 bytes
ちなみに、サンプルは printf を使っているが、シリアルにログを出したい時は、 printf では無く、 "esp_log.h" を #include して ESP_LOGI("TAG", "message v=%d", 5); などを使った方が良さそう。
ところで、コマンドは並べて書けるので、開発時は以下のようなコマンドを頻繁に叩くことになると思う。
idf.py -p /dev/ttyUSB0 build flash monitor
その他コマンドが豊富にある。特に使うかもしれないのは、fullclean menuconfig など。
Import M5GFX
https://github.com/m5stack/M5GFX を導入する。
動かすだけなら、以下のように components に突っ込むだけで出来た。
cd components
git clone https://github.com/m5stack/M5GFX
Run M5GFX samples
esp-idf のトップディレクトリに戻り、work ディレクトリを作る。今回は、この下でプロジェクトを作って作業をすることにする。hello_world をコピーし、念の為、ビルド出来ることを確認する。
cp -ar examples/get-started/hello_world work/
cd work/hello_world
idf.py fullclean
idf.py -p /dev/ttyUSB0 build flash monitor
M5GFX は C++ 。hello_world_main.c を hello_world_main.cpp に置き換える。
work/hello_world/main/CMakeLists.txt を書き換える。ファイル名の変更の他、M5GFX に依存させるために PRIV_REQUIRES "M5GFX" を追加した。
idf_component_register(SRCS "hello_world_main.cpp"
                       PRIV_REQUIRES "M5GFX"
                       INCLUDE_DIRS "")
M5GFX のサンプルをコピペして動かしたいが、arduino IDE 向けしか入っていないので、自前でsetup と loop を呼び実行することにする。
esp-idf はタスクの概念を持っていて、複数のコードを並列して動かすことができる。
app_main 関数で雑に無限ループしても良いが、せっかくなので、xTaskCreatePinnedToCore を使うことにする。
ref: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/freertos.html
// hello_world_main.cpp
// ============================================================================
// BEGIN BarGraph.ino
// https://github.com/m5stack/M5GFX
// //examples/Basic/BarGraph/BarGraph.ino
// FIXME: 下記URLのコードをここに貼り付ける
// https://github.com/m5stack/M5GFX/blob/a8e406af056f3b1cb331c0b66126ab828197755e/examples/Basic/BarGraph/BarGraph.ino 
// END BarGraph.ino
// ============================================================================
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
TaskHandle_t g_handle = nullptr;
void runMainLoop(void *args) {
  setup();
  for (;;) {
    loop();
    // avoid `The following tasks did not reset the watchdog in time`
    vTaskDelay(1);
  }
  vTaskDelete(g_handle);
}
void initializeTask() {
  xTaskCreatePinnedToCore(&runMainLoop, "task1-main", 8192, nullptr, 1,
                          &g_handle, 1);
  configASSERT(g_handle);
}
extern "C" {
void app_main(void) {
  //
  initializeTask();
}
}
idf.py -p /dev/ttyUSB0 build flash monitor でビルド・書き込みして、BarGraphのデモが動いたら ok。
Implement M5GFX App
その他、サンプルからは気づきにくい M5GFX に関連する問題などを列挙。
- ノイズ音がする
 - ちらつく
 
ノイズ音がする
GPIO25 と GND をジャンパー線で接続すると解消しました。以下の記事が詳しいようです。
- https://macsbug.wordpress.com/2019/09/27/m5stack-speaker-noise-reduction/
 - https://community.m5stack.com/topic/61/noise-on-speaker
 
ちらつく
M5GFX の描画命令はバッファリングをしない。M5Canvas を使って、スプライトに書き込み、スプライトを貼り付けることでバッファリングを行うのが一般的らしい。
M5GFX g_display;
M5Canvas g_canvas(&g_display);
int g_time = 0;
void setupDisplay() {
  g_display.init();
  g_display.startWrite();
  g_canvas.createSprite(g_display.width(), g_display.height());
}
void renderDisplay() {
  int kW = g_display.width();
  int kH = g_display.height();
  int kL = std::min(kW, kH) * 4 / 10;
  g_display.waitDisplay();
  g_canvas.fillRect(0, 0, kW, kH, TFT_BLACK);
  float a1 = 0.01 * g_time;
  float a2 = a1 + PI * 2 / 3;
  float a3 = a1 - PI * 2 / 3;
  g_canvas.fillTriangle(std::cos(a1) * kL + kW / 2, std::sin(a1) * kL + kH / 2,
                        std::cos(a2) * kL + kW / 2, std::sin(a2) * kL + kH / 2,
                        std::cos(a3) * kL + kW / 2, std::sin(a3) * kL + kH / 2,
                        TFT_WHITE);
  g_canvas.pushSprite(0, 0);
  g_display.display();
  g_time += 3;
}
上記の実装でも、線が入るようなちらつきが発生する場合がある。恐らく描画中に割り込みが入っていることが原因で、適切にスレッドを譲れば解消する[3]。手元の環境では、vTaskDelay(15 / portTICK_PERIOD_MS); で現象が発生し、vTaskDelay(40 / portTICK_PERIOD_MS); で抑えられた。
Control GPIO (buttons, speaker)
ボタンやスピーカーは GPIO に繋がっているため、arduino や RaspberryPi の工作と同じ感覚で操作できる。割り込みも出来る。
buttons
- ボタンは GPIO 39, 38, 37 に接続されている。
 - 
gpio_configで設定、gpio_get_levelで値が読み取れる。 - 
examples/peripherals/gpio/に割り込みの実装例が入っている。 
以下は割り込みを使わず、関数呼び出しで値を取得する例。
void initialize() {
  gpio_config_t io_conf = {};
  io_conf.intr_type = GPIO_INTR_DISABLE;
  io_conf.pin_bit_mask =
      (1ull << GPIO_NUM_39) | (1ull << GPIO_NUM_38) | (1ull << GPIO_NUM_37);
  io_conf.mode = GPIO_MODE_INPUT;
  io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
  gpio_config(&io_conf);
}
std::array<bool, 3> update() {
  bool a = gpio_get_level(GPIO_NUM_39);
  bool b = gpio_get_level(GPIO_NUM_38);
  bool c = gpio_get_level(GPIO_NUM_37);
  return std::array<bool, 3>{a, b, c};
}
speaker
PWM 制御をするには、driver/ledc.h を使う。
duty と hpoint については、http://blueeyes.sakura.ne.jp/2021/01/31/3672/ が詳しい。公式の文書では、ESP32 Technical Reference Manual の 14 LED PWM Controller に書かれている。
void initialize() {
  gpio_config_t config;
  config.intr_type = GPIO_INTR_DISABLE;
  config.pin_bit_mask = (1ull << GPIO_NUM_25);
  config.mode = GPIO_MODE_OUTPUT;
  config.pull_up_en = GPIO_PULLUP_DISABLE;
  config.pull_down_en = GPIO_PULLDOWN_DISABLE;
  gpio_config(&config);
}
void setSound(uint32_t freq, bool enable) {
  ledc_timer_config_t timer_config = {};
  timer_config.speed_mode = LEDC_HIGH_SPEED_MODE;
  timer_config.duty_resolution = LEDC_TIMER_8_BIT;
  timer_config.timer_num = LEDC_TIMER_3;
  timer_config.freq_hz = freq;
  timer_config.clk_cfg = LEDC_AUTO_CLK;
  ledc_timer_config(&timer_config);
  ledc_channel_config_t channel_config = {};
  channel_config.gpio_num = GPIO_NUM_25;
  channel_config.speed_mode = LEDC_HIGH_SPEED_MODE;
  channel_config.channel = LEDC_CHANNEL_1;
  channel_config.intr_type = LEDC_INTR_DISABLE;
  channel_config.timer_sel = timer_config.timer_num;
  channel_config.duty = enable ? 0x7F : 0x00;
  channel_config.hpoint = 0x0;
  ledc_channel_config(&channel_config);
}
void runSpeakerLoop(void *args) {
  vTaskDelay(3000 / portTICK_PERIOD_MS);
  g_my_io.setSound(262, true);
  vTaskDelay(500 / portTICK_PERIOD_MS);
  g_my_io.setSound(294, true);
  vTaskDelay(500 / portTICK_PERIOD_MS);
  g_my_io.setSound(330, true);
  vTaskDelay(500 / portTICK_PERIOD_MS);
  g_my_io.setSound(330, false);
  vTaskDelete(nullptr);
}
ところで、 channel_config.channel = LEDC_CHANNEL_7 とすると duty=0 でディスプレイが道連れになって消えたり、timer_config.clk_cfg = LEDC_AUTO_CLK 以外が動作しなかったり、未だ細かい点が把握できていません…。何か間違いがあれば後ほど修正します。
sample code
動作確認に使用した、全体像のコードです
Discussion