universalisos/docs/esp32-mcuboot-uboot-strategy.md
Fábio Coutada b5b29759af docs: AGENTS.md + strategy doc — U-Boot é obrigatório, GDB stub mandatório em todos os testes
AGENTS.md:
- Rule 2 strengthened: U-Boot é compromisso firme com lista de tasks
- New Rule 10: GDB/GDB stub obrigatório em todos os testes (QEMU + HW)
- Architecture Reality Check: added ESP32+U-Boot entry
- Verification Workflow: completo com comandos GDB para cada target

docs/esp32-mcuboot-uboot-strategy.md:
- Chain B (U-Boot) agora é MANDATÓRIA para todos os targets
- Chain A é stepping stone de desenvolvimento apenas
- Chain C (bootstub) será eliminada
- Open decisions marcadas como resolvidas
- Tabela de viabilidade: Xtensa passou de 🔴 risky para 🟡 commitment
2026-07-17 14:10:09 +01:00

224 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/`, `<app>/`, `domains.yaml`. Sysbuild:
1. Builds `<app>` 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 <target> elf2image --min-rev <rev> \
--flash-freq <f> <CONFIG_ESP_BOOTLOADER_OFFSET>
```
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.