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.
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 EnabledandFor OPI Flash, Use Default Flash Boot Modeare 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.
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.
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:
| Suffix | Means | Build options needed |
|---|---|---|
N4, N8, N16 | 4/8/16 MB flash, no PSRAM | PSRAM disabled — the defaults are correct |
R2 | 2 MB quad PSRAM | PSRAM: enabled (quad) |
R8 | 8 MB octal PSRAM | PSRAM: OPI PSRAM |
N16R8 | 16 MB flash + 8 MB octal PSRAM | PSRAM: OPI PSRAM; often Flash Mode: OPI |
N8R8 | 8 MB flash + 8 MB octal PSRAM | Same 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.
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:
| Menu | Set to |
|---|---|
| PSRAM | OPI PSRAM |
| Flash Mode | OPI 80MHz (only if the flash is octal; otherwise leave QIO) |
| Flash Size | 16MB (128Mb) — match the N-suffix |
| Partition Scheme | Any 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
The fix in ESP-IDF
Same two decisions, different names. Run idf.py menuconfig after
idf.py set-target esp32s3:
| Setting | Where | Config 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.