universalisos/docs/esp32-mcuboot-uboot-bringup-report.md
Fábio Coutada 3961c6c42a docs: ESP32 boot chain comparison and bring-up report
- 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).
2026-07-16 16:44:25 +01:00

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 comparison
  • docs/esp32-mcuboot-uboot-strategy.md — Original strategy document
  • esp-hal-3rdparty-rel-master.c/components/soc/esp32/include/soc/ext_mem_defs.h — Official memory map