M5StickCの内蔵LEDを使う

いまさらですが、手を広げ過ぎたので基本機能をまず終わらせます。。。

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

概要

M5StickCには赤色LEDが内蔵されており、光らせることができます。一般的にArduinoのプログラムを作る時には「Hello World」のかわりにLEDを光らせるLチカをすることが多いです。

注意事項

一般的なArduinoだとGPIO13とかにLEDがついていることが多いですが、開発ボードによって内蔵していなかったり、違うPINにつながっていたりします。

M5StickCは裏側を見ればわかるのですが、GPIO10に接続されています。また、LOWに落とすことでLEDが点灯しますので注意してください。

サンプルスケッチ

#include <M5StickC.h>

void setup(){
  M5.begin();

  // LED ON(GPIO_NUM_10 or M5_LED)
  pinMode(GPIO_NUM_10, OUTPUT);
  digitalWrite(GPIO_NUM_10, LOW);
}

void loop() {
}

非常にシンプルですが、最低限設定しないといけないものがpinMode()です。ここでOUTPUTに設定しないとdigitalWrite()の値をいくら変更してもLEDは光りませんでした。

また、一般的なLチカの場合、PINをHIGHにして、抵抗とLEDに接続したものをGNDに繋ぐことで点灯させています。こちらの回路の方がシンプルなのですが、ボードから電源供給できる量はあまり大きくありません。

M5StickCのように、外部の電源からボード側のGNDに流す回路の方が電源管理的に楽なんだと思います。

まとめ

名前はM5_LEDってのが定義されていますが、裏側の番号みて10を指定するほうがLEDって思えるより楽な気がします。

ただしM5StickCのLEDはLOWで点灯ってのは覚えておいてください。忘れても確かめれば大丈夫なんですが、pinMode()の設定をしていないとHIGHでもLOWでも点灯しないので気をつけてください。

ESP32のスリープ時の設定を調べた

esp_sleep_pd_config()での省電力設定まわりを調べました。現時点の情報ですので最新情報はM5StickC非公式日本語リファレンスを確認してください。

概要

スリープ時にタイマーやタッチなど複数のウェイクアップソースを指定できますが、指定したソースに応じて電源管理のオプションが異なっています。

初期値の調査と、各オプションの詳細を調べてみました。

初期値

esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH  , ESP_PD_OPTION_AUTO);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_AUTO);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_AUTO);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL        , ESP_PD_OPTION_OFF);

上記が初期値です。XTALは初期値が設定されていなかったので、NO扱いになっているはずです。

つまり全部AUTOですので、ウェイクアップソースによってONかOFFが決められています。

ソース別初期値

PERIPHSLOWFASTXTALMAX
初期値AUTOAUTOAUTOOFFX
TIMEROFFONONOFFX
TOUCHOFFONONOFFX
EXT0ONONONOFFX
EXT1OFFONONOFFX
ULPOFFONONOFFX
GPIOONONONOFFX
UARTOFFONONOFFX

EXT0とGPIOだけPERIPHがONで、それ以外はOFFになっています。

ESP_PD_DOMAIN_MAXはenumの最大値を確認するための宣言なので、実際に指定すると範囲外エラーになります。

ESP_PD_DOMAIN_RTC_PERIPH

周辺機器への電源供給を制御します。digitalWrite()などで、HIGHの出力を継続する場合などはONにする必要があります。 EXT0とGPIOを利用する場合にはONになりますが、それ以外の復帰をする場合にはOFFになります。

ただし、OFFにしてもULPが動いている間は一時的にONになり、ULPが停止するとOFFに戻ります。

同じくタッチセンサーを利用している場合にも、タッチタイマーが定期的に動いており、タッチ情報を取得しているごく短い時間だけONになり、すぐにOFFに戻ります。

ESP_PD_DOMAIN_RTC_SLOW_MEM

ULPやディープスリープから復帰した場合に保持しておくメモリ領域への電源供給を制御します。 OFFにするとRTC_DATA_ATTR指定された変数や、RTC_SLOW_MEM[]変数の値がおかしくなります。

またULPを指定した場合にOFFにするとULP自体が動きません。

ESP_PD_DOMAIN_RTC_FAST_MEM

ブートに関係するメモリですが、フラッシュメモリの初期化が遅い場合などに遅延を入れるコードなどが入っているようです。 OFFにしても、他に影響を与えない場合もありますが、環境によってはフラッシュメモリのエラーが発生して起動が失敗します。

ESP_PD_DOMAIN_XTAL

水晶振動子への電源供給を制御します。通常OFFで動いており、どんな場合にONにするのかがわかりませんでした。

まとめ

下記設定で、ディープスリープするのが一番省電力ですが、FAST_MEMをOFFにしているため、初期化が遅いフラッシュメモリを搭載していると復帰時にエラーになります。

とはいえ、日本で使えるESP32の場合はフラッシュメモリもパッケージングされているので、OFFにしても問題が起こることはないような気もします。

esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH  , ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);

ちょっとこの辺は環境と、使っているライブラリのバージョンで動きが変わるようですので、実際に使う環境で試してみるのがいいと思います。

ESP32のディープスリープを調べる

こちらもなかなか情報が少なかったです。現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスで確認してください。

概要

ESP32のディープスリープは省電力で動かすためには必須の機能ですが、ディープスリープに入ったから、すべての機能が使えなくなるわけではないようです。

ディープスリープから復帰した場合には、リセットがかかったときと同じくsetup()から動きます。この時メモリの内容などは消えていますが、スローメモリの内容だけは残っています。

起動したのか、復帰したのかはesp_sleep_get_wakeup_cause()の戻り値を見ることで確認が可能です。

復帰には以下の種類があります。

  • タイマー
  • タッチセンサー
  • EXT0(RTC_IO)
  • EXT1(RTC_CNTL)
  • ULP

タイマーは指定時間後に復帰するので単純です。タッチセンサーも指定したPADに触れた場合に復帰します。

EXTの2種類はちょっとわかりにくくて、EXT0はプルアップなどが可能で、1つのGPIOをHIGHかLOWのトリガーを指定して復帰できます。簡単な反面1つのGPIOでしか受け付けないのと、プルアップなどで電力を消費してしまいます。

EXI1はプルアップなどが利用できないのですが、複数のGPIOに対してどれかがHIGHになったか、すべてLOWになったかのトリガーで復帰できます。

ULPはコプロセッサーのプログラム内部から復帰できます。ULPからは一部のGPIOやI2Cなどにアクセスが可能ですので、複雑な処理も可能です。

復帰のトリガーは複数設定し、タイマーをかけつつ、タッチセンサーでも復帰することなども可能です。

タイマーのサンプル

RTC_DATA_ATTR int bootCount = 0; // RTCスローメモリに変数を確保

void setup() {
  // シリアル初期化
  Serial.begin(115200);

  // 初回起動だけはシリアル初期化待ち
  if( bootCount == 0 ){
    delay(1000);
  }

  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動 PAD=%d\n", esp_sleep_get_touchpad_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // 1000000us = 1sのタイマー設定
  esp_sleep_enable_timer_wakeup(1000000);

  // ディープスリープ
  esp_deep_sleep_start();
}

void loop() {
}

RTC_DATA_ATTRをつけた変数はスローメモリ領域に確保されます。そのためディープスリープから復帰しても内容が保存されています。ULPを利用するときは自分でアドレスを管理しないとプログラム領域とバッティングするので注意が必要です。

内容自体はシンプルにタイマーで復帰までの時間を指定するだけです。ただしタイマーの精度はそれほど高くないので、徐々に時間はずれていくと思います。

タッチのサンプル

RTC_DATA_ATTR int bootCount = 0; // RTCスローメモリに変数を確保

void callback(){
}

void setup(){
  // シリアル初期化
  Serial.begin(115200);

  // 初回起動だけはシリアル初期化待ち
  if( bootCount == 0 ){
    delay(1000);
  }

  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動 PAD=%d\n", esp_sleep_get_touchpad_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // GPIO32, 33のタッチ有効、しきい値は環境によって異なるので反応しない場合には増減が必要
  // ただし32と33のタッチだけはなぜか番号が入れ替わっているので、GPIO32を取得するときは33を指定する必要がある
  static int threshold = 16;
  pinMode(GPIO_NUM_32, INPUT);
  pinMode(GPIO_NUM_33, INPUT);
  touchAttachInterrupt(GPIO_NUM_32, callback, threshold);
  touchAttachInterrupt(GPIO_NUM_33, callback, threshold);

  // タッチパッドをウェイクアップソースとして有効にする
  esp_sleep_enable_touchpad_wakeup();

  // ディープスリープ
  esp_deep_sleep_start();
}

void loop(){
}

タッチの場合、コールバック関数を登録する必要があり、タッチすると復帰します。内容的にはシンプルです。

EXT0のサンプル

RTC_DATA_ATTR int bootCount = 0; // RTCスローメモリに変数を確保

void setup(){
  // シリアル初期化
  Serial.begin(115200);

  // 初回起動だけはシリアル初期化待ち
  if( bootCount == 0 ){
    delay(1000);
  }

  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動 PAD=%d\n", esp_sleep_get_touchpad_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // GPIO37(M5StickCのHOMEボタン)がLOWになったら起動
  pinMode(GPIO_NUM_37, INPUT_PULLUP);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, LOW);

  // ディープスリープ
  esp_deep_sleep_start();
}

void loop(){
}

GPIOの入力をトリガーに復帰します。通常のプログラムと同じような感じで指定することができます。トリガーになるIOが1つだけの場合には、一番単純な復帰方法です。

EXT1のサンプル

RTC_DATA_ATTR int bootCount = 0; // RTCスローメモリに変数を確保

void setup(){
  // シリアル初期化
  Serial.begin(115200);

  // 初回起動だけはシリアル初期化待ち
  if( bootCount == 0 ){
    delay(1000);
  }

  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動 PAD=%d\n", esp_sleep_get_touchpad_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // GPIO26かGPIO36がHIGHになったら起動
  pinMode(GPIO_NUM_26, INPUT);
  pinMode(GPIO_NUM_36, INPUT);
  esp_sleep_enable_ext1_wakeup(BIT64(GPIO_NUM_26) | BIT64(GPIO_NUM_36), ESP_EXT1_WAKEUP_ANY_HIGH);

  // M5StickCだとGPIO0がプルアップされているのでGNDに落とすことでも起動した
  //pinMode(GPIO_NUM_0, INPUT);
  //esp_sleep_enable_ext1_wakeup(BIT64(GPIO_NUM_0), ESP_EXT1_WAKEUP_ALL_LOW);

  // ディープスリープ
  esp_deep_sleep_start();
}

void loop(){
}

EXT1は一般的なGPIOではなく、RTC経由でのIOになるので、指定方法がちょっと違います。またプルアップが使えないので、M5StickCのホームボタンなどは利用できません。反面複数のIOに対してトリガーを設定できます。

ULPのサンプル

#include "esp32/ulp.h"

// スローメモリー変数割当
enum {
  SLOW_BOOT_COUNT,      // 起動回数

  SLOW_PROG_ADDR        // プログラムの先頭アドレス
};

void ULP_PROG(uint32_t us) {
  // ULPの起動間隔を設定
  ulp_set_wakeup_period(0, us);

  // ULPプログラム
  const ulp_insn_t  ulp_prog[] = {
    I_WAKE(),           // メインチップ起動
    I_HALT(),           // ULPプログラム停止
  };

  // 変数の分プログラムを後ろにずらして実行
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(SLOW_PROG_ADDR, ulp_prog, &amp;size);
  ulp_run(SLOW_PROG_ADDR);
}

void setup(){
  // シリアル初期化
  Serial.begin(115200);

  // 初回起動の特殊処理
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  if( wakeup_reason == ESP_SLEEP_WAKEUP_UNDEFINED ){
    // シリアル初期化待ち
    delay(1000);

    // ULPを1秒間隔で起動
    ULP_PROG(1000000);

    // 起動回数初期化
    RTC_SLOW_MEM[SLOW_BOOT_COUNT] = 0;
  }

  // 起動回数カウントアップ
  RTC_SLOW_MEM[SLOW_BOOT_COUNT]++;
  Serial.printf("起動回数: %d ", RTC_SLOW_MEM[SLOW_BOOT_COUNT]);

  // 起動方法取得
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動 PAD=%d\n", esp_sleep_get_touchpad_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // ULPをウェイクアップソースとして有効にする
  esp_sleep_enable_ulp_wakeup();

  // ディープスリープ
  esp_deep_sleep_start();
}

void loop(){
}

スローメモリの管理を自分でする必要があるのでRTC_DATA_ATTRが使えません。また、ULPはI_END()でULPのタイマーを止めない限り、動き続けていますので注意が必要です。複雑なIOをトリガーにする場合にはULPを使う必要がありますが、単純なトリガーの場合にはEXT0などを利用したほうが簡単だと思います。

まとめ

EXT1とかULPの復帰例の情報がないので、使っている人ほとんどいない気がします。また、ディープスリープ中で動いているものを止めることで、さらに省電力にすることが可能です。

そのうちまとめますが、こばさんの「ESP32 Deep Sleep のテスト (Hibernation mode)」が現状一番わかり易いと思います。

また、サンプルソースの最新版はGitHubにアップされています。

ESP32のライトスリープを調べる

ディープスリープより情報がないかも?

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

概要

一時的にチップの処理をとめて、省電力するモードです。ディープスリープはリセットされたように、setup()から実行し直されますが、ライトスリープはスリープした場所から再開されます。

一般的にディープスリープはよく使われていますが、ライトスリープはあまり使われていないようです。ただ、トリガーまで省電力で待機するなどを簡単に実現できます。

復帰の種類は以下の7種類あります。

  • タイマー
  • タッチセンサー
  • EXT0(RTC_IO)
  • EXT1(RTC_CNTL)
  • ULP
  • GPIO
  • UART

上5つはディープスリープと同じ方法です。GPIOはほぼEXT0と同じ機能ですが、複数のGPIOを対象にすることができます。UARTはシリアルの受信をトリガーに復帰することができます。

タイマーのサンプル

int bootCount = 0; // ライトスリープは通常の変数で良い

void setup() {
  // シリアル初期化
  Serial.begin(115200);

  // シリアル初期化待ち
  delay(1000);

  // 1000000us = 1sのタイマー設定
  esp_sleep_enable_timer_wakeup(1000000);
}

void loop() {
  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動 PAD=%d\n", esp_sleep_get_touchpad_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // シリアルをすべて出力する
  Serial.flush();

  // ライトスリープ
  esp_light_sleep_start();
}

ディープスリープと違い、ライトスリープした場所に復帰します。そして変数などもすべて保存されているのでスローメモリを使う必要はありません。プログラムも普通のものとそれほど変わらなく組むことが可能です。

タッチのサンプル

int bootCount = 0;  // ライトスリープは通常の変数で良い
int touchPad = 0;   // タッチされたPAD

// タッチコールバック関数
void callback32(){
  touchPad = 32;
}
void callback33(){
  touchPad = 33;
}

void setup() {
  // シリアル初期化
  Serial.begin(115200);

  // シリアル初期化待ち
  delay(1000);

  // GPIO32, 33のタッチ有効、しきい値は環境によって異なるので反応しない場合には増減が必要
  // ただし32と33のタッチだけはなぜか番号が入れ替わっているので、GPIO32を取得するときは33を指定する必要がある
  static int threshold = 16;
  pinMode(GPIO_NUM_32, INPUT);
  pinMode(GPIO_NUM_33, INPUT);
  touchAttachInterrupt(GPIO_NUM_32, callback32, threshold);
  touchAttachInterrupt(GPIO_NUM_33, callback33, threshold);

  // タッチパッドをウェイクアップソースとして有効にする
  esp_sleep_enable_touchpad_wakeup();
}

void loop() {
  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // ライトスリープでsp_sleep_get_touchpad_wakeup_status()を呼び出したらリブートする? PADはコールバックで判定する
  //esp_sleep_get_touchpad_wakeup_status();
  Serial.printf("PAD: %d ", touchPad);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // シリアルをすべて出力する
  Serial.flush();

  // ライトスリープ
  esp_light_sleep_start();
}

タッチも割り込み関数によって復帰しますが、なぜかsp_sleep_get_touchpad_wakeup_status()を呼び出すとエラーがでてリセットされました。ディープスリープでは使えたのですが、ライトスリープでは使えないようです。

EXT0のサンプル

int bootCount = 0; // ライトスリープは通常の変数で良い

void setup() {
  // シリアル初期化
  Serial.begin(115200);

  // シリアル初期化待ち
  delay(1000);

  // GPIO37(M5StickCのHOMEボタン)がLOWになったら起動
  pinMode(GPIO_NUM_37, INPUT_PULLUP);
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, LOW);
}

void loop() {
  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // シリアルをすべて出力する
  Serial.flush();

  // ライトスリープ
  esp_light_sleep_start();
}

特に特殊なことは必要ありません。

EXT1のサンプル

int bootCount = 0; // ライトスリープは通常の変数で良い

void setup() {
  // シリアル初期化
  Serial.begin(115200);

  // シリアル初期化待ち
  delay(1000);

  // GPIO26かGPIO36がHIGHになったら起動
  pinMode(GPIO_NUM_26, INPUT);
  pinMode(GPIO_NUM_36, INPUT);
  esp_sleep_enable_ext1_wakeup(BIT64(GPIO_NUM_26) | BIT64(GPIO_NUM_36), ESP_EXT1_WAKEUP_ANY_HIGH);

  // M5StickCだとGPIO0がプルアップされているのでGNDに落とすことでも起動した
  //pinMode(GPIO_NUM_0, INPUT);
  //esp_sleep_enable_ext1_wakeup(BIT64(GPIO_NUM_0), ESP_EXT1_WAKEUP_ALL_LOW);
}

void loop() {
  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // シリアルをすべて出力する
  Serial.flush();

  // ライトスリープ
  esp_light_sleep_start();
}

こちらもディープスリープと同じような処理です。

ULPのサンプル

#include "esp32/ulp.h"

int bootCount = 0; // ライトスリープは通常の変数で良い

// スローメモリー変数割当
enum {
  SLOW_PROG_ADDR        // プログラムの先頭アドレス
};

void ULP_PROG(uint32_t us) {
  // ULPの起動間隔を設定
  ulp_set_wakeup_period(0, us);

  // ULPプログラム
  const ulp_insn_t  ulp_prog[] = {
    I_WAKE(),           // メインチップ起動
    I_HALT(),           // ULPプログラム停止
  };

  // 変数の分プログラムを後ろにずらして実行
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(SLOW_PROG_ADDR, ulp_prog, &amp;size);
  ulp_run(SLOW_PROG_ADDR);
}

void setup() {
  // シリアル初期化
  Serial.begin(115200);

  // シリアル初期化待ち
  delay(1000);

  // ULPを1秒間隔で起動
  ULP_PROG(1000000);

  // ULPをウェイクアップソースとして有効にする
  esp_sleep_enable_ulp_wakeup();
}

void loop() {
  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // シリアルをすべて出力する
  Serial.flush();

  // ライトスリープ
  esp_light_sleep_start();
}

ULPもディープスリープとほぼ同じです。

GPIOのサンプル

int bootCount = 0; // ライトスリープは通常の変数で良い

void setup() {
  // シリアル初期化
  Serial.begin(115200);

  // シリアル初期化待ち
  delay(1000);

  // GPIO37(M5StickCのHOMEボタン)かGPIO39(M5StickCの右ボタン)がLOWになったら起動
  pinMode(GPIO_NUM_37, INPUT_PULLUP);
  pinMode(GPIO_NUM_39, INPUT_PULLUP);
  gpio_wakeup_enable(GPIO_NUM_37, GPIO_INTR_LOW_LEVEL);
  gpio_wakeup_enable(GPIO_NUM_39, GPIO_INTR_LOW_LEVEL);

  // GPIOをウェイクアップソースとして有効にする
  esp_sleep_enable_gpio_wakeup();
}

void loop() {
  // 起動回数カウントアップ
  bootCount++;
  Serial.printf("起動回数: %d ", bootCount);

  // 起動方法取得
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason){
    case ESP_SLEEP_WAKEUP_EXT0      : Serial.printf("外部割り込み(RTC_IO)で起動\n"); break;
    case ESP_SLEEP_WAKEUP_EXT1      : Serial.printf("外部割り込み(RTC_CNTL)で起動 IO=%llX\n", esp_sleep_get_ext1_wakeup_status()); break;
    case ESP_SLEEP_WAKEUP_TIMER     : Serial.printf("タイマー割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD  : Serial.printf("タッチ割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_ULP       : Serial.printf("ULPプログラムで起動\n"); break;
    case ESP_SLEEP_WAKEUP_GPIO      : Serial.printf("ライトスリープからGPIO割り込みで起動\n"); break;
    case ESP_SLEEP_WAKEUP_UART      : Serial.printf("ライトスリープからUART割り込みで起動\n"); break;
    default                         : Serial.printf("スリープ以外からの起動\n"); break;
  }

  // シリアルをすべて出力する
  Serial.flush();

  // ライトスリープ
  esp_light_sleep_start();
}

EXT0に似ていますが、複数のGPIOを指定することができます。

UARTのサンプル

すみません、M5StickCでは利用できないので、準備していません。

[SOLVED]esp_sleep_enable_uart_wakeup with CONFIG_CONSOLE_UART_CUSTOM configuration」に情報が少しだけあります。

基本的にUARTで受信トリガーにできるのは標準PIN配置のUARTだけで、UART0ではGPIO3、UART1ではGPIO9をRXとして設定する必要があります。

また、受信した信号エッジ数を復帰トリガーとしているので、最初の数文字は取りこぼします。他の信号線が接続されていない状況以外では積極的に利用することはないと思います。

まとめ

ライトスリープはちょっとした省電力にはいいと思いますが、扱いはちょっと難しいかもしれません。バッテリー駆動の場合にはディープスリープを使っちゃうので、あまり出番はないのかな?

また、サンプルソースの最新版はGitHubにアップされています。

ESP32でULPを使ったBlink その2と逆アセンブラ

コードの内容を見直しと、なぜか逆アセンブラを作ってみました。最新情報はM5StickC非公式日本語リファレンスをみてください。

サンプルコード

#include "esp32/ulp.h"
#include "driver/rtc_io.h"
#include "ulputil.h"

// スローメモリー変数割当
enum {
  SLOW_BLINK_STATE,     // Blinkの状態保持

  SLOW_PROG_ADDR        // プログラムの先頭アドレス
};

void ULP_BLINK(uint32_t us) {
  // ULPの起動間隔を設定
  ulp_set_wakeup_period(0, us);

  // メモリ初期化
  memset(RTC_SLOW_MEM, 0, 8000);

  // 変数初期化
  RTC_SLOW_MEM[SLOW_BLINK_STATE] = 0;

  // ブリンクするPIN(14bitオフセットして指定する)
  const int pin_blink_bit = RTCIO_GPIO26_CHANNEL + 14;
  const gpio_num_t pin_blink = GPIO_NUM_26;

  // GPIO26の初期化(出力に設定して初期値0)
  rtc_gpio_init(pin_blink);
  rtc_gpio_set_direction(pin_blink, RTC_GPIO_MODE_OUTPUT_ONLY);
  rtc_gpio_set_level(pin_blink, 0);

  // ULPプログラム
  const ulp_insn_t  ulp_prog[] = {
    I_MOVI(R3, SLOW_BLINK_STATE),           // R3 = SLOW_BLINK_STATE
    I_LD(R0, R3, 0),                        // R0 = RTC_SLOW_MEM[R3(SLOW_BLINK_STATE)]
    M_BL(1, 1),                             // IF R0 < 1 THAN GOTO M_LABEL(1)

    // R0 => 1の時実行
    I_WR_REG(RTC_GPIO_OUT_REG, pin_blink_bit, pin_blink_bit, 1), // pin_blink_bit = 1
    I_MOVI(R0, 0),                          // R0 = 0
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(SLOW_BLINK_STATE)] = R0
    M_BX(2),                                // GOTO M_LABEL(2)

    // R0 < 1の時実行
    M_LABEL(1),                             // M_LABEL(1)
    I_WR_REG(RTC_GPIO_OUT_REG, pin_blink_bit, pin_blink_bit, 0),// pin_blink_bit = 0
    I_MOVI(R0, 1),                          // R0 = 1
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(SLOW_BLINK_STATE)] = R0

    M_LABEL(2),                             // M_LABEL(2)
    I_HALT()                                // プログラム停止
  };

  // 変数の分プログラムを後ろにずらして実行
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(SLOW_PROG_ADDR, ulp_prog, &amp;size);
  ulp_run(SLOW_PROG_ADDR);
}

void setup() {
  // デバッグ出力用
  Serial.begin(115200);

  // 300ms間隔でULPプログラムを実行
  ULP_BLINK(300000);
}

void loop() {
  // デバッグ出力
  ulpDump(0, 20, SLOW_PROG_ADDR);
  delay(1000);
}

元にしたソースはesp32 ulp programmingですが、デクリメントとかインクリメントとかしていましたが、コストを調べると直値のMOVEと同じだったので、0と1を直接代入するように買えています。

変数もenumで最初に定義して、プログラムのロード場所と実行開始のアドレスを指定しています。

デバッグしていて、なぜか逆アセンブラも作っていました。

0000 : 00C30000 DATA     0                              // ST ADDR:0x0006
0001 : 72800003 PROG MOVE   R3,   0                     // R3 = 0
0002 : D000000C PROG LD     R0, R3,   0                 // R0 = MEM[R3+0]
0003 : 820A0001 PROG JUMPR  +  5,   1, LT               // IF R0 < 1 THAN GOTO 0x0008
0004 : 1AD40500 PROG REG_WR 0x0000,  21,  21,   1       // RTC_IO[21:21] = 1
0005 : 72800000 PROG MOVE   R0,   0                     // R0 = 0
0006 : 6800000C PROG ST     R0, R3,   0                 // MEM[R3+0] = R0
0007 : 8000002C PROG JUMP   0x000B                      // GOTO 0x000B
0008 : 1AD40100 PROG REG_WR 0x0000,  21,  21,   0       // RTC_IO[21:21] = 0
0009 : 72800010 PROG MOVE   R0,   1                     // R0 = 1
000A : 6800000C PROG ST     R0, R3,   0                 // MEM[R3+0] = R0
000B : B0000000 PROG HALT                               // HALT
000C : 00000000 PROG NOP                                // NOP
000D : 00000000 PROG NOP                                // NOP
000E : 00000000 PROG NOP                                // NOP
000F : 00000000 PROG NOP                                // NOP
0010 : 00000000 PROG NOP                                // NOP
0011 : 00000000 PROG NOP                                // NOP
0012 : 00000000 PROG NOP                                // NOP
0013 : 00000000 PROG NOP                                // NOP

こんな感じの出力がシリアルにでます。一番先頭の変数は書き込まれる場所が2箇所あって、上位16ビットでどこから書き込まれたのかがわかります。上記の場合には0x0006で0(消灯)に設定した状態の出力です。

0000 : 01430001 DATA     1                              // ST ADDR:0x000A

もう一箇所はADDR:0x000Aで1(点灯)に設定しています。

まだ使っている命令だけしかキレイにしていないので、もうちょっと手を入れたらライブラリとして公開しようかな。

まとめ

いろいろ中を見ていました、Arduinoから使えるマクロだと、全命令をサポートしていないですね。ステージカウントレジスタを使っている命令が全滅でした。ループのカウンターで使うと便利そうなのですが、まあなくても他のレジスタ使えばなんとかなるはずです。

ESP32でULPを使ったBlink

ESP32に搭載されているコプロセッサーのULPまわりを調べました。最新情報はM5StickC非公式日本語リファレンスにまとめてあります。

ULPとは

ULP (Ultra Low Power)で、省電力で動くコプロセッサーです。ESP32のメインコアとは別にあり、メインコアがスリープ状態でも動かすことができます。

省電力で動く反面、複雑な処理や高速処理ができないデメリットと、プログラミングがアセンブラなところがネックです。

そして情報が極端に少ないです。

サンプルスケッチ

#include "esp32/ulp.h"
#include "driver/rtc_io.h"

void ULP_BLINK_RUN(uint32_t us);

void setup() {
  // 300ms間隔でULPプログラムを実行
  ULP_BLINK_RUN(300000);
}

void loop() {
}

void ULP_BLINK_RUN(uint32_t us) {
  // ULPの起動間隔を設定
  ulp_set_wakeup_period(0, us);

  // ブリンクの状態を保存する変数初期化(0からプログラムが収納されるので、1999から逆順利用が安全)
  const int var_blink_state = 1999;
  RTC_SLOW_MEM[var_blink_state] = 0;
  
  // ブリンクするPIN(14bitオフセットして指定する)
  const int pin_blink_bit = RTCIO_GPIO26_CHANNEL + 14;
  const gpio_num_t pin_blink = GPIO_NUM_26;

  // GPIO26の初期化(出力に設定して初期値0)
  rtc_gpio_init(pin_blink);
  rtc_gpio_set_direction(pin_blink, RTC_GPIO_MODE_OUTPUT_ONLY);
  rtc_gpio_set_level(pin_blink, 0);

  // ULPプログラム
  const ulp_insn_t  ulp_prog[] = {
    I_MOVI(R3, var_blink_state),            // R3 = var_blink_state
    I_LD(R0, R3, 0),                        // R0 = RTC_SLOW_MEM[R3(var_blink_state)]
    M_BL(1, 1),                             // IF R0 < 1 THAN GOTO M_LABEL(1)

    // R0 == 1の時実行(LED点灯)
    I_WR_REG(RTC_GPIO_OUT_REG, pin_blink_bit, pin_blink_bit, 1), // pin_blink_bit = 1
    I_SUBI(R0, R0, 1),                      // R0 = R0 - 1
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(12)] = R0
    M_BX(2),                                // GOTO M_LABEL(2)

    // R0 < 1の時実行(LED消灯)
    M_LABEL(1),                             // M_LABEL(1)
    I_WR_REG(RTC_GPIO_OUT_REG, pin_blink_bit, pin_blink_bit, 0),// pin_blink_bit = 0
    I_ADDI(R0, R0, 1),                      // R0 = R0 + 1
    I_ST(R0, R3, 0),                        // RTC_SLOW_MEM[R3(var_blink_state)] = R0

    M_LABEL(2),                             // M_LABEL(2)
    I_HALT()                                // プログラム停止
  };

  // 実行
  size_t size = sizeof(ulp_prog) / sizeof(ulp_insn_t);
  ulp_process_macros_and_load(0, ulp_prog, &amp;size);
  ulp_run(0);
}

300ms間隔でGPIO26に接続したLEDを光らせるサンプルです。本当はM5StickCで内蔵しているGPIO10が使いたかったのですが、ULPからはアクセスできないpinでした。

上記のようにGPIO26にLEDと抵抗を接続してBlinkしています。

解説

本来はアセンブラなので、.sファイルを使って開発をしますが、通常の環境ではアセンブラが使えないので、組み込みマクロを使ってプログラムをしています。

以下ハマりどころだけ解説します。

ピンについて

まずGPIOですが、そのままの番号ではなく、RTCのピン番号に14ビットオフセットした値で指定する必要があります。内部的にはRTC経由でアクセスをおり、初期化などもRTC経由で行っています。

RTCIO_GPIO36_CHANNELはesp32_gpioMux[GPIO_NUM_36].rtcでも同じ値です。RTCで利用できないIOで、RTCIO_GPIO03_CHANNELなどの場合には未定義でコンパイルエラーになりますが、 esp32_gpioMux[GPIO_NUM_3].rtcは-1になるので注意してください。

RTCGPIOBitArduinoでの記述
RTC_GPIO0GPIO3614RTCIO_GPIO36_CHANNEL + 14
RTC_GPIO1GPIO3715RTCIO_GPIO37_CHANNEL + 14
RTC_GPIO2GPIO3816RTCIO_GPIO38_CHANNEL + 14
RTC_GPIO3GPIO3917RTCIO_GPIO39_CHANNEL + 14
RTC_GPIO4GPIO3418RTCIO_GPIO34_CHANNEL + 14
RTC_GPIO5GPIO3519RTCIO_GPIO35_CHANNEL + 14
RTC_GPIO6GPIO2520RTCIO_GPIO25_CHANNEL + 14
RTC_GPIO7GPIO2621RTCIO_GPIO26_CHANNEL + 14
RTC_GPIO8GPIO3322RTCIO_GPIO33_CHANNEL + 14
RTC_GPIO9GPIO3223RTCIO_GPIO32_CHANNEL + 14
RTC_GPIO10GPIO0424RTCIO_GPIO04_CHANNEL + 14
RTC_GPIO11GPIO0025RTCIO_GPIO00_CHANNEL + 14
RTC_GPIO12GPIO0226RTCIO_GPIO02_CHANNEL + 14
RTC_GPIO13GPIO1527RTCIO_GPIO15_CHANNEL + 14
RTC_GPIO14GPIO1328RTCIO_GPIO13_CHANNEL + 14
RTC_GPIO15GPIO1229RTCIO_GPIO12_CHANNEL + 14
RTC_GPIO16GPIO1430RTCIO_GPIO14_CHANNEL + 14
RTC_GPIO17GPIO2731RTCIO_GPIO27_CHANNEL + 14

上記の関係になっており、RTCではすべてのピンに接続されていません。GPIO26はRTC_GPIO7に対応しており、7+14で21ビット目になります。

スローメモリについて

ESP32では8000バイトのスローメモリと呼ばれる領域があり、ULPと通常のプログラムの両方からアクセスが可能です。uint32_tとして定義してあるので、添字0から1999までの2000個の変数になります。

32ビットですが、実際に利用するのは下位16ビットだけで、上位16ビットは ULPで保存したときの5ビット左シフトしたプログラムカウンター(PC)が設定されています。

また、ULPプログラムのsizeが11の場合には、添字0から10までプログラムが収納されていますので、自由に使えるのは添字11から1999までになります。sizeをみて連番を割り振るか、1999から逆順に使っていくなどしないとプログラムを壊すので注意が必要です。

まとめ

やっぱり情報が極端に少ないです。スローメモリも内容をダンプしながら確認して、やっと理解ができました。アセンブラで記述するときには.globalのラベルを宣言しておいて、C言語からそのラベル名でアクセスできるのですが、マクロの場合には自分で管理しないと駄目みたいですね。

ESP32でキューを使う

古くなっている可能性があるので、最新情報はM5StickC非公式日本語リファレンスで確認してください。

キューとは

処理が必要なデータの集まりで、主に別タスクにデータ処理を依頼する場合に利用します。通常はキューの最後に追加するFIFOですが、先頭に割り込んで追加することもできます。

キューの種類

  • 通常のキュー
  • メールボックス(最後のアイテムのみ保持)

1つのメッセージしか保存しないメールボックスと呼ばれるキューがあるようです。最後の情報を表示するなどの場合はこれを使うのかな?

通常は複数のメッセージが保存できるキューを使います。FreeRTOSのキューはメッセージを送信すると、送信したデータをコピーしてキューに保存しますので送信元でそのデータを書き換えたりしても安全に利用することができます。

サンプルスケッチ

// キューの大きさ
#define QUEUE_LENGTH 4

// 通常のキュー
QueueHandle_t xQueue;

// Mailbox用のキュー
QueueHandle_t xQueueMailbox;

void setup() {
  // キューアイテム
  uint8_t data = 1;

  // デバッグ出力準備
  Serial.begin(115200);
  Serial.println("Queue Test");

  // キュー作成
  xQueue = xQueueCreate( QUEUE_LENGTH, sizeof( uint8_t ) );

  // メールボックス用途は長さ1のみ
  xQueueMailbox = xQueueCreate( 1, sizeof( uint8_t ) );

  // 最後に追加(1)
  xQueueSend(xQueue, &amp;data, 0);
  data++;

  // 先頭に追加(2)
  xQueueSendToFront(xQueue, &amp;data, 0);
  data++;

  // 最後に追加(3)
  xQueueSendToBack(xQueue, &amp;data, 0);
  data++;

  // 上書き送信(4)
  xQueueOverwrite(xQueueMailbox, &amp;data);
  data++;

  // 上書き送信(5)
  xQueueOverwrite(xQueueMailbox, &amp;data);
  data++;

  // キューの数確認
  Serial.printf( "uxQueueMessagesWaiting = %d\n", uxQueueMessagesWaiting(xQueue) );

  // キューの追加可能数確認
  Serial.printf( "uxQueueSpacesAvailable = %d\n", uxQueueSpacesAvailable(xQueue) );

  // キューの状態取得
  Serial.printf( "xQueueIsQueueEmptyFromISR = %d\n", xQueueIsQueueEmptyFromISR(xQueue) );
  Serial.printf( "xQueueIsQueueFullFromISR = %d\n", xQueueIsQueueFullFromISR(xQueue) );
  Serial.printf( "uxQueueMessagesWaitingFromISR = %d\n", uxQueueMessagesWaitingFromISR(xQueue) );

  // Peek受信(何度受信しても同じ値)
  Serial.println( "Peek Test" );
  xQueuePeek( xQueue, &amp;data, 0 );
  Serial.println( data );
  xQueuePeek( xQueue, &amp;data, 0 );
  Serial.println( data );

  // 通常受信(213の順で受信し、受信成功した場合のみアイテムが更新される)
  int ret;
  Serial.println( "Receive Test" );
  ret = xQueueReceive( xQueue, &amp;data, 0 );
  Serial.printf( "ret = %d, data = %d\n", ret, data );
  ret = xQueueReceive( xQueue, &amp;data, 0 );
  Serial.printf( "ret = %d, data = %d\n", ret, data );
  ret = xQueueReceive( xQueue, &amp;data, 0 );
  Serial.printf( "ret = %d, data = %d\n", ret, data );
  ret = xQueueReceive( xQueue, &amp;data, 0 );
  Serial.printf( "ret = %d, data = %d\n", ret, data );
  ret = xQueueReceive( xQueue, &amp;data, 0 );
  Serial.printf( "ret = %d, data = %d\n", ret, data );

  // Mailbox受信(最後に送信したデータのみ受信)
  Serial.println( "Mailbox Test" );
  xQueuePeek( xQueueMailbox, &amp;data, 0 );
  Serial.println( data );

  // キュークリア
  Serial.println( "xQueueReset Test" );
  xQueueReset(xQueue);
  xQueueReset(xQueueMailbox);

  // キューセット
  Serial.println( "QueueSet Test" );
  QueueSetHandle_t xQueueSet;
  xQueueSet = xQueueCreateSet( QUEUE_LENGTH + 1 ); // Setするキューの合計
  xQueueAddToSet( xQueue, xQueueSet );
  xQueueAddToSet( xQueueMailbox, xQueueSet );

  // アイテム追加
  data = 100;
  xQueueSend(xQueue, &amp;data, 0);
  data++;
  xQueueSend(xQueue, &amp;data, 0);
  data++;
  xQueueSend(xQueueMailbox, &amp;data, 0);

  // 取得してみる
  while(1){
    // 取得可能なキューを取得する
    QueueHandle_t queue = xQueueSelectFromSet( xQueueSet, 0 );
    if( queue == NULL ){
      // キューがなくなったので終了
      break;
    }

    // キューの種類を調べる
    if( queue == xQueue ){
      Serial.print( "xQueue " );
    } else if( queue == xQueueMailbox ) {
      Serial.print( "xQueueMailbox " );
    } else {
      Serial.print( "? " );
    }
    
    // 受信
    ret = xQueueReceive( queue, &amp;data, 0 );
    Serial.printf( "ret = %d, data = %d\n", ret, data );
  }

  // キュー削除
  vQueueDelete(xQueue);
  vQueueDelete(xQueueMailbox);
}

void loop() {
}

実行結果

Queue Test
uxQueueMessagesWaiting = 3
uxQueueSpacesAvailable = 1
xQueueIsQueueEmptyFromISR = 0
xQueueIsQueueFullFromISR = 0
uxQueueMessagesWaitingFromISR = 3
Peek Test
2
2
Receive Test
ret = 1, data = 2
ret = 1, data = 1
ret = 1, data = 3
ret = 0, data = 3
ret = 0, data = 3
Mailbox Test
5
xQueueReset Test
QueueSet Test
xQueue ret = 1, data = 100
xQueue ret = 1, data = 101
xQueueMailbox ret = 1, data = 102

解説

いろいろごちゃごちゃ処理を書いてありますが、xQueueCreate()でキューを作成して、xQueueSend()で送信して、xQueueReceive()で受信できます。キューの先頭に割り込ませたい場合にはxQueueSendToFront()で送信します。

割り込みの内部から呼び出した場合にはFromISRが最後についている関数群があるのでそちらを利用します。

QueueSetは複数のタスクからキューにアクセスするときの機能なので、Arduinoでのユースケースは思いつかないです。

まとめ

おそらくは受信は通知で、送信はキューを使ってメッセージ処理をするのが王道のはずです。

あと標準のFreeRTOSと、ESP32で使えるようにしたESP-IDF用FreeRTOSと、それをArduinoで使えるようにした Arduino用FreeRTOSがあり、3つとも使える機能がちょっと違うのでわかりにくいです。

ドキュメントはESP-IDF用FreeRTOSが一番無難なんですが、Arduino版で使えない関数などがあるので、Arduino IDEで実際にコンパイルしてリファレンスは書いています。概要は日本語で公開されているFreeRTOSのオフィシャルが一番充実していますがArduinoでは使えない関数とか、ESP-IDFで拡張した機能とかがわからないです。

ESP32で別タスクに通知を送信する

最新情報はM5StickC非公式日本語リファレンスを参考にしてください。

タスク周りを調べた結果のアウトプットです。別タスクへはキューと通知がありますが、データを渡さない場合には、通知が簡単に利用できそうです。

概要

別タスクへのデータ受け渡しは、通知とキューが利用できます。通知は通信などの受信タスクに受信を通知するなどで内部利用されています。渡せるデータは少ないので実際にデータを受け渡すのではなく、その通知をトリガーに処理を再開させるなどの用途に使います。

送信タスクなどに送信データを受け渡す場合には、キューを利用したほうが適しています。

サンプルスケッチ

TaskHandle_t taskHandle;

void testTask(void *pvParameters) {
  uint32_t ulNotifiedValue;
  while (1) {
    // 通知が来るまで待機する。値のクリアはしない
    xTaskNotifyWait( 0,
                     0,
                     &amp;ulNotifiedValue,
                     portMAX_DELAY );
    Serial.println( pcTaskGetTaskName(NULL) );
    Serial.println( ulNotifiedValue );
  }
}

void setup() {
  Serial.begin(115200);

  // Core0でタスク起動
  xTaskCreatePinnedToCore(
    testTask,
    "loopTask1",
    8192,
    NULL,
    1,
    &amp;taskHandle,
    0
  );
}

void loop() {
  delay(500);

  // 通知送信(値は0から増えていき、100の値は無視される)
  xTaskNotify(taskHandle, 100, eIncrement );
}

xTaskNotifyWait()で通知を受信します。portMAX_DELAYを指定することで、通知が来るまでタイムアウト無しで待機します。実際にはタイムアウト処理をいれることが推奨されています。

上記のように数値を送信することができますが、何個の通知が溜まっているかぐらいの確認しかできませんので、数値に依存しない実装のほうが好ましいと思います。

数値を使わないのであればulTaskNotifyTake()の方がスッキリします。

    // 通知が来るまで待機する。値のクリアはしないので通知が受け取れている限り1のまま
    int ulNotifiedValue = ulTaskNotifyTake( pdFALSE, portMAX_DELAY );

まとめ

受信待ちや、外部からのトリガー入力を待っている場合には通知は有効だと思います。反面送信待ちなどについてはキューを利用したほうが楽に実装が可能です。

ESP32のマルチタスクとCPU利用率を調べる

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

タスク周りを調べていて、気になったので調査してみました。

タスクの仕組み

ESP32ではオープンソースのリアルタイムOSであるFreeRTOSが動いています。FreeRTOSはTickと呼ばれる単位で動いていて、環境によって違いますが通常1Tickが1msになっています。

Tick単位でタスクに割り当てるCPUリソースを分配しているので、CPU利用率を調べる場合には、単位時間あたりのTick数と、タスクで利用したTick数もしくは、利用されていなかったTick数がわかれば計算できます。

ESP32のCPUコア

ESP32はCPUコアがCore0とCore1の2つあり、Arduinoを実行した場合にはsetup()とloop()はCore1で動いています。そして無線関係の処理はCore0で動いているようです。

loop()とは非同期で動かしたい場合や、高負荷の処理を実行したい場合には別タスクで起動することで実現できます。

ウォッチドッグ

ハングアップ監視用にウォッチドッグという機能があります。CPUが利用していないときに定期的に更新するカウンターがあり、一定時間以上更新されない場合にはハングアップしたとみなして、再起動する仕組みです。

ArduinoではCore0はウォッチドッグが有効化されており、Core1は無効化されています。そのためloop()内部で無限ループを実行してもリセットされませんが、Core0に無限ループをするとウォッチドッグによりリセットされます。

ウォッチドッグの回避方法

ウォッチドッグを回避するためにはカウンターを更新するか、ウォッチドッグを停止させるかです。

void loopTest(void *pvParameters) {
  while (1) {
    Serial.print( xPortGetCoreID() ); // 動作確認用出力
    vPortYield();                     // vPortYield()ではウォッチドッグに影響しない
    yield();                          // yield()ではウォッチドッグに影響しない
    delay(0);                         // 1以上にするとウォッチドッグのリセットがなくなる
    delayMicroseconds(1000000);       // delayMicroseconds()はウォッチドッグとは関係ない
    millis();                         // millis()もウォッチドッグとは関係ない
  }
}
 
void setup() {
  Serial.begin(115200);
 
  // Core0でタスク起動
  xTaskCreateUniversal(
    loopTest,
    "loopTest",
    8192,
    NULL,
    1,
    NULL,
    0
  );

  // ウォッチドッグ停止
  //disableCore0WDT();
}
 
void loop() {
}

上記がサンプルコードですが、このまま実行するとウォッチドッグでリセットがかかると思います。回避するためにはdelay(0)の数字を1ms以上にするか、disableCore0WDT()でウォッチドッグを停止しましょう。ただ1msにすると他のタスクがそのCPUコアで実行する余裕がないので、可能な範囲でなるべく大きい数字を入れたほうがいいと思います。

ウォッチドッグ自体は長時間かかる処理をするときに、途中にdelay(1)を追加できない処理をする場合にのみ一時的に無効にして、その処理が終わった有効に戻すのが推奨の動作だと思います。

delay()以外にウォッチドッグを回避できる関数はvTaskDelay()やvTaskDelayUntil()がありますが、こちらはdelay()の内部で使われている関数であり、実時間ではなくTick数を指定するので、通常はdelay()でよいと思います。

CPU利用率の計算

2つのCPUコアにタスクを分散しても、どれぐらい余裕が残っているのかがわからないと思いますので、CPU利用率を計算してみたいと思います。

#include "esp_freertos_hooks.h"

TaskHandle_t taskHandle[2];

long core0IdleCount = 0;
long core0TickCount = 0;
long core1IdleCount = 0;
long core1TickCount = 0;

void testTask(void *pvParameters) {
  while (1) {
    // 1-500msのCPU時間を消費
    delayMicroseconds( random( 1, 500000 ) );

    // 1-500msの待機
    delay( random( 1, 500 ) );
  }
}

// Idle時にカウントアップ
bool core0IdleHook(void) {
  core0IdleCount++;
  return true;
}

// Tick数カウントアップ
void core0TickHook(void) {
  core0TickCount++;
}

// Idle時にカウントアップ
bool core1IdleHook(void) {
  core1IdleCount++;
  return true;
}

// Tick数カウントアップ
void core1TickHook(void) {
  core1TickCount++;
}

void setup() {
  Serial.begin(115200);

  // ハック関数を登録
  esp_register_freertos_idle_hook_for_cpu(&amp;core0IdleHook, 0);
  esp_register_freertos_tick_hook_for_cpu(&amp;core0TickHook, 0);
  esp_register_freertos_idle_hook_for_cpu(&amp;core1IdleHook, 1);
  esp_register_freertos_tick_hook_for_cpu(&amp;core1TickHook, 1);

  // Core0でタスク起動
  xTaskCreateUniversal(
    testTask,
    "testTask1",
    8192,
    NULL,
    1,
    &amp;taskHandle[0],
    0
  );

  // core1でタスク起動
  xTaskCreateUniversal(
    testTask,
    "testTask2",
    8192,
    NULL,
    1,
    &amp;taskHandle[1],
    1
  );
}

void loop() {
  // カウント出力
  Serial.printf("===============================================================\n");
  Serial.printf("core0IdleCount  = %7ld, core0TickCount  = %7ld, Idle = %5.1f %%\n", core0IdleCount, core0TickCount, core0IdleCount * 100.0 / core0TickCount );
  Serial.printf("core1IdleCount  = %7ld, core1TickCount  = %7ld, Idle = %5.1f %%\n", core1IdleCount, core1TickCount, core1IdleCount * 100.0 / core1TickCount );

  // カウント初期化
  core0IdleCount = 0;
  core0TickCount = 0;
  core1IdleCount = 0;
  core1TickCount = 0;

  // 2秒に一度表示する
  delay(2000);
}

ちょっと長いのですが、esp_freertos_hooks.hを読み込んでFreeRTOSのHook系の関数を使えるようにします。

Tick毎に呼ばれるコールバック関数と、他のタスクが動いていない場合に実行されるIdleタスクから呼ばれるコールバック関数を登録して、カウントをしています。

タスクはCPUコア別に2つ動かして、50%ぐらいのCPU利用をするような処理を書いてあります。

実行すると、loop()でdelay(2000)しているので約2秒ごとにCPU利用率が表示されます。1Tickは1msなのでTickCountは2000で、IdleCountはだいたい1000前後の数字になっています。

ただし無線を使うと。。。

#include "esp_freertos_hooks.h"

#include <BluetoothSerial.h>

BluetoothSerial SerialBT;

long core0IdleCount = 0;
long core1IdleCount = 0;

// Idle時にカウントアップ
bool core0IdleHook(void) {
  core0IdleCount++;
  return true;
}

// Idle時にカウントアップ
bool core1IdleHook(void) {
  core1IdleCount++;
  return true;
}

void setup() {
  Serial.begin(115200);

  // ハック関数を登録
  esp_register_freertos_idle_hook_for_cpu(&amp;core0IdleHook, 0);
  esp_register_freertos_idle_hook_for_cpu(&amp;core1IdleHook, 1);

  SerialBT.begin("ESP32");
}

int lastDisp = 0;

void loop() {
  if( lastDisp + 1000 < millis() ){
    lastDisp = millis();
    
    // カウント出力
    Serial.printf("core0IdleCount = %7ld, core1IdleCount = %7ld\n", core0IdleCount, core1IdleCount );
  
    // カウント初期化
    core0IdleCount = 0;
    core1IdleCount = 0;
  }

  SerialBT.println(millis());

  delay(10);
}

ここからが本題で、無線を使った場合にどれぐらいのCPU利用率かを調べたかったのですが、結論からいうと調べることができませんでした。

まず、無線を利用しながらTickに対するコールバック関数を登録するとハングアップします。Idleのみであれば動いたので、上記のコードはIdleのみに減らしたものです。1秒に一度CPU利用率を表示して、10msごとにBluetoothSerialで文字列を送信するサンプルです。

そして、実際に動かしてみてBluetoothSerialを接続したところ、無線系の処理が動いているCore0のIdle数が3000を超えます!

Tick数的には1秒なので1000以下のはずですが、なぜか増えます。。。おそらく無線系の処理の中でウォッチドッグでリセットされるのを防ぐために、Idleタスクを定期的に呼び出す処理があり、そのためカウンターが上がっているように思えます。

まとめ

CPU利用率は無線を使うと難しそうでした。コンパイルオプションとかを変えることで統計情報とかを利用したvTaskGetRunTimeStats()とかを使えるのですが、標準では使えないようです。

タスク周りは通知周りはリファレンスように検証しましたが、キューとかイベントとかまだ調べないといけないことがたくさんありそうです。

ESP32のesp32-hal.h周りのマルチタスクを調べる

M5StickC非公式日本語リファレンスで項目を書くためにesp32-halの項目を調べました。

サンプルコード

void loopTest(void *pvParameters) {
  while (1) {
    Serial.print( xPortGetCoreID() ); // 動作確認用出力
    vPortYield();                     // vPortYield()ではウォッチドッグに影響しない
    yield();                          // yield()ではウォッチドッグに影響しない
    delay(0);                         // 1以上にするとウォッチドッグのリセットがなくなる
    delayMicroseconds(1000000);       // delayMicroseconds()はウォッチドッグとは関係ない
  }
}

void setup() {
  Serial.begin(115200);

  // Core0でタスク起動
  xTaskCreateUniversal(
    loopTest,
    "loopTest0",
    8192,
    NULL,
    1,
    NULL,
    0
  );

  // Core1でタスク起動
  xTaskCreateUniversal(
    loopTest,
    "loopTest1",
    8192,
    NULL,
    1,
    NULL,
    1
  );

  // ウォッチドッグ停止
  //disableCore0WDT();
  //disableCore1WDT();  // 起動直後は有効化されていないのでエラーがでる

  // ウォッチドッグ起動
  //enableCore0WDT();
  //enableCore1WDT();
}

void loop() {
}

解説

他サイトのサンプルですとxTaskCreatePinnedToCore()かxTaskCreate()を使っているケースばかりでした。

ESP32の1.0.2からxTaskCreateUniversal()が増えたようでして、内部でxTaskCreatePinnedToCore()かxTaskCreate()を呼び出しています。

xTaskCreate()はシングルコア用の古い関数で、無効なcore番号を指定すると内部で呼び出されてCore0でタスクが起動していました。

通常はCore1でloop()などが実行されており、ウォッチドッグ は無効です。Core0はウォッチドッグが有効化されており、長時間ブロックしているとハングアップしたとみなされ、リセットが入ります。

リセットを回避する方法はdisableCore0WDT()でウォッチドッグを無効化するか、処理の中で定期的にdelay(1)を呼び出すかが必要です。delay(0)とかyield()では回避できませんでした。

まとめ

リファレンスで関数群が大体モーラできたかと思っていましたが、xTaskとかvPort系の関数が抜けていました。どんどん対象が広がっていきます、、、