Boot failures · GPIO · ESP32-S3

ESP32-S3 boot loop with no crash log: check your GPIO numbers

The board resets in a loop. There is no panic handler output, no backtrace, and not one line of your own Serial.print — just rst:0x8 (TG1WDT_SYS_RST) over and over. The usual suspects (power, flash settings, a bad library) all check out. Before you spend an evening on build options, look at which GPIO numbers your code writes to in setup().

Applies to: ESP32-S3 (and S2) Toolchain: any Published 8 August 2026

The symptom, precisely

What makes this failure distinctive is what is missing:

ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x8 (TG1WDT_SYS_RST),boot:0x8 (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fce3808,len:0x44c
entry 0x403c98d8
<-- nothing. then it all repeats.

Read that carefully, because it is good news disguised as bad. The entry 0x403c98d8 line means the second-stage bootloader ran and handed control to your application. The image is fine. The flash settings are fine. Your code started — and then, a few instructions in, the chip reset without a word.

TG1WDT_SYS_RST is Timer Group 1's watchdog firing. That is the watchdog that guards early startup: if the application stops making progress before the RTOS is up, this is what resets it. There is no panic message because there was no exception — nothing crashed. The CPU simply stopped getting anywhere.

The absence of a backtrace is the diagnostic. A Guru Meditation Error with a stack trace is a normal bug, and you debug it normally. A silent watchdog reset right after entry means execution stopped in a way the exception handler never saw — and on the S3, the classic way to do that is to break the chip's own access to its flash.

Why a pinMode() can reset the chip

An ESP32-S3 does not hold your program in RAM. It executes it from external SPI flash through a cache: instructions are fetched over the SPI bus on demand, transparently, for the entire life of the program. The same is true of PSRAM if your board has it and your code uses it.

That bus is wired to GPIO pins. They are not "reserved" in any way the compiler understands — pinMode(26, OUTPUT) is a perfectly legal call and will compile without a murmur. What it does at runtime is take a pin the flash chip is talking on and reconfigure it as a general-purpose output.

The consequence is immediate and total. The next time the CPU needs an instruction that is not already in cache, the fetch comes back as garbage. There is no code left to run — including the code that would have handled the fault. So nothing is printed, nothing is caught, and a few milliseconds later the watchdog resets the chip. Then it boots, reaches your setup(), drives the pin again, and does it all over.

ESP32-S3 CPU + cache instruction fetch GPIO 26-32 (SPI0/1) SPI FLASH your program pinMode(26, OUTPUT); bus broken → no next instruction
Fig. 1 — The program is running over those pins. Taking one for your own use cuts the branch you are standing on.

This also explains a detail that confuses people: the failure is often intermittent-looking across builds. If the offending pinMode() happens to sit in a stretch of code already pulled into cache, execution can limp on for a few more instructions and reach a different failure. Add a line, change the layout, and the same bug expresses itself slightly differently.

The pins to avoid on an ESP32-S3

ESP32-S3 GPIO hazards
GPIOSeverityWhy
26–32 Fatal The SPI0/1 bus to the flash chip. Driving any of these breaks instruction fetch immediately.
33–37 Fatal on octal modules Used by octal (OPI) flash and octal PSRAM. On an N16R8-class module these are in use; on a module with neither, they can be free. Treat as unusable unless you have checked your exact module.
22–25 Fatal These GPIO numbers do not exist on the S3. The call silently does nothing useful and the pin you meant is untouched — which produces the equally baffling "my relay never switches" bug.
19, 20 Caution The native USB D−/D+ pads. Usable as GPIO only if you are not using native USB — and that includes the USB-Serial-JTAG console you are probably flashing over.
43, 44 Caution Default UART0 TX/RX. Taking them costs you the serial console you need to debug everything else.
0, 3, 45, 46 Caution Strapping pins, read at reset. An external pull on these changes boot mode, flash voltage or JTAG source. Safe as outputs after boot; risky if something holds them at reset.
Do not trust the silkscreen. Many S3 dev boards print a pin header position next to a GPIO number that the module does not expose, or that is committed to flash on that particular variant. The module datasheet for your exact part number is the authority — the board's silkscreen is a suggestion.

Why ESP32 tutorials walk you straight into it

Here is the part that makes this bug a rite of passage rather than a rare accident.

On the original ESP32, GPIO 25, 26 and 27 are excellent pins. They are broken out on every dev board, they have no strapping duties, two of them are the DAC outputs, and as a result they appear in an enormous number of tutorials, library examples, wiring diagrams and Fritzing sketches as the obvious choice for a relay, an LED or a servo.

On the ESP32-S3, GPIO 26 and 27 are flash bus pins and GPIO 25 does not exist.

So the single most-copied ESP32 pin assignment on the internet is precisely the one that bricks an S3 into a silent boot loop. You do not have to make a mistake to hit this — you only have to follow a tutorial written for the older chip, which is most of them. The code compiles, flashes, and produces a board that resets forever with no explanation.

This applies to generated and AI-written code too. A language model that learned ESP32 wiring from the same tutorial corpus will also reach for 25/26/27, and will do so just as confidently for an S3. If you are pasting in code you did not write, check its pin constants against the table above before you flash it.

Pins that are safe to start from

For a general-purpose output — a relay, an LED, a servo signal, a NeoPixel data line — these are unencumbered on a standard ESP32-S3 module:

GPIO  4  5  6  7        // no boot duty, no bus duty
GPIO 15 16 17 18        // same; 15/16 are also the USB-JTAG alt pins on some boards
GPIO  8  9 10 11 12 13  // generally fine; 8 is an ADC/strapping-adjacent pin on some modules

Add a peripheral constraint on top of that: ADC2 channels do not work while Wi-Fi is active on any ESP32 family, so an analog input on a Wi-Fi project should use an ADC1 pin. On the S3 that is GPIO 1–10.

The general rule that saves the most time: pick your pins from your module's datasheet once, write them down as named constants at the top of the file, and never inline a raw GPIO number. The bug is much easier to spot in a block of #define RELAY_1 4 than scattered through two hundred lines of setup.

Finding the offending pin in your own code

  1. Grep for the numbers before you do anything else. Search your project for 25, 26, 27 and anything in 33–37 appearing as a pin argument. In most cases you will find it in under a minute and can stop here.
    grep -nE "pinMode|digitalWrite|attach|setPin|ledcAttach" src/*.cpp *.ino
  2. Comment out all pin initialisation in setup(). If the board then boots and reaches your first Serial.print, the cause is confirmed and only the identity of the pin is left.
  3. Reintroduce them in halves. A binary search over the pin-init block finds the exact line in three or four flashes.
  4. Print before you touch anything. Put a Serial.begin(115200) and a distinctive marker as the literal first statements of setup(), with a short delay(200) after. Whether that marker appears tells you whether you are dying before or after your own code starts — which separates this problem from a build-options problem in one step.
Watch out for pins your libraries choose. Display, SD-card, camera and Ethernet libraries frequently carry default pin maps that were written for the original ESP32. A library you never passed a pin number to may still be driving one. Check the defaults in its header rather than assuming an unconfigured library is inert.

In our own tool this is now a pre-flight check rather than a debugging session: ESP-GenUI keeps a table of per-chip pin hazards and flags any diagram whose hardware nodes land on one before you compile, because we lost a genuinely embarrassing amount of time to exactly this — chasing flash and PSRAM build options on a board that was resetting for a completely different reason. The lesson generalises past our tool: validate pin numbers against the target chip at build time, because the runtime failure gives you nothing to work with.

The same trap on other ESP32 chips

Every ESP32 variant reserves a different block for its flash bus, so the "safe" pins from one chip are hazards on another. The families in current use:

Flash-bus and unusable GPIOs by chip
ChipFlash / PSRAM busAlso worth avoiding
ESP32 (classic)6–1134–39 are input-only; strapping on 0, 2, 12, 15
ESP32-S226–3222–25 absent; USB on 19, 20
ESP32-S326–3722–25 absent; USB 19, 20; UART0 43, 44
ESP32-C312–17USB on 18, 19; strapping on 2, 8, 9
ESP32-C624–30USB 12, 13; UART 16, 17; strapping 4, 5, 8, 9, 15
ESP32-C212–17UART 19, 20; strapping 8, 9
ESP32-H215–21USB 26, 27; UART 23, 24

Note how thoroughly the numbers move: GPIO 26 is a flash pin on the S2 and S3, a flash pin on the C6, a USB pin on the H2, and a perfectly good DAC output on the classic ESP32. There is no portable "safe pin" across the family, which is why porting a working project from one ESP32 to another is a pin-map exercise before it is anything else.