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, &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にアップされています。

コメント

  1. takanotume24 より:

    はじめまして。分かりやすい記事をありがとうございます。もしご存知でしたら教えていただきたいのですが、「EXT0はプルアップなどが可能で、1つのGPIOをHIGHかLOWのトリガーを指定して復帰できます。簡単な反面1つのGPIOでしか受け付けないのと、プルアップなどで電力を消費してしまいます。」とありますがEXT0をトリガーにしつつ、電力消費を減らす方法はあるのでしょうか?
    M5StickCで esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, HIGH); としたのですが、確かに一度も復帰しなくとも1日程度でバッテリーを使いきってしまいます。
    rtc_gpio_pulldown_en(GPIO_NUM_37);
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, HIGH);
    も試したのですが、EXT0をトリガーにしてDeepSleepした直後に復帰してしまいました。
    「ボタンを押したら復帰し、その後は次にボタンが押されるまでDeepSleep」を実現したいです。

    • takanotume24 より:

      すみません、書き間違いと検証不足の点が見つかりました。
      まず間違いはプルアップした時のトリガーは esp_sleep_enable_ext0_wakeup(GPIO_NUM_37, LOW);で設定しています。
      検証不足だったのは、プルアップをpinMode(GPIO_NUM_37, INPUT_PULLUP);で設定していたことです。rtc_gpio_pullup_en(GPIO_NUM_37)とした時のバッテリーの持ちも比較する必要があるかもしれません。

      • たなかまさゆき より:

        axp192も何かすればもう少し省電力いけるかもしれません
        プルアップだけだとそんなには電力使わないはずなので、もう少し省電力は追い込めるはずです、、、
        年末ぐらいまでには省電力をもう一度研究し直す予定です!

  2. たなかまさゆき より:

    バッテリーライフの調査はじめました!
    しかしながら、計測に時間がかかりすぎるので、なかなかすぐには結果がでないと思います。

    未検証の予測ですが、通信を使わないのであればsetCpuFrequencyMhz(10)でCPUクロックを落として

    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);

    上記みたいなオプションで電源OFF(プルアップがあるのでESP_PD_OPTION_OFFはできないはず)

    AXPは液晶の明るさ落とすだけでもかなり違いますが、スリープ系を使えばもう少しだけ落とせるはずです(が、いま壊れている、、、?)

    【以下参考ブログ】
    https://lang-ship.com/blog/?p=914
    https://lang-ship.com/blog/?p=921
    https://lang-ship.com/blog/?p=962
    https://lang-ship.com/blog/?p=1107