IoTデバイスをバッテリや太陽電池などで長期間動作させるためには、IoTデバイスを低消費電力化する必要があります。 「AmbientでIoTをはじめよう」の第25回は、M5StackでIoTデバイスの消費電流を測定する電流モニタを作り、ESPr Developer 32の消費電流を測ってみます。
IoTデバイスは、制御するマイコンの処理によって消費電流が変化します。 特にWi-FiやBluetoothなどの無線通信は、比較的大きな電流を消費しますし、いくつかのセンサは大きな電流を消費するものがあります。 従って、センサや通信モジュールにアクセスするプログラムによってIoTデバイスの消費電力は異なってきます。
IoTデバイスの低消費電力を知るには、ある瞬間の消費電流だけでなく、プログラム実行中の消費電流の積算値を調べる必要があります。 例えばそのデバイスが5分周期でデータを測定し、クラウドサービスに送信しているとしたら、5分間トータルの消費電流を把握し、改善する必要があります。 そのため、ある区間の総消費電流が測れる電流モニタを作ります。
電流センサモジュールとしては「Conta™ 電流センサ max4A PAC1710搭載」を使います。
この電流センサモジュールには、10mΩの抵抗(シャント抵抗)が搭載されています。バッテリと測定対象のデバイスの間にこのシャント抵抗を入れ、シャント抵抗の両端の電圧を測ることでバッテリからデバイスに流れる電流を測定します。シャント抵抗を流れる電流の測定にはPAC1710という電流センサが使われています。
PAC1710の電流測定には3つのパラメータがあります。一つは電流の測定範囲、もう一つは測定時間、3つ目は測定回数です。
測定範囲はシャント抵抗が10mΩの場合、±1A、±2A、±4A、±8Aの範囲が選択できます。今回はESP32など低消費電力のマイコンを測定対象にするので、±1Aのレンジで測定するように設定します。
測定時間は、測定にかける時間で、2.5ミリ秒から320ミリ秒まで設定できます。この測定時間は測定精度とトレードオフの関係にあります。測定範囲を±1Aにした場合、測定時間を2.5ミリ秒にすると15.6mAの精度で測定でき、20ミリ秒だと1.95mA、デフォルト設定の80ミリ秒だと0.49mAになります。マイコンの消費電流を測定する場合、細かい測定間隔で詳細な電流値を知りたいので、悩ましいトレードオフですが、今回は20ミリ秒、1.95mAの精度で測定することにします。
測定回数は、複数回測定して平均値を計算する機能で、1回(デフォルト)、2回、4回、8回が選択できます。今回はデフォルトの1回にします。
PAC1710はI2Cでマイコンと通信します。デフォルトのI2Cアドレスは0x18です。
M5StackとConta™ 電流センサモジュールはI2Cで通信するので、次のように接続します。モジュールにはプルアップ抵抗がついていないので、プログラムでプルアップします。
M5Stack | Conta™ 電流センサモジュール |
---|---|
3V3 | 3.3V |
G | GND |
SDA | SDA |
SCL | SCL |
M5Stackに添付されているジャンパワイヤなどを使って接続します。
測定対象のバッテリとデバイスはConta™ 電流センサモジュールの端子台につなぎます。写真のように端子台を上に見たときに、電流が端子台の右から左に流れるようにつなぎます。この電流センサは逆方向の電流も測れるので、逆につないでもセンサが壊れることはありませんが、得られた値がプラスマイナス逆になってしまいます。
測定対象のバッテリとデバイスは、次の写真のようにバッテリのプラスを電流センサモジュールの右側の端子台につなぎ、左の端子台にデバイスをつなぎます。バッテリとデバイスのグランドは、電流センサのグランド(GND)につなぎます。
プログラム実行中の消費電流の積算値を測るために、10ミリ秒間隔で消費電流を測定し、1,000件のデータをメモリーに記録します。10ミリ秒間隔で1,000件なので、測定期間は10秒です。
周期的な処理は次のようにESP32 Arduinoのタイマー機能を使って実現します。 sampling
ミリ秒ごとにタイマー割り込み処理関数 onTimer0
が呼び出されるようにします。周期処理では、 t0flag
という変数を0にしてタイマー割り込みを待ち、割り込み処理関数で t0flag
を1にすることで待ちを解除し、 sampling
ミリ秒ごとに周期処理がおこなわれるようにします。
volatile int t0flag; // 割り込み待ち変数 | |
void IRAM_ATTR onTimer0() { // 割り込み処理関数 | |
t0flag = 1; | |
} | |
samplingTimer = timerBegin(TIMER0, 80, true); // 1マイクロ秒のタイマーを初期設定する | |
timerAttachInterrupt(samplingTimer, &onTimer0, true); // 割り込み処理関数を設定する | |
timerAlarmWrite(samplingTimer, sampling * 1000, true); // samplingミリ秒のタイマー値を設定する | |
timerAlarmEnable(samplingTimer); // タイマーを起動する | |
while (true) { // 周期処理 | |
t0flag = 0; | |
while (t0flag == 0) { // タイマー割り込みを待つ | |
delay(0); | |
} | |
// 周期処理本体 | |
} | |
timerAlarmDisable(samplingTimer); // タイマーを停止する |
電流センサPAC1710にアクセスする部分はスイッチサイエンスのサンプルプログラムを利用しました。ただし、スイッチサイエンスのサンプルプログラムではI2Cでデータを読み込むときに、 requestFrom
に第3パラメータとして false
を渡し、リード後にI2Cバスを開放しないようにしていますが、これだとデータが読めなかったので、第3パラメータを渡さず、バスを開放するようにしました。
Wire.requestFrom(id, datasize, false); // リクエスト後にI2Cバスを開放しない
↓
Wire.requestFrom(id, datasize); // リクエスト後にI2Cバスを開放する
PAC1710は、電流の測定範囲をデフォルトの±8Aから±1Aに変更します。また測定時間をデフォルトの80ミリ秒から20ミリ秒に変更します。
void setup() {
// ...
// PAC1710の設定を変更する
int c1cnf[] = {0B00110000}; // Sample time: 20ms, Range: ±10mV
datasend(DEVID, PAC1710::REG::C1_VSAMP_CFG, c1cnf, 1);
}
電流値は、次のように dataread
関数で生データを読み出します。測定範囲を±1Aに設定したので、最大値が1A(1000mA)、分解能が2047なので、生データに (1000.0 / 2047)
を掛けて電流値を計算しています。データシートでは、測定時間を20ミリ秒に設定したときの分解能は9ビット(511)なので、511で割るべきだと思うのですが、実際に得られた値からすると2047で割ると正しい電流値が得られるようです。
int ch1Vsense[2] = {0};
dataread(DEVID, PAC1710::REG::C1_SVRES_H, ch1Vsense, 2);
float amp = ( (int16_t(ch1Vsense[0] << 8 | (ch1Vsense[1])) >>4) * (1000.0 / 2047));
周期処理本体では、電流センサPAC1710から電流値と電圧値を読み、メモリに記録していきます。電流値のしきい値を決め、そのしきい値を超えたら測定対象のIoTデバイスが動き出したと判断して、記録を開始するようにしています。
bool started = false; // 測定開始フラグ | |
int indx = 0; // 記録するデーターのインデックス | |
while (true) { | |
t0flag = 0; | |
while (t0flag == 0) { // タイマー割り込みを待つ | |
delay(0); | |
} | |
int ch1Vsense[2] = {0}; | |
dataread(DEVID, PAC1710::REG::C1_SVRES_H, ch1Vsense, 2); | |
if (!started) { | |
// 電流値がしきい値(startthreshold)未満だったら、測定を始めない | |
if (amp > -startthreshold && amp < startthreshold) { | |
continue; | |
} | |
started = true; // 電流値がしきい値を超えたら測定開始 | |
} | |
ampbuf[indx] = amp; // 電流値をメモリーに記録する | |
if (++indx >= NSAMPLES) { // データー数がサンプル数を超えたら、周期処理を終わる | |
break; | |
} | |
} |
測定が終わったら、測定データをCSVファイルにしてSDカードに書き出します。最後に測定データをM5StackのLCDに表示してプログラムを終了します。
今回のプログラムは10秒間電流値を測定して、測定結果をSDカードに書き出し、LCDに表示して終わりなので、 setup
関数ですべての処理をおこない、loop
関数の中身は空です。プログラム全体は次のようになります。
/* | |
* PAC1710で10ミリ秒毎に1000回、10秒、電流値を測定し、SDカードに書く | |
*/ | |
#include <M5Stack.h> | |
#include <Wire.h> | |
#include "PAC1710.hpp" | |
#include "menu.h" | |
void beep(int freq, int duration, uint8_t volume); | |
#define DEVID PAC1710::ADDR::OPEN // Resistor OPEN (N.C.) at ADDR_SEL pin | |
const int VSHUNT_mOHM = 10; | |
int sampling = 10; // サンプリング間隔(ミリ秒) | |
#define NSAMPLES 1000 // 10ms x 1000 = 10秒 | |
float startthreshold = 20.0; // 記録を開始する電流値(ミリA) | |
void datasend(int id,int reg,int *data,int datasize) { | |
Wire.beginTransmission(id); | |
Wire.write(reg); | |
for(int i=0;i<datasize;i++) { | |
Wire.write(data[i]); | |
} | |
Wire.endTransmission(); | |
} | |
int dataread(int id,int reg,int *data,int datasize) { | |
Wire.beginTransmission(id); | |
Wire.write(reg); | |
Wire.endTransmission(false); | |
Wire.requestFrom(id, datasize); | |
int i=0; | |
while((i<datasize) && Wire.available()) { | |
data[i] = Wire.read(); | |
i++; | |
} | |
return Wire.endTransmission(true); | |
} | |
uint16_t getID() { | |
int id[2] = {0}; | |
dataread(DEVID, PAC1710::REG::PID, id, 2); | |
return (id[0] << 8) | id[1]; | |
} | |
#define TIMER0 0 | |
hw_timer_t * samplingTimer = NULL; | |
float ampbuf[NSAMPLES]; | |
float voltbuf[NSAMPLES]; | |
Menu menu; | |
volatile int t0flag; | |
void IRAM_ATTR onTimer0() { | |
t0flag = 1; | |
} | |
#define X0 10 | |
#define Y0 220 | |
void drawData(float maxamp) { | |
M5.Lcd.fillRect(0, 0, 320, 220, BLACK); | |
for (int i = 0; i < 299; i++) { | |
int y0 = map((int)ampbuf[i * 3], 0, (int)maxamp, Y0, 0); | |
int y1 = map((int)ampbuf[(i + 1) * 3], 0, (int)maxamp, Y0, 0); | |
M5.Lcd.drawLine(i + X0, y0, i + 1 + X0, y1, WHITE); | |
} | |
M5.Lcd.drawLine(X0, Y0, 310, Y0, WHITE); | |
M5.Lcd.drawLine(X0, 0, X0, Y0, WHITE); | |
} | |
void setup() { | |
M5.begin(); | |
pinMode(21, INPUT_PULLUP); // SDAをプルアップする | |
pinMode(22, INPUT_PULLUP); // SCLをプルアップする | |
Wire.begin(); | |
M5.Lcd.setTextSize(2); | |
M5.Lcd.fillScreen(BLACK); | |
if (getID() != 0x585D) { | |
M5.Lcd.setCursor(20, 100); | |
M5.Lcd.print("Can not find PAC1710"); | |
} | |
int c1cnf[] = {0B00110000}; // Sample time: 20ms, Range: -10mV to 10mV | |
datasend(DEVID, PAC1710::REG::C1_VSAMP_CFG, c1cnf, 1); | |
menu.setMenu("start", "", ""); | |
M5.Lcd.setCursor(20, 100); | |
M5.Lcd.print("Press A button"); | |
M5.Lcd.setCursor(40, 120); | |
M5.Lcd.print("to start sampling"); | |
while (true) { | |
M5.update(); | |
if (M5.BtnA.wasPressed()) break; | |
} | |
M5.Lcd.fillScreen(BLACK); | |
beep(2000, 100, 2); | |
samplingTimer = timerBegin(TIMER0, 80, true); // 1マイクロ秒のタイマーを初期設定する | |
timerAttachInterrupt(samplingTimer, &onTimer0, true); // 割り込み処理関数を設定する | |
timerAlarmWrite(samplingTimer, sampling * 1000, true); // samplingミリ秒のタイマー値を設定する | |
timerAlarmEnable(samplingTimer); // タイマーを起動する | |
bool started = false; | |
int indx = 0; | |
float maxamp = 0; | |
M5.Lcd.fillRect(50, 100, 200, 10, BLACK); | |
while (true) { | |
t0flag = 0; | |
while (t0flag == 0) { // タイマー割り込みを待つ | |
delay(0); | |
} | |
int ch1Vsense[2] = {0}; | |
int ch1Vsource[2] = {0}; | |
dataread(DEVID, PAC1710::REG::C1_SVRES_H, ch1Vsense, 2); // CHANNEL 1 VSENSE RESULT REGISTER | |
dataread(DEVID, PAC1710::REG::C1_VVRES_H, ch1Vsource, 2); // CHANNEL 1 VSOURCE RESULT REGISTER | |
float amp = ( (int16_t(ch1Vsense[0] << 8 | (ch1Vsense[1])) >>4) * (1000.0 / 2047)); | |
float volt = (int16_t((ch1Vsource[0] << 3) | (ch1Vsource[1] >> 5) ) * 19.531); | |
if (!started) { | |
// 電流値がしきい値(startthreshold)未満だったら、測定を始めない | |
if (amp > -startthreshold && amp < startthreshold) { | |
continue; | |
} | |
started = true; // 電流値がしきい値を超えたら測定開始 | |
} | |
ampbuf[indx] = amp; // 電流値をメモリーに記録する | |
voltbuf[indx] = volt; // 電圧値をメモリーに記録する | |
maxamp = max(amp, maxamp); | |
M5.Lcd.setCursor(100, 100); | |
M5.Lcd.print(indx * 100 / NSAMPLES); M5.Lcd.print(" %"); | |
if (++indx >= NSAMPLES) { // データー数がサンプル数を超えたら、周期処理を終わる | |
break; | |
} | |
} | |
timerAlarmDisable(samplingTimer); // タイマーを停止する | |
M5.Lcd.fillScreen(BLACK); | |
M5.Lcd.setTextSize(2); | |
M5.Lcd.setCursor(20, 100); | |
beep(2000, 400, 2); | |
char fname[20]; | |
sprintf(fname, "/curLog.csv"); | |
for (int i = 0; SD.exists(fname); i++) { | |
sprintf(fname, "/curLog(%d).csv", i + 1); | |
} | |
File f = SD.open(fname, FILE_WRITE); | |
if (f) { | |
f.println("time, current(mA), volt(mV)"); | |
for (int i = 0; i < NSAMPLES; i++) { | |
f.printf("%d, %.2f, %.2f\r\n", sampling * i, ampbuf[i], voltbuf[i]); | |
} | |
f.close(); | |
M5.Lcd.print("Data written to"); | |
M5.Lcd.setCursor(40, 120); | |
M5.Lcd.print(fname); | |
} else { | |
M5.Lcd.printf("open error %s", fname); | |
} | |
menu.setMenu("view", "", ""); | |
M5.Lcd.setCursor(40, 160); | |
M5.Lcd.print("Press A button"); | |
M5.Lcd.setCursor(40, 180); | |
M5.Lcd.print("to view data"); | |
while (true) { | |
M5.update(); | |
if (M5.BtnA.wasPressed()) { | |
drawData(maxamp); | |
} | |
} | |
} | |
void loop() { | |
} |
PAC1710のドライバを含むプログラム全体はGithubに公開しました。
https://github.com/AmbientDataInc/M5Stack_CurrentMonitor/tree/master/PAC1710
開発した電流モニタを使って、ESPr Developer 32の消費電流を測ってみました。
測定対象にしたのは、ESP32を搭載した開発ボードESPr Developer 32で、温湿度センサーSi7021をI2Cで接続し、バッテリで駆動しました。温度、湿度を測定してデーターをクラウドサービスAmbientに送信し、Deep sleepするプログラムを動かして評価しました。プログラムは次のような流れです。プログラム全体は最後に掲載します。
バッテリとESPr Developer 32の間に電流モニタをつなぎ、電流モニタのプログラムを動かしてから、バッテリをセットしてESPr Developer 32を動かすと、プログラムの最初からの電流の様子が測定できます。測定データはSDカードに書かれるので、それをパソコンで読み、Excelでグラフ化してみました。
測定結果からは次のようなことが分かります。
(もう一桁高い精度の電流センサで同様の測定をおこなったところ、ESPr Developer 32のDeep sleep時の消費電流は0.5mA程度でした。)
今回開発した電流モニタは1.95mAの精度なので、あまり詳細な測定にはなっていませんが、大まかな消費電流の傾向は把握できます。特に、Deep sleep機能を使ってIoTデバイスを間欠動作させることで、消費電流を下げられていることが具体的に確認できます。また、Wi-Fiアクセスポイントへの接続に大きな電流を消費していることも分かります。さらに消費電流を下げるためには、毎回Wi-Fi APに接続せず、通常はセンサデータを取得してメモリに保存してすぐにDeep sleepし、何回かに1回、まとめてデータをクラウドサービスに送信するといった工夫が有効です。
測定対象のプログラムは次のようなものです。
/* | |
* ESP32でSi7021を読み、Ambientに送り、DeepSleepする | |
*/ | |
#ifdef ARDUINO_M5Stack_Core_ESP32 | |
#include <M5Stack.h> | |
#endif | |
#include <WiFi.h> | |
#include "Adafruit_Si7021.h" | |
#include <Ambient.h> | |
#define TIME_TO_SLEEP 60 /* Time ESP32 will go to sleep (in seconds) */ | |
const char* ssid = "ssid"; | |
const char* password = "password"; | |
WiFiClient client; | |
Ambient ambient; | |
unsigned int channelId = 100; // AmbientのチャネルID | |
const char* writeKey = "writeKey"; // ライトキー | |
Adafruit_Si7021 sensor = Adafruit_Si7021(); | |
void setup() { | |
#ifdef ARDUINO_M5Stack_Core_ESP32 | |
M5.begin(); | |
#endif | |
unsigned long starttime = millis(); | |
Serial.begin(115200); | |
while (!Serial) ; | |
WiFi.begin(ssid, password); // Wi-Fiネットワークに接続する | |
while (WiFi.status() != WL_CONNECTED) { // 接続したか調べる | |
delay(0); | |
Serial.print("."); | |
} | |
Serial.println("WiFi connected"); | |
Serial.print("IP address: "); | |
Serial.println(WiFi.localIP()); // ローカルIPアドレスをプリントする | |
ambient.begin(channelId, writeKey, &client); // チャネルIDとライトキーを指定してAmbientの初期化 | |
if (!sensor.begin()) { | |
Serial.println("Did not find Si7021 sensor!"); | |
while (true) ; | |
} | |
float temp = sensor.readTemperature(); | |
float humid = sensor.readHumidity(); | |
Serial.printf("temp: %.2f, humid: %.2f\r\n", temp, humid); | |
ambient.set(1, temp); // Ambientのデータ1に温度をセットする | |
ambient.set(2, humid); // データ2に湿度をセットする | |
ambient.send(); // Ambientに送信する | |
#ifdef ARDUINO_M5Stack_Core_ESP32 | |
M5.Lcd.writecommand(ILI9341_DISPOFF); | |
M5.Lcd.setBrightness(0); | |
#endif | |
// Deep sleepする時間を計算する | |
uint64_t sleeptime = TIME_TO_SLEEP * 1000000 - (millis() - starttime) * 1000; | |
esp_deep_sleep(sleeptime); // DeepSleepモードに移行 | |
// ここには戻らない | |
} | |
void loop(){ | |
} |
M5Stackと電流センサモジュールを使って、IoTデバイスの消費電流を測定する電流モニタを作りました。精度には課題がありますが、IoTデバイスの大まかな消費電流の傾向がつかめます。簡単な部品で作れるので、皆さんもご自分で作ったIoTデバイスの消費電流を見てみてはいかがでしょうか?