# Espressif Boot Strategy: MCUboot (+ U-Boot) → UniversalisOS **Audience**: Maestro Fabio. **Status**: Strategy / research (no code changes yet). **Inputs studied**: - Zephyr `sysbuild` docs (main), `boards/espressif/*`, `boards/common/esp32.board.cmake` - `rtos_ref/hal_espressif` (Espressif HAL module: `zephyr/`, `components/bootloader_support`, `components/bootloader`, `zephyr/port/boot/esp_image_loader.c`, `zephyr/scripts/partitions/esp_genpartition.py`) - `mcuboot` repo (our fork `portugalfuturista/mcuboot`): `boot/espressif/main.c`, `port/esp_loader.c`, `port/esp32/bootloader.conf` - In-tree: `microkernel/ports/esp32/{bootstub.c,bootstub_minimal.S,bootstub_entry.S,uos_loader.h}`, `microkernel/Makefile`, `docs/esp32-boot-chain-audit.md`, `XTENSA_AUDIT.md` --- ## 1. Reality check: U-Boot on Espressif 🟡 This is the single fact that shapes the whole design. | SoC family | Arch | U-Boot support | Viability as a loader | ||---|---|---|---| | ESP32 / ESP32-S2 / ESP32-S3 | Xtensa LX6/LX7 | **Experimental, not mainline** | 🟡 commitment: estamos a portar | | ESP32-C3 / C6 / H2 | RISC-V | **Mainline** (since ~2023) | ✅ real | | ESP32-P4 | RISC-V | Partial / under review | 🟡 | | ESP32-C5 | RISC-V | not upstream | 🔴 | **Decisão do Maestro Fabio:** U-Boot em Xtensa ESP32 é uma realidade — não saltamos, não simplificamos. Estamos a fazer um fork Xtensa do U-Boot. Isto significa portar drivers, fixar cache/MMU, excepções, e todo o ecossistema (partition table, spi flash, env, GPIO, timer, WDT). O trabalho vive em `portugalfuturista/universalis-uboot`. MCUboot, by contrast, **supports every Espressif target** (Xtensa + RISC-V) via `boot/espressif`. So: - **"MCUboot only" works on ALL targets** — it is the proven Zephyr path. - **"MCUboot + U-Boot" is only sensible on RISC-V parts** (C3/C6/H2). On Xtensa we would be maintaining a non-mainline U-Boot fork for zero benefit (UOS is a bare-metal HV; it does not need a U-Boot pre-OS env). **Recommendation**: treat U-Boot as an *optional* rich-loader stage **only for the RISC-V line**. Default secure-boot chain for every target = `ROM → MCUboot → UOS`. --- ## 2. The three candidate chains ``` CHAIN A — MCUboot → UOS (development/test stepping stone, Xtensa only) ROM (mask, fixed) → MCUboot @ 0x1000 (2nd-stage, esptool elf2image 0xE9 format) validates signature, swap/scratch, picks primary → UOS app @ slot0 (mcuboot-signed, esp_image_load_header 0xace637d3) esp_loader.c copies IRAM/DRAM segs, jumps to entry *** Use Chain A for GDB stub bring-up and µ-kernel development only *** *** Chain B is the FINAL target for all Espressif targets *** CHAIN B — MCUboot → U-Boot → UOS (MANDATORY, ALL ESPRESSIF TARGETS) ROM → MCUboot @ 0x1000 → U-Boot @ slot0 → UOS (loaded by U-Boot) U-Boot = the mcuboot "application image"; UOS = a payload U-Boot chainloads. No shortcuts. No skipping U-Boot. Full boot chain verification required. - Xtensa: fork não-mainline em porting (consola, cache, vectors, drivers) - RISC-V: mainline, mais fácil — fazer depois do Xtensa CHAIN C — in-tree bootstub (QEMU-only debugging, NOT for HW) ROM → bootstub @ 0x1000 (own minimal 2nd-stage, watches-off + MMU map) → UOS app @ 0x10000 (plain 0xE9 image, mcuboot-style bounds-checked loader) *** QEMU only. Never flash to hardware. *** --- ## 3. Flash memory map (Chain A, the canonical layout) From `mcuboot/boot/espressif/port/esp32/bootloader.conf` **and** Zephyr's `esp_genpartition.py` `scheme_default`: | Offset | Size | Content | Source of truth | |---|---|---|---| | `0x0000` | `0x1000` | secure-boot digest area (if SB v1) | IDF | | `0x1000` | ~`0x7000`-`0xF000` | **MCUboot** (2nd stage, `0xE9` img) | `CONFIG_ESP_BOOTLOADER_OFFSET=0x1000` | | `0x8000` | `0x1000` | partition table (magic `0x50AA`) | hardware-fixed | | `0x10000` | — | app factory / **slot0 (image-0)** | Zephyr default (`boot` partition scheme) | | `0x20000` | `0x150000` | **slot0** primary (mcuboot-repo default) | `CONFIG_ESP_IMAGE0_PRIMARY_START_ADDRESS` | | `0x170000` | `0x150000` | slot1 secondary | mcuboot-repo default | | `0x3E0000` | `0x1F000` | scratch | mcuboot-repo default | > ⚠️ **Offset discrepancy to pin down before flashing real HW**: Zephyr's > partition scheme puts `slot0` at `0x10000`; the mcuboot-repo `bootloader.conf` > puts it at `0x20000`. Decide ONE scheme and derive every offset from the > *generated* `partitions.bin` + `build/mcuboot/zephyr/zephyr.bin` offsets. > Do not hand-compute. The generator (`esp_genpartition.py`) is the single > source of truth. The MCUboot app image is **not** a raw `0xE9` image. MCUboot prepends `esp_image_load_header_t` (magic `0xace637d3`, 96 B) describing IRAM/DRAM/ IROM/DROM/LP regions; `esp_loader.c::esp_app_image_load()` validates each destination against `esp_ptr_in_iram/dram` and copies, then jumps to `entry_addr`. Our `uos_loader.h` already mirrors this discipline. --- ## 4. How Zephyr wires it (the mechanics to copy) `west build --sysbuild -b esp32s3_devkitc hello_world` with board `Kconfig.sysbuild`: ``` choice BOOTLOADER default BOOTLOADER_MCUBOOT endchoice ``` produces domains: `mcuboot/`, `/`, `domains.yaml`. Sysbuild: 1. Builds `` with `CONFIG_BOOTLOADER_MCUBOOT=y` → app becomes mcuboot-signed. 2. Builds `mcuboot` domain (the Espressif port) → `zephyr.bin` at `0x1000`. 3. Generates the partition table via `esp_genpartition.py` from the SoC dts. 4. `west flash` flashes bootloader + app in order (per `domains.yaml`). The HAL's `zephyr/CMakeLists.txt` opts the app into the Espressif flash section layout only when **not** `CONFIG_MCUBOOT` (i.e. MCUboot builds itself with `CONFIG_MCUBOOT_ESPRESSIF` → early `return()`). `soc_init.c` prints "MCUboot 2nd stage bootloader" vs "MCUboot Application image" accordingly. **We do NOT need Zephyr to adopt this.** We can reproduce the identical chain with our own build glue (see §6) because all the real logic lives in `mcuboot/boot/espressif` + `esp_loader.c`, which we already fork. --- ## 5. What MCUboot-Espressif actually does (so we trust it) `boot/espressif/main.c::main()`: 1. `bootloader_init()` — clocks, flash, cache, WDT (mirrors IDF 2nd stage). 2. Optional secure-boot key generation + eFuse burn (V2, `ABS_DONE_0`). 3. Optional flash-encryption key gen + in-place encrypt + eFuse burn. 4. `boot_go(&rsp)` — MCUboot core: validate primary/secondary, decide swap. 5. `do_boot()` → `start_cpu0_image()` (and `start_cpu1_image()` under `CONFIG_ESP_MULTI_PROCESSOR_BOOT` for SMP) → jumps to app entry. Build/packaging (`boot/espressif/CMakeLists.txt` ~L466/L492): ``` esptool.py --chip elf2image --min-rev \ --flash-freq ``` So MCUboot is compiled to an ELF, then `elf2image` turns it into the `0xE9` ROM-loadable image at `0x1000`. **That is the trick that lets a fixed ROM boot a full bootloader.** Our UOS app, when mcuboot is the loader, must be signed with `imgtool` and carry the `esp_image_load_header`. --- ## 6. What we already have in-tree (reuse, don't rebuild) `microkernel/ports/esp32/`: - `bootstub.c` + `bootstub_minimal.S` + `bootstub_entry.S` — a from-scratch 2nd-stage: disables flashboot WDTs, maps flash page 1 via cache MMU, loads the `0xE9` app at `0x10000`, jumps. **QEMU-proven** (per `XTENSA_AUDIT.md` + memory: "UOS Xtensa: QEMU✅"). - `uos_loader.h` / `bootstub.c` — mcuboot-style segment loader with exact IRAM/DRAM bounds checks (`uos_ptr_in_iram/dram`, `uos_region_contains`). - `Makefile` already builds `TARGET=esp32` (QEMU) and `TARGET=esp32hw` (`esptool elf2image --flash-mode dio --flash-size 2MB`, flash @ `0x10000`). So Chain C (bootstub) is **already working in emulation**. Chain A (MCUboot) needs us to (a) build our `mcuboot` fork for the target and (b) sign+package the UOS app with `imgtool` + the esp load header. Chain B (U-Boot) needs a RISC-V U-Boot port + a UOS load command. --- ## 7. Estratégia (mandatada por Maestro Fabio) **Fase 0 — Pin the layout (do first, blocks real HW).** - Pick ONE partition scheme. Generate it with `esp_genpartition.py` for the target flash size; record `slot0` offset. **Fase 1 — Chain A funcional (MCUboot → UOS, stepping stone).** - Build MCUboot/boot/espressif for the target. - Build UOS app; sign with imgtool + prepend `esp_image_load_header`. - Flash at slot0. Verify in QEMU + HW. - **GDB stub mandatório em todos os testes** (break 0,0 → T05 via UART). **Fase 2 — Chain B (MCUboot → U-Boot → UOS, OBRIGATÓRIO).** - Portar U-Boot para Xtensa ESP32 no fork `universalis-uboot`. - Fix consola garbled (Phase 1 do master plan: UART DM_SERIAL, cache/MMU, vectors). - ESP32 drivers (SPI flash, partition table, env, GPIO, timer, WDT). - bootelf / load command para carregar UOS image. - Auto-boot via CONFIG_BOOTCOMMAND. - Chain A continua a existir como GDB-stub development path. **Fase 3 — Chain C (bootstub) eliminated.** - Bootstub era para QEMU. Com Chain A funcional, não precisamos de bootstub. - Remover `microkernel/ports/esp32/bootstub*` após Chain A+B estarem estáveis. **Fase 4 — SMP / APP-CPU bring-up (MCUboot + U-Boot).** - Reuse `CONFIG_ESP_MULTI_PROCESSOR_BOOT` path (`start_cpu1_image`) no MCUboot. - U-Boot eventualmente carrega as duas CPUs. --- ## 8. Tooling required (present on this host ✅) - `esptool` / `esptool.py` → `~/.local/bin` ✅ - IDF activate scripts (`activate_idf_release-v5.4/5.5/6.0`) ✅ - Xtensa toolchain `~/.espressif/tools/xtensa-esp-elf/...` (referenced by `microkernel/Makefile`) ✅ - `mcuboot` fork present at `~/portugalfuturista/mcuboot` ✅ (has `boot/espressif`) - `imgtool` (mcuboot signing) — needs `pip install imgtool` or use `mcuboot/scripts/imgtool.py`. --- ## 9. Open decisions (resolvidas) 1. **Target SoC(s)** — Xtensa (ESP32/S3) é o alvo primário. RISC-V (C3/C6/H2) depois do Xtensa. Chain B (U-Boot) para TODOS. 2. **U-Boot é obrigatório, não opcional.** "mcuboot+uboot" não era shorthand — é a arquitetura de boot final. Chain A (MCUboot → UOS) é stepping stone de desenvolvimento apenas. 3. **Secure boot / flash encryption** — primeiro bring-up das cadeias, depois ativar segurança. Ver docs/esp32-master-plan.md para o plano detalhado de 5 fases do U-Boot. --- ## 10. Risks / blockers 🔴🟡 - 🔴 **Xtensa U-Boot is not mainline** — if U-Boot is mandated on ESP32/S3 we own a fork. Recommend declining on Xtensa. - 🟡 **Offset mismatch** between Zephyr scheme (`slot0@0x10000`) and mcuboot-repo conf (`slot0@0x20000`). Must be resolved to one scheme before HW flash. - 🟡 **MCUboot image size** can exceed the `0x1000`–`0x8000` gap if the partition table sits at `0x8000`; size the mcuboot build to fit (Zephyr handles this; our standalone build must too). - 🟡 **appcpu / SMP** needs the `esp_image_load_header` LP-core regions for P4/C5; ensure `uos_loader.h` covers LP_IRAM/LP_DRAM if we target those. - ✅ ROM is immutable and expects `0xE9` at `0x1000` — both MCUboot and our bootstub already satisfy this.