M5StickCのバッテリー拡張HATをためす

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

概要

日本未発売のHATでバッテリーを拡張するものが発売されましたので、ためしてみました。

18650C HAT

細長い方のHATです。中身に18650バッテリーを搭載しています。

付属品は、バッテリーの蓋をあけるための六角レンチと、M5stickC本体を固定するためのM2ネジがついています。

蓋をあけてみるとこんな感じです。

同梱されていたバッテリーは2200mAhと記載されていましたが、カラにしてから充電したところちゃんと容量がありました。

18650バッテリーは書いてある容量通りのことが少ないので、個人的に買えるものはたいてい書いてあるより容量が少ないです。

おそらく、検品で容量が少ないのがAIiExpresとかAmazonで小売されているんだと思います。。。

内部構造

このHATは基板に部品がありません!

バッテリーの+端子をM5stickCのBAT端子に直結してあります。底にUSB端子もありますが、USBの5VをM5stickCの5V INに直結してあります。

ぱっとみたところ、ダイオードとかもなかったのでバッテリーのプラス・マイナスを間違ってつけると危ないかもしれません。

外部バッテリーの充電もM5stickCを充電して、M5stickCのバッテリーから外部バッテリーを充電する方向になります。この充電方法はM5stickCのバッテリーに負荷がかかるので、寿命が短くなる懸念があります。

PowerC HAT

16340バッテリーを2本搭載したほうのHATです。

付属品はありません。M5StickCへの給電の他にUSB-A端子があり、モバイルバッテリーとしても利用できます。ただし、低電流の場合には自動的に電源OFFになる、普通のモバイルバッテリー設定でした。

モバイルバッテリー用には電源ボタンがあり、1度押すと電源ONで、2度押すと電源OFFになります。M5StickCに給電する場合には電源関係なく給電しているので、モバイルバッテリー用に独立した回路のようでした。

付属しているバッテリーは750mAhと記載されており、実際に充電したら800mAh以上ありました。このバッテリーは内部に保護回路があるような動きですが、詳しくは追いかけていません。

内部構造

モバイルバッテリーとM5stickCへの給電部分は分離されているようで、M5stickCへはバッテリーの+端子とM5stickCのBAT端子に直結してあります。そしてなぜか0, 26, 36の端子にも3Vが出力されていました。

この3Vが気になるのですが、検証はできていません。GPIO26をアナログ入力に使うとおかしな値になりそうな気がします。

充電は充電専用USB端子がありますので、そこから充電できます。モバイルバッテリー用の回路があるので、細かい制御はちゃんとできているように思えます。

まとめ

見た目の完成度は18650Cが高いですね。長時間M5stickCを使う場合は接続すれば完成です。反面電池交換に工具が必要なので、定期的に交換しないといけないようとだと本体ごと交換の方がいいかもしれません。そして充電がM5stickC本体経由なのが気になります。

PowerCはケースが無いのですが、組み込みとかで動かすのには適していそうな気がします。電池交換がしやすいですからね。とはいえ、16340は国内だとちゃんとしたものが手に入らないと思いますので注意が必要です。

両方ともですが、バッテリーの充電は充電機を使うのをおすすめします。そっちのほうが電池に適した充電をしてくれるのと、バッテリー容量がわかるので管理がしやすいです。

ただ、この構造だったら自分で直結しても変わらないような気が、、、

M5Stackの新製品 ATOM MatrixとATOM Liteが販売開始

概要

昨日販売開始されたATOM MatrixとATOM LiteをM5StickCを含めて、仕様の差を確認しました。

マトリクス

商品ATOM MatrixATOM LiteM5StickC
USBType-CType-CType-C
チップESP32-PICO-D4ESP32-PICO-D4ESP32-PICO-D4
フラッシュ4MByte4MByte4MByte
LCDXX160×80(0.96 inch)
NeoPixel LED25(5×5)1X
赤色LEDXX1
赤外線LED111
ボタン112
IMUMPU6886(6軸)XMPU6886(6軸)
マイクXXSPM1423(I2S)
GPIO6(Dupont Pins)6(Dupont Pins)3(Dupont Pins)
GROVE互換端子111
バッテリーXX80mAh
サイズ24 * 24 * 14mm24 * 24 * 10mm48.2 * 25.5 * 13.7mm
重さ1g1g15.1g
付属ケーブルXXType-A to Type-Cケーブル
マウントアクセサリXXWearable & Wall

全体的にはベースが同じなのでM5StickCと基本同じですが、LCDのかわりにNeoPixel LEDを搭載しているのがATOMです。マイクとバッテリーも搭載されていないので注意してください。

付属ケーブルもついていないので、M5Stackの製品をこれまで持っていない人はハマりがちです。Macユーザーで、Type-C to Type-Cのケーブルなどを使うと通信できない可能性があります。M5StickCから設計を見直してくれてType-Cとしてちゃんと動くのであれば問題ないのですが、、、

MatrixとLiteの差としては、NeoPixel LEDの数ですが、LiteにはIMUがのっていないので注意してください。厚みもちょっと違いますね。ただ、本当に重さは1gなんだろうか? 絶対に間違っている気がします。

IMUも本文には搭載しているような、搭載していないような書き方なので、中の人にTwitterで聞いたら無いってことだったので、記載ミスはわりかしよく起こりますので注意してください。

技適は?

現在まだ未取得です。申請はするみたいですので、1月中には技適取得版の販売が開始されそうです。

国内販売は?

まだ取り扱いはないですが、スイッチサイエンスさんで取り扱う予定とのことがTwitterの情報でありました。

個人的な予想価格としてATOM Matrixが1,980円で、ATOM Liteが1,480円ぐらいを超えない金額になると思います。

もう数百円値段が下がると嬉しいな。もしくはUSBケーブル追加でつけるとか、、、

まとめ

技適もまだ取れていないので、国内販売が開始されるぐらいに実際に利用してみたいと思っています。

ATOM Liteはバッテリーが必要ない環境で、ちょっとESP32を使いたいって場合に最適かもしれません。外付けバッテリーも計画されているし、GPIOもM5StickCより多いので組み込みにはよさそうですね。

M5StickCのCardKB HATをためす

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

概要

M5StickCにキーボードを拡張できるCardKB HATを購入したので、紹介したいと思います。I2C接続のキーボードで入力ができますが、細かい処理は自分で作る必要がありました。

スケッチ

#include <M5StickC.h>
#include <Wire.h>

#define CARDKB_ADDR 0x5F

void setup()
{
  M5.begin();
  Serial.begin(115200);
  Wire.begin(0, 26);
  pinMode(5, INPUT);
  digitalWrite(5, HIGH);
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setRotation(3);
  M5.Lcd.setCursor(1, 10);
  M5.Lcd.setTextColor(YELLOW);
  M5.Lcd.setTextSize(2);
  M5.Lcd.printf("IIC Address: 0x5F\n");
  M5.Lcd.printf(">>");
}
void loop()
{
  Wire.requestFrom(CARDKB_ADDR, 1);
  while (Wire.available())
  {
    char c = Wire.read(); // receive a byte as characterif
    if (c != 0)
    {
      if ( c == 0x0d ) {
        M5.Lcd.setCursor(1, 20);
        Serial.println("RET");
      } else {
        M5.Lcd.printf("%c", c);
        Serial.println(c, HEX);
      }
      // M5.Speaker.beep();
    }
  }
  // delay(10);
}

今現在HATのスケッチ例が存在しません。UNIT版のCardKBスケッチを改造して動かす必要があります。

Wire.begin(0, 26);

の行でHAT側の端子0と26を指定して動かします。

固定方法

裏側にネジ穴が空いているので、M5StickCにネジ穴がある新型の場合固定することができます。

固定しなくても使えますが、パカパカしちゃうのでなるべく両面テープとか、ネジで固定したほうがいいと思います。

本体のネジ穴の深さが4ミリ、基板の厚さが約1ミリとしてM2の5ミリネジを利用して固定しています。ただしHATにはネジが付属しないので、小さめのサイズのネジセットはあらかじめ用意したほうがいいかもしれません。

まとめ

キーボードの処理周りを自分で制御するのはちょっと大変なので、すぐに使いこなすのは難しいと思います。

ローマ字変換して、Web APIとかで変換するとかかな、、、

このへんとかでがんばるのがいいのかな、、、

Servoテスターを使ってみた

概要

サーボモーターのテスターがあってので、なんとなく購入してみました。昨日の記事で書こうと思っていて忘れたので、別記事です。。。

購入物

上記のものをAliexpressで購入して使ってみました。ServoモーターはMiniなのかMicroなのかよくわからないですが、単品で購入できるなかでは一番安かった気がします。

よくあるSG90互換Servoですね。ちゃんとした用途には本物のSG90を利用しましょう!

接続方法

片側にGND、電源、シグナルの3ピンがでていて、反対側に3チャンネルのServo端子がついています。

ちょうどM5stickCの端子がその並びなので、そのまま差し込んでみました。

Servoモーターは茶色がGNDって覚えて接続してください。とりあえず複数のServoモーターを購入したので、動作検証に使いました。

モード

Servoテスターには3つのモードがあります。ボタンを押すことでモードが切り替わります。

マニュアルモード

ノブをまわすとServoも動きます。Servoが動く範囲などを確認するときに便利だと思います。

ニュートラルモード

たぶん入力のシグナルをそのままServoに出力するモードかな?

未使用なのでよくわかりません。

オートモード

自動でServoが上限と下限まで動かします。Servoによっては限界値を超える設定が入って負荷になるかも?

まとめ

必要かと言われると、ちょっと微妙ですが複数のServoを購入してとりあえず動作確認したい場合には便利かな?

まあ1ドルぐらいなので、持っていてもいいかなー。

このServoモーターは安いですが、本物に比べるとトルクが低いので負荷がかかる場所に使うと問題が起こったり、すぐに壊れたりすると思います。

メーター的な負荷のかからない使い方とかで、試験的に使うんだったらいいのかな?

M5StickCのServoHATをためす

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

概要

ServoHATを購入したのでためしてみました。サンプルスケッチを動かしただけです。

ServoHATとは?

M5StickCに接続して使えるHATで、合計8個までのServoが接続できます。Servoを動かすための電源として、16340バッテリーを使っています。

またHATにはSTM32F030F4が搭載されており、M5StickCとI2C通信を行いServoと、HATに搭載されているLEDを制御しています。

M5StickCだけだと8チャンネルの信号と制御できないので、I2C接続のI・Oエキスパンダー的な動きをしています。

接続例

こんな感じになります。Servoはこの検証用だけのために購入した激安Servoです。8個で千円しませんでした。おそらく負荷をかけるような動作には使っちゃいけないやつだと思います。

このServoの到着が遅れたので、HATを買ってから結構時間がたってしまいました。一つだけ持っていたServoを接続して動作試験をしていたのですが、このServoが届いたころには、Servo HATが壊れました、、、

おそらく電源スイッチが壊れたと思うのですが、現在壊れたHATが行方不明なので詳細不明です。他にもショートしていた故障が発生していた事例もあるので気をつけて使ってください。

M5StickCを外した状態

M5StickCを外すとServoの動きは止まります。しかしLEDは最後の状態で光ったままになります。

通常外すことはないと思いますが、HATと本体の固定ができないのでパカパカした状態になりますので注意して使ってください。

16340バッテリーとは?

到着した時期によって違うみたいですが、割と初期と最近で2つ購入していますが、両方とも750mAhと書いてある電池でした。

使い切ってから充電したところ860mAhと出ましたので、容量詐欺でないバッテリーです。この手のバッテリーはこのサイズでも1000を超える容量を書いてあって、嘘が多いので本体で充電するよりは充電した容量がわかる充電器を使うのをおすすめします。

ちなみにCR123Aっていうのは、同じ大きさのリチウム電池で充電できませんので注意してください。

16340バッテリーの充電方法は?

M5StickCから自動的に充電されます。M5StickCを接続すると常にバッテリー端子に電圧が出ていましたので接続することで自動的に充電します。

この仕様は一見便利なのですが、Servo動作中にもM5StickC本体から電源を吸われている気がします、、、

個人的に専用充電器以外はあまり信用していないので、本体で充電したくないのですが、切り離せないので仕方ありません。。。

充電のON、OFFができる方が良かったな、、、

充電OFFにできるのであれば、CR123Aなどの充電できないタイプの乾電池を変わりに使うことができます。16340のタイプの充電池は思ったより安全じゃないので、使い方を間違うと事故につながったり、過放電してすぐに使えなくなったりする危険があります。

M5StickCにバッテリーから給電されるの?

給電されません。16340はServoとLEDにのみ使われます。むしろ充電としてM5StickC本体のバッテリーをServo HATに吸われます。

まとめ

世の中にはServoがなによりも好きな人と、それほど興味がない人の二種類がいるらしいです。私はどうやら後者のようですので、このまま封印の可能性が高いです。。。

バッテリーHAT的に使えると便利なのですが、バッテリーHATは別に発売されてしまったので、そちらが届き次第レビューしたいと思います。

また、16340とか18650は最近ハンディタイプの扇風機などでよく使われているバッテリーで、非常に便利で高性能なのですが、そもそもはノートパソコンのバッテリーとか、コードレス掃除機とかのバッテリーに組み込んで使う用途で開発されているため、安全装置が内蔵されていません。

バッテリーを使う装置側で安全回路を追加して使うため、設計がミスしていると危険な場合があります。ServoHATがどうなっているのかまでは検証していませんが、十分に気をつけて利用してください。

そのためバッテリーへの充電も本体じゃなくって、もう少しちゃんとした回路が入っている充電器を使うことをおすすめします。

また、日本製の18650はそもそも単体で使うことを許容していないことが多いみたいで、一般への小売もしていないみたいです。

M5StickCをワイヤレス充電する

概要

M5StickCでワイヤレス充電ができるか確かめてみました。長期的な運用に耐えられるかは検証していませんので、ネタ的に楽しんでください。

完成物

音がでますので注意しましょう。そして音に意味はないです!

仕組み

上記の商品をただつなげただけです!

給電側

M5StickCの5VアウトとGND端子に給電側をつないでいます。5Vを供給しているだけなので、普通の電源でも問題ありません。念の為AXP192の消費電力を確認しながら使っています。

この上にM5StickCの手提げをのせて、下を隠しています。

受電側

動画撮影時に見えないように、がんばって貼り付けています。

充電速度

カタログスペックですと300mAのワイヤレス充電ですが、200mA前後は問題なく充電していたようです。ちょっと位置がずれたり距離が離れると50mAとか100mAになりますが、まあこんなものでしょう!

まとめ

思いついたからやってみただけで、実用性までは考えていません。電源周りの工作なので、実際に試すときにはちゃんと検証してから使ってみてください。

とくに充電とかの電源系はノイズが乗っている場合があり、ノイズの影響を受けたり、急な電圧変動で機材が壊れたりする可能性がありますので、あまりおすすめしません!

M5StickC(ESP32)からBluetooth小型ポータブルレシートプリンタ「PAPERANG」を制御する

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

概要

前日紹介したプリンターをESP32から制御してみました。まずはコード値固定でボタンを押すと、フェードするだけの動作です。

スケッチ

#include "BluetoothSerial.h"
#include <M5StickC.h>

BluetoothSerial SerialBT;

uint8_t address[6]  = {0x00, 0x15, 0x83, 0x??, 0x??, 0x??};
bool connected;

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

  // Init
  SerialBT.begin("ESP32test", true);

  // Connect
  connected = SerialBT.connect(address);
  if (connected) {
    Serial.println("Connected Succesfully!");
  } else {
    while (!SerialBT.connected(10000)) {
      Serial.println("Failed to connect. Make sure remote device is available and in range, then restart app.");
    }
  }

  // CRC32 Key
  uint8_t key[14];
  key[ 0] = 0x02;
  key[ 1] = 0x18;
  key[ 2] = 0x00;
  key[ 3] = 0x04;
  key[ 4] = 0x00;
  key[ 5] = 0x78;
  key[ 6] = 0x7A;
  key[ 7] = 0xCE;
  key[ 8] = 0x33;
  key[ 9] = 0x2C;
  key[10] = 0x89;
  key[11] = 0x80;
  key[12] = 0xF0;
  key[13] = 0x03;

  Serial.printf( "CRC32 Key write = %d\n", SerialBT.write(key, 14) );

  delay(100);
}

void loop() {
  M5.update();

  if ( M5.BtnA.wasPressed() ) {
    uint8_t msg[12];
    msg[ 0] = 0x02;
    msg[ 1] = 0x1a;
    msg[ 2] = 0x00;
    msg[ 3] = 0x02;
    msg[ 4] = 0x00;
    msg[ 5] = ',';
    msg[ 6] = 0x01;
    msg[ 7] = 0x8b;
    msg[ 8] = 'V';
    msg[ 9] = '#';
    msg[10] = 'T';
    msg[11] = 0x03;

    Serial.printf( "Feed write = %d\n", SerialBT.write(msg, 12) );
  }

  if (Serial.available()) {
    SerialBT.write(Serial.read());
  }
  if (SerialBT.available()) {
    Serial.printf( "rx : %02X\n", SerialBT.read());
  }
  delay(20);
}

検証コードなので、制御コード直打ちです。

MACアドレスの他に名前で接続することもできますが、検索に時間がかかるのと接続が安定しないのでなるべくMACアドレスのほうが好ましいです。

ボタンを押すとプリンタがフィードして、白い紙を少し吐き出します。

解説

PAPERANGはBluetoothSerialで待ち受けており、そこに接続して制御コードを投げ込むことで制御します。

一般的にESP32が受け元になって、パソコンから接続する場合が多いですが、この場合にはPAPERANGがSlaveで、ESP32がMasterになります。

このMasterモードはESP32の1.0.4からでないと使えない接続モードです。

最初にCRC32でキーを送信し、その後にコマンドを送ることで制御が可能です。送るコマンドもCRC32でパリティをつけてあげないと受け取ってくれません。

ただし、キーは固定値なのでまずは接続ができるのかの検証のため、Pythonで送信しているデータをそのままESP32で投げてみて、動くかの確認をしました。

まとめ

いけそうです!

Pythonのライブラリはフォークされていて、Python3.7で動くものもありましたが、まったく構造から違うものになっていて、マニュアルは古いままって状態なので自分で作り直したほうがいい気がします。

ESP32から制御できそうなので、M5Stackの画面をそのまま印刷することもできそうです。ただこのプリンターって2年以上前の物で、P2って新しいモデルがすでに販売されていて、P2はまた違う制御っぽいのでどこまで追いかけるかはわかりません。

もし興味がある人がいれば、ライブラリ化まで行いたいと思います。。。

Bluetooth小型ポータブルレシートプリンタ「PAPERANG」

概要

上記のプリンタを触ってみました。APIとかは公開されていませんでしたが、Githubにあったライブラリを使って、印刷できました!

PAPERANGとは?

Aliexpressとかでよく売っている、バッテリー内蔵の小型サーマルプリンタです。レシート用紙へ印刷できます。

中国からのは技適が通っていないので、ちょっと手がでなかったのですが、じゃんぱらで安売りしていたのは技適ありの国内版です。

対象の技適は上記です。

値段は安売りされているのと、中国から直接買うので同じぐらいの値段です。技適心配しなくていいので買ってみました。

基本的にはパソコンまたはスマートフォンから印刷します。パソコン向けアプリは

中国語のページにしかありませんでしたが、中身は翻訳されていました。ただセットアップしようとすると、Windowsさんから怒られたので、中身を解凍して直接プログラムを実行してみました。

スマートフォンで印刷するのであれば、AppStoreなどから落としてきて、素直に印刷できます。

外部連携

SDKなどは公開されていないので、オフィシャルアプリ以外からは印刷することができません。

ただし、上記の人が解析したライブラリを公開してくれていました。

ただし、Python2.7でPyBluezなどのBluetoothが利用できるモジュールを入れる必要があります。

この環境作るのにものすごい時間がかかりました。。。

Windows環境で構築するのはおすすめしません。Raspberry Piとかで作ったほうが絶対的に楽だと思います。

サンプルプログラム

from message_process import BtManager

mmj = BtManager()

# Print a pure black image with 300 lines
img = "\xff" * 48 * 300
mmj.sendImageToBt(img)
mmj.disconnect()

実行結果

黒い線がでました!

まとめ

とりあえず動かしてみるところまではできました。たぶん実用レベルにするにはライブラリを全部解析して、Python3系で動くようにして、それから他のプラットフォームに移植かな。。。

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へ連絡お願いします。

M5StickCバッテリーライフ検証 その5

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

概要

その1その2その3その4に引き続き、 ESP-NOWを利用して、1秒間隔でデータ送信した場合の送信時間を計測してみました。

受信側スケッチ

#include <M5StickC.h>
#include <esp_now.h>
#include <WiFi.h>

esp_now_peer_info_t slave;

// 受信コールバック
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  float *bat = (float*)data;
  Serial.println(*bat);
}

void setup() {
  M5.begin();
  M5.Lcd.fillScreen(BLACK);

  // ESP-NOW初期化
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  esp_now_init();

  // ESP-NOWコールバック登録
  esp_now_register_recv_cb(OnDataRecv);
}

void loop() {
  delay(1);
}

最低限のスケッチで検証しています。

送信側スケッチ

#include <M5StickC.h>
#include <esp_now.h>
#include <WiFi.h>

esp_now_peer_info_t slave;

void setup() {
  setCpuFrequencyMhz(80);
  M5.begin();
  M5.Axp.ScreenBreath(7);
  M5.Lcd.fillScreen(BLACK);
  
  // ESP-NOW初期化
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  esp_now_init();

  // マルチキャスト用Slave登録
  memset(&slave, 0, sizeof(slave));
  for (int i = 0; i < 6; ++i) {
    slave.peer_addr[i] = (uint8_t)0xff;
  }
  esp_now_add_peer(&slave);
}

void loop() {
  float bat = M5.Axp.GetBatVoltage();
  esp_now_send(slave.peer_addr, (uint8_t*)&bat, sizeof(bat));
  delay(1000);
}

前回までの検証は白画面でしたが、今回間違って黒画面です。こちらも最低限のコードです。最初はCPU周波数と明るさの行が無いもので検証し、その後に追加しています。

結果

条件動作分数
BluetoothSerial(画面12, CPU240) 1秒間隔26
BluetoothSerial(画面7, CPU80) 1秒間隔41158%
ESP-NOW(画面12, CPU240) 1秒間隔37142%
ESP-NOW(画面7, CPU80) 1秒間隔52200%
BluetoothSerial(画面12, CPU240) 0.1秒間隔2596%
ESP-NOW(画面12, CPU240) 0.1秒間隔34131%

BluetoothSerialに比べると、ESP-NOWの方が省電力で使えそうです。30分までであれば、現実的な明るさの画面で使うことも視野に入ると思います。

まとめ

一時間超えるかなと思ったら、超えませんでした。BluetoothSerialよりESP-NOWが一部グラフが下回っているところがあるので、バッテリーの状況によって継続時間も変わってくると思います。

0.1秒間隔でも画面を少し暗くすれば30分は使えそうですね。