Boot failures · Build options · ESP32-S3

Octal Flash Mode Enabled boot loop on the ESP32-S3

Your S3 resets in a loop, and every cycle prints a line about octal flash. It reads like an error, so that is where everyone starts looking. It is not an error — it is the chip reporting a fact about your hardware. The real fault is two board options away, and the giveaway is which line comes after it.

Applies to: ESP32-S3 (N8R8, N16R8, XIAO S3, …) Arduino IDE & ESP-IDF Published 8 August 2026

What the loop looks like

ESP-ROM:esp32s3-20210327
Build:Mar 27 2021
rst:0x8 (TG1WDT_SYS_RST),boot:0x8 (SPI_FAST_FLASH_BOOT)
Octal Flash Mode Enabled
For OPI Flash, Use Default Flash Boot Mode
mode:DOUT, clock div:1
load:0x3fce3808,len:0x44c
entry 0x403c98d8
<-- and around again, forever

Two things in there are worth separating carefully, because conflating them is what sends people down the wrong path:

  • Octal Flash Mode Enabled and For OPI Flash, Use Default Flash Boot Mode are informational. The ROM prints them on any board with octal flash, working or not. A healthy N16R8 prints them on every single boot.
  • rst:0x8 (TG1WDT_SYS_RST) is the failure: Timer Group 1's watchdog reset the chip because startup stopped making progress.

You are seeing a normal boot banner attached to a startup hang. Searching the octal line finds you nothing useful because that line is not broken.

The octal line is not the error

Confirm this for yourself in ten seconds: flash the stock Blink sketch to the same board with the same board settings. If it also loops, your application is irrelevant and the problem is in the build configuration — which is where this article applies. If Blink runs fine and prints the octal lines happily on the way up, the octal lines were never your problem and you should look at your own code instead.

Reading the reset reason. TG1WDT_SYS_RST means the app started and stopped progressing. Compare with RTCWDT_RTC_RESET plus invalid header, which means the app never started at all — a different failure with a different fix.

What actually hangs

An ESP32-S3 module can carry PSRAM — extra RAM in the same package as the chip — wired one of two ways: quad (four data lines) or octal (eight). Octal is faster and is what the larger modules use. The same distinction exists for the flash chip itself: QSPI or OPI.

Neither wiring is auto-negotiated. The startup code initialises PSRAM using whichever mode it was compiled for, and the default in both Arduino IDE and ESP-IDF is quad, with PSRAM disabled entirely in the Arduino case.

Point quad-mode initialisation at octal-wired PSRAM and it does not fail cleanly. It issues commands the memory does not answer and waits for a response that never comes, inside early startup, before the RTOS is running and before any console output is flushed. Timer Group 1's watchdog is the only thing still watching. It fires, the chip resets, and the cycle repeats — giving you a loop with no error message, because nothing ever got far enough to report one.

ROM boot OK app entry OK PSRAM init (quad) hardware is octal waits forever TG1WDT reset forever
Fig. 1 — The loop. Everything up to PSRAM init works, which is why the banner looks so healthy.

Decode your module: N16R8 and friends

Espressif encodes the memory configuration in the module part number, printed on the metal can. It is the fastest way to know what you have:

Reading an ESP32-S3 module part number
SuffixMeansBuild options needed
N4, N8, N164/8/16 MB flash, no PSRAMPSRAM disabled — the defaults are correct
R22 MB quad PSRAMPSRAM: enabled (quad)
R88 MB octal PSRAMPSRAM: OPI PSRAM
N16R816 MB flash + 8 MB octal PSRAMPSRAM: OPI PSRAM; often Flash Mode: OPI
N8R88 MB flash + 8 MB octal PSRAMSame as above with 8 MB flash size

An R8 in the part number is the whole diagnosis. If your module says R8 and your build has PSRAM set to "Disabled" or "QSPI PSRAM", you have found it.

Flash mode and PSRAM mode are independent. A module can have octal PSRAM in package while its flash is wired quad — a normal layout, not an edge case. So "set both to OPI" is not always right: set PSRAM from the R-suffix, and only set Flash Mode to OPI if the flash itself is octal. Getting Flash Mode wrong in the other direction produces its own boot failure.

Boards that are not sold under an Espressif module number need looking up individually. The Seeed XIAO ESP32-S3, for instance, carries octal PSRAM and needs the OPI PSRAM setting despite nothing in its name saying R8.

The fix in Arduino IDE

Under Tools, with your S3 board selected:

Tools menu settings for an N16R8-class board
MenuSet to
PSRAMOPI PSRAM
Flash ModeOPI 80MHz (only if the flash is octal; otherwise leave QIO)
Flash Size16MB (128Mb) — match the N-suffix
Partition SchemeAny scheme that fits the flash size you just set

On the command line, the same choices ride on the FQBN as a comma-separated suffix:

arduino-cli compile \
  --fqbn esp32:esp32:esp32s3:FlashMode=opi,PSRAM=opi,FlashSize=16M \
  MySketch
Change a board option, rebuild everything. These settings affect the bootloader, not just your sketch. In Arduino IDE, changing them and clicking Upload rebuilds what is needed; if you are scripting, clear the build directory rather than trusting an incremental build to notice.

The fix in ESP-IDF

Same two decisions, different names. Run idf.py menuconfig after idf.py set-target esp32s3:

menuconfig equivalents
SettingWhereConfig symbol
Enable PSRAM Component config → ESP PSRAM CONFIG_SPIRAM=y
PSRAM mode → Octal Component config → ESP PSRAM → SPI RAM config → Mode CONFIG_SPIRAM_MODE_OCT=y
Octal flash Serial flasher config CONFIG_ESPTOOLPY_OCT_FLASH=y
Flash size Serial flasher config → Flash size CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y

Put them in sdkconfig.defaults so they survive a fullclean and travel with the project:

# sdkconfig.defaults — ESP32-S3 N16R8
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_ESPTOOLPY_OCT_FLASH=y
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y

One option worth knowing if you ship the same firmware to boards with and without PSRAM: CONFIG_SPIRAM_IGNORE_NOTFOUND=y lets a build that expects PSRAM boot anyway on a board that has none, instead of aborting. It does not rescue a mode mismatch — octal hardware in quad mode still hangs — but it makes a single binary tolerant of the simpler difference.

Why auto-detection can't save you

A reasonable question at this point is why the toolchain does not just work this out. Plug the board in, ask what it is, build accordingly.

The problem is that chip detection answers a narrower question than you need. esptool talks to the ROM loader and gets back ESP32-S3. That is the die. It says nothing about how much PSRAM is in the package or how it is wired, because those live in the module, not the chip — and every N8, N16R8 and XIAO S3 reports the identical chip name.

There is a better signal: the S3 records its in-package flash and PSRAM configuration in eFuse, and you can read it over the same serial connection. That is what we ended up doing in ESP-GenUI — read the PSRAM capability field during board detection and preselect OPI PSRAM when it reports octal, rather than defaulting to quad and letting the user discover the difference from a boot loop. If you maintain your own flashing scripts, it is worth the afternoon: the field distinguishes octal 8 MB from quad 2 MB from none at all, which is exactly the choice you would otherwise be asking the user to make from a part number they may not be able to read without a magnifier.

Absent that, treat the module part number as required input, not a detail: write it in your README next to the build command.

Verify it worked

Booting is necessary but not sufficient — a board can boot with PSRAM silently absent, and you will only find out when a large allocation fails weeks later. Print the size at startup:

// Arduino
Serial.printf("PSRAM: %u bytes\n", ESP.getPsramSize());
// Expect ~8388608 on an R8 module. 0 means PSRAM did not initialise.

// ESP-IDF
ESP_LOGI(TAG, "PSRAM: %u",
         (unsigned) heap_caps_get_total_size(MALLOC_CAP_SPIRAM));

A board that boots and reports 0 has PSRAM disabled in the build, not broken hardware. That is the state you are left in if you fixed the loop by turning PSRAM off — which does stop the crash, and quietly costs you 8 MB of RAM.