WatchDogタイマ(番犬タイマ)は、万が一ソフトウエアがxxな事態に陥ったときにRESETなどかけて制御を取り戻してくれる「転ばぬ先の杖」であります。40年ほど前のマイコンでは、ちょっとヘビイ・デューティっぽい機種にのみ搭載されていましたが、この頃はほぼほぼ漏れなく搭載されている標準機能かと思います。
※「MicroPython的午睡」投稿順 Indexはこちら
しかし正常に動いていれば(動いているように見えていれば)WatchDogなど気にすることもなしです。通常、最初からWatchDogしかけようなどと思うのは、
万が一にもハングすると危ない、困る、ヘビイ・デューティな用途
かとも思います。まあ折角標準搭載されているのだし、しかけておいても悪いことはない、とも言えます。しかし、今回仕掛けるべしと思い至ったのは以下の邪な状況なのであります。いつもの泥縄。
なんだかよく分からないけれどハングするんだよね
背景説明
M5ATOMLite上のMicroPythonで動かしている温湿度に気圧などを測定するスクリプトの測定結果を、別シリーズにてサーバ上のデータベースに記録するようにいたしました。ここで問題勃発。40時間以上正常に動作(したように見える)後ハング。サーバー(Raspberry Pi 3)側には問題ないようです。「先っぽ」側の問題。再度テストしてみると数時間でも落ちることがあり、原因究明しないとなりません。
本シリーズの第57回で「出来た」MicroPythonスクリプトの測定結果をホストのNode-REDからSqliteデータベースに書き込むようにしたのはこちらの回です。
「とりあえず」ハングしたらデバイスの電源切らずに復活させたいです。その意図は、
-
- 落ちた状況の情報を収集して原因究明と対策に役立てたい
- 復帰したら、ちゃっかり測定に戻らせたい
ハングしたまま電源切ってしまうとRAM内容も飛んでしまうので原因究明しずらいです。なんとか制御を取り戻してどこでどう落ちているのだか調べたいです。その点で1はあるべき姿?だけれども、2は頬かむりの臭い物に蓋路線か。それでも数秒程度で測定に復帰させられれば、遠くからみていれば「ATOMLiteは常時稼働中」に見えるかと。
そこで伝家の宝刀、ウオッチドッグタイマ(WDT)にお出まし願うことにいたしました。しかし、MicroPythonでWDT使ったことないです。そこで今回は、本命のM5ATOMLiteでなく、同じESP32 generic port のMicroPythonを書き込んだESP32 DevKitC機(アイキャッチ画像)でWDTを「練習」してみました。いろいろいじり易いので。
ESP32でのWDTの使い方ドキュメント
ESP32用クイックリファレンスのWDTの項目は以下です。
使い方自体は難しくありません。タイムアウト値を与えてWDTを初期化した後は、「タイムアウト時間が経過する前に」でWDTにfeed()しつづければよいのです。もしタイムアウト時間が超過してしまうとRESETがかかります。ソフトが暴走しているのなら、これで復帰する筈。
ただちょっと不親切なのが、タイムアウト時間に関する制限です。最低1秒ということは書かれており、例題に5秒という時間設定があります。しかし上限は書かれてないです。M5ATOMLiteで行うときは2~3秒にしようと考えています。今回のテスト・サンプルでは最長60秒の設定まで試してみました。それでもOKでした。feed()を頻繁に入れるのは面倒な場合がありますが、かなり長めのタイムアウトでも行けそうなので、負担はほとんど無いように思えます。
さらに参照すべきがRESET後のステータスです。何といっても、WDTでRESETを書けて「復帰させる」のは良いですが、何もしないとRESET=>不具合=>WDTでRESET=>また不具合。。。というようなドアホな無限ループに落ち込む可能性も無きにしも非ずです。
上記のページにあるように、RESET後、その原因のステータスコードを読み出すことが可能です。その要因の中にはWDTによるRESETもあります。今回やってみたところでは、WDT-RESETがかかった場合はちゃんと識別できるようでした。この機能を使えば、WDTによるRESET後、原因究明のための情報収集をやることができるでしょう(そして、頬かむりして復旧もできるだろ~と。)
WDTをテストするためのスクリプト
以下のスクリプトの中、mainLoop()の中の以下の行でタイムアウト値をms単位で指定しています。以下は60000ms=60秒の設定例です。
wdt = machine.WDT(timeout=60000)
WDTへのfeedは、同じmainLoop()関数が「回る」度に与えるようになっています。mainLoop()の回転周期は、main()関数内でms周期を引数にあたえて以下のように起動しています。
uasyncio.run(mainLoop(10000))
またテスト用に外付けのスイッチをピン15に設定しています。ピン15はプルアップをイネーブルにしてあるので、スイッチが倒れていない場合、HIGHが読めます。スイッチを倒すとLOWです。これにより
-
- スイッチが倒れていたら先ほどのドアホな無限ループにならないように実行を打ち切ってREPLに制御を移す
- スイッチが倒れていたらfeed()をかけず、WDTが吠えるように仕向ける
という2つを行っています。
import machine import uasyncio switchpin = machine.Pin(15, machine.Pin.IN, machine.Pin.PULL_UP) async def reportStatus(period_s): loopCount = 0 while True: loopCount += 1 print(loopCount) await uasyncio.sleep(period_s) async def mainLoop(period_ms): global switchpin taskStatus = uasyncio.create_task(reportStatus(1)) wdt = machine.WDT(timeout=60000) while True: if switchpin.value() != 0: wdt.feed() await uasyncio.sleep_ms(period_ms) def chkRcause(): rstCause = machine.reset_cause() if rstCause == machine.PWRON_RESET: print("POWER ON") elif rstCause == machine.HARD_RESET: print("HARD") elif rstCause == machine.WDT_RESET: print("WDT") elif rstCause == machine.DEEPSLEEP_RESET: print("DEEPSLEEP") elif rstCause == machine.SOFT_RESET: print("SOFT") else: print("UNKNOWN") return rstCause def main(): global switchpin if switchpin.value() == 0: print("END OF WDT TEST.") else: chkRcause() uasyncio.run(mainLoop(10000)) if __name__ == "__main__": main()
まず、MicroPythonインタプリタをREPLに接続したままWDT-RESETを発生させてみました。このときのタイムアウトは6秒に設定。こんな感じ。
番犬が吠えて、RESETかかっているみたいです。
実際に動かすときは、デバイス側に自動起動される main.py としてスクリプトを搭載します。これにより、電源投入したら(勿論、REPLなどの接続なしに)スクリプトが起動されます。手順的にはRESETかかると boot.py が最初に実行され、その後、main.py が起動されます。
ただし、先ほども述べたとおり、WDT-RESETの無限ループに落ち込むのは避けたかったので、外付けスイッチで「復帰」と「中断」を選べるようにいたしました。
なお、上記のサンプルコードには含まれていないですが、WDTリセットを検出して、情報収集モードに入れるにはmain関数内で以下のような判断を入れれば良さそうです。
if chkRcause() == machine.WDT_RESET:
動作試験
実際にWDTがかかるところをテストするために起動したところが以下に。
WDTがタイムアウトしたところが以下に。
そしてWDTでRESETがかかった後の様子が以下に(例の外付けスイッチを倒してREPLに戻るようにした場合)
まあ、こういう普通の状態であれば、ウオッチドッグは普通にかかりますな。でも本番の状況は良く分かりませぬ。本当に戻ってくるのか?戻ってこれないような状態に落ち込んではいないだろうかな。やってみないと分からないっと。
戻ったとしてもその後の原因究明どうしようかね。最終的にはリモートで発生したトラブルの情報を収集してホストで解析できるように(その上で自動復帰)したいですが。