この記事では,ESP32をバッテリー駆動するためのヒントをお届けします.

はじめに

ESP32とは?

ESP32とは,WiFi/BLEを搭載したIoTマイコンです.

CPUが240MHzのデュアルコアで,RAMも520kBと, マイコンにしてはスペックが高いところが魅力です.

シリアル通信などのペリフェラルも充実していて, UART, I2C, SPIなどのピンを好きなGPIOピンに割り当てることができて超便利です!!

また,公式のSDKにFreeRTOSというRTOSが組み込まれていて, 容易にマルチタスクのアプリケーションを動作させることができます.

ESP32についての記事一覧はこちら

電流消費ハンパないって!!

そんなESP32は,省エネ仕様を謳っています.

バッテリー駆動も普通にできるのかと思いきや, スリープなどを考えずに使用すると, CPUだけでも常時50mA程度消費し, WiFiと同時に起動すると100mAを超えるほどになります.

これでは,どんなに大きなバッテリーを用意しても数日しか持ちません.

そこで今回は,ESP32をバッテリー駆動させるため,Power Management 機能について紹介します.

Power Management

概要

ESP32 の Power Management は,ESP32公式でサポートされている機能です.

以下の3つを制御して,省エネを図ります.

  • CPUの周波数
  • APB (ペリフェラルのクロック) の周波数
  • Light-Sleep

実際には,Power Management 機能を有効化した状態で,

  // FreeRTOS の delay 関数
  // 1000 [ms]
  vTaskDelay(1000 / portTICK_PERIOD_MS);

などを実行すると,その間はCPUのクロックを止めてくれるというものです.

参考

動作環境は ESP-IDF

PowerManagementのLight-Sleepを使用するためには, make menuconfig を編集する必要があるので, ESP-IDF環境で実行する必要があります.

ESP-IDFの導入はこちら

Power Management 機能の使い方

make menuconfigの設定

Power Management における Light-Sleep を有効化するためには, make menuconfig

  • Component config/FreeRTOS/Tickless idle support

にチェックを入れます.

これで,FreeRTOSのdelay中にクロックを止めることができます.(FreeRTOSにそういう機能がちゃんとある)

Light-Sleepを使用しない場合,Arduino環境でも一応動作するようですが,240MHzが40MHzに落ちるだけなので,あまり効果がないかもしれません.

インクルードファイル

#include "esp_pm.h"

初期化

以下のコードを実行すると,PowerManagement機能がスタートします.

#include "esp_pm.h"

void pm_init() {
    esp_pm_config_esp32_t esp_pm_config_esp32;
    esp_pm_config_esp32.max_cpu_freq = RTC_CPU_FREQ_240M; //< make menuconfigで設定したCPU周波数
    esp_pm_config_esp32.min_cpu_freq = RTC_CPU_FREQ_XTAL;
    esp_pm_config_esp32.light_sleep_enable = true;
    esp_pm_configure(&esp_pm_config_esp32);
}

省エネ動作

例えば,以下のような関数を呼べば,その間スリープとなります.

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void sleep_ms(uint32_t ms) {
  vTaskDelay(ms / portTICK_PERIOD_MS);
}

スリープの禁止

シリアル通信をするときや,何かを駆動するときなど, delayはするけどCPUのクロックを変えたくない場合があります.

そんなときは,Power Management Locksを使います.

PowerManagementの各機能

  • CPUの周波数
  • APB (ペリフェラルのクロック) の周波数
  • Light-Sleep

に対して,ロックハンドルを生成し, ロックの獲得,解放を行います.

実行例は以下の通りです.

#include "esp_pm.h"

// CPU の周波数固定ハンドル
esp_pm_lock_handle_t lock_handle_cpu;
// APB の周波数固定ハンドル
esp_pm_lock_handle_t lock_handle_apb;
// Light-Sleepの禁止ハンドル
esp_pm_lock_handle_t lock_handle_light;

// ハンドルの初期化
void pm_lock_init() {
  // CPU の周波数固定ハンドル
    esp_pm_lock_create(ESP_PM_CPU_FREQ_MAX, 0, "cpu", &lock_handle_cpu);
  // APB の周波数固定ハンドル
    esp_pm_lock_create(ESP_PM_APB_FREQ_MAX, 0, "apb", &lock_handle_apb);
  // Light-Sleepの禁止ハンドル
    esp_pm_lock_create(ESP_PM_NO_LIGHT_SLEEP, 0, "light", &lock_handle_light);
}

// ロックの獲得
void acquire(esp_pm_lock_handle_t lock_handle) {
  esp_pm_lock_acquire(lock_handle);
}

// ロックの解放
void release(esp_pm_lock_handle_t lock_handle) {
    esp_pm_lock_release(out_handle);
}

// 実行例
void pm_lock_example(){
  pm_lock_init(); //< ロックハンドルを初期化

  acquire(lock_handle_apb); //< APB周波数を固定
  // シリアル通信などを行う
  release(lock_handle_apb); //< 解放
}

C++ ラッパ

C言語はわかりづらいので,C++のラッパを作ってみました.

Github上のコードが最新です.以下の説明は古い可能性があります.

PowerManagement クラス

直観的に使えるようになっています.

class PowerManagement {
public:
  static void init();
  static void printLockStatus();

  class Lock {
  public:
    Lock(esp_pm_lock_type_t lock_type, const char *name);
    void acquire();
    void release();

  private:
    esp_pm_lock_handle_t lock_handle;
  };
};

実行例

#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "PowerManagement.h"

extern "C" void app_main() {
  // PowerManagement機能の有効化
  PowerManagement::init();
  // ロックハンドルの生成
  PowerManagement::Lock cpu_clock_lock(ESP_PM_CPU_FREQ_MAX, "my cpu lock");
  PowerManagement::Lock apb_clock_lock(ESP_PM_APB_FREQ_MAX, "my apb lock");
  PowerManagement::Lock light_sleep_lock(ESP_PM_NO_LIGHT_SLEEP, "my light-sleep lock");

  cpu_clock_lock.acquire(); //< ロック
  // write tasks under normal CPU clock frequency
  cpu_clock_lock.release(); //< 解放

  vTaskDelay(3000 / portTICK_PERIOD_MS); //< Sleep

  apb_clock_lock.acquire(); //< ロック
  // write tasks under normal ABP clock frequency
  apb_clock_lock.release(); //< 解放

  vTaskDelay(3000 / portTICK_PERIOD_MS); //< Sleep

  light_sleep_lock.acquire(); //< ロック
  // write tasks under no light-sleep
  light_sleep_lock.release(); //< 解放

  vTaskDelay(portMAX_DELAY);  //< eternal sleep
}

サンプルプロジェクト

上記のサンプルコードにログ出力のコードを追加したものです.

一応,そのまま動くはずです.

実行例

  unzip PowerManagement.zip
  cd PowerManagement
  make menuconfig # シリアルポートの設定
  make flash monitor

注意

Light-Sleepの注意

Light-Sleepの間は,普通のGPIOはオープン状態になります.

例えば,LEDを繋いでいた場合は,消えてしまいます.

代わりに,RTC_GPIOを使うことで,Light-Sleep中もキープさせることができます.

例えば,Light-Sleep中でもLEDを光らせたければ,以下のようにします.

#include "driver/rtc_io.h"

void rtc_gpio_test() {
  // rtc_gpio init
  gpio_num_t pin_led = GPIO_NUM_27; //< LEDをつけたピン
  rtc_gpio_init(pin_led); //< 初期化
  rtc_gpio_set_direction(pin_led, RTC_GPIO_MODE_OUTPUT_ONLY); //< 出力に設定
  
  // Light-Sleep中でも持続するLED
  rtc_gpio_set_level(pin_led, 1); //< LED点灯
  rtc_gpio_hold_en(pin_led); //< 持続を有効化
  vTaskDelay(1000 / portTICK_PERIOD_MS); //< Light-Sleep
  rtc_gpio_hold_dis(pin_led); //< 持続を無効化
}

詳しくは,公式リファレンス - GPIO をご覧ください.

まとめ

今回の内容でESP32のバッテリー駆動への足掛かりになりました.

さらに,Modem-Sleepという,Wi-Fiの省エネを図るスリープもあるので,後日紹介したいと思います.

では,みなさんも良きESP32ライフを!

参考