M5StickCでの省電力ノウハウ

この記事はM5Stack Advent Calendar 2019 25日目の記事です。

概要

M5StickCを省電力で動かすための情報です。Arduino Coreで検証していますが、どんな環境でも使えると思います。

計測環境

M5StickCの5V IN端子にマルチメーター経由で電源を接続して、マルチメーターの電流計で計測しています。

誤差が出やすい環境ですので、数値の絶対値はあまり信用しないでください。また、外部電源を接続していますので、バッテリーにも多少充電されています。

AXP192の値との差

測定値消費電流(mA)
マルチメーター53.1
M5.Axp.GetVinCurrent() 外部電源電流57.5
M5.Axp.GetBatCurrent() バッテリー電流55.0

マルチメーターの値が一番低かったです。マルチメーターとAXP192の外部電源電流で4.4mAも差がありました。

外部電源を外して、バッテリー駆動にした場合には55mAでしたが、実際のところどの数字が正しいのかはわかりません。AXP192の計測自体で消費電流が変わってしまうので、本文ではマルチメーターの数値を元にしています。

初期状態での消費電流割合

機能消費電流(mA)
ESP32(CPU MAX)43.0
画面(12)20.4
5V DCDC1.3
MIC0.1
RTC0.1
AXP192 ADC0.1
AXP192基本機能1.4
ベース1.7
合計68.1

ざっくりとした、M5StickCの最低限のスケッチの場合の消費電流の割合です。

#include <M5StickC.h>

void setup() {
  M5.begin();
}

void loop() {
}

一番支配的なのがCPUで、その後画面になります。

このデータは、徐々にパラメーターを変えながら機能を無効にしていった差分をまとめたものになります。

CPU消費電流の下げ方 その1 delay()

状態消費電流(mA)差分(mA)
CPU MAX68.10
delay(1)53.3-14.8

loop()の中身にdelay()がない場合には、無限ループでloop()が呼び出されている状態ですので、中身がなくても電力を使ってしまいます。

delay(1)を入れることで約15mAの消費電流が下がりました。また、delayの数値をもっと大きくしても、あまり消費電流は下がりませんでした。

ただし、処理が増えて来ると消費電流はあがりますので、絶対的に消費電流を下げる方法ではありません。

CPU消費電流の下げ方 その2 setCpuFrequencyMhz()

状態消費電流(mA)差分(mA)
ESP32 240MHz(無線利用可)28.20
ESP32 160MHz(無線利用可)19.5-8.7
ESP32 80MHz(無線利用可)15.4-12.8
ESP32 40MHz8.4-19.8
ESP32 20MHz6.4-21.8
ESP32 10MHz5.6-22.6
CPU 電源供給OFF0-28.2

delay(1)をした状態でsetCpuFrequencyMhz()でCPU周波数を変更した場合の差分です。

無線を利用する場合には80以上を指定する必要があるので注意してください。また、処理がない場合の消費電流なので、重い処理をすることでより消費電流が増えます。

CPU電源供給OFFのみAXP192経由でDCDC1への電源共有をOFFにしています。この他にスリープすることでもう少し消費電力を減らすことが可能だと思います。

画面消費電流の減らし方

状態消費電流(mA)差分(mA)
LCD 明るさ1220.10.0
LCD 明るさ1114.2-5.9
LCD 明るさ109.0-11.1
LCD 明るさ94.2-15.9
LCD 明るさ81.0-19.0
LCD 明るさ70.2-19.9
OFF0.0-20.1

M5.Axp.ScreenBreath()で画面の明るさを変更した場合の消費電流です。OFFはAXP192でLDO2への電源供給をOFFにしています。

OFFにしても、明るさ7でもほとんど変わりませんので、OFFにするのであればなんとか読めることができる7を使ってもいいと思います。

ただし、8か9ぐらいの明るさがないと文字を読むのは難しい気がします。

5V OUT用DCDC(EXTEN端子)

ESP32のスリープを検証しているときに、思ったより駆動時間が伸びないので、なにか消費電流を使っているのだと思っていたのですが、5V OUT用のDCDCが1.3mA以上使っていました。測定する状況によって違うのですが、2mAぐらい計測された場合もあります。

M5StickCは80mhAのバッテリーを搭載しているので、1.3mAだとしても24時間で31.2mhAの電力を5V OUTのDCDCで使っていることになります。

利用しないのであればOFFにしておくことをおすすめします。OFFにするとバッテリー電圧がそのまま出力される状態になります。

OFFにするやり方は後ろにある検証用スケッチを参考にしてもらいたいですが、次バージョンのSDKで初期化時にOFFにするパラメーターが追加されています。

その他

AXP192基本機能の1.4mAは電源OFFにしないと減らすことはできません。その他の機能は0.1mA単位なので、減らしてもいいですが、それほど効果はないようです。

検証用スケッチ

#include <M5StickC.h>

int delayTime = 1;

void setup() {
  M5.begin();
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.printf("Current Test\n");
}

void loop() {
  while (Serial.available()) {
    String command = Serial.readString();
    command.trim();
    if (command == "") {
      // Skip
    } else if (command == "0") {
      delayTime = 0;
      Serial.print("Command : delayTime = ");
      Serial.println(delayTime);
    } else if (command == "1") {
      delayTime = 1;
      Serial.print("Command : delayTime = ");
      Serial.println(delayTime);
    } else if (command == "10") {
      delayTime = 10;
      Serial.print("Command : delayTime = ");
      Serial.println(delayTime);
    } else if (command == "100") {
      delayTime = 100;
      Serial.print("Command : delayTime = ");
      Serial.println(delayTime);
    } else if (command == "1000") {
      delayTime = 1000;
      Serial.print("Command : delayTime = ");
      Serial.println(delayTime);
    } else if (command == "5000") {
      delayTime = 5000;
      Serial.print("Command : delayTime = ");
      Serial.println(delayTime);
    } else if (command == "AXP") {
      Serial.println("Command : AXP");
      Serial.printf("AXP192 Status\n");
      Serial.printf("\n");

      Serial.printf("Battery\n");
      Serial.printf(" State:%6d\n"  , M5.Axp.GetBatState());      // バッテリーが接続されているか(常に1のはず)
      Serial.printf(" Warn :%6d\n"  , M5.Axp.GetWarningLevel());  // バッテリー残量警告 0:残あり, 1:残なし
      Serial.printf(" Temp :%6.1f\n", M5.Axp.GetTempInAXP192());  // AXP192の内部温度
      Serial.printf(" V(V) :%6.3f\n", M5.Axp.GetBatVoltage());    // バッテリー電圧(3.0V-4.2V程度)
      Serial.printf(" I(mA):%6.1f\n", M5.Axp.GetBatCurrent());    // バッテリー電流(プラスが充電、マイナスが放電)
      Serial.printf(" W(mW):%6.1f\n", M5.Axp.GetBatPower());      // バッテリー電力(W=V*abs(I))

      Serial.printf("ASP\n");
      Serial.printf(" V(V) :%6.3f\n", M5.Axp.GetAPSVoltage());    // ESP32に供給されている電圧

      Serial.printf("VBus(USB)\n");
      Serial.printf(" V(V) :%6.3f\n", M5.Axp.GetVBusVoltage());   // USB電源からの電圧
      Serial.printf(" I(mA):%6.1f\n", M5.Axp.GetVBusCurrent());   // USB電源からの電流

      Serial.printf("VIN(5V-In)\n");
      Serial.printf(" V(V) :%6.3f\n", M5.Axp.GetVinVoltage());    // 5V IN端子からの電圧
      Serial.printf(" I(mA):%6.1f\n", M5.Axp.GetVinCurrent());    // 5V IN端子からの電流
    } else if (command == "COUL_OFF") {
      Serial.println("Command : COUL_OFF");
      M5.Axp.DisableCoulombcounter();
    } else if (command == "COUL_ON") {
      Serial.println("Command : COUL_ON");
      M5.Axp.EnableCoulombcounter();
    } else if (command == "ADC_OFF") {
      Serial.println("Command : ADC_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x82);
      Wire1.write(0x00);
      Wire1.endTransmission();
      Wire1.beginTransmission(0x34);
      Wire1.write(0x83);
      Wire1.write(0x00);
      Wire1.endTransmission();
    } else if (command == "ADC_ON") {
      Serial.println("Command : ADC_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x82);
      Wire1.write(0xff);
      Wire1.endTransmission();
      Wire1.beginTransmission(0x34);
      Wire1.write(0x83);
      Wire1.write(0xff);
      Wire1.endTransmission();
    } else if (command == "EXTEN_10_OFF") {
      Serial.println("Command : EXTEN_10_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x10);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() & ~(1 << 2);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x10);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "EXTEN_10_ON") {
      Serial.println("Command : EXTEN_10_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x10);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 2);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x10);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "DCDC2_10_OFF") {
      Serial.println("Command : DCDC2_10_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x10);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 0);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x10);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "DCDC2_10_ON") {
      Serial.println("Command : DCDC2_10_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x10);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 0);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x10);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "EXTEN_OFF") {
      Serial.println("Command : EXTEN_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() & ~(1 << 6);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "EXTEN_ON") {
      Serial.println("Command : EXTEN_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 6);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "DCDC2_OFF") {
      Serial.println("Command : DCDC2_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() & ~(1 << 4);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "DCDC2_ON") {
      Serial.println("Command : DCDC2_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 4);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "LDO3_OFF") {
      Serial.println("Command : LDO3_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() & ~(1 << 3);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "LDO3_ON") {
      Serial.println("Command : LDO3_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 3);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "LDO2_OFF") {
      Serial.println("Command : LDO2_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() & ~(1 << 2);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "LDO2_ON") {
      Serial.println("Command : LDO2_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 2);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "DCDC3_OFF") {
      Serial.println("Command : DCDC3_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() & ~(1 << 1);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "DCDC3_ON") {
      Serial.println("Command : DCDC3_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 1);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "DCDC1_OFF") {
      Serial.println("Command : DCDC1_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() & ~(1 << 0);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "DCDC1_ON") {
      Serial.println("Command : DCDC1_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 0);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x12);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "RTC_OFF") {
      Serial.println("Command : RTC_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x35);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() & ~(1 << 7);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x35);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "RTC_ON") {
      Serial.println("Command : RTC_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x35);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 7);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x35);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "GPIO0_OFF") {
      Serial.println("Command : GPIO0_OFF");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x90);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() & ~(1 << 2);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x90);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "GPIO0_ON") {
      Serial.println("Command : GPIO0_ON");
      Wire1.beginTransmission(0x34);
      Wire1.write(0x90);
      Wire1.endTransmission();
      Wire1.requestFrom(0x34, 1);
      uint8_t state = Wire1.read() | (1 << 2);
      Wire1.beginTransmission(0x34);
      Wire1.write(0x90);
      Wire1.write(state);
      Wire1.endTransmission();
    } else if (command == "CPU_10") {
      Serial.println("Command : CPU_10");
      setCpuFrequencyMhz(10);
    } else if (command == "CPU_20") {
      Serial.println("Command : CPU_20");
      setCpuFrequencyMhz(20);
    } else if (command == "CPU_40") {
      Serial.println("Command : CPU_40");
      setCpuFrequencyMhz(40);
    } else if (command == "CPU_80") {
      Serial.println("Command : CPU_80");
      setCpuFrequencyMhz(80);
    } else if (command == "CPU_160") {
      Serial.println("Command : CPU_160");
      setCpuFrequencyMhz(160);
    } else if (command == "CPU_240") {
      Serial.println("Command : CPU_240");
      setCpuFrequencyMhz(240);
    } else if (command == "LCD_7") {
      Serial.println("Command : LCD_7");
      M5.Axp.ScreenBreath(7);
    } else if (command == "LCD_8") {
      Serial.println("Command : LCD_8");
      M5.Axp.ScreenBreath(8);
    } else if (command == "LCD_9") {
      Serial.println("Command : LCD_9");
      M5.Axp.ScreenBreath(9);
    } else if (command == "LCD_10") {
      Serial.println("Command : LCD_10");
      M5.Axp.ScreenBreath(10);
    } else if (command == "LCD_11") {
      Serial.println("Command : LCD_11");
      M5.Axp.ScreenBreath(11);
    } else if (command == "LCD_12") {
      Serial.println("Command : LCD_12");
      M5.Axp.ScreenBreath(12);
    } else if (command == "RESET") {
      Serial.println("Command : RESET");
      ESP.restart();
    } else {
      Serial.print("Command? : ");
      Serial.println(command);
    }
  }

  delay(delayTime);
}

シリアルモニタからコマンドを送信することで、状態が変わるスケッチです。

リアルタイム系無線利用時の省電力方針

常時無線通信を行う場合には、CPUクロックを80MHz以上にする必要があります。基本80で動かしたほうがよいと思います。

通信間隔は1秒でも0.1秒でも、それほど消費電流に差はないようです。1分ぐらいまで間隔をあけると消費電流は下がっていました。

無線方式には主にWi-FiとBluetooth、ESP-NOWがありますが、ESP-NOWが一番省電力です。反面受信側にもESP32が必要になるので、ノートパソコンなどで直接受信した場合にはBluetooth Serialの利用が手軽に使えます。

上記にリアルタイム系の通信継続時間を検証しているので、参考にしてみてください。

リアルタイム系無線利用時の推定動作時間の計算方法

状態差分(mA)
ESP-NOW 0.1秒間隔送信3.4
ESP-NOW 0.01秒間隔送信6.2

一番最初の表から基本となるプログラムの消費電流を計算し、上記の差分を足します。

上記のブログでは0.1秒間隔の場合56.6mAでした。

推定稼働時間 = 推定消費電流(mA) / バッテリー容量(80mhA) × 安全係数(0.8)

上記に当てはめると56.6 / 80 * 0.8で0.63時間(35.7分)みたいに計算できます。最後の安全係数はバッテリーからのDCDCロスなどです。

実測だと37分ぐらい動いていましたので、そこから安全係数を逆算しています。ただしバッテリーがヘタると、もっと悪化しますので0.7ぐらいで計算しておいたほうが安全かもしれません。

ただ無線系はデータが少ないのと、結構測定データによって誤差があるので、実測してみるしかないかもしれません。

スリープ系無線利用時の省電力方針

ESP32がディープスリープで消費電流がなくなったとしても、AXP192が動き続けていますので、長時間の稼働は難しいです。

Wi-Fiアクセスポイントに接続する場合には、CPU周波数を240MHzで高速に通信を終わらせて、すばやく無線をOFFにしたほうが全体の消費電流が下がるパターンもあるようです。

上記の検証では半日しか動かすことができませんでした。AXP192の利用していない機能をOFFにすることでおそらく24時間前後の動作をすることは可能だと思いますが、AXP192自体で1.4mA程度消費電流がありそうなので、そのへんが限界だと思います。

さらなる長時間稼働のために

外部バッテリーを使うのが一番だと思います。

何個か外部バッテリー系HATが販売されていますがServo HATなどは、バッテリーを拡張するものではなく、外部接続したServoへの電源供給を目的としてものなので、外部バッテリーとしては利用できません。

バッテリー系のHATを利用してください。この記事にために先月末にバッテリー系HATを2種類注文しましたが、まだ到着していませんのでまだ私も使ったことがありません。

内蔵バッテリーが80mhAに対して、1000mhA以上のバッテリーを利用できると思うので、10倍以上の稼働時間になると思われます。

まとめ

マルチメーターは秒単位での消費電流変化しか測定できません。無線系はもっと短い時間で消費電流が変化しているので、あまり正しい数値が測定できていません。

シャント抵抗とオシロスコープなどを利用すれば、もう少し短い時間軸で測定が可能ですので、今後実験をしたいと思っています。

不明点や、調べてほしい点があったらブロクへのコメントかTwitterへ連絡お願いします。

コメントする

メールアドレスが公開されることはありません。

管理者承認後にページに追加されます。公開されたくない相談はその旨本文に記載するかTwitterなどでDM投げてください。またスパム対策として、日本語が含まれない投稿は無視されますのでご注意ください。