I'm implementing a custom bare-metal bootloader for the original ESP32 (Xtensa LX6) and I'm stuck at the final step: jumping from the bootloader to an application image stored in external Flash at physical address 0x110000.
The application image is valid (magic 0xE9 is correct, headers parse fine), but the jump itself either triggers an exception or executes garbage. I need to understand the correct hardware sequence for performing this jump without any libraries — no ESP-IDF, no esp_rom_*, no HAL, only direct register access.
Chip: ESP32 (original, Xtensa LX6, dual-core)
Flash size: 4 MB
App image location: Physical Flash address 0x110000
Compiler: ESP_IDF (bare-metal flags)
Framework: PlatformIO — pure C/C++ + inline asm
I need to answer three things:
How to convert the physical Flash address 0x110000 into the correct virtual IROM address that the CPU can execute from?
What is the exact hardware sequence for MMU reconfiguration + cache invalidation on Xtensa LX6?
What CPU state (stack pointer, registers, interrupts, windowed register file) must be prepared before executing jx?
Step 1 — Compute MMU page
Code: Select all
#define FLASH_PAGE_SIZE 0x10000UL // 64 KB
uint32_t phys_page = 0x110000 / FLASH_PAGE_SIZE; // = 0x11Code: Select all
#define IBUS_MMU_TABLE 0x3FF10000UL
volatile uint32_t* mmu = (volatile uint32_t*)IBUS_MMU_TABLE;
mmu[0] = (phys_page & 0xFF) | (1 << 8); // page + valid bitCode: Select all
#define PRO_CACHE_CTRL 0x3FF14000UL
volatile uint32_t* ctrl = (volatile uint32_t*)PRO_CACHE_CTRL;
*ctrl |= (1 << 0);
while (*ctrl & (1 << 0)) { /* spin */ }Code: Select all
uint32_t entry_addr = /* parsed from image header */;
__asm__ __volatile__(
"movi a1, 0x3FFD0000\n" // reset stack pointer
"mov a2, %0\n" // load entry into a2
"jx a2\n" // jump
:
: "r"(entry_addr)
: "a1", "a2", "memory"
);Either a Load/Store Prohibited exception, or
Execution of garbage instructions, or
Silent hang right after jx a2.
Q1. What is the correct formula to derive the virtual IROM address from a physical Flash address on ESP32?
Is it 0x400D0000 + (phys_addr & 0xFFFF) or something else?
Q2. Is writing directly to the MMU table at 0x3FF10000 enough, or does the TRM require a specific sequence like: disable cache → configure MMU → enable cache → wait for ready flag?
Q3. On Xtensa LX6 with windowed registers, do I need to:
disable all interrupts first?
reset the register window (e.g. wsr a0, WINDOWBASE)?
clear a0 (return address) before the jump?
Q4. Are there any reserved memory regions that the bootloader must not touch?
ESP32 Technical Reference Manual — chapters on External Memory, Cache, MMU
https://www.espressif.com/sites/default ... ual_en.pdf
esp_image_loader.c from ESP-IDF (I understand the logic, but I want to reimplement it from scratch)
Xtensa ISA documentation for jx and cache instructions.
Any of the following would be extremely helpful:
A minimal working snippet of a bare-metal jump to an app in external Flash on ESP32.
Pointers to the exact TRM sections describing the required cache/MMU sequence.
A reference implementation (even in another Espressif bootloader) that I can study.
A list of common pitfalls when doing this kind of low-level jump on Xtensa LX6.
Thanks a lot for your time!
