- esp32-boot-chain-comparison.md: IDF vs MCUboot->U-Boot comparison covering memory regions, cache/MMU sequences, image formats, and full feature matrix. - esp32-mcuboot-uboot-bringup-report.md: complete bring-up log with root cause analysis (IROM at 0x40800000, I-bus/D-bus split, cache bus masks, 64KB alignment requirement).
5.3 KiB
ESP32 MCUboot → U-Boot Boot Chain — Bring-Up Report
Date: 2026-07-16 Target: ESP32-D0WDQ6-V3 (Xtensa LX6) Status: ✅ Working
Overview
UniversalisOS boot chain on ESP32: ROM (1st stage) → MCUboot (2nd stage) → U-Boot.
MCUboot validates and loads U-Boot from the primary flash slot. U-Boot runs from IROM (flash-cached at 0x40800000) with data in DRAM at 0x3FFAF000.
The Fundamental Discovery
ESP32 IROM is at 0x40800000, not 0x400D0000
The ESP32 memory map (from esp-hal-3rdparty/components/soc/esp32/include/soc/ext_mem_defs.h):
| Region | Bus | Address Range | Purpose |
|---|---|---|---|
| IRAM0 | IBUS0 | 0x400D0000-0x40400000 | SRAM / configurable cache |
| IRAM1 | IBUS1 | 0x40400000-0x40800000 | SRAM / configurable cache |
| IROM0 | IBUS2 | 0x40800000-0x40C00000 | Flash-cached execution (dedicated) |
| DROM0 | DBUS0 | 0x3F400000-0x3F800000 | Flash-cached data |
| DRAM1 | DBUS1 | 0x3F800000-0x3FC00000 | PSRAM / configurable data cache |
The ESP32's IROM (where flash-MMU-mapped code executes) is at 0x40800000. This differs from newer ESP32 variants (S3, C3, etc.) where IROM starts at 0x400C0000 or 0x400D0000.
D-Bus Cannot Write to I-Bus Addresses
The ESP32 has a split bus architecture:
- I-Bus (0x40000000+): instruction fetch, ROM, IRAM, IROM
- D-Bus (0x3FFxxxxx+): data access, DRAM, DROM
A memcpy((void*)0x40080000, ...) uses D-bus to write to an I-bus address.
The D-bus routes such writes through the flash cache controller, which requires
MMU page table entries for the target address. Without valid entries, the write
either goes nowhere or faults. Direct IRAM loading via memcpy is impossible
on ESP32 from software.
The Solution: IROM Execution via Flash MMU
Espressif's own bootloader ("CACHE_APP" segments) and all IDF applications run from IROM — the flash MMU maps flash pages to IROM virtual addresses. The CPU fetches instructions via I-cache → MMU → SPI flash controller. No memcpy needed.
Our MCUboot uses cache_flash_mmu_set() to map U-Boot's flash pages to
IROM0 at 0x40800000, then jumps to the entry point there.
The Cache Bus Mask Problem
The ESP32 DPORT register DPORT_PRO_CACHE_CTRL1_REG (0x3FF00044) controls
which cache buses are enabled. After bootloader_reset_mmu() (called during
MCUboot init), only DROM0 bus (bit 0) is unmasked. The IROM0 bus (bit 2)
remains masked — meaning instruction fetches from IROM never reach the
flash cache.
The fix: clear bits 2 and 3 (IROM0 and IROM1 masks) before re-enabling cache.
Image Format
U-Boot is wrapped with an esp_image_load_header_t (magic 0xACE637D3) that
MCUboot's Espressif port understands:
[ MCUboot header 32B ] magic=0x96f3b83d
[ Load header 96B ] magic=0xACE637D3 entry=0x40801854 irom_map=0x40800000
[ Padding to 64KB ] ESP32 MMU requires 64KB-aligned flash addresses
[ IROM data ] U-Boot .text + .ResetVector.text
[ DRAM data ] U-Boot .rodata + .data
[ MCUboot TLV ] SHA256 signature
The mk_esp_mcuboot_image.py tool wraps U-Boot's ELF into this format.
Files Changed
MCUboot (portugalfuturista/mcuboot)
| File | Change |
|---|---|
boot/espressif/port/esp_loader.c |
IROM flash-MMU mapping, cache bus unmask, DRAM offset fix |
boot/espressif/CMakeLists.txt |
Relaxed IDF HAL version check (6.0.0 → 6.1.0) |
boot/espressif/port/esp32/bootloader.conf |
Added CONFIG_ESPRESSIF_BOOTLOADER_MCUBOOT |
U-Boot (portugalfuturista/universalis-uboot)
| File | Change |
|---|---|
arch/xtensa/include/asm/arch-esp32/core.h |
Rewritten: IROM base 0x40800000, VECBASE, vectors |
board/espressif/esp32/u-boot.lds |
Rewritten: IROM execution, vectors discarded |
include/configs/esp32.h |
XTENSA_SYS_TEXT_ADDR → 0x40800400 |
arch/xtensa/cpu/start.S |
Moved _start to .text, stack in DRAM |
tools/mk_esp_mcuboot_image.py |
New: ELF→MCUboot image wrapper with 64KB alignment |
board/espressif/esp32/Kconfig |
Added CONFIG_2ND_STAGE_BOOT symbol |
arch/xtensa/config.mk |
Restored to original |
Flash Layout (4MB ESP32)
| Offset | Size | Content |
|---|---|---|
| 0x1000 | ~30KB | MCUboot bootloader |
| 0x20000 | ~213KB | U-Boot (primary slot) |
| 0x170000 | 1.5MB | Secondary slot (OTA) |
Verified Boot Flow
ROM (mask ROM @ 0x40000000)
│ Loads MCUboot segments from flash @ 0x1000, jumps to entry
│
└─ MCUboot
│ bootloader_init() → reset MMU, init SPI flash
│ main() → do_boot()
│ esp_app_image_load():
│ 1. Load DRAM via bootloader_mmap + memcpy → 0x3FFAF000
│ 2. Cache_Read_Disable(0) + Cache_Flush(0)
│ 3. cache_flash_mmu_set(0, 0, 0x40800000, 0x30000, 64KB, 2)
│ 4. DPORT unmask IROM0/IROM1 buses
│ 5. Cache_Read_Enable(0) + isync
│ 6. Jump to 0x40801854 (_start)
│
└─ U-Boot 2025.04
│ _start: 'X' → UART, VECBASE → 0x40800000, DRAM zero, reloc
│ board_init_f → board_init_r → U-Boot prompt
│
└─ U-Boot prompt: loads UniversalisOS
Reference
docs/esp32-boot-chain-comparison.md— Full IDF vs our chain comparisondocs/esp32-mcuboot-uboot-strategy.md— Original strategy documentesp-hal-3rdparty-rel-master.c/components/soc/esp32/include/soc/ext_mem_defs.h— Official memory map