All uncommitted work from ESP32 GDB stub development session. Includes: - GDB stub (uos_gdbstub.c/.h/_entry.S) - Startup vector table rewrite - ESP32 HAL integration files - Boot chain design docs - AGENTS.md absolute rules (commit before refactor, no unauthorized changes) - All prior deepseek session work This commit prevents further data loss. No claims of correctness.
698 lines
26 KiB
Markdown
698 lines
26 KiB
Markdown
# ESP32 Complete Boot Chain Audit
|
||
|
||
**Source**: ESP-IDF v5.5.4 + esp-qemu
|
||
**Audited**: components/bootloader_support, components/spi_flash, esp-qemu hw/xtensa/esp32.c, hw/misc/esp32_dport.c
|
||
**Target**: Bare-metal UniversalisOS on ESP32 (Xtensa LX6 dual-core)
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
1. [Stage 0: ROM Bootloader](#stage-0-rom-bootloader)
|
||
2. [Stage 1: 2nd-Stage Bootloader (ESP-IDF)](#stage-1-2nd-stage-bootloader)
|
||
3. [Image Header Format (.bin)](#image-header-format)
|
||
4. [Partition Table Format](#partition-table-format)
|
||
5. [Flash Cache/MMU](#flash-cachemmu)
|
||
6. [Secure Boot & Flash Encryption](#secure-boot--flash-encryption)
|
||
7. [Minimum Bare-Metal Boot Requirements](#minimum-bare-metal-boot-requirements)
|
||
8. [Complete Address Map Reference](#complete-address-map-reference)
|
||
|
||
---
|
||
|
||
## Stage 0: ROM Bootloader
|
||
|
||
The ROM bootloader is in mask ROM (not flash). It executes from power-on/reset. It is **not user-modifiable**.
|
||
|
||
### What the ROM Bootloader Does
|
||
|
||
1. **Reads strap pins** to determine boot mode:
|
||
- If strap mode has bit 4 set (`strap & 0x10`) or strap is `0x0c` → **flash boot mode** (normal)
|
||
- Otherwise → download mode
|
||
- Source: `esp32.c:156`: `bool flash_boot_mode = ((strap_mode & 0x10) || (strap_mode & 0x1f) == 0x0c);`
|
||
|
||
2. **Reads the image header at `0x1000`** (CONFIG_BOOTLOADER_OFFSET_IN_FLASH):
|
||
- The ROM expects the ESP32 image format (magic byte `0xE9`)
|
||
- It reads the 24-byte `esp_image_header_t` from flash offset `0x1000`
|
||
- From the header, it learns: segment count, SPI flash mode/speed/size, entry address
|
||
|
||
3. **Configures SPI flash** based on the image header fields:
|
||
- `spi_mode` (QIO/QOUT/DIO/DOUT)
|
||
- `spi_speed` (div by 1/2/3/4)
|
||
- `spi_size` (1MB to 128MB)
|
||
- Sets up the WP pin and drive strength from header fields
|
||
|
||
4. **Loads segments** to RAM:
|
||
- Iterates through each segment header (`load_addr`, `data_len`)
|
||
- Segments with load addresses in IRAM/DRAM ranges are **copied to RAM**
|
||
- Segments with addresses in IROM/DROM ranges are **left in flash** (mapped via cache later)
|
||
|
||
5. **Hardware state left for the 2nd-stage bootloader:**
|
||
- **CPU0 (PRO CPU)** running, CPU1 (APP CPU) in reset/stalled
|
||
- **Stack**: Valid ROM stack (top at `0x3FFE_3F20` = `SOC_ROM_STACK_START`)
|
||
- **SPI flash configured** (mode/speed/size from header)
|
||
- **Flash cache**: Initially enabled for ROM (DROM0 unmasked in normal boot, masked in serial boot)
|
||
- **Watchdogs**: RWDT and MWDT0 flashboot protection **auto-enabled**
|
||
- RWDT: RTC WDT flash boot mode
|
||
- MWDT0: Timer Group 0 WDT flash boot mode
|
||
- These will reset the chip if the bootloader takes too long
|
||
- **Entry point**: PC set to `entry_addr` from the image header
|
||
- **CPU clock**: May be at default/low speed (bootloader will increase it)
|
||
|
||
### ROM Bootloader → call_start_cpu0()
|
||
|
||
The ROM bootloader loads the 2nd-stage bootloader image from `0x1000` and jumps to its entry point. In ESP-IDF, the entry point is `call_start_cpu0()` in `bootloader_start.c`.
|
||
|
||
---
|
||
|
||
## Stage 1: 2nd-Stage Bootloader
|
||
|
||
Entry point: `call_start_cpu0()` in `components/bootloader/subproject/main/bootloader_start.c`
|
||
|
||
### Call Flow
|
||
|
||
```
|
||
call_start_cpu0() [bootloader_start.c:26]
|
||
├─ bootloader_before_init() (optional hook)
|
||
├─ bootloader_init() [bootloader_esp32.c:164]
|
||
│ ├─ bootloader_init_mem() (memory init)
|
||
│ ├─ bootloader_clear_bss_section()
|
||
│ ├─ bootloader_common_vddsdio_configure()
|
||
│ ├─ bootloader_check_rated_cpu_clock()
|
||
│ ├─ bootloader_clock_configure() (set CPU to 240MHz/80MHz APB)
|
||
│ ├─ bootloader_console_init() (UART, now can use ESP_LOG)
|
||
│ ├─ bootloader_reset_mmu() (full MMU/cache reset)
|
||
│ ├─ bootloader_flash_update_id()
|
||
│ ├─ bootloader_flash_xmc_startup()
|
||
│ ├─ bootloader_read_bootloader_header()
|
||
│ ├─ bootloader_check_bootloader_validity() (verify own image)
|
||
│ ├─ bootloader_init_spi_flash()
|
||
│ ├─ bootloader_check_wdt_reset() (dump WDT reset info if applicable)
|
||
│ ├─ bootloader_config_wdt() (disable flashboot WDT)
|
||
│ └─ bootloader_enable_random()
|
||
│
|
||
├─ bootloader_utility_load_partition_table() [bootloader_utility.c:144]
|
||
│ └─ Reads partition table from 0x8000 (ESP_PARTITION_TABLE_OFFSET)
|
||
│
|
||
├─ bootloader_utility_get_selected_boot_partition()
|
||
│ └─ Determines boot partition (factory/OTA/test)
|
||
│
|
||
└─ bootloader_utility_load_boot_image()
|
||
├─ bootloader_load_image() [esp_image_format.c:313]
|
||
│ └─ image_load() (verify + load segments)
|
||
└─ load_image() → unpack_load_app() → set_cache_and_start_app()
|
||
├─ Configure flash MMU for DROM/IROM
|
||
├─ Enable cache buses
|
||
├─ Cache_Read_Enable(0)
|
||
└─ Jump to entry_addr (function pointer call)
|
||
```
|
||
|
||
### Watchdog Configuration (bootloader_config_wdt)
|
||
|
||
Source: `bootloader_init.c:70-100`
|
||
|
||
```c
|
||
// 1. Disable RWDT flashboot protection
|
||
wdt_hal_write_protect_disable(&rwdt_ctx);
|
||
wdt_hal_set_flashboot_en(&rwdt_ctx, false); // RWDT flashboot OFF
|
||
wdt_hal_write_protect_enable(&rwdt_ctx);
|
||
|
||
// 2. Optionally enable bootloader RWDT (CONFIG_BOOTLOADER_WDT_ENABLE)
|
||
// Default: 9000ms timeout
|
||
|
||
// 3. Disable MWDT0 flashboot protection
|
||
wdt_hal_write_protect_disable(&mwdt_ctx);
|
||
wdt_hal_set_flashboot_en(&mwdt_ctx, false); // MWDT0 flashboot OFF
|
||
wdt_hal_write_protect_enable(&mwdt_ctx);
|
||
```
|
||
|
||
**Key point**: The ROM bootloader enables two flashboot watchdogs:
|
||
- **RWDT (RTC WDT)**: In RTC_CNTL, flash boot protection stage
|
||
- **MWDT0 (Timer Group 0 WDT)**: In TIMG0, flash boot protection stage
|
||
|
||
Both auto-expire after a timeout (~9 seconds for MWDT0). The 2nd-stage bootloader **must disable both** or it will be reset. This is likely why bare-metal code crashes—the watchdogs fire.
|
||
|
||
### MMU/Cache Reset (bootloader_reset_mmu)
|
||
|
||
Source: `bootloader_esp32.c:43-69`
|
||
|
||
```c
|
||
Cache_Read_Disable(0); // Disable cache for CPU0
|
||
Cache_Read_Disable(1); // Disable cache for CPU1
|
||
Cache_Flush(0); // Flush cache for CPU0
|
||
Cache_Flush(1); // Flush cache for CPU1
|
||
mmu_init(0); // Initialize MMU table for CPU0
|
||
mmu_init(1); // Initialize MMU table for CPU1
|
||
|
||
// Clear DROM0 mask (ROM boot leaves it unmasked, serial boot masks it)
|
||
DPORT_REG_CLR_BIT(DPORT_PRO_CACHE_CTRL1_REG, DPORT_PRO_CACHE_MASK_DROM0);
|
||
```
|
||
|
||
### Partition Table Loading
|
||
|
||
Source: `bootloader_utility.c:144-273`
|
||
|
||
1. Memory-maps 0xC00 bytes from `0x8000` (`ESP_PARTITION_TABLE_OFFSET`)
|
||
2. Calls `esp_partition_table_verify()` — checks magic bytes `0x50AA` on each entry
|
||
3. Iterates entries, populates `bootloader_state_t`:
|
||
- `bs->factory` — factory app partition position
|
||
- `bs->ota[N]` — OTA app partition slots
|
||
- `bs->test` — test app partition
|
||
- `bs->ota_info` — OTA data partition
|
||
4. Terminated by entry with type=0xFF, subtype=0xFF
|
||
|
||
### App Image Loading
|
||
|
||
Source: `esp_image_format.c:161` → `image_load()`
|
||
|
||
1. **Read 24-byte header** from partition offset
|
||
2. **Verify magic** = `0xE9` (`ESP_IMAGE_HEADER_MAGIC`)
|
||
3. **Process each segment**:
|
||
- Read 8-byte segment header (`load_addr`, `data_len`)
|
||
- If `should_load(load_addr)` → `memcpy` data from flash-mmap to `load_addr`
|
||
- If `should_map(load_addr)` → skip (will be cache-mapped at startup)
|
||
4. **Checksum**: XOR all words, compare with appended checksum byte (initial value `0xEF`)
|
||
5. **SHA-256** (if `hash_appended` flag set): verify appended 32-byte hash
|
||
6. **Signature** (if secure boot): verify appended signature
|
||
|
||
### should_load() vs should_map() Decision
|
||
|
||
Source: `esp_image_format.c:889-940`
|
||
|
||
```c
|
||
// should_map(): flash-mapped, NOT loaded to RAM
|
||
bool should_map(uint32_t load_addr) {
|
||
bool is_irom = (load_addr >= 0x400D0000) && (load_addr < 0x40400000); // IROM
|
||
bool is_drom = (load_addr >= 0x3F400000) && (load_addr < 0x3F800000); // DROM
|
||
return (is_irom || is_drom);
|
||
}
|
||
|
||
// should_load(): copied to RAM (IRAM/DRAM/RTC)
|
||
bool should_load(uint32_t load_addr) {
|
||
if (should_map(load_addr)) return false; // Flash-mapped segments not loaded
|
||
if (load_addr < 0x10000000) return false; // Reserved (padding, MD5 block)
|
||
// RTC memory skipped during deep sleep wake
|
||
return true;
|
||
}
|
||
```
|
||
|
||
### Jump to Application (set_cache_and_start_app)
|
||
|
||
Source: `bootloader_utility.c:1036-1158`
|
||
|
||
```c
|
||
// 1. Disable cache
|
||
Cache_Read_Disable(0);
|
||
Cache_Flush(0);
|
||
|
||
// 2. Reset MMU table
|
||
mmu_hal_unmap_all();
|
||
|
||
// 3. Map DROM segment (rodata in flash)
|
||
// DROM vaddr: 0x3F400000-0x3F800000
|
||
cache_flash_mmu_set(0, 0, drom_load_addr, drom_addr, 64, drom_page_count);
|
||
cache_flash_mmu_set(1, 0, drom_load_addr, drom_addr, 64, drom_page_count);
|
||
|
||
// 4. Map IROM segment (text in flash)
|
||
// IROM vaddr: 0x400D0000-0x40400000
|
||
cache_flash_mmu_set(0, 0, irom_load_addr, irom_addr, 64, irom_page_count);
|
||
cache_flash_mmu_set(1, 0, irom_load_addr, irom_addr, 64, irom_page_count);
|
||
|
||
// 5. Enable cache buses for both cores
|
||
cache_ll_l1_enable_bus(0, bus_mask);
|
||
cache_ll_l1_enable_bus(1, bus_mask);
|
||
|
||
// 6. Re-enable cache
|
||
Cache_Read_Enable(0);
|
||
|
||
// 7. Jump to entry point
|
||
typedef void (*entry_t)(void) __attribute__((noreturn));
|
||
entry_t entry = ((entry_t) entry_addr);
|
||
(*entry)(); // NEVER RETURNS
|
||
```
|
||
|
||
---
|
||
|
||
## Image Header Format
|
||
|
||
### esp_image_header_t (24 bytes)
|
||
|
||
Source: `esp_app_format.h:81-111`
|
||
|
||
```
|
||
Offset Size Field
|
||
------ ---- -----
|
||
0x00 1 magic = 0xE9 (ESP_IMAGE_HEADER_MAGIC)
|
||
0x01 1 segment_count (max 16)
|
||
0x02 1 spi_mode (0=QIO, 1=QOUT, 2=DIO, 3=DOUT, 4=FAST_READ, 5=SLOW_READ)
|
||
0x03 1 [7:4] spi_speed (0=÷2, 1=÷3, 2=÷4, 0xF=÷1)
|
||
[3:0] spi_size (0=1MB, 1=2MB, 2=4MB, 3=8MB, 4=16MB, ...)
|
||
0x04 4 entry_addr (little-endian, address to jump to)
|
||
0x08 1 wp_pin (WP pin config, 0xEE=disabled by IDF bootloader)
|
||
0x09 3 spi_pin_drv[3] (drive strength for SPI flash pins)
|
||
0x0C 2 chip_id (0x0000=ESP32, 0x0002=S2, 0x0005=C3, ...)
|
||
0x0E 1 min_chip_rev (minimum chip revision)
|
||
0x0F 2 min_chip_rev_full (major*100 + minor)
|
||
0x11 2 max_chip_rev_full
|
||
0x13 4 reserved[4]
|
||
0x17 1 hash_appended (1 = SHA-256 appended after checksum)
|
||
```
|
||
|
||
**Total**: 24 bytes. Verified: `ESP_STATIC_ASSERT(sizeof(esp_image_header_t) == 24)`.
|
||
|
||
### Segment Header (8 bytes each)
|
||
|
||
Source: `esp_app_format.h:117-120`
|
||
|
||
```
|
||
Offset Size Field
|
||
------ ---- -----
|
||
0x00 4 load_addr (destination virtual address)
|
||
0x04 4 data_len (length of segment data, must be 4-byte aligned)
|
||
```
|
||
|
||
### Complete Binary Layout
|
||
|
||
```
|
||
[0x1000 for bootloader, or partition offset for app]
|
||
│
|
||
├── esp_image_header_t (24 bytes)
|
||
│ magic=0xE9, segment_count=N, entry_addr=0xXXXX...
|
||
│
|
||
├── Segment 0
|
||
│ ├── esp_image_segment_header_t (8 bytes): load_addr, data_len
|
||
│ └── data (data_len bytes, 4-byte aligned)
|
||
│
|
||
├── Segment 1
|
||
│ ├── esp_image_segment_header_t (8 bytes)
|
||
│ └── data
|
||
│
|
||
├── ... (up to 16 segments)
|
||
│
|
||
├── Checksum (1 byte)
|
||
│ XOR of all data words (header + segments), initial value 0xEF
|
||
│
|
||
├── [Optional] SHA-256 hash (32 bytes)
|
||
│ Present if hash_appended=1 in header
|
||
│ SHA-256 of everything from magic byte to checksum byte (inclusive)
|
||
│
|
||
└── [Optional] Secure boot signature
|
||
Present if secure boot enabled
|
||
```
|
||
|
||
### Checksum Algorithm
|
||
|
||
```c
|
||
uint8_t checksum = 0xEF; // ESP_ROM_CHECKSUM_INITIAL
|
||
// For each 32-bit word in header + all segment data:
|
||
checksum ^= (word & 0xFF) ^ ((word >> 8) & 0xFF) ^
|
||
((word >> 16) & 0xFF) ^ ((word >> 24) & 0xFF);
|
||
// Append as single byte after last segment
|
||
```
|
||
|
||
---
|
||
|
||
## Partition Table Format
|
||
|
||
### Location
|
||
|
||
- **Offset**: `0x8000` (`CONFIG_PARTITION_TABLE_OFFSET`, default)
|
||
- **Size**: `0x1000` (one flash sector = 4096 bytes)
|
||
- **Max data**: `0xC00` bytes (`ESP_PARTITION_TABLE_MAX_LEN`)
|
||
|
||
### Entry Format (32 bytes each)
|
||
|
||
Source: `esp_flash_partitions.h:94-101`
|
||
|
||
```
|
||
Offset Size Field
|
||
------ ---- -----
|
||
0x00 2 magic = 0x50AA (ESP_PARTITION_MAGIC, little-endian: bytes AA 50)
|
||
0x02 1 type (0x00=APP, 0x01=DATA, 0x02=BOOTLOADER, 0x03=PART_TABLE)
|
||
0x03 1 subtype (APP: 0x00=factory, 0x10+0n=OTA, 0x20=test; DATA: 0x00=ota_data, 0x01=rf, 0x02=wifi, ...)
|
||
0x04 4 offset (flash offset, little-endian)
|
||
0x08 4 size (partition size, little-endian)
|
||
0x0C 16 label (null-terminated ASCII name, padded with 0x00)
|
||
0x1C 4 flags (bit 0: encrypted, bit 1: readonly)
|
||
```
|
||
|
||
### Terminator Entry
|
||
|
||
```
|
||
magic = 0x50AA → Wait, no.
|
||
type = 0xFF
|
||
subtype = 0xFF
|
||
```
|
||
|
||
Actually the terminator uses **type=0xFF, subtype=0xFF** but the magic is still `0x50AA`. No—the standard is:
|
||
- Normal entries: magic = `0x50AA`
|
||
- MD5 checksum entry: magic = `0xEBEB`, followed by 16-byte MD5 of all prior entries
|
||
- End marker: type=0xFF, subtype=0xFF (with valid magic `0x50AA`)
|
||
|
||
### Minimum Partition Table
|
||
|
||
For bare-metal, you need at minimum:
|
||
|
||
```
|
||
Entry 0 (app):
|
||
magic=0x50AA, type=0x00, subtype=0x00 (factory)
|
||
offset=0x10000, size=0x100000 (1MB)
|
||
label="factory\0\0\0\0\0\0\0\0\0"
|
||
flags=0x00000000
|
||
|
||
Entry 1 (terminator):
|
||
magic=0x50AA, type=0xFF, subtype=0xFF
|
||
offset=0, size=0
|
||
label=all zeros
|
||
flags=0
|
||
```
|
||
|
||
### Why You Need a Partition Table
|
||
|
||
Even for bare-metal, the 2nd-stage bootloader **requires** a valid partition table to find the app image. The bootloader calls `bootloader_utility_load_partition_table()` which fails (and reboots) if:
|
||
- The magic bytes don't match `0x50AA`
|
||
- The table verification fails
|
||
|
||
**Alternative**: You can skip the partition table entirely by NOT using the 2nd-stage bootloader. Place your code directly at `0x1000` (the bootloader offset), and the ROM bootloader will load it directly. This is the true "bare minimum" approach.
|
||
|
||
---
|
||
|
||
## Flash Cache/MMU
|
||
|
||
### How Memory-Mapped Flash Works
|
||
|
||
The ESP32 has a flash cache subsystem that maps portions of SPI flash into the CPU address space. This allows executing code directly from flash (XIP - eXecute In Place).
|
||
|
||
### MMU Table
|
||
|
||
Source: `esp32_dport.c` + `esp32_dport.h`
|
||
|
||
**Register addresses** (in DPORT address space, base `0x3FF0_0000`):
|
||
|
||
| Table | Register Base | Size |
|
||
|-------|--------------|------|
|
||
| PRO DROM0 MMU | `0x3FF1_0000` | 64 entries × 4 bytes = 256 bytes |
|
||
| PRO IRAM0 MMU | `0x3FF1_0100` | 64 entries × 4 bytes = 256 bytes |
|
||
| APP DROM0 MMU | `0x3FF1_2000` | 64 entries × 4 bytes = 256 bytes |
|
||
| APP IRAM0 MMU | `0x3FF1_2100` | 64 entries × 4 bytes = 256 bytes |
|
||
|
||
Each MMU entry is 32 bits:
|
||
- **Bits [8:0]**: Physical page number in flash (MMU_ENTRY_MASK = 0x1FF = 512 pages)
|
||
- **Bit [8]**: Invalid flag (0x100 = ESP32_CACHE_MMU_INVALID_VAL)
|
||
- Each page = `0x10000` (64KB) = `ESP32_CACHE_PAGE_SIZE`
|
||
|
||
**Total mappable per region**: 64 pages × 64KB = **4MB** per cache region.
|
||
|
||
### Cache-Mapped Address Ranges (ESP32)
|
||
|
||
Source: `soc.h:170-191`
|
||
|
||
| Region | Start | End | Size | Purpose |
|
||
|--------|-------|-----|------|---------|
|
||
| **DROM** | `0x3F40_0000` | `0x3F80_0000` | 4MB | Data ROM (rodata from flash) |
|
||
| **IROM** | `0x400D_0000` | `0x4040_0000` | 3MB+ | Instruction ROM (code from flash) |
|
||
| **DRAM** | `0x3FFA_E000` | `0x4000_0000` | ~330KB | Internal SRAM (data) |
|
||
| **IRAM** | `0x4008_0000` | `0x400A_A000` | ~168KB | Internal SRAM (instruction) |
|
||
| D/IRAM | `0x3FFE_0000` | `0x4000_0000` | 128KB | Dual-port (byte-swapped) |
|
||
| RTC IRAM | `0x400C_0000` | `0x400C_2000` | 8KB | RTC fast memory (instruction) |
|
||
| RTC DRAM | `0x3FF8_0000` | `0x3FF8_2000` | 8KB | RTC fast memory (data) |
|
||
| RTC DATA | `0x5000_0000` | `0x5000_2000` | 8KB | RTC slow memory |
|
||
| EXTRAM | `0x3F80_0000` | `0x3FC0_0000` | 4MB | PSRAM (if present) |
|
||
|
||
### Cache Control Registers
|
||
|
||
Source: `dport_reg.h`
|
||
|
||
| Register | Address | Key Bits |
|
||
|----------|---------|----------|
|
||
| `DPORT_PRO_CACHE_CTRL` | `0x3FF0_0040` | bit 3: `CACHE_ENABLE` |
|
||
| `DPORT_PRO_CACHE_CTRL1` | `0x3FF0_0044` | bit 0: `MASK_IRAM0`, bit 3: `MASK_DRAM1`, bit 4: `MASK_DROM0` |
|
||
| `DPORT_APP_CACHE_CTRL` | `0x3FF0_0048` | bit 3: `CACHE_ENABLE` |
|
||
| `DPORT_APP_CACHE_CTRL1` | `0x3FF0_004C` | Same bit layout as PRO |
|
||
|
||
When a MASK bit is **set**, that cache region is **disabled** (masked). The bootloader clears these bits to enable mapping.
|
||
|
||
### How MMU Mapping Works (from QEMU source)
|
||
|
||
Source: `esp32_dport.c:238-270`
|
||
|
||
```c
|
||
// For each MMU entry that changed:
|
||
uint32_t mmu_entry = crs->mmu_table[i]; // entry value
|
||
if (mmu_entry & 0x100) { // INVALID flag
|
||
// Fill with illegal access return value
|
||
fill_val = crs->illegal_access_retval;
|
||
} else {
|
||
// Calculate physical flash address
|
||
phys_addr = (mmu_entry & 0x1FF) * 0x10000; // page_num * 64KB
|
||
// Read from flash block device
|
||
blk_pread(flash_blk, phys_addr, 0x10000, cache_page, 0);
|
||
}
|
||
```
|
||
|
||
The mapping formula is:
|
||
```
|
||
virtual_page_index = MMU table index (0-63)
|
||
physical_page = MMU_entry & 0x1FF
|
||
|
||
vaddr = DROM_BASE + virtual_page_index * 0x10000
|
||
paddr = physical_page * 0x10000
|
||
```
|
||
|
||
### cache_flash_mmu_set() ROM Function
|
||
|
||
The bootloader uses this ROM function (from `esp32/rom/cache.h`) to set MMU entries:
|
||
```c
|
||
int cache_flash_mmu_set(int cpu_no, int table_no, uint32_t vaddr,
|
||
uint32_t paddr, int psize, int num_pages);
|
||
// cpu_no: 0=PRO, 1=APP
|
||
// table_no: 0=DROM0, 1=IRAM0
|
||
// psize: 64 (page size in KB)
|
||
// Fills MMU table entries mapping vaddr→paddr for num_pages pages
|
||
```
|
||
|
||
---
|
||
|
||
## Secure Boot & Flash Encryption
|
||
|
||
### Secure Boot: OFF by Default
|
||
|
||
**Secure boot is NOT enabled by default.** It must be explicitly configured via Kconfig and eFuses.
|
||
|
||
Source: `esp_image_format.c:36-46`
|
||
|
||
```c
|
||
#ifdef CONFIG_SECURE_SIGNED_ON_BOOT
|
||
#define SECURE_BOOT_CHECK_SIGNATURE 1
|
||
#else
|
||
#define SECURE_BOOT_CHECK_SIGNATURE 0 // Default: no signature check
|
||
#endif
|
||
```
|
||
|
||
**Secure boot is controlled by eFuses**:
|
||
- ESP32 V1: `ABS_DONE_0` eFuse bit
|
||
- ESP32 V2: Secure boot key + signature verification
|
||
|
||
**Once burned, secure boot is IRREVERSIBLE.** Factory ESP32 chips ship with secure boot **disabled**.
|
||
|
||
### Can Bare-Metal Ignore Secure Boot?
|
||
|
||
**YES** — if secure boot has never been enabled (eFuses not burned):
|
||
- No signature verification occurs
|
||
- No flash encryption is active
|
||
- Your binary does not need any signature
|
||
|
||
**NO** — if secure boot has been previously enabled:
|
||
- The ROM bootloader will reject unsigned images
|
||
- You cannot bypass it (eFuse is one-time-programmable)
|
||
- Flash contents must be encrypted if flash encryption is also on
|
||
|
||
### Flash Encryption: OFF by Default
|
||
|
||
Flash encryption is also disabled by default. It's controlled by the `FLASH_CRYPT_CNT` eFuse. The bootloader auto-enables it only if `CONFIG_SECURE_FLASH_ENC_ENABLED` is set in Kconfig (default: NO).
|
||
|
||
---
|
||
|
||
## Minimum Bare-Metal Boot Requirements
|
||
|
||
### Option A: True Bare-Metal (No 2nd-Stage Bootloader)
|
||
|
||
Place your code directly at `0x1000`. The ROM bootloader will load it.
|
||
|
||
**Flash layout:**
|
||
```
|
||
0x0000: [empty / bootloader digest area]
|
||
0x1000: YOUR BINARY (ESP32 image format)
|
||
```
|
||
|
||
**Your binary must have:**
|
||
1. Valid `0xE9` magic byte
|
||
2. Correct segment count
|
||
3. At least one segment with load address in IRAM (`0x4008_0000`+) or DRAM (`0x3FFA_E000`+)
|
||
4. `entry_addr` pointing to your code in IRAM
|
||
5. Valid checksum byte after last segment
|
||
6. SHA-256 hash appended (if `hash_appended=1`)
|
||
|
||
**Your code's first instructions must:**
|
||
1. **Disable the flashboot watchdogs** (THE #1 REASON FOR CRASHES):
|
||
```c
|
||
// Disable RWDT flashboot protection
|
||
WRITE_PERI_REG(RTC_CNTL_WDTWPROTECT_REG, 0x50D83AA1); // write protect key
|
||
CLEAR_PERI_REG_MASK(RTC_CNTL_WDTCONFIG0_REG, RTC_CNTL_WDT_FLASHBOOT_EN);
|
||
SET_PERI_REG_MASK(RTC_CNTL_WDTWPROTECT_REG, 0); // re-enable protection
|
||
|
||
// Disable MWDT0 flashboot protection
|
||
WRITE_PERI_REG(TIMG_WDTWPROTECT_REG(0), 0x50D83AA1);
|
||
CLEAR_PERI_REG_MASK(TIMG_WDTCONFIG0_REG(0), TIMG_WDT_FLASHBOOT_EN);
|
||
SET_PERI_REG_MASK(TIMG_WDTWPROTECT_REG(0), 0);
|
||
```
|
||
2. Set up stack pointer (ROM leaves one, but you may want your own)
|
||
3. Configure CPU clock if needed (default may be low)
|
||
4. Initialize UART if you want serial output
|
||
5. If executing from flash: configure flash cache/MMU
|
||
|
||
**To execute code from flash (XIP)**, you need to:
|
||
1. Set up MMU entries mapping your flash offset to `0x400D_0000` (IROM) or `0x3F40_0000` (DROM)
|
||
2. Clear mask bits in `DPORT_PRO_CACHE_CTRL1_REG`
|
||
3. Enable cache in `DPORT_PRO_CACHE_CTRL_REG` (bit 3)
|
||
|
||
**Simplest approach**: Put ALL code in IRAM. The ROM bootloader loads IRAM segments directly to RAM. No cache/MMU setup needed.
|
||
|
||
### Option B: Use ESP-IDF Bootloader + Bare-Metal App
|
||
|
||
This is easier — let the ESP-IDF bootloader do the hard work.
|
||
|
||
**Flash layout:**
|
||
```
|
||
0x0000: [bootloader digest area]
|
||
0x1000: ESP-IDF bootloader (from esptool/IDF build)
|
||
0x8000: Partition table (minimum: 1 app entry + terminator)
|
||
0x10000: YOUR APP (ESP32 image format, loaded by bootloader)
|
||
```
|
||
|
||
**Your app binary:**
|
||
1. Must have valid ESP32 image format (magic `0xE9`, segments, checksum, hash)
|
||
2. Entry point will be called by the bootloader as `void entry(void)` (naked function call)
|
||
3. The bootloader has already:
|
||
- Disabled watchdogs
|
||
- Configured SPI flash and cache
|
||
- Set CPU to full speed (240MHz)
|
||
- Mapped DROM/IROM if your app has flash segments
|
||
- Enabled cache
|
||
|
||
**Your app's entry function receives:**
|
||
- No arguments
|
||
- Valid stack (from bootloader)
|
||
- Cache enabled
|
||
- Watchdogs disabled
|
||
- CPU at full speed
|
||
- Single core (CPU1 still in reset)
|
||
|
||
### Option C: Use esptool to Create Proper Images
|
||
|
||
```bash
|
||
# Create a proper ESP32 image from raw binary
|
||
esptool.py --chip esp32 image_info your_app.bin
|
||
|
||
# Or use esptool's elf2image
|
||
esptool.py --chip esp32 elf2image your_app.elf -o your_app.bin
|
||
|
||
# Flash the binary
|
||
esptool.py --chip esp32 --port /dev/ttyUSB0 write_flash 0x10000 your_app.bin
|
||
```
|
||
|
||
### Why Bare-Metal Likely Crashes
|
||
|
||
1. **Watchdog timeout** (MOST LIKELY): Flashboot RWDT/MWDT0 fire after ~9 seconds. The ROM bootloader enables them. If your code doesn't disable them, the chip resets.
|
||
2. **No valid image header**: If the binary at `0x1000` or `0x10000` doesn't start with `0xE9` magic, the ROM bootloader or 2nd-stage bootloader rejects it.
|
||
3. **Invalid checksum**: The 1-byte XOR checksum must be correct.
|
||
4. **Code in flash without cache/MMU**: If your entry point is in IROM/DROM address range but you haven't set up the flash cache, the CPU fetches garbage.
|
||
5. **Stack issues**: The ROM stack is at `0x3FFE_3F20`. If your code uses deep stack before setting up its own, it may corrupt bootloader data.
|
||
6. **Wrong entry address**: `entry_addr` in the header must point to valid, loaded code.
|
||
|
||
---
|
||
|
||
## Complete Address Map Reference
|
||
|
||
### Flash Layout (Default ESP-IDF)
|
||
|
||
| Offset | Size | Content |
|
||
|--------|------|---------|
|
||
| `0x0000` | `0x1000` | Bootloader secure boot digest (if secure boot V1) |
|
||
| `0x1000` | `0x7000` | 2nd-stage bootloader image |
|
||
| `0x8000` | `0x1000` | Partition table |
|
||
| `0x9000` | varies | OTA data / NVS / other data partitions |
|
||
| `0x10000` | varies | Application image (factory partition) |
|
||
|
||
### CPU Memory Map (ESP32)
|
||
|
||
```
|
||
0x00000000 - 0x1FFFFFFF [Reserved / frame buffer]
|
||
0x20000000 - 0x2FFFFFFF [Frame buffer (QEMU)]
|
||
0x3F400000 - 0x3F7FFFFF DROM (flash rodata via cache, 4MB max)
|
||
0x3F800000 - 0x3FBFFFFF EXTRAM (PSRAM, 4MB max)
|
||
0x3FF00000 - 0x3FF01FFF DPORT registers
|
||
0x3FF10000 - 0x3FF12FFF Flash MMU tables (PRO + APP, DROM0 + IRAM0)
|
||
0x3FF40000 - 0x3FF5FFFF APB peripherals (UART, SPI, GPIO, etc.)
|
||
0x3FF80000 - 0x3FF81FFF RTC FAST memory (data, 8KB)
|
||
0x3FF90000 - 0x3FF9FFFF [Internal SRAM, byte-accessible]
|
||
0x3FFAE000 - 0x3FFFFFFF DRAM (internal SRAM, ~330KB)
|
||
0x3FFE0000 - 0x3FFFFFFF D/IRAM region (128KB, byte-swapped IRAM alias)
|
||
0x40000000 - 0x4006FFFF IROM (mask ROM, cached)
|
||
0x40070000 - 0x40077FFF PRO cache (ICACHE0, 32KB)
|
||
0x40078000 - 0x4007FFFF APP cache (ICACHE1, 32KB)
|
||
0x40080000 - 0x400A9FFF IRAM (internal SRAM for instructions, ~168KB)
|
||
0x400A0000 - 0x400BFFFF D/IRAM region (IRAM alias, 128KB)
|
||
0x400C0000 - 0x400C1FFF RTC FAST memory (instruction, 8KB)
|
||
0x400D0000 - 0x403FFFFF IROM (flash instruction via cache, 3MB+ max)
|
||
0x50000000 - 0x50001FFF RTC SLOW memory (8KB, data/instruction)
|
||
```
|
||
|
||
### Key Register Addresses
|
||
|
||
| Register | Address | Purpose |
|
||
|----------|---------|---------|
|
||
| `DPORT_PRO_CACHE_CTRL_REG` | `0x3FF00040` | PRO cache enable (bit 3) |
|
||
| `DPORT_PRO_CACHE_CTRL1_REG` | `0x3FF00044` | PRO cache mask (bit 0=IRAM0, bit 3=DRAM1, bit 4=DROM0) |
|
||
| `DPORT_APP_CACHE_CTRL_REG` | `0x3FF00048` | APP cache enable |
|
||
| `DPORT_APP_CACHE_CTRL1_REG` | `0x3FF0004C` | APP cache mask |
|
||
| `DPORT_FLASH_MMU_TABLE_PRO` | `0x3FF10000` | PRO DROM0 MMU (64 entries) |
|
||
| PRO IRAM0 MMU | `0x3FF10100` | PRO IRAM0 MMU (64 entries) |
|
||
| `DPORT_FLASH_MMU_TABLE_APP` | `0x3FF12000` | APP DROM0 MMU (64 entries) |
|
||
| APP IRAM0 MMU | `0x3FF12100` | APP IRAM0 MMU (64 entries) |
|
||
| RTC_CNTL WDT regs | `0x3FF48000+` | RWDT configuration |
|
||
| TIMG0 WDT regs | `0x3FF5F000+` | MWDT0 configuration |
|
||
|
||
### MMU Entry Format
|
||
|
||
```
|
||
Bits [8:0] = Physical flash page number (each page = 64KB = 0x10000)
|
||
Bit [8] = Invalid flag (0x100) - if set, accesses return illegal_access_retval
|
||
Bits [31:9] = Reserved/unused
|
||
```
|
||
|
||
Maximum flash addressable: 512 pages × 64KB = **32MB**.
|
||
|
||
---
|
||
|
||
## Summary: Minimum Steps to Boot Bare-Metal on Real ESP32
|
||
|
||
### Absolute Minimum (Code at 0x1000, all-IRAM)
|
||
|
||
1. **Create image with `0xE9` magic** at flash offset `0x1000`
|
||
2. **Single segment**: load_addr = `0x40080000` (IRAM), data = your code
|
||
3. **entry_addr** = address within that IRAM range
|
||
4. **Append checksum**: XOR all 4-byte words (init `0xEF`), append as 1 byte
|
||
5. **First instructions in your entry code**:
|
||
- Disable RWDT flashboot (write `RTC_CNTL_WDTWPROTECT` = `0x50D83AA1`, clear flashboot bit, re-protect)
|
||
- Disable MWDT0 flashboot (same pattern with `TIMG0_WDTWPROTECT`)
|
||
- Set up stack pointer
|
||
- Jump to main
|
||
|
||
### Easier Path (Use ESP-IDF Bootloader)
|
||
|
||
1. **Flash ESP-IDF bootloader** at `0x1000`
|
||
2. **Flash minimal partition table** at `0x8000` (1 factory entry + terminator)
|
||
3. **Flash your app** at `0x10000` with proper image format
|
||
4. **Your app entry** receives a clean environment (watchdogs off, cache on, CPU at 240MHz)
|
||
5. The bootloader jumps to your entry as `void entry(void)` — just start executing
|
||
|
||
**The #1 crash cause is almost certainly the flashboot watchdogs** not being disabled if you're going true bare-metal at `0x1000`. If using the IDF bootloader path, watchdogs are already handled.
|