technique
Debugging resets and crashes
Why a board reboots, loops, or goes silent, and a method for finding out: reset reasons, watchdogs, boot modes, and leaving yourself notes in memory that survives a reset.
Before this
This page assumes you are comfortable with:
- techniqueThe boot sequenceWhat runs between power-on and your first line of code: the ROM, the second-stage bootloader, the app image, and how to read the boot log.
- prerequisiteMemory maps and registersHow a chip gives every RAM byte and every hardware control a numbered address, so reading or writing an address talks to the hardware.
Why you need this
The hardest ESP32 bugs print no error. The board reboots every few seconds, goes quiet, or works after flashing and fails after the reset button. This is stage 6 of the pipeline, "Debug": turning "it keeps resetting" into a specific cause.
The idea
A reset restarts the processor from the beginning. Something always causes it, and the chip usually tells you what. The skill is to collect that evidence before guessing.
Read the boot log first
Right after a reset, the chip's ROM (the fixed code built into the chip) prints a few lines over serial before your program runs. Two fields matter most, explained on The boot sequence:
rst:is the reset reason: what caused this reset.boot:is the boot mode: where the ROM decided to go next.
These lines are from the author's notes on the ESP32-C6:
| Boot log field | Meaning | Where to look |
|---|---|---|
rst:0x15 (USB_UART_HPSYS) |
The host computer's activity on the USB line caused the reset | The program on the computer, not the board |
boot:0xc (SPI_FAST_FLASH_BOOT) |
The ROM is starting the app from flash | Normal |
boot:0x4 (DOWNLOAD...) then "waiting for download" |
The ROM entered its download mode | Your app will not run; something sent the chip to the flasher |
A warm reset restarts the processor while parts of the chip keep power and some state; a cold reset (pressing the RST button, which pulls the chip's EN pin, or a power cycle) starts from much less. They take different paths, and a bug can live on only one of them.
Watchdogs
A watchdog is a hardware timer that resets the chip unless the software "feeds" it (restarts its count) in time. It exists so a hung program recovers by itself. Espressif's ESP-IDF watchdog documentation for the C6 describes three, and the author's notes add a fourth that matters for raw programs:
| Watchdog | Guards against | When it fires |
|---|---|---|
| Interrupt watchdog (IWDT) | Interrupt handlers blocked from running too long | Panic with "Interrupt wdt timeout on CPU0", then reset |
| Task watchdog (TWDT) | A FreeRTOS task running without yielding | By default a warning and backtrace; a panic and reset if configured |
| RTC watchdog | A hang during boot, from power-on until your main function | Resets the chip; ESP-IDF disables it just before your main function |
| Super watchdog (on the C6, per the author's notes; registers in the LP_WDT block) | Not covered by ESP-IDF's watchdog guide; armed when the ROM hands over | Resets the chip; an ESP-IDF build handles it, a raw image must disable it itself |
Panics, brownouts, and tracebacks
In C with ESP-IDF, a crash such as an illegal instruction or a bad memory access goes to the panic handler. Espressif's fatal-errors guide gives the format: Guru Meditation Error: Core 0 panic'ed (Illegal instruction). Exception was unhandled. By default it prints the registers and a backtrace (the chain of function calls that led to the crash), then restarts. idf.py monitor turns the backtrace addresses into function names and line numbers.
A brownout is the supply voltage sagging below what the chip needs. The same guide says the C6's brownout detector is on by default and prints "Brownout detector was triggered" before resetting. On a CYD, the author's notes limit external add-ons to about 40 mA from the 3.3 V rail; a sensor or LED strip that draws more is a classic brownout source.
In MicroPython, an uncaught exception prints a Python traceback and drops to the REPL instead of resetting. machine.reset_cause() returns one of machine.PWRON_RESET, HARD_RESET, WDT_RESET, DEEPSLEEP_RESET, or SOFT_RESET (per the MicroPython machine documentation), so a program can log why it restarted.
Case study: the C6 reset loop
This is a real debugging session from the ESP32 Inspector project, recorded in the author's knowledge base. The Inspector can run a user's RISC-V assembly on an ESP32-C6 as a raw RAM image: no ESP-IDF, no second-stage bootloader, just a small startup stub that sets the stack pointer, disables the watchdogs, and calls the program. The image looped. It turned out to be two separate loops.
Loop 1: every browser connection. When a browser opened the serial port, the chip reset with rst:0x15 (USB_UART_HPSYS) and came back in boot:0x4, the ROM's download mode. By default, a reset triggered over the USB line re-enters the downloader, so the app never ran again. The ESP-IDF second-stage bootloader quietly changes that default; the raw image had no bootloader, so it had to do it itself. The fix is one register write: set LP_AON_USB_RESET_DISABLE, bit 31 of LP_AON_USB_REG at 0x600B1044. After that, USB-line resets came up boot:0xc (SPI_FAST_FLASH_BOOT) and the image rebooted into itself. These lines are excerpted from the Inspector's startup stub (CRT0_S in startup.ts), RISC-V assembly for the C6, hardware-proven:
li t2, 0x80000000 # BIT31 = USB_RESET_DISABLE
li t0, 0x600B1044 # LP_AON_USB_REG
lw t1, 0(t0)
or t1, t1, t2
sw t1, 0(t0)
It reads the register, sets one bit with OR, and writes it back, leaving the other 31 bits alone (see Binary and hexadecimal).
Loop 2: the RST button. Browser connections now worked, but pressing the physical RST button, a cold reset, still produced a silent reboot about every 3.2 s. The cause was a wrong key. The super watchdog's registers are write-protected: you must first write an unlock key to a protect register. The stub used 0x8F1D312A, the key from the classic ESP32. On the C6 the key is 0x50D83AA1. With the wrong key the disable write was silently ignored, and the super watchdog stayed armed. The fixed lines, from the same stub (RISC-V, hardware-proven):
li t0, 0x600B1C20 # LP_WDT_SWD_WPROTECT: unlock super-WDT
li t1, 0x50D83AA1 # super-WDT key on C6 = LP_WDT_SWD_WKEY_VALUE.
# Was 0x8F1D312A (the old-ESP32 key): the
# wrong key silently rejects the write, so the
# super-WDT stays armed and fires ~3.2 s after
# a COLD reset (physical RST) -> reset loop.
# ...
sw t1, 0(t0)
li t0, 0x600B1C1C # LP_WDT_SWD_CONFIG
lw t2, 0(t0)
li t1, 0x40040000 # SWD_DISABLE (bit30) | SWD_AUTO_FEED_EN (bit18)
or t2, t2, t1
sw t2, 0(t0)
Why had warm resets hidden this? Resetting with esptool runs esptool's flasher stub first, and that stub had already disarmed the super watchdog. So "warm works, cold loops" was the clue: the difference between the two paths was exactly the work someone else had been doing for the program. A third change came out of the same session: the stub had a speculative re-initialization of the USB hardware, and on a cold boot it broke output entirely (the app ran but sent nothing). The ROM had already brought USB up, so the re-initialization was removed.
Worked example
How do you tell "the startup stub hangs" from "the program runs and then something kills it" when the board prints nothing? You leave notes in memory that survives a reset.
The C6 has a few always-on registers in its low-power domain. LP_AON_STORE0 at 0x600B1000 is a plain storage word that, per the author's notes, keeps its value through a warm reset. The method: stamp a different number into it at each stage of the stub, let the board reset, then read the number back with esptool. Its connection resets the chip warmly, so the note survives to be read.
This stamp was written for this page to show the shape (RISC-V, not hardware-tested); the session put one after each stage:
li t0, 0x600B1000 # LP_AON_STORE0
li t1, 3 # this stage was reached
sw t1, 0(t0)
The procedure, with esptool 5's hyphenated command names; these commands were written for this page:
esptool --chip esp32c6 write-mem 0x600B1000 0
(press the physical RST button and wait for the loop)
esptool --chip esp32c6 read-mem 0x600B1000
Clearing to 0 first proves that any value you read was written after the RST press. With markers 1 to 5 placed in stub order (one placement that fits the stub's stages):
| Value read back | Last stage reached | What it proves |
|---|---|---|
| 0 | none | The stub never started, or died before reaching the first stamp |
| 1 | stack pointer set | A hang or fault in the USB reset write |
| 2 | USB reset disable written | A problem in the timer-group or RTC watchdog writes |
| 3 | timer-group and RTC watchdogs disabled | A problem in the super watchdog writes |
| 4 | super watchdog write done | A problem just before calling the program |
| 5 | about to call the program | Startup completed; the program was running when the reset came |
The session read back 5. Combined with a reset every 3.2 s, that meant startup finished and a watchdog was killing a running program, not a startup fault. Every watchdog the stub disabled was then a suspect, and the one using a different key from the others was the culprit.
The author's notes also record the theories refuted during the USB loop investigation, with evidence, so nobody chases them again:
| Theory | Evidence against it |
|---|---|
| A sticky "force download boot" flag | LP_AON_SYS_CFG (0x600B1034) bit 30 read 0 while stuck |
| The BOOT pin stuck low | GPIO_IN bit 9 read high |
| Power or cable brownout | Every idle state was stable on the same port and cable |
| The ROM downloader resetting itself by watchdog | An abandoned downloader sat stable for over 15 s |
In an ESP32 project
Debugging closes the pipeline and points back into it. Most reset loops are stage 3 problems: boot mode, the bootloader, and what a raw image must do itself (see The boot sequence and RISC-V assembly on the ESP32-C6 and P4). The C6 loop also involved stage 4: the native USB port that resets the chip when a computer opens it is covered on Serial over UART and USB. The ESP32 Inspector's monitor, reachable from its board detection page, is now built so that the page itself can never drive an endless reset cycle.
Common mistakes
- Blaming the board for a host-caused reset.
rst:0x15means the computer's USB activity reset the chip. Symptom: a loop that stops when you close the serial monitor. The author's notes recommend watching whether the USB device is present without opening the port, which separates chip-side loops from client-driven ones. - Testing only warm resets. esptool's reset cannot reproduce a cold RST press. Symptom: works after every flash, loops after the button or a power cycle.
- Copying a register value from a sibling chip. Keys, bases, and bit positions differ between the ESP32, C6, and P4. Symptom: a write that does nothing, with no error.
- Mistaking a blocked write for a hang. On native USB a raw program's output waits until the host reads. Symptom: the program seems frozen with no monitor open.
- Feeding the brownout. Too much current from the 3.3 V rail. Symptom: "Brownout detector was triggered" when the radio or a peripheral switches on.
- Fixing by guesswork. A speculative change, like the USB re-initialization here, can add a second bug. Symptom: a new silent failure on a path that used to work.
Cost
The marker method costs a few instructions per stage and one esptool round trip per experiment, and nothing in a shipped build once the stamps are removed. The stub's fixes cost a handful of instructions at boot. The expensive resource is the maker's time, and two cheap habits save most of it: read the rst: and boot: fields before forming a theory, and record each refuted theory with its evidence.
Going further
- The ESP-IDF "Watchdogs" and "Fatal Errors" guides, for the timeout settings and the panic output in full.
- The reset and clock chapter of the ESP32-C6 Technical Reference Manual, for every reset reason and the low-power watchdogs.
- RISC-V assembly on the ESP32-C6 and P4, for the startup stub in context.
- Over-the-air updates, for ESP-IDF's automatic rollback after a crashing update.
Back to ESP32 development: assembly, C, MicroPython, and CircuitPython