🙄

【ESP32】ESP-IDFへの移行後に発生したMQTT通信後のスタックオーバーフローエラー対処法

2024/06/02に公開

現在、方言を話すおしゃべり猫型ロボット「ミーア」を開発中。

https://mia-cat.com/

以前、こちらの記事で、VS Code上のPlatformIO IDEでArduinoだけではなくESP-IDFフレームワークに対応した。

ようやくビルドできるようになって、ホッとしたのも束の間、SmartConfig経由でWifi接続後にMQTT通信を確立したら、下記エラーが出て、延々と再起動するようになった。

SyncShadow is already started
MQTTPubSubClient::onMessage: $aws/things/XXXXXXX
***ERROR*** A stack overflow in task loopTask has been detected.
Backtrace: 0x400829fa:0x3ffbdec0 0x401684e5:0x3ffbdee0 0x4008a3d2:0x3ffbdf00 0x4008c622:0x3ffbdf80 0x4008a548:0x3ffbdfa0 0x4008a4fa:0x3ffbe020 0x4008a548:0x0000523a |<-CORRUPTED

ELF file SHA256: a98d6ccbac74aac1
Rebooting...

今回は、上記エラー対応を記載。

loopTaskとは何か

loopTaskはESP32プログラムのメインループタスクを指す。これは、Arduinoフレームワークでのloop()関数に対応しており、ESP32上で実行される無限ループのタスク。なので、main.cppに記載されているvoid loop()内の再帰処理に原因がある可能性が高い。

スタックオーバーフローの原因

スタックオーバーフローは、タスクが割り当てられたスタックメモリを使い果たしてしまう現象。これには以下のような原因がある。

  1. 過度の再帰呼び出し: 関数が再帰的に呼び出され、スタックを使い果たす。
  2. 大きなローカル変数: 一時的なローカル変数が多すぎる、または大きすぎる。
  3. 無限ループ: 無限ループの中でスタックを消費する処理が行われている。

スタックに関しては、下記参照

https://kazulog.fun/dev/why-use-object-oriented-programming/#toc16

MQTTメッセージの受信処理やパース処理が原因でスタックオーバーフローが発生することがある。特に大きなメッセージを処理する際には注意が必要。

Arduinoでは生じていなかったのに、なぜESP-IDFでは生じるのか?

ArduinoとESP-IDFフレームワークの違いによって、スタックサイズやタスクのメモリ管理に違いが生じ、スタックオーバーフローが発生する可能性がある。

デフォルトのスタックサイズ:

  • Arduino: タスクのスタックサイズは比較的多めに設定されているため、スタックオーバーフローが発生しにくい。
  • ESP-IDF: デフォルトではタスクのスタックサイズが小さいことがあり、Arduinoフレームワークと同じコードでスタックオーバーフローが発生する可能性がある

タスク管理:

  • Arduinoフレームワーク: 高レベルの抽象化が行われており、タスク管理がシンプル。
  • ESP-IDFフレームワーク: より低レベルのタスク管理が行われており、細かな設定が可能だが、その分、設定の不備が問題を引き起こすことがある。

デバックメッセージの追加

原因特定のため、void loop()関数内の原因がありそうなところに、デバッグメッセージを追加する

現在のvoid loop()関数とloop()内で呼び出している関数は下記。


// Wi-Fi接続中の処理を実行
void executeWiFiConnectedRoutines() {
  static unsigned long lastOneMinutesEventUnixTime = getNowUnixTime(); // 1分ごとのイベント実行時刻を記録

  // デバイスシャドウの状態監視
  SyncShadow::getInstance().loop();

  // デバイス設定の変更を適用する
  applyAndReportConfigUpdates();

  // 1分に1回実行
  if ((getNowUnixTime() - lastOneMinutesEventUnixTime) > 60) {
    executeOneMinutesAction();
    lastOneMinutesEventUnixTime = getNowUnixTime();
  }
}

void loop() {
  if (inSafeMode) {
    safeModeLoop();
    return;
  }

  monitorWiFiConnectionChange();

  if (isWiFiConnected()) {
    executeWiFiConnectedRoutines();
  }

  buttonManager.handleButtonPress();

  ExpressionService::getInstance().render();

  delay(10);
}

uxTaskGetStackHighWaterMark(NULL)関数を使って、現在のタスクがどれだけのスタック領域を使用していないか(未使用スタック量)を取得し、その値をシリアルポートに出力する。

uxTaskGetStackHighWaterMark(NULL)

  • この関数は、指定されたタスクのスタックの「高水位マーク」(High Water Mark)を取得する。「高水位マーク」とは、タスクがこれまでに使用したスタックの最大値を示す。つまり、スタックが一番少なくなった時の残りのスタック量を返す。これを使うことで、タスクが実際にどれだけのスタックを使用しているのかを知ることができる。
  • 引数としてNULLを渡すと、現在実行中のタスクの高水位マークを取得する

といわけで、未使用スタック量が少なくなりそうな各処理の前後に上記デバッグメッセージを追加する。


// Wi-Fi接続中の処理を実行
void executeWiFiConnectedRoutines() {
  static unsigned long lastOneMinutesEventUnixTime = getNowUnixTime(); // 1分ごとのイベント実行時刻を記録
  // デバッグメッセージを追加
  uint32_t freeStack = uxTaskGetStackHighWaterMark(NULL);
  Serial.print("Free stack at executeWiFiConnectedRoutines start: ");
  Serial.println(freeStack);
  // デバイスシャドウの状態監視
  SyncShadow::getInstance().loop();
  // デバッグメッセージを追加
  freeStack = uxTaskGetStackHighWaterMark(NULL);
  Serial.print("Free stack after SyncShadow loop: ");
  Serial.println(freeStack);

  // デバイス設定の変更を適用する
  applyAndReportConfigUpdates();
  // デバッグメッセージを追加
  freeStack = uxTaskGetStackHighWaterMark(NULL);
  Serial.print("Free stack after applyAndReportConfigUpdates: ");
  Serial.println(freeStack);

  // 1分に1回実行
  if ((getNowUnixTime() - lastOneMinutesEventUnixTime) > 60) {
    executeOneMinutesAction();
    lastOneMinutesEventUnixTime = getNowUnixTime();
  }
  // デバッグメッセージを追加
  freeStack = uxTaskGetStackHighWaterMark(NULL);
  Serial.print("Free stack at executeWiFiConnectedRoutines end: ");
  Serial.println(freeStack);
}

void loop() {
  // デバッグメッセージを追加してスタック使用状況を確認
  uint32_t freeStack = uxTaskGetStackHighWaterMark(NULL);
  Serial.print("Free stack at loop start: ");
  Serial.println(freeStack);

  if (inSafeMode) {
    safeModeLoop();
    return;
  }

  monitorWiFiConnectionChange();

  if (isWiFiConnected()) {
    // デバッグメッセージを追加
    freeStack = uxTaskGetStackHighWaterMark(NULL);
    Serial.print("Free stack before executeWiFiConnectedRoutines: ");
    Serial.println(freeStack);

    executeWiFiConnectedRoutines();

    // デバッグメッセージを追加
    freeStack = uxTaskGetStackHighWaterMark(NULL);
    Serial.print("Free stack after executeWiFiConnectedRoutines: ");
    Serial.println(freeStack);
  }

  buttonManager.handleButtonPress();

  // デバッグメッセージを追加
  freeStack = uxTaskGetStackHighWaterMark(NULL);
  Serial.print("Free stack before ExpressionService: ");
  Serial.println(freeStack);

  ExpressionService::getInstance().render();

  // デバッグメッセージを追加
  freeStack = uxTaskGetStackHighWaterMark(NULL);
  Serial.print("Free stack after ExpressionService: ");
  Serial.println(freeStack);

  delay(10);

  // デバッグメッセージを追加
  freeStack = uxTaskGetStackHighWaterMark(NULL);
  Serial.print("Free stack at loop end: ");
  Serial.println(freeStack);
}

再度ビルドしてログを見る。

デバッグメッセージの結果

デバッグメッセージから、

続きは、こちらで記載しています。
https://kazulog.fun/dev/mqtt-stack-overlfow/

Discussion