ESP32の「Brownout detector was triggered」とGuru Meditationの解決方法
ESP32の「Brownout detector was triggered」と再起動の繰り返しを解決。電源・ケーブル・コンデンサの対策、rstコード、Guru Meditation、Task WDT、バックトレースの読み方を解説します。
ESP32 brownout detector was triggeredというメッセージは、ボードの電源電圧が一瞬だけ安全な下限を下回り、チップが自分を守るために再起動したことを意味します。原因はほとんどの場合コードではなく電源側にあります。非力なUSBポート、細くて長いケーブル、容量不足のレギュレーター、あるいはESP32と同じラインから電源を取っているサーボやLEDテープなどです。ESP32が再起動を繰り返すもう一つの大きな原因はソフトウェアのクラッシュで、その場合はシリアルモニタにGuru Meditation Error、task_wdt、Stack canaryといった行が表示されます。
うれしいことに、ESP32はメッセージの読み方さえわかれば、なぜ再起動したのかを教えてくれます。このガイドでは、まず起動時の行に出るrstコードとエラーメッセージの見分け方を学び、次に適切な電源、ケーブル、コンデンサでブラウンアウトを根本から解決します。続いてLoadProhibitedやStoreProhibitedなどのGuru Meditationエラー、スタックオーバーフロー、ヌルポインタ、ウォッチドッグのタイムアウトを取り上げ、ESP Exception Decoderでクラッシュの原因をコードの行まで特定します。最後に、起動処理を妨げることがあるストラッピングピン(GPIO0、2、12、15)を見ていきます。

ESP32が再起動を繰り返すのはなぜ?まずメッセージを読む
トラブルシューティングを始める前に、シリアルモニタを115200ボーで開き、ボードのENボタンを押してください。起動のたびに、ESP32内部のROMブートローダーがまず「ets Jun 8 2016 00:22:57」のような日付の行を表示し、続いてrst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)のような行を表示します。rstの部分はボードが再起動した理由を、bootの部分はどのモードで起動したかを示します。ボードがループにはまっている場合は、これらの行のすぐ上に、前回の実行の「最後の言葉」であるエラーメッセージがあります。表示されるメッセージと確認すべき場所を、次の表にまとめました。
| シリアルモニタの表示 | 意味 | 確認する場所 |
|---|---|---|
| Brownout detector was triggered(ブラウンアウト検出) | 電源電圧がしきい値を下回った | 電源、ケーブル、コンデンサ |
| Guru Meditation Error: Core 1 panic'ed (LoadProhibited)(読み取り禁止) | コードが無効なアドレスから読み取ろうとした | ヌルポインタ、配列の範囲外 |
| Guru Meditation Error: Core 1 panic'ed (StoreProhibited)(書き込み禁止) | コードが無効なアドレスに書き込もうとした | ヌルポインタ、バッファオーバーフロー |
| Stack canary watchpoint triggered (loopTask)(スタックの番兵領域に到達) | タスクのスタックがいっぱいになった | 大きなローカル配列、再帰 |
| task_wdt: Task watchdog got triggered(タスクウォッチドッグが作動) | タスクが5秒を超えてCPUを手放さなかった | 待ち時間のないループ、止まったタスク |
| Interrupt wdt timeout on CPU0 / CPU1(割り込みウォッチドッグのタイムアウト) | 割り込みハンドラ、または割り込み禁止の区間が300 msより長くかかった | 長いISR、ISR内でのSerial |
| boot:0x3 (DOWNLOAD_BOOT...) waiting for download(書き込み待ち) | ボードが書き込みモードで起動した | 起動時にGPIO0がLOW |
| invalid headerやflash read err(フラッシュ読み取りエラー) | フラッシュメモリを読み取れなかった | 起動時にGPIO12がHIGH、書き込みの中断 |
「Brownout detector was triggered」とは?なぜ起こるのか
ブラウンアウトとは、電源が完全に切れるわけではなく、電圧が短時間だけ下がる現象です。ESP32には電源ラインを常に監視するブラウンアウト検出回路があり、Arduinoコアではそのしきい値が最も低いレベルの約2.43 Vに設定されています。電圧がこの値を下回った瞬間、チップは無線部とフラッシュメモリを保護し、シリアルモニタに「Brownout detector was triggered」と表示して自分自身を再起動します。理由は単純で、低い電圧ではフラッシュを誤って読み取ったり、保存したデータが壊れたり、CPUがでたらめな命令を実行したりするおそれがあるからです。検出回路は、ボードが予測できない動作をする代わりに、きれいにリセットされるようにします。ESP32-WROOM-32モジュールは3.0〜3.6 Vで動作するよう設計されているので、ラインが2.43 Vまで下がった時点で、すでに安全な範囲を大きく外れています。
ブラウンアウトのメッセージの後、起動時の行にはArduinoコアのバージョンによってrst:0xc (SW_CPU_RESET)かrst:0x3 (SW_RESET)が表示されます。これは、検出回路の割り込みがメッセージを表示した後、ソフトウェアでチップを再起動するためです。これに惑わされないでください。同じコードが原因はブラウンアウトだったとチップのメモリに記録するので、esp_reset_reason()はESP_RST_BROWNOUTを返します。ハードウェア自体のブラウンアウトリセットが働いた場合は、行がrst:0xf (RTCWDT_BROWN_OUT_RESET)になります。ESP-IDF 5をベースにした3.xコアでは、この行は情報レベルのログ(I BOD: Brownout detector was triggered)になりました。Arduinoのコンパイル済みライブラリはエラーレベルのログしか表示しないため、メッセージはたいてい表示されず、ボードは黙って再起動します。起動時に原因不明のrst:0x3やrst:0xcが出る場合は、後述の診断用スケッチでリセット原因を確認してください。電圧が下がる理由を理解するために、ESP32がどれくらいの電流を消費するか見てみましょう。
| 動作状態 | 標準的な電流(ESP32チップ) |
|---|---|
| ディープスリープ(RTCタイマー有効) | 約10 µA |
| ライトスリープ | 約0.8 mA |
| Wi-Fiオフ、CPU 240 MHz | 30〜68 mA |
| Wi-Fi受信(RX) | 95〜100 mA |
| Wi-Fi送信、802.11b、19.5 dBm | 約240 mA |
| 起動時の無線キャリブレーションと接続の瞬間 | 数マイクロ秒のピークで400 mAを超えることがある |
この表の差こそがブラウンアウト問題の核心です。コードがWi-Fiをオンにするまでボードは約50 mAで待機していますが、WiFi.begin()の行で電流は数マイクロ秒のうちに5〜6倍に跳ね上がります。電源、ケーブル、レギュレーターがこの急な要求に追いつけないと、電圧が落ち込みます。そのためEspressifは、ESP32に少なくとも500 mAを供給できる電源を推奨しています。症状はおなじみのものです。ボードが起動してシリアルモニタに数行を表示し、Wi-FiやBluetoothが始まった瞬間にリセットされ、これが無限ループになります。サーボが動いたとき、リレーが切り替わったとき、LEDテープが白く点灯したときにも同じことが起こりえます。
ブラウンアウトのよくある原因
- 細いUSBケーブル、長いUSBケーブル:安価なケーブルの細い電源線は、往復で最大0.5 Ωの抵抗になることがあります。オームの法則により、400 mAのピークではこうしたケーブルで0.2 Vが失われます。
- 非力なUSBポートやハブ:キーボードやモニターのポート、セルフパワーでないハブ、一部のノートパソコンの前面ポートでは、ピーク電流のときに電圧が下がります。
- レギュレーターのドロップアウト:多くのボードに載っているAMS1117は、入力と出力の間に1.0〜1.3 Vの差が必要です。5Vピンの電圧がおよそ4.3 Vを下回ると、出力を3.3 Vに保てません。
- 電池の直結:3.7 Vのリチウムイオン電池を5Vピンにつなぐと、レギュレーターのドロップアウトのために出力が3 Vを下回ります。単3電池2本は、負荷がかかるとすぐにESP32の最低電圧を下回ります。
- Arduino Unoの3.3Vピン:このピンから取り出すことが推奨されているのは約50 mAだけです。Wi-Fiを使うESP32にはまったく足りません。
- 同じラインの負荷:サーボ、リレー、ブザー、DCモーター、WS2812BテープがESP32と同じ5 Vで動いていると、動作のたびに電圧が引き下げられます。
- ブレッドボードとジャンパーワイヤー:接触の甘い箇所や細いジャンパーワイヤーは、接続のたびに少しずつ抵抗を増やします。電源をブレッドボード経由で配線すると、さらに悪化します。
- ESP32-CAM:カメラ、Wi-Fi、フラッシュLEDが同時に動くと電流がとても大きくなります。このボードは5 V、少なくとも2 Aの電源を好みます。詳しくはESP32-CAMのガイドを参照してください。
対策1:電源とケーブルを見直す
最初の、そして最も効果的な一歩は、より強い電源です。動作確認済みの短くて太いデータ通信用ケーブルでボードをつなぎ、パソコン背面のUSBポートか、少なくとも1 Aの定格の5 V ACアダプターに直接接続してください。USB 2.0ポートは500 mA、USB 3.0ポートは900 mAを供給するよう設計されています。Espressifが推奨する500 mAはUSB 2.0ポートの限界ちょうどなので、ケーブルの損失が加わると問題が起こり始めます。ケーブルを交換しただけでエラーが消えたなら、原因はおそらくケーブルの抵抗でした。ケーブルで失われる電圧は、「オームの法則」ツールでV = I × Rから計算できます。

| 電源の構成 | ブラウンアウトの危険度 | メモ |
|---|---|---|
| 短くて太いケーブルと背面のUSBポート | 低い | 開発中にいちばん簡単な対策 |
| 長くて細いケーブル、前面ポート、ハブ | 高い | ピーク電流のときにケーブルの抵抗が電圧を引き下げる |
| 1 A以上の定格の5 V ACアダプターを5VピンかVINピンへ | 低い | 常設のプロジェクトにおすすめ |
| 既製の3.3 V電源を3V3ピンに直接 | 電源しだい | 少なくとも500 mAを供給でき、3.6 Vを決して超えないこと |
| 3.7 Vのリチウムイオン電池をAMS1117経由で | 高い | 電池が約4 Vを下回ると、出力が3 V未満になる |
| リチウムイオン電池と低ドロップアウト(LDO)の3.3 Vレギュレーター、または昇降圧コンバーター | 低い | 電池が放電しても安定した3.3 Vを供給する |
| 単3電池2本を3V3ピンに直接 | 非常に高い | 負荷がかかると電圧がすぐにESP32の最低電圧を下回る |
電池で動かすプロジェクトには、さらに注意が必要です。18650などのリチウムイオン電池は、満充電で4.2 V、空になると約3.0 Vになります。この範囲は、3V3ピンへの直結(4.2 Vは上限の3.6 Vを超える)にも、AMS1117を通る5Vピンにも合いません。正しい方法は、5 Vの昇圧モジュールを通して電池を5Vピンにつなぐか、少なくとも500 mAの定格の低ドロップアウトのレギュレーター(例:ME6211やHT7833)とコンデンサを使って3V3ラインに直接供給するかのどちらかです。電池でプロジェクトがどれくらい動くかは「電池の持続時間」ツールで見積もれます。充電回路についてはTP4056充電モジュールのガイドで紹介しています。
対策2:3V3とGNDの間にコンデンサを追加する
コンデンサは小さな貯水タンクのように働き、電流のピーク時に、レギュレーターとケーブルが追いつくまで自らエネルギーを供給します。ボードの3V3とGNDピンの間に、できるだけボードの近くで100〜470 µFの電解コンデンサを追加し、その隣に100 nFのセラミックコンデンサも付けてください。電解コンデンサはミリ秒単位の電圧降下を、セラミックコンデンサはごく短い高周波のリップルを受け持ちます。効果はΔV = I × Δt / Cでおおまかにわかります。470 µFのコンデンサだけで200 µsの間200 mAを余分に供給しても、電圧は0.085 Vしか下がりません。セラミックコンデンサの104という表記は100 nFを意味します。ほかの表記は「コンデンサコード」ツールで解読できます。

対策3:サーボ、モーター、LEDテープの電源を分ける
ESP32自体に正しく電源を供給していても、同じラインの負荷が電圧を引き下げることがあります。SG90のような小さなサーボでも動き始めるときに数百mAを流します。WS2812B LEDは白色の最大輝度で約60 mAを使うので、30個のLEDのテープでは1.8 Aに達することがあります。これらの負荷はESP32の5Vピンや3V3ピンからではなく、別の5 V電源から供給してください。たとえば、12 VのACアダプターをLM2596降圧モジュールで5 Vに下げる方法があります。最も大切なルールは、両方の電源のGNDを互いにつなぐことです。共通のグラウンドがないと信号線に基準がなくなり、サーボやテープがでたらめに動きます。詳しくはArduinoがリセットを繰り返すときとGNDと共通グラウンドで解説しています。

ブラウンアウト検出を無効にするのは解決策?
ネットで最もよく見かける「解決策」は、1行で検出回路をオフにする方法です。以下のコードはESP32のブラウンアウト用レジスタをクリアして検出回路を無効にし、エラーメッセージは消えます。しかし電圧の落ち込みは続いています。誰も警告しなくなっただけです。このコードは診断にだけ使ってください。検出回路をオフにしてボードが問題なく動くなら、原因は間違いなく電源だと確かめられたことになります。その後はこの行を削除して、電源を直してください。
1#include "soc/soc.h"
2#include "soc/rtc_cntl_reg.h"
3
4void setup() {
5 // YALNIZ TEŞHİS İÇİN: brownout dedektörünü kapat / DIAGNOSIS ONLY: disable the brownout detector
6 WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
7
8 Serial.begin(115200);
9 Serial.println("Brownout dedektoru kapali / brownout detector disabled");
10}
11
12void loop() {
13}rstコード:ESP32は再起動の理由を教えてくれる
ROMが起動のたびに表示するrst:の値は、前回の実行がどのように終わったかを示します。通常の起動ではbootの値は0x13 (SPI_FAST_FLASH_BOOT)で、ボードがフラッシュからプログラムを開始することを意味します。次の表は、よく目にするコードの一覧です。ROMのコードだけでは足りないこともあります。ESP.restart()とGuru Meditation後の再起動はどちらも0xcと表示され、ブラウンアウトはコアのバージョンによって0xcか0x3と表示されます。これらを見分けるには、次の節のesp_reset_reason()のスケッチを使ってください。
| rstコード | 名前 | 意味 |
|---|---|---|
| 0x1 | POWERON_RESET | 初めて電源が入った、またはENボタンが押された |
| 0x3 | SW_RESET | ソフトウェアがデジタル系全体をリセットした。新しいコアのバージョンではブラウンアウトの後にも表示される |
| 0x5 | DEEPSLEEP_RESET | ボードがディープスリープから復帰した(正常) |
| 0x7 / 0x8 | TG0WDT_SYS_RESET / TG1WDT_SYS_RESET | タイマーグループのウォッチドッグがハードウェアでシステムをリセットした。割り込みが長く禁止されたままだったか、パニックハンドラさえ動けなかった |
| 0x9 | RTCWDT_SYS_RESET | RTCウォッチドッグがデジタルコアをリセットした |
| 0xc | SW_CPU_RESET | ソフトウェアによる再起動:ESP.restart()、Guru Meditationの後、またはArduinoコアでのブラウンアウトの後 |
| 0xf | RTCWDT_BROWN_OUT_RESET | ハードウェアによるブラウンアウトリセット:電源電圧が不安定 |
| 0x10 | RTCWDT_RTC_RESET | RTCウォッチドッグがRTCを含むチップ全体をリセットした。起動中に固まるボードでよく見られる |
診断用スケッチ:リセット原因、メモリ、スタックを表示する
以下のコードを別のスケッチとして書き込むか、その行を問題のあるプロジェクトのsetup()とloop()に追加してください。ボードが起動するたびに、リセット原因を読みやすい文字で、起動回数とメモリの状態とともにシリアルモニタに表示します。RTC_DATA_ATTRを付けたカウンターはRTCメモリに置かれるので、ソフトウェアリセットやディープスリープでは消えず、電源を完全に切ったときだけクリアされます。カウンターが数秒ごとに増えていくなら、ボードがリセットループに入っていることがすぐにわかります。
1#include <esp_system.h>
2
3RTC_DATA_ATTR uint32_t bootCount = 0; // yazılım resetinde korunur / survives software resets
4
5// Reset nedenini okunabilir yazıya çevir / turn the reset reason into readable text
6const char* reasonText(esp_reset_reason_t r) {
7 switch (r) {
8 case ESP_RST_POWERON: return "POWERON: guc acilisi ya da EN / power-on or EN button";
9 case ESP_RST_SW: return "SW: ESP.restart()";
10 case ESP_RST_PANIC: return "PANIC: Guru Meditation";
11 case ESP_RST_INT_WDT: return "INT_WDT: kesme watchdog'u / interrupt watchdog";
12 case ESP_RST_TASK_WDT: return "TASK_WDT: gorev watchdog'u / task watchdog";
13 case ESP_RST_WDT: return "WDT: diger watchdog / other watchdog";
14 case ESP_RST_DEEPSLEEP: return "DEEPSLEEP: derin uykudan uyanma / deep sleep wake-up";
15 case ESP_RST_BROWNOUT: return "BROWNOUT: gerilim dusmesi / supply voltage dropped";
16 default: return "UNKNOWN: bilinmiyor / unknown";
17 }
18}
19
20void setup() {
21 Serial.begin(115200);
22 delay(1000); // Seri Monitör'ün açılmasını bekle / wait for the Serial Monitor
23 bootCount++;
24 Serial.printf("Acilis sayisi / boot count: %u\n", (unsigned)bootCount);
25 Serial.printf("Reset nedeni / reset reason: %s\n", reasonText(esp_reset_reason()));
26}
27
28void loop() {
29 // Bellek durumu / memory status
30 Serial.printf("Bos heap / free heap: %u B, en dusuk / minimum: %u B, en buyuk blok / largest block: %u B\n",
31 (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMinFreeHeap(), (unsigned)ESP.getMaxAllocHeap());
32 // loopTask yığınında hiç kullanılmamış en küçük alan / smallest unused space on the loopTask stack
33 Serial.printf("loopTask bos yigin / free stack: %u B\n", (unsigned)uxTaskGetStackHighWaterMark(NULL));
34 delay(5000);
35}ループで表示される値は、表に出ない問題も明らかにします。空きヒープが周回のたびに少しずつ減るなら、プログラムにメモリリークがあり、ボードは遅かれ早かれクラッシュします。最大ブロックが空きヒープよりずっと小さいなら、メモリが断片化しています。これはたいてい、ループの中で伸び縮みを繰り返すStringオブジェクトが原因です。loopTaskの空きスタックが数百バイトを下回ったら、スタックオーバーフローは目前です。リセット原因がBROWNOUTなら電源の節に、PANIC、TASK_WDT、INT_WDTなら以下のソフトウェアの節に集中してください。
Guru Meditation Errorとは?クラッシュ出力を1行ずつ読む
Guru Meditation Errorは、ESP32のCPUが回復できないエラーに遭遇したときに、パニックハンドラが表示するクラッシュレポートです。名前は昔のAmigaコンピューターのエラー画面に由来します。レポートは最初は怖く見えますが、大事なのは数行だけです。Core 1はエラーがどちらのコアで起きたかを示します。Arduinoではsetup()とloop()がコア1で動き、Wi-Fiスタックはコア0で動きます。かっこ内の名前は例外の種類です。レポートが終わると、ボードは「Rebooting...」と表示して再起動し、起動時の行にはrst:0xcが表示されます。
| 行 | 意味 |
|---|---|
| Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.(例外が処理されなかった) | コア1のコードが無効なアドレスから読み取ろうとし、そのエラーが処理されなかった |
| PC : 0x400d1a2c | クラッシュした命令のアドレス。0x400dで始まるなら、自分のコードかライブラリがフラッシュから実行されていた |
| EXCCAUSE: 0x0000001c | 例外コード。0x1c、つまり28はLoadProhibitedを意味する |
| EXCVADDR: 0x00000000 | アクセスされたアドレス。0やごく小さな数ならヌルポインタを示す |
| Backtrace: 0x400d1a29:0x3ffb2270 0x400d0f6e:0x3ffb2290 ... | 呼び出しの連鎖。各ペアはPC:SPで、デコーダーでコードの行に変換できる |
| ELF file SHA256: ...(ELFファイルのハッシュ) | クラッシュしたプログラムの指紋。デコーダーにはまったく同じビルドが必要 |
| Rebooting...(再起動中) | パニックハンドラがソフトウェアでチップを再起動する |
LoadProhibited、StoreProhibitedなどの例外の種類
| 例外 | EXCCAUSE | 何が起きた? | よくある原因 |
|---|---|---|---|
| LoadProhibited | 28 (0x1c) | 無効なアドレスからの読み取り | ヌルポインタや未初期化のポインタ、削除済みのオブジェクト |
| StoreProhibited | 29 (0x1d) | 無効なアドレスへの書き込み | ヌルポインタ経由の書き込み、配列からはみ出すバッファ |
| InstrFetchProhibited | 20 (0x14) | 無効なアドレスからの命令の取り出し | 代入されていない関数ポインタやコールバック |
| IllegalInstruction | 0 (0x0) | 不正な命令 | 壊れたスタック、値を返すべき関数でのreturnの抜け |
| LoadStoreAlignment | 9 (0x9) | アラインメントされていないメモリアクセス | バイト配列をuint32_t*のような型にキャストする |
| IntegerDivideByZero | 6 (0x6) | 整数のゼロ除算 | ゼロになりうるカウンターで割る |
| Unhandled debug exception(処理されないデバッグ例外) | ― | デバッグ例外 | Stack canary watchpoint triggered:スタックオーバーフロー |
| Interrupt wdt timeout on CPU0/CPU1(割り込みウォッチドッグのタイムアウト) | ― | 割り込みウォッチドッグの時間切れ | 長いISR、ISR内でのSerialやdelay |
| Cache disabled but cached memory region accessed(キャッシュ無効中のアクセス) | ― | フラッシュが使用中のときに、フラッシュ上のコードにアクセスした | IRAM_ATTRのない割り込みハンドラ |
これらのエラーで最も多いのはヌルポインタの使用です。NULLはアドレス0で、ESP32ではアドレス0の周辺に読み取れるメモリがありません。そのため、空のポインタが指す場所から読み取るとLoadProhibitedに、そこへ書き込むとStoreProhibitedになります。この場合、EXCVADDRは絶好の手がかりです。0x00000000はポインタそのものを、0x00000004や0x00000010のような小さな値は空のポインタの中のフィールド(NULLにそのフィールドのオフセットを足したもの)を指しています。Arduinoのプロジェクトで最も多い原因は、関数が「見つからない」という意味でNULLを返し、その値をチェックせずに使うことです。以下の例では、strstr()は文字列が見つからないとNULLを返します。チェックがないと、ボードはEXCVADDR 0x00000005でクラッシュします。
1const char* response = "temp=23.5;hum=40"; // sunucudan gelen yanıt / reply from a server
2
3void setup() {
4 Serial.begin(115200);
5 delay(500);
6
7 // YANLIŞ: "pres=" yoksa strstr NULL döner ve kart çöker (EXCVADDR 0x00000005)
8 // WRONG: if "pres=" is missing strstr returns NULL and the board crashes (EXCVADDR 0x00000005)
9 // float pressure = atof(strstr(response, "pres=") + 5);
10
11 // DOĞRU: işaretçiyi kullanmadan önce NULL kontrolü yap / RIGHT: check for NULL before using the pointer
12 const char* found = strstr(response, "pres=");
13 if (found != nullptr) {
14 float pressure = atof(found + 5);
15 Serial.println(pressure);
16 } else {
17 Serial.println("pres alani bulunamadi / pres field not found");
18 }
19}
20
21void loop() {
22}スタックオーバーフロー:「Stack canary watchpoint triggered」エラー
ESP32では各タスクに固定サイズのスタックがあり、ローカル変数、関数の呼び出し、戻りアドレスがそこに保存されます。Arduinoでsetup()とloop()を実行するloopTaskのスタックは8192バイトです。loop()の中で10,000バイトのローカル配列を宣言したり、自分自身を呼び出す関数(再帰)や、ローカル変数の多いライブラリ関数を使ったりすると、この上限を超えます。スタックの末尾には番兵(canary)領域があり、スタックがそこに達すると、ボードはGuru Meditation Error: Core 1 panic'ed (Unhandled debug exception)と表示し、そのすぐ下にDebug exception reason: Stack canary watchpoint triggered (loopTask)と表示します。かっこ内の名前は、スタックがあふれたタスクです。同じ問題が***ERROR*** A stack overflow in task loopTask has been detected.と表示されることもあります。対処法は次のとおりです。
- 大きな配列をスタックから移す:配列をグローバルに宣言するか、前にstaticを付けます。すると配列はスタックではなく、常駐するRAMに置かれます。
- loopTaskのスタックを大きくする:スケッチの先頭にSET_LOOP_TASK_STACK_SIZE(16 * 1024);と書くと、loop()のスタックが16 KBになります。
- 自分のタスクに十分な領域を与える:ESP32では、xTaskCreateとxTaskCreatePinnedToCoreに渡すスタックサイズの単位はバイトです。ほかのプラットフォームのFreeRTOSのようなワード単位ではありません。Serial.printfを使うだけのタスクでも1024バイトでは足りないので、4096から始めてください。
- 測る:uxTaskGetStackHighWaterMark(NULL)は、タスクがこれまで一度も使わなかったスタックの最小量をバイト単位で返します。数百バイトを下回ったら、スタックを大きくしてください。
- 再帰を避ける:自分自身を呼び出す関数はループに書き換えるか、深さを制限します。
タスクウォッチドッグ:「task_wdt: Task watchdog got triggered」エラー
ウォッチドッグは、止まったプログラムに気づいてボードを救うタイマーです。ESP32のタスクウォッチドッグ(Task WDT)は、登録されたタスクが一定の間隔で「生きています」と知らせることを期待しています。Arduinoコアでは、デフォルトでコア0のアイドル(IDLE)タスクを5秒のタイムアウトで監視します。コア0で動くタスクが5秒間CPUを手放さないとIDLEタスクが動けず、シリアルモニタにE (10257) task_wdt: Task watchdog got triggered.で始まるメッセージが表示されます。その後に、時間内にウォッチドッグをリセットしなかったタスクの一覧(たいていは- IDLE (CPU 0))と、Tasks currently running:の下にその時点で動いていたタスクが続きます。Arduinoコアでは、これがtask_wdt: Aborting.という行とともにパニックを起こし、ボードが再起動します。
よくある原因は、xTaskCreatePinnedToCoreでコア0に固定したループが一度もvTaskDelay()を呼ばない、センサーの応答をタイムアウトなしのwhileループで待つ、AsyncWebServerなどのライブラリのコールバック内でdelay()や長い処理を行う、といったものです。最後のケースでは、一覧にIDLEではなくasync_tcpタスクが表示されます。対策は、長い処理を小さく分け、ループにvTaskDelay(1)のような短い待ち時間を入れることです。loop()関数自体はデフォルトでは監視されていません。必要ならsetup()でenableLoopWDT()を呼ぶと、loop()が5秒を超えて止まったときにもボードが再起動します。disableCore0WDT()でウォッチドッグを止めるとエラーは見えなくなりますが、止まったコードは直りません。タスクとコアについて詳しくはESP32のFreeRTOSとデュアルコアを参照してください。以下のコードは、正しいタスクの構成とスタックオーバーフローへの対策の両方を示しています。
1SET_LOOP_TASK_STACK_SIZE(16 * 1024); // loop() yığını 8 KB yerine 16 KB / loop() stack 16 KB instead of 8 KB
2
3volatile uint32_t sampleCount = 0;
4
5// 0. çekirdekte çalışan görev / task running on core 0
6void sensorTask(void* parameter) {
7 for (;;) {
8 sampleCount = sampleCount + 1; // sensörünü burada oku / read your sensor here
9 vTaskDelay(pdMS_TO_TICKS(10)); // IDLE görevine sıra ver, task_wdt tetiklenmez / let IDLE run, no task_wdt
10 }
11}
12
13void setup() {
14 Serial.begin(115200);
15 // Yığın boyutu bayt cinsindendir: 4096 bayt / stack size is in bytes: 4096 bytes
16 xTaskCreatePinnedToCore(sensorTask, "sensorTask", 4096, NULL, 1, NULL, 0);
17}
18
19void loop() {
20 static uint8_t buffer[10000]; // static: yığında değil kalıcı RAM'de / static: in RAM, not on the stack
21 memset(buffer, 0, sizeof(buffer));
22
23 Serial.printf("Ornek / samples: %u, loopTask bos yigin / free stack: %u B\n",
24 (unsigned)sampleCount, (unsigned)uxTaskGetStackHighWaterMark(NULL));
25 delay(1000);
26}Interrupt wdt timeoutとIRAM_ATTR:割り込みでのクラッシュ
割り込みウォッチドッグ(Interrupt WDT)はずっと気が短く、割り込みハンドラ(ISR)や割り込みを禁止したコードの区間が300 msより長くかかると、ボードはGuru Meditation Error: Core 1 panic'ed (Interrupt wdt timeout on CPU1)でクラッシュします。ISRの中でSerial.print、delay()、I2C、Wi-Fiを使うことが最も多い原因です。フラッシュメモリも落とし穴です。Preferences、EEPROM、LittleFSがフラッシュに書き込む間、フラッシュキャッシュは一瞬無効になります。その間に割り込みが来て、そのハンドラがフラッシュ上にあると、ボードはCache disabled but cached memory region accessed(キャッシュが無効なのにキャッシュ領域にアクセスした)と報告します。対策は、ISRにIRAM_ATTRを付けてRAMに置き、ISRの中ではフラグを立てるだけにすることです。実際の処理はloop()が行います。ISRとloop()で共有する変数はvolatileにする必要があります。
1const int BUTTON_PIN = 27; // buton GPIO 27 ile GND arasında / button between GPIO 27 and GND
2volatile bool buttonPressed = false; // ISR ile loop arasında paylaşılan bayrak / flag shared by the ISR and loop
3uint32_t pressCount = 0;
4
5void IRAM_ATTR onButton() { // IRAM_ATTR: fonksiyon flaşta değil RAM'de durur / the function lives in RAM, not flash
6 buttonPressed = true; // yalnızca bayrak kaldır, Serial yok / only set a flag, no Serial
7}
8
9void setup() {
10 Serial.begin(115200);
11 pinMode(BUTTON_PIN, INPUT_PULLUP);
12 attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onButton, FALLING);
13}
14
15void loop() {
16 if (buttonPressed) {
17 buttonPressed = false;
18 pressCount++;
19 Serial.printf("Butona basildi / button pressed: %u\n", (unsigned)pressCount); // yavaş iş burada / slow work here
20 }
21}ESP Exception DecoderでBacktraceをコードの行に変換する
Backtraceの行のアドレスは、それだけでは何もわかりません。それをソースコードのファイル、関数、行番号に変換するツールがESP Exception Decoderです。古いArduino IDE 1.8では「ツール」メニューに追加するプラグインでした。Arduino IDE 2では、同じ名前のコミュニティ製の拡張機能を使います。
- GitHubのdankeboy36/esp-exception-decoderプロジェクトのReleasesから.vsixファイルをダウンロードします。Arduino IDE 2には1.xのリリースを選んでください。新しい2.xのリリースはVS Code向けです。
- Arduino IDEを閉じ、ファイルをWindowsなら%UserProfile%\.arduinoIDE\pluginsに、macOSとLinuxなら~/.arduinoIDE/pluginsにコピーします。フォルダがなければ作成してください。
- IDEを開き、クラッシュするスケッチを開いて検証でコンパイルし、ボードに書き込みます。
- シリアルモニタを開き、クラッシュが再び起きるのを待ちます。
- Ctrl+Shift+P(macOSではCmd+Shift+P)でコマンドパレットを開き、ESP Exception Decoder: Show Decoder Terminalを実行します。
- シリアルモニタからBacktraceの行を含むGuru Meditationのブロックをコピーし、開いたターミナルに貼り付けます。ツールが各アドレスをファイル名、関数、行番号とともに表示します。
拡張機能をインストールできない場合は、同じ作業を手動でも行えます。「スケッチ」>「コンパイルしたバイナリを出力」メニューを使うと、スケッチフォルダ内のbuildフォルダに拡張子.elfのファイルができます。ESP32のツールチェーンに含まれるxtensa-esp32-elf-addr2line -pfiaC -e sketch.ino.elf 0x400d1a29 0x400d0f6eというコマンドで、これらのアドレスを関数名と行番号に変換できます。PlatformIOを使っているなら、platformio.iniにmonitor_filters = esp32_exception_decoderの行を追加するだけで、シリアルモニタでクラッシュ出力が自動的にデコードされます。
ストラッピングピン:GPIO0、GPIO2、GPIO12、GPIO15が起動を妨げる仕組み
ESP32は起動時に、特定のピンのレベルを見てどう起動するかを決めます。これらはストラッピングピンと呼ばれ、意味を持つのはリセットの瞬間のレベルだけです。起動が終われば通常のGPIOとして使えます。問題になるのは、そのピンにつないだセンサー、ボタン、モジュールが、起動時にピンを間違ったレベルに引っぱってしまう場合です。するとボードはプログラムをまったく開始しない、書き込みモードに入る、フラッシュを読めない、のいずれかになります。外から見ると、これも「ESP32が起動しない」「リセットを繰り返す」ように見えます。ボードのピン配置の全体は「ピン配置」ツールで確認できます。
| ピン | 内部抵抗 | 起動時の役割 | 間違ったレベルだとどうなる? |
|---|---|---|---|
| GPIO0 | プルアップ | LOWで書き込み(ダウンロード)モード、HIGHで通常起動 | boot:0x3 (DOWNLOAD_BOOT)と書き込み待ちになり、プログラムが動かない |
| GPIO2 | プルダウン | 書き込みモードに入るにはLOWかフローティングである必要がある。通常起動には関係しない | HIGHに引っぱる回路があると書き込みができない |
| GPIO12 (MTDI) | プルダウン | フラッシュの電源電圧を選ぶ:LOWで3.3 V、HIGHで1.8 V | HIGHだと3.3 Vのフラッシュに正しく電源が供給されず、ボードが起動しないかリセットループに入る |
| GPIO15 (MTDO) | プルアップ | LOWでROMの起動メッセージを止める | 起動時の行がシリアルモニタに表示されず、診断が難しくなる |
| GPIO5 | プルアップ | SDIOのタイミングを設定する | 開発ボードで問題になることはまれ |
新しいボードではストラッピングピンが異なります。ESP32-S3ではGPIO0、GPIO3、GPIO45、GPIO46が、ESP32-C3ではGPIO2、GPIO8、GPIO9が起動に影響します。ESP32-S3は無印ESP32と同じXtensaの例外名を使います。一方、RISC-VベースのESP32-C3では、LoadProhibitedの代わりにLoad access fault、StoreProhibitedの代わりにStore access faultと表示され、EXCVADDRの役割はMTVALレジスタが担います。ブラウンアウトのメッセージはどのモデルでも同じです。
MicroPythonでのESP32とRaspberry Pi Pico
ESP32をMicroPythonでプログラムしても、ブラウンアウトはまったく同じように起こります。問題はソフトウェアではなくハードウェアにあるからです。一方、Pythonコードのエラーは、Guru Meditationの代わりに読みやすいTracebackとして表示され、ボードがクラッシュすることはめったにありません。リセット原因はmachine.reset_cause()で読み取れますが、注意が必要です。MicroPythonのESP32版はブラウンアウトを独立した値として返さず、電源投入と同じPWRON_RESETとして報告します。何も触っていないのにボードがPWRON_RESETで起動を繰り返すなら、やはり電源を疑ってください。以下のコードはESP32用です。Raspberry Pi Picoでは、machineモジュールが知っているリセットの種類はもっと少なくなります。PicoではVSYS入力の昇降圧レギュレーターが1.8〜5.5 Vのどの電圧でも動作するので、ブラウンアウトの問題はずっとまれです。ボードについて知るにはRaspberry Pi Picoとは?を参照してください。
1import machine, gc, time
2
3# Reset nedenleri (ESP32 için) / reset causes (for ESP32)
4causes = {
5 machine.PWRON_RESET: "PWRON: guc acilisi ya da brownout / power-on or brownout",
6 machine.HARD_RESET: "HARD: cokme ya da machine.reset() / crash or machine.reset()",
7 machine.WDT_RESET: "WDT: watchdog",
8 machine.DEEPSLEEP_RESET: "DEEPSLEEP: derin uykudan uyanma / deep sleep wake-up",
9 machine.SOFT_RESET: "SOFT: Ctrl+D ile yazilim reseti / soft reset with Ctrl+D",
10}
11print("Reset nedeni / reset cause:", causes.get(machine.reset_cause(), "bilinmiyor / unknown"))
12
13while True:
14 gc.collect() # çöp toplayıcıyı çalıştır / run the garbage collector
15 print("Bos RAM / free RAM:", gc.mem_free(), "bayt / bytes")
16 time.sleep(5)よくある間違いと対策
| 症状 | 考えられる原因 | 対策 |
|---|---|---|
| Wi-Fiが始まるとBrownout detector was triggeredが出る | ケーブル、ポート、レギュレーターがピーク電流に追いつけない | 短くて太いケーブル、1 AのACアダプター、3V3とGNDの間に470 µF + 100 nF |
| サーボが回ったりリレーが切り替わったりするとリセットされる | 負荷がESP32と電源ラインを共有している | 負荷を別の5 V電源から供給し、GNDをつなぐ |
| 電池が少し減るとリセットが始まる | AMS1117のドロップアウト | LDOか昇降圧コンバーターを使う |
| Guru Meditation、EXCVADDR 0x00000000 | ヌルポインタ | 使う前にポインタを確認する |
| Stack canary watchpoint triggered (loopTask)(スタックオーバーフロー) | loop()のスタックがいっぱい | 大きな配列をstaticにするか、SET_LOOP_TASK_STACK_SIZEを使う |
| task_wdt: - IDLE (CPU 0)(タスクウォッチドッグ) | コア0で待ち時間のないループ | ループにvTaskDelayを追加する |
| Interrupt wdt timeout on CPU1(割り込みウォッチドッグ) | ISRに時間がかかりすぎる | ISRではフラグを立てるだけにし、IRAM_ATTRを使う |
| ボードが無反応、boot:0x3と書き込み待ち | 起動時にGPIO0がLOW | GPIO0の回路を外し、ENを押す |
| ボードがまったく起動しない、フラッシュ読み取りエラー | 起動時にGPIO12がHIGH | GPIO12のプルアップやモジュールを別のピンに移す |
| 書き込み時にFailed to connect to ESP32(ESP32に接続できない) | GPIO2がHIGH、またはボードが書き込みモードに入れない | GPIO2を空け、書き込み開始時にBOOTを押し続ける |
ボードが安定して動いたら:次は何をする?
- 電池にやさしいデバイス:ESP32のディープスリープを使えば、測定の合間にボードを眠らせ、消費電流をマイクロアンペアの範囲に抑えられます。
- 無線アップデート:ESP32のOTAで、USBケーブルなしでコードを書き込めます。
- リアルタイムのダッシュボード:ESP32のWebSocketで、センサーのデータをブラウザでリアルタイムに確認できます。
- 通知を送る:ESP32のTelegramボットを使えば、ボードが再起動するたびにリセット原因をスマホで受け取れます。
- 光の演出:別の電源を使って、WS2812B NeoPixelテープを安全に駆動しましょう。
ESP32の再起動の問題のほとんどは、電源の問題か、数行のバグのあるコードのどちらかです。メッセージを正しく読めれば、どちらもすぐに解決できます。ボードの基本をおさらいするにはESP32とは?を参照してください。BlueGraysアプリには、LED、サーボ、NeoPixelテープ、Wi-Fi接続、Webサーバー、OTA更新、ディープスリープを使ったESP32のプロジェクトがあり、それぞれにステップごとの回路図とコードが付いています。ここで学んだ電源と診断のテクニックを、実際のプロジェクトで試せます。
よくある質問
ESP32の「brownout detector was triggered」はどうすれば直りますか?
このエラーは、ESP32の電源電圧が一瞬だけ約2.43 Vを下回ったことを意味します。まず短くて太いUSBケーブルと、1 A以上の定格の5 V電源を使い、次に3V3とGNDの間に100〜470 µFの電解コンデンサと100 nFのセラミックコンデンサを追加してください。サーボ、リレー、LEDテープなどの負荷は別の電源から供給し、GNDを互いにつなぎます。
ESP32が再起動を繰り返すのはなぜですか?
最も多い原因は、電源不足(ブラウンアウト)、ソフトウェアのクラッシュ(Guru Meditation)、ウォッチドッグのタイムアウト、起動時に間違ったレベルになっているストラッピングピンです。シリアルモニタを115200ボーで開いてENを押すと、リセット直前の最後のメッセージと起動時のrstコードから原因がわかります。esp_reset_reason()関数を使えば、コードからも読み取れます。
Guru Meditation Error LoadProhibitedとはどういう意味ですか?
ESP32のコードが無効なメモリアドレスから読み取ろうとしたことを意味します。EXCVADDRが0やごく小さな数なら、ほぼ確実にヌルポインタが使われています。Backtraceの行をESP Exception Decoderに貼り付けると、エラーが起きたファイルと行がわかります。
ESP32のブラウンアウト検出は無効にできますか?
できます。WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0)の行で無効になりますが、警告が出なくなるだけです。電圧の落ち込みは続くので、フラッシュのデータが壊れたり、ボードが別のエラーでクラッシュしたりすることがあります。問題が電源から来ていることを確かめるために、短時間だけ使ってください。
ESP32の「Task watchdog got triggered」はどうすれば直りますか?
このメッセージは、監視対象のタスクが5秒間CPUを手放さなかったことを意味します。Arduinoでは、一覧にたいていコア0のIDLEタスクが表示されます。自分のタスクのループにvTaskDelay()を追加し、センサーを待つときはタイムアウトを使い、Webサーバーのコールバックでdelay()や長い処理を避けてください。ウォッチドッグを止めるとエラーは見えなくなりますが、原因は直りません。
ESP32にはどんな電源が必要ですか?
Espressifは、ESP32に少なくとも500 mAを供給できる電源を推奨しています。実際には、5 V・1 AのACアダプターなら安全な余裕があります。ESP32-CAMを使う場合や、サーボやLEDテープなどの負荷を追加する場合は、2 A以上の定格の電源を選び、できればそれらの負荷を別のラインから供給してください。
この記事を読んだ後に作れるプロジェクト
全プロジェクトを見る
ESP32でLEDを点滅
ESP32のGPIO出力の基本を学びます。デジタルピンからLEDを点灯・消灯させて、点滅させます。Arduino UNOの定番のLED点滅プロジェクトのESP32版です。Arduino環境で使う基本的な点滅コマンドは同じですが、ESP32のピン配置と3.3 VのGPIO電圧レベルはArduino UNOとは異なります。

ESP32 Wi-Fi STA接続(LCDディスプレイ付き)
このプロジェクトでは、ESP32をステーション(STA)モードで家庭のWi-Fiネットワークに接続し、安全で障害に強い接続を実現します。SSIDとパスワードを別のsecrets.hファイルに保存してGitへの流出を防ぎ、自動再接続の仕組みを実装します。RSSIによる信号強度とIPアドレスはI2C LCDディスプレイに表示します。内蔵LEDは状態表示に使います。実運用を想定した接続管理の仕組みです。

ESP32サーボモーター(ポテンショメーター制御)
このプロジェクトでは、ポテンショメーターでSG90サーボモーターを制御します。ポテンショメーターから読み取ったアナログ値を0~180度の範囲に変換し、サーボの角度を調整します。ESP32Servoライブラリは、ESP32の出力に対応した適切なGPIOでサーボ信号を生成します。このプロジェクトではGPIO 18を使います。ロボットアームやカメラ用ジンバル、自動ドアの機構に使われる基本的な制御方法です。

ESP32 WS2812B NeoPixel LEDテープの制御
WS2812B(NeoPixel)は、1本のデータピンで連結したLEDを個別に制御できるスマートなRGB LEDです。このプロジェクトでは、8個のLEDを備えたWS2812BテープをAdafruit_NeoPixelライブラリで制御し、滑らかな虹色のアニメーションを作ります。ESP32の3.3VロジックレベルはほとんどのWS2812Bのロットで問題なく動作しますが、不安定な場合に備え、470Ωの直列抵抗と1000µFのコンデンサを使う対策も紹介します。アニメーションは、HSV色空間の色相(hue)をずらして作ります。

ESP32非同期Webサーバー(LCDにIPアドレスを表示)
このプロジェクトでは、ESP32を同じWi-Fiネットワーク上のスマートフォンやパソコンから操作できる非同期Webサーバーにします。ESPAsyncWebServerライブラリを使うと、従来のWebServer.hのように1つのリクエストで処理がブロックされず、複数のリクエストを同時に処理できます。ブラウザーでESP32のIPアドレスを開き、ON/OFFボタン(/onと/offのルート)を押すと、GPIO 5に接続したLEDを点灯・消灯できます。

ESP32のディープスリープで節電(OLED + RTC RAMカウンター)
ESP32のディープスリープモードは、CPUとほとんどの周辺機器を停止し、消費電流を約10 µAまで抑えます。電池で動くIoT機器に欠かせない機能です。このプロジェクトでは、タイマー(15秒)、外部ピン(EXT0、ボタンはGPIO 33)、タッチ(T0/GPIO 4)の3種類の復帰方法を使います。RTC_DATA_ATTRを付けた変数はディープスリープ中も保持され、起動回数、タイマーによる復帰回数、ボタンによる復帰回数を継続して記録できます。復帰するたびにSSD1306 OLEDディスプレイに復帰理由と回数を表示します。

ESP32 OTA更新:無線でファームウェアを書き込み(LCD付き)
このプロジェクトでは、USBケーブルを使わず、Wi-Fi経由でESP32に新しいファームウェアを書き込む方法を学びます。ArduinoOTAライブラリを使うと、ESP32がネットワーク上で検出され、Arduino IDEの「ネットワークポート」タブにbluegrays-otaとして自動表示されます。初回はUSBで書き込み、その後の更新はすべて無線で行えます。設置済みのデバイスにとって非常に重要な機能です。I2C LCDディスプレイには、Wi-Fiの状態、IPアドレス、ホスト名、OTA書き込みの進捗率、エラーメッセージをトルコ語と英語で表示します。

ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)
PIRセンサーが動きを検知するとブザーとLEDを作動させ、同時にTelegramで持ち主に即座に通知するスマート防犯アラームです。アラームが繰り返される頻度を抑えるため、30秒のクールダウン(待ち時間)を設けています。この仕組みはPIRの検知が正しいかを確認したり、誤検知をなくしたりするものではありません。家の防犯、ガレージの監視、倉庫や作業部屋などに使えます。ESP32-CAMなら写真付きの通知を追加でき、通常のESP32 DevKit V1ではテキストで通知します。
ほかの記事
BlueGraysアプリ
この回路をスマホでステップごとに作る
回路図、コード、部品リストを1つの画面で確認できます。Arduino、ESP32、Picoのプロジェクトを200件以上用意しています。

