私は複数台のTimer Cameraを畑に設置して、畑の状況を遠隔地から観測しています。
それぞれのTimer Cameraでは、以下のような繰り返し処理を行っています。
「スタンバイ状態から復帰」→「静止画を撮影」→「モバイルWi-Fiルータに接続」→「Webサーバに画像データを送信」→「10分間のスタンバイ状態に移行」
さて、Timer Cameraに限らず、IoTデバイスを継続的に動作させようとすると、まれにデバイスが何らかの原因でフリーズしてしまい、処理がその時点で停止してしまうことがあります。
フリーズする頻度はデバイスによる個体差が大きく、上記の畑へのTimer Camera設置の際にも、しばしばフリーズしてしまうデバイスは「動作不安定品」として除外し、安定して動作するデバイスだけを選んで使用するようにしています。
このような、デバイスのフリーズ対策として「ウォッチドッグタイマー」という機能があります。
ChatGPTにウォッチドッグタイマーについて質問すると、以下のような回答が得られました。
ウォッチドッグタイマー(Watchdog Timer)は、コンピューターシステムや組み込みシステムにおいて、異常な状態やフリーズなどが発生した際に、システムをリセットして正常な状態に戻すための機能です。このタイマーは、定期的な信号を受け取り、もしもその信号が一定の期間内に受信されない場合、システムは異常動作と見なして自動的に再起動されます。
ウォッチドッグタイマーは、システムが停止したり、応答しなくなったりした場合に、自動的にリセットをかけて正常な状態に戻すことができるため、信頼性の向上に寄与します。特に組み込みシステムやリアルタイムシステムなどで利用され、ソフトウェアやハードウェアのトラブルによる障害を最小限に抑えるのに役立ちます。
今回は、動作が不安定なTimer Cameraにウォッチドッグタイマーを適用し、動作が安定するかどうか確認してみたいと思います。
先日、ESP32の起動理由の確認方法を調べました(記事は こちら)ので、これを使って調査を行います。
まずはウォッチドッグタイマーを使わず、以下のスケッチでTimer Cameraを動作させてみます。
#include "battery.h"
#include "bmm8563.h"
#include <WiFi.h>
#include "Ambient.h"
WiFiClient client;
Ambient ambient;
const char* ssid = "XXXXXXXX";
const char* password = "XXXXXXXX";
unsigned int channelId = XXXXX;
const char* writeKey = "XXXXXXXX";
void setup() {
Serial.begin(115200);
bat_init();
bmm8563_init();
bmm8563_setTimerIRQ(30);
esp_sleep_enable_timer_wakeup(30*1000*1000);
Serial.println("### START");
connect_wifi();
ambient.begin(channelId, writeKey, &client);
float val1 = (float)bat_get_voltage()/1000; // バッテリー電圧
int val2 = (int)esp_reset_reason(); // 起動理由
ambient.set(1, val1);
ambient.set(2, val2);
ambient.send();
WiFi.disconnect(true);
Serial.println("### FINISH");
sleepCam();
}
void loop() {
}
boolean connect_wifi() {
Serial.printf("### CONNECTING TO %s\n", ssid);
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println("### CONNECTED");
}
void sleepCam() {
Serial.println("### GO TO STANDBY MODE");
bat_disable_output();
esp_deep_sleep_start();
}
30秒毎にスタンバイ状態から復帰し、その時の起動理由とバッテリー電圧をAmbientに送信、送信が終わるとスタンバイ状態に移行するというものです。
Ambientに送信されたデータをCSVとしてダウンロードし、Excelでグラフ表示しました。
起動理由は数値で表示されていますが、それぞれの数値の内容は以下のとおりです。
1 | 電源オンによる起動 |
8 | ディープスリープからの復帰 |
9 | brown outによるリセット |
最初はTimer CameraをACアダプタにつないで動かしているので、起動理由は「8:ディープスリープからの復帰」となっていますが、ACアダプタを外して内蔵バッテリーで動き出してからは、起動理由は基本的に「1:電源オンによる起動」に変わります。
また、今回の結果ではしばしば「9:brown outによるリセット」が発生しており、それにより数十分にわたりデータ送信できていない期間も存在します。
先日、「brown outによるリセット」を無効化する方法も調べました(記事は こちら)ので、次はこの機能を反映してみます。
#include "battery.h"
#include "bmm8563.h"
#include <WiFi.h>
#include "Ambient.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
WiFiClient client;
Ambient ambient;
const char* ssid = "XXXXXXXX";
const char* password = "XXXXXXXX";
unsigned int channelId = XXXXX;
const char* writeKey = "XXXXXXXX";
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
Serial.begin(115200);
bat_init();
bmm8563_init();
bmm8563_setTimerIRQ(30);
esp_sleep_enable_timer_wakeup(30*1000*1000);
Serial.println("### START");
connect_wifi();
ambient.begin(channelId, writeKey, &client);
float val1 = (float)bat_get_voltage()/1000; // バッテリー電圧
int val2 = (int)esp_reset_reason(); // 起動理由
ambient.set(1, val1);
ambient.set(2, val2);
ambient.send();
WiFi.disconnect(true);
Serial.println("### FINISH");
sleepCam();
}
void loop() {
}
boolean connect_wifi() {
Serial.printf("### CONNECTING TO %s\n", ssid);
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println("### CONNECTED");
}
void sleepCam() {
Serial.println("### GO TO STANDBY MODE");
bat_disable_output();
esp_deep_sleep_start();
}
結果はこちらです。
起動理由から「9:brown outによるリセット」はなくなりましたが、内蔵バッテリーで動かしているにも関わらず、何故か時々「8:ディープスリープからの復帰」が発生しています。また、まだバッテリー電圧が高い時点で処理が停止しています。この時点でデバイスがフリーズしてしまったようです。
それでは、次はいよいよ「ウォッチドッグタイマー」を適用してみます。
ChatGPTによると、Arduino IDEスケッチにて、ESP32にウォッチドッグタイマーを適用するには、以下の記述を追加すれば良いようです。
#include <esp_task_wdt.h>
#define WDT_TIMEOUT 120 // WDT時間を指定
void setup() {
esp_task_wdt_init(WDT_TIMEOUT, true); // WDTを設定
esp_task_wdt_add(NULL); // WDTの開始
}
void loop() {
esp_task_wdt_reset(); // 処理が正常に進んでいる場合にタイマーを初期化
}
setup内でウォッチドッグタイマーを起動、loop内でタイマーを初期化しています。通常に動作している間はloop内の処理を行う度にタイマーが初期化されるので、システムリセットには至りません。しかしデバイスがフリーズしてしまいloop内の処理が行われないとタイマーは初期化されず、設定時間を超えるとシステムが強制的にリセットされます。
スケッチを以下のように変更します。
#include "battery.h"
#include "bmm8563.h"
#include <WiFi.h>
#include "Ambient.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include <esp_task_wdt.h>
WiFiClient client;
Ambient ambient;
const char* ssid = "XXXXXXXX";
const char* password = "XXXXXXXX";
unsigned int channelId = XXXXX;
const char* writeKey = "XXXXXXXX";
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
esp_task_wdt_init(60, true);
esp_task_wdt_add(NULL);
Serial.begin(115200);
bat_init();
bmm8563_init();
bmm8563_setTimerIRQ(30);
esp_sleep_enable_timer_wakeup(30*1000*1000);
Serial.println("### START");
connect_wifi();
ambient.begin(channelId, writeKey, &client);
float val1 = (float)bat_get_voltage()/1000; // バッテリー電圧
int val2 = (int)esp_reset_reason(); // 起動理由
ambient.set(1, val1);
ambient.set(2, val2);
ambient.send();
WiFi.disconnect(true);
Serial.println("### FINISH");
sleepCam();
}
void loop() {
// esp_task_wdt_reset();
}
boolean connect_wifi() {
Serial.printf("### CONNECTING TO %s\n", ssid);
WiFi.begin(ssid, password);
while(WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println("### CONNECTED");
}
void sleepCam() {
Serial.println("### GO TO STANDBY MODE");
bat_disable_output();
esp_deep_sleep_start();
}
本スケッチではsetup内でスタンバイ状態に移行しますので(正常に処理が行われている限りloop内の処理は実行されない)、loop内でのタイマー初期化は行いません。
結果はこちらです。
起動理由のうち、6が「ウォッチドッグタイマーによるリセット」です。
1 | 電源オンによる起動 |
6 | ウォッチドッグタイマーによるリセット |
8 | ディープスリープからの復帰 |
9 | brown outによるリセット |
何度か「6:ウォッチドッグタイマーによるリセット」がかかっていますが、デバイスはフリーズせず、内蔵バッテリーが空になるまで途中で中断することもなくデータを送信できています。
このように、所望のとおりTimer Cameraを安定して動作させることができました。
なお、「動作不安定品」として保管していた他のデバイス(全部で8台)も引っ張り出してきて同様の調査を行ったところ、いずれのデバイスとも内蔵バッテリーが空になるまで安定して動作しました。
Timer Cameraは電源周りの回路構成が特殊なため、電源ノイズによるフリーズが特に起こりやすいようですが、今回調査した「ウォッチドッグタイマー」機能は、Timer CameraだけではなくESP32デバイス共通で使える機能なので、長期間にわたり安定稼働させたいESP32デバイスには適用しておいた方がよさそうです。