From aaf6a36c55a2acbcb4feebeaf86dc07c89a92c34 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 4 Apr 2026 13:55:15 +0200 Subject: [PATCH] esp32s3: add flash XIP boot assembly with cache/MMU init The ESP32-S3 ROM bootloader loads IRAM/DRAM into SRAM but does not configure the flash cache or MMU. Previously the target incorrectly reused the ESP32 boot assembly (esp32.S) which lacks flash XIP support. Add a dedicated esp32s3.S boot assembly that: - Sets up windowed-ABI registers, stack, and FPU - Disables all watchdog timers (RTC, TIMG0, TIMG1, Super WDT) - Configures VECBASE and clears PS.EXCM before any callx4 - Calls ROM functions to configure cache modes: rom_config_instruction_cache_mode (16KB, 8-way, 32B line) rom_config_data_cache_mode (32KB, 8-way, 32B line) - Initializes MMU, maps flash page 0 for IROM and DROM, clears bus-shut bits, and enables both caches - Jumps to runtime.main in IROM (flash) Update the linker script (esp32s3.ld) to place .text and .rodata in flash-mapped regions (IROM/DROM) with proper alignment for the MMU page size. Update esp32s3-interrupts.S with proper exception vector handlers. Point esp32s3.json at the new esp32s3.S instead of esp32.S. Signed-off-by: deadprogram --- src/device/esp/esp32s3.S | 317 +++++++++++++++++++++++++++++++++++ targets/esp32s3-interrupts.S | 46 ++++- targets/esp32s3.json | 2 +- targets/esp32s3.ld | 202 +++++++++++++--------- 4 files changed, 484 insertions(+), 83 deletions(-) create mode 100644 src/device/esp/esp32s3.S diff --git a/src/device/esp/esp32s3.S b/src/device/esp/esp32s3.S new file mode 100644 index 000000000..3a731350c --- /dev/null +++ b/src/device/esp/esp32s3.S @@ -0,0 +1,317 @@ +// Startup code for the ESP32-S3 (Xtensa LX7, windowed ABI). +// +// The ROM bootloader loads IRAM/DRAM segments into SRAM but does NOT +// configure flash cache/MMU. We must: +// 1. Set up the windowed-ABI register file and stack pointer. +// 2. Set VECBASE and clear PS.EXCM (needed for callx4 window overflows). +// 3. Disable watchdog timers. +// 4. Configure the flash cache and MMU so IROM/DROM are accessible. +// 5. Jump to runtime.main (in IROM). +// +// Cache/MMU init sequence (from NuttX esp_loader.c / ESP-IDF bootloader / esp-hal): +// Phase A — configure cache modes: +// a. rom_config_instruction_cache_mode(16KB, 8-way, 32B) +// b. rom_Cache_Suspend_DCache() +// c. rom_config_data_cache_mode(32KB, 8-way, 32B) +// d. Cache_Resume_DCache(0) +// Phase B — map flash pages: +// e. Disable caches +// f. Cache_MMU_Init() — reset all MMU entries to invalid +// g. Cache_Set_IDROM_MMU_Size() — set IROM/DROM entry split +// h. Write MMU entries mapping flash page 0 for IROM and DROM +// i. Clear bus-shut bits +// j. Enable caches + isync + +#define PS_WOE 0x00040000 + +// ----------------------------------------------------------------------- +// Boot entry point — placed in IRAM by the linker. +// ----------------------------------------------------------------------- +.section .text.call_start_cpu0 + .literal_position + .align 4 +.Lstack_top_addr: + .long _stack_top +.Lmain_addr: + .long main +.Lvector_table_addr: + .long _vector_table +// WDT register addresses +.Lwdt_key: + .long 0x50D83AA1 +.Lrtc_wdt_protect: + .long 0x600080B0 +.Lrtc_wdt_config0: + .long 0x60008098 +.Ltimg0_wdt_protect: + .long 0x6001F064 +.Ltimg0_wdt_config0: + .long 0x6001F048 +.Ltimg1_wdt_protect: + .long 0x60020064 +.Ltimg1_wdt_config0: + .long 0x60020048 +.Lswd_protect: + .long 0x600080B8 +.Lswd_key: + .long 0x8F1D312A +.Lswd_conf: + .long 0x600080B4 +.Lswd_disable: + .long 0x40000000 +// ROM function addresses (from ESP-IDF esp32s3.rom.ld) +.Lrom_config_icache: + .long 0x40001a1c +.Lrom_config_dcache: + .long 0x40001a28 +.Lrom_suspend_dcache: + .long 0x400018b4 +.LCache_Resume_DCache: + .long 0x400018c0 +.LCache_Disable_ICache: + .long 0x4000186c +.LCache_Disable_DCache: + .long 0x40001884 +.LCache_MMU_Init: + .long 0x40001998 +.LCache_Set_IDROM_MMU_Size: + .long 0x40001914 +.LCache_Enable_ICache: + .long 0x40001878 +.LCache_Enable_DCache: + .long 0x40001890 +// Cache/MMU register addresses +.Lmmu_table_base: + .long 0x600C5000 +.Licache_ctrl1_reg: + .long 0x600C4064 +.Ldcache_ctrl1_reg: + .long 0x600C4004 + +.global call_start_cpu0 +call_start_cpu0: + + // ---- 1. Windowed-ABI register file setup ---- + + // Disable WOE so we can safely manipulate WINDOWSTART. + rsr.ps a2 + movi a3, ~(PS_WOE) + and a2, a2, a3 + wsr.ps a2 + rsync + + // Set WINDOWSTART to 1 << WINDOWBASE (mark only current window as valid). + rsr.windowbase a2 + ssl a2 + movi a2, 1 + sll a2, a2 + wsr.windowstart a2 + rsync + + // Load stack pointer. + l32r a1, .Lstack_top_addr + + // Re-enable WOE. + rsr.ps a2 + movi a3, PS_WOE + or a2, a2, a3 + wsr.ps a2 + rsync + + // Enable FPU (coprocessor 0). + movi a2, 1 + wsr.cpenable a2 + rsync + + // ---- 2. Disable all watchdog timers (IMMEDIATELY, before any delay) ---- + l32r a3, .Lwdt_key + movi a4, 0 + + // RTC WDT + l32r a2, .Lrtc_wdt_protect + memw + s32i a3, a2, 0 + l32r a5, .Lrtc_wdt_config0 + memw + s32i a4, a5, 0 + memw + s32i a4, a2, 0 + + // TIMG0 WDT + l32r a2, .Ltimg0_wdt_protect + memw + s32i a3, a2, 0 + l32r a5, .Ltimg0_wdt_config0 + memw + s32i a4, a5, 0 + memw + s32i a4, a2, 0 + + // TIMG1 WDT + l32r a2, .Ltimg1_wdt_protect + memw + s32i a3, a2, 0 + l32r a5, .Ltimg1_wdt_config0 + memw + s32i a4, a5, 0 + memw + s32i a4, a2, 0 + + // Super WDT + l32r a2, .Lswd_protect + l32r a3, .Lswd_key + memw + s32i a3, a2, 0 + l32r a5, .Lswd_conf + l32r a6, .Lswd_disable + memw + s32i a6, a5, 0 + memw + s32i a4, a2, 0 + + // ---- 3. Set VECBASE and clear PS.EXCM ---- + // VECBASE must be set before any callx4 so that window overflow + // exceptions (triggered by register window rotation) route to our + // handlers in IRAM, not the ROM's default vectors. + l32r a8, .Lvector_table_addr + wsr.vecbase a8 + rsync + + // Clear PS.EXCM (bit 4) and PS.INTLEVEL (bits 0-3). + // The ROM bootloader may leave EXCM=1; with EXCM set any callx4 + // window overflow would become a double exception. + // Set PS.UM (bit 5) so level-1 exceptions route to User vector. + rsr.ps a2 + movi a3, ~0x1F + and a2, a2, a3 + movi a3, 0x20 + or a2, a2, a3 + wsr.ps a2 + rsync + + // ---- 4. Configure flash cache and MMU ---- + // + // ROM function calls use callx4 (windowed ABI): + // a4 = target address (overwritten with return addr by call mechanism) + // a5 = stack pointer for callee (becomes callee's a1 via entry) + // a6 = first argument (becomes callee's a2) + // a7 = second argument (becomes callee's a3) + // a8 = third argument (becomes callee's a4) + // Registers a0-a3 are preserved across callx4; a4-a11 may be clobbered. + + // Phase A: Configure cache modes (required for cache hardware to function). + // Without this, the cache doesn't know its size/associativity/line-size + // and cannot service flash accesses. + + // 4a. Configure ICache mode: 16KB, 8-way, 32-byte line + movi a6, 0x4000 // cache_size = 16KB + movi a7, 8 // ways = 8 + movi a8, 32 // line_size = 32 + mov a5, a1 + l32r a4, .Lrom_config_icache + callx4 a4 + + // 4b. Suspend DCache before configuring it + mov a5, a1 + l32r a4, .Lrom_suspend_dcache + callx4 a4 + + // 4c. Configure DCache mode: 32KB, 8-way, 32-byte line + movi a6, 0x8000 // cache_size = 32KB + movi a7, 8 // ways = 8 + movi a8, 32 // line_size = 32 + mov a5, a1 + l32r a4, .Lrom_config_dcache + callx4 a4 + + // 4d. Resume DCache + movi a6, 0 + mov a5, a1 + l32r a4, .LCache_Resume_DCache + callx4 a4 + + // Phase B: Map flash pages into MMU. + + // 4e. Disable ICache + mov a5, a1 + l32r a4, .LCache_Disable_ICache + callx4 a4 + + // 4f. Disable DCache + mov a5, a1 + l32r a4, .LCache_Disable_DCache + callx4 a4 + + // 4g. Initialize MMU (resets all 512 entries to invalid = 0x4000) + mov a5, a1 + l32r a4, .LCache_MMU_Init + callx4 a4 + + // 4h. Set IDROM MMU size: even 256/256 split. + // Each entry is 4 bytes, so 256 entries = 0x400 bytes per region. + movi a6, 0x400 // irom_mmu_size (256 entries × 4 bytes) + movi a7, 0x400 // drom_mmu_size (256 entries × 4 bytes) + mov a5, a1 + l32r a4, .LCache_Set_IDROM_MMU_Size + callx4 a4 + + // 4i. Write MMU entries: map flash page 0 for both IROM and DROM. + // MMU table at 0x600C5000: entries 0-255 = ICache, 256-511 = DCache. + // Entry value 0 = flash page 0, valid (SOC_MMU_VALID = 0). + // Our .text (IROM, VMA 0x42000xxx) and .rodata (DROM, VMA 0x3C000xxx) + // both reside in flash page 0 (first 64KB). + l32r a8, .Lmmu_table_base // a8 = 0x600C5000 + movi a9, 0 // flash page 0 + s32i a9, a8, 0 // Entry 0: ICache VMA 0x42000000 + addmi a10, a8, 0x400 // a10 = 0x600C5400 (entry 256) + s32i a9, a10, 0 // Entry 256: DCache VMA 0x3C000000 + memw + + // 4j. Clear bus-shut bits so core 0 can access ICache and DCache buses. + l32r a8, .Licache_ctrl1_reg // 0x600C4064 + movi a9, 0 + s32i a9, a8, 0 // Clear all ICACHE_CTRL1 shut bits + l32r a8, .Ldcache_ctrl1_reg // 0x600C4004 + s32i a9, a8, 0 // Clear all DCACHE_CTRL1 shut bits + memw + + // 4k. Enable ICache (arg: autoload = 0) + movi a6, 0 + mov a5, a1 + l32r a4, .LCache_Enable_ICache + callx4 a4 + + // 4l. Enable DCache (arg: autoload = 0) + movi a6, 0 + mov a5, a1 + l32r a4, .LCache_Enable_DCache + callx4 a4 + + // Flush instruction pipeline so new cache/MMU config takes effect. + isync + + // ---- 5. Jump to main (in IROM) ---- + // Re-clear PS.EXCM in case ROM calls changed processor state. + rsr.ps a2 + movi a3, ~0x1F + and a2, a2, a3 + movi a3, 0x20 + or a2, a2, a3 + wsr.ps a2 + rsync + + mov a5, a1 + l32r a4, .Lmain_addr + callx4 a4 + + // If main returns, loop forever. +1: j 1b + +// ----------------------------------------------------------------------- +// tinygo_scanCurrentStack — tail-jump to tinygo_scanstack. +// ----------------------------------------------------------------------- +.section .text.tinygo_scanCurrentStack + +.global tinygo_scanCurrentStack +tinygo_scanCurrentStack: + j tinygo_scanstack diff --git a/targets/esp32s3-interrupts.S b/targets/esp32s3-interrupts.S index bdc731fd1..96a586e51 100644 --- a/targets/esp32s3-interrupts.S +++ b/targets/esp32s3-interrupts.S @@ -172,11 +172,12 @@ _nmi_vector: j _nmi_vector // ----------------------------------------------------------------------- -// Offset 0x300 — Kernel exception (stub — loops forever) +// Offset 0x300 — Kernel exception +// Diagnostic: steady 1s ON / 1s OFF blink. // ----------------------------------------------------------------------- .org _vector_table + 0x300 _kernel_vector: - j _kernel_vector + j _handle_kernel_exc // jump to handler below table // ----------------------------------------------------------------------- // Offset 0x340 — User exception / level-1 interrupt @@ -190,11 +191,18 @@ _level1_vector: j _handle_level1 // jump to full handler (PC-relative, no literal pool) // ----------------------------------------------------------------------- -// Offset 0x3C0 — Double exception (stub — loops forever) +// Offset 0x3C0 — Double exception +// Diagnostic: solid LED ON (no blinking). // ----------------------------------------------------------------------- .org _vector_table + 0x3C0 _double_vector: - j _double_vector + movi a0, 1 + slli a0, a0, 21 + movi a1, 6 + slli a1, a1, 28 + addmi a1, a1, 0x4000 + s32i a0, a1, 0x0C // LED ON permanently +1: j 1b // spin forever // ----------------------------------------------------------------------- // Level-1 interrupt handler — lives outside the vector table so there @@ -225,6 +233,36 @@ _double_vector: .LhandleInterrupt_addr: .word handleInterrupt +// ----------------------------------------------------------------------- +// Kernel exception handler (out-of-table). +// a3 = EXCCAUSE+1 (set in _kernel_vector stub). +// Blinks GPIO21 a3 times, long pause, repeat forever. +// 1 blink = cause 0 (Illegal instruction) +// 3 blinks = cause 2 (Instruction fetch error) +// 6 blinks = cause 5 (Alloca / window check in entry) +// 29 blinks = cause 28 (LoadProhibitedCause) +// ----------------------------------------------------------------------- +_handle_kernel_exc: + // Steady 1-second ON / 1-second OFF blink. + // This is the KERNEL exception handler. + movi a0, 1 + slli a0, a0, 21 // a0 = GPIO21 bit + movi a1, 6 + slli a1, a1, 28 + addmi a1, a1, 0x4000 // a1 = 0x60004000 +_hke_blink: + s32i a0, a1, 0x0C // LED ON + movi a2, 5 + slli a2, a2, 23 // ~1s @40MHz +1: addi a2, a2, -1 + bnez a2, 1b + s32i a0, a1, 0x08 // LED OFF + movi a2, 5 + slli a2, a2, 23 // ~1s @40MHz +1: addi a2, a2, -1 + bnez a2, 1b + j _hke_blink + .global _handle_level1 _handle_level1: // --- allocate 96-byte exception frame on the interrupted stack --- diff --git a/targets/esp32s3.json b/targets/esp32s3.json index 9eac6dfbd..73435864d 100644 --- a/targets/esp32s3.json +++ b/targets/esp32s3.json @@ -12,7 +12,7 @@ "libc": "picolibc", "linkerscript": "targets/esp32s3.ld", "extra-files": [ - "src/device/esp/esp32.S", + "src/device/esp/esp32s3.S", "targets/esp32s3-interrupts.S", "src/internal/task/task_stack_esp32.S" ], diff --git a/targets/esp32s3.ld b/targets/esp32s3.ld index 28181f5b5..858aee199 100644 --- a/targets/esp32s3.ld +++ b/targets/esp32s3.ld @@ -1,15 +1,25 @@ -/* Linker script for the ESP32-S3 */ - +/* Linker script for the ESP32-S3 (flash XIP) + * + * The ESP32-S3 has 512KB of internal SRAM: + * - SRAM0 (32KB): 0x40370000-0x40377FFF — used by ICache when flash XIP is + * active, so we MUST NOT place code here. + * - SRAM1 (416KB): dual-mapped as IRAM 0x40378000-0x403DFFFF and + * DRAM 0x3FC88000-0x3FCEFFFF. + * + * Flash is memory-mapped via the cache: + * - DROM (read-only data): 0x3C000000, up to 32MB + * - IROM (executable code): 0x42000000, up to 32MB + * The MMU uses 64KB pages, so the bottom 16 bits of the virtual address + * and the flash offset must match. Dummy sections handle this alignment. + */ MEMORY { - /* Note: DRAM and IRAM below are actually in the same 416K address space. */ - DRAM (rw) : ORIGIN = 0x3FC88000, LENGTH = 416K /* Internal SRAM 1 (data bus) */ - IRAM (x) : ORIGIN = 0x40370000, LENGTH = 416K /* Internal SRAM 1 (instruction bus) */ + DRAM (rw) : ORIGIN = 0x3FC88000, LENGTH = 416K + IRAM (x) : ORIGIN = 0x40378000, LENGTH = 416K /* SRAM1 only (SRAM0 used by ICache) */ - /* Note: DROM and IROM below are actually in the same 32M address space. */ - DROM (r) : ORIGIN = 0x3C000000, LENGTH = 32M /* Data bus (read-only) */ - IROM (rx) : ORIGIN = 0x42000000, LENGTH = 32M /* Instruction bus */ + DROM (r) : ORIGIN = 0x3C000000, LENGTH = 32M /* Flash data bus (read-only) */ + IROM (rx) : ORIGIN = 0x42000000, LENGTH = 32M /* Flash instruction bus */ } /* The entry point. It is set in the image flashed to the chip, so must be @@ -19,9 +29,25 @@ ENTRY(call_start_cpu0) SECTIONS { + /* Dummy section so that .rodata starts right after the image header + * and DROM segment header in the flash image. + */ + .rodata_dummy (NOLOAD): ALIGN(4) + { + . += 0x18; /* esp_image_header_t at start of flash */ + . += 0x8; /* DROM segment header (8 bytes) */ + } > DROM + + /* Constant global variables, stored in flash (DROM). */ + .rodata : ALIGN(4) + { + *(.rodata*) + . = ALIGN (4); + } >DROM + /* Put the stack at the bottom of DRAM, so that the application will * crash on stack overflow instead of silently corrupting memory. - * See: http://blog.japaric.io/stack-overflow-protection/ */ + */ .stack (NOLOAD) : { . = ALIGN(16); @@ -29,86 +55,106 @@ SECTIONS _stack_top = .; } >DRAM - /* Constant literals and code. Loaded into IRAM for now. Eventually, most - * code should be executed directly from flash. - * Note that literals must be before code for the l32r instruction to work. - */ -.text.call_start_cpu0 : ALIGN(4) -{ - *(.literal.call_start_cpu0) - *(.text.call_start_cpu0) -} >IRAM AT >DRAM - -/* Xtensa exception/interrupt vector table — must be 0x400-aligned. */ -.text.exception_vectors : ALIGN(0x400) -{ - *(.text.exception_vectors) -} >IRAM AT >DRAM - -/* Level-1 interrupt handler (called from the vector stub). */ -.text._handle_level1 : ALIGN(4) -{ - *(.literal._handle_level1) - *(.text._handle_level1) -} >IRAM AT >DRAM - -/* All other code and literals */ -.text : ALIGN(4) -{ - *(.literal .text) - *(.literal.* .text.*) - *(.text) - *(.text.*) -} >IRAM AT >DRAM - - /* Constant global variables. - * They are loaded in DRAM for ease of use. Eventually they should be stored - * in flash and loaded directly from there but they're kept in RAM to make - * sure they can always be accessed (even in interrupts). - */ - .rodata : ALIGN(4) - { - *(.rodata) - *(.rodata.*) - } >DRAM - - /* Mutable global variables. - */ - .data : ALIGN(4) - { - _sdata = ABSOLUTE(.); - *(.data) - *(.data.*) - _edata = ABSOLUTE(.); - } >DRAM - - /* Check that the boot ROM stack (for the APP CPU) does not overlap with the - * data that is loaded by the boot ROM. There may be ways to avoid this - * issue if it occurs in practice. - * The magic value here is _stack_sentry in the boot ROM ELF file. - */ - ASSERT(_edata < 0x3ffe1320, "the .data section overlaps with the stack used by the boot ROM, possibly causing corruption at startup") - - /* Global variables that are mutable and zero-initialized. - * These must be zeroed at startup (unlike data, which is loaded by the - * bootloader). - */ + /* Global variables that are mutable and zero-initialized. */ .bss (NOLOAD) : ALIGN(4) { . = ALIGN (4); _sbss = ABSOLUTE(.); - *(.bss) - *(.bss.*) + *(.bss .bss.*) . = ALIGN (4); _ebss = ABSOLUTE(.); } >DRAM + + /* Mutable global variables, initialized by the ROM bootloader. */ + .data : ALIGN(4) + { + . = ALIGN (4); + _sdata = ABSOLUTE(.); + *(.data .data.*) + *(.dram*) + . = ALIGN (4); + _edata = ABSOLUTE(.); + } >DRAM + + /* Dummy section to skip past stack+bss+data in IRAM (dual-mapped with DRAM). */ + .iram_dummy (NOLOAD): ALIGN(4) + { + . += SIZEOF(.stack); + . += SIZEOF(.bss); + . += SIZEOF(.data); + } > IRAM + + /* IRAM segment: boot code, interrupt vectors, and any code that must + * run from RAM. Loaded into SRAM by the ROM bootloader. + */ + .iram : ALIGN(4) + { + /* Boot entry point and its literals */ + *(.literal.call_start_cpu0) + *(.text.call_start_cpu0) + + /* Xtensa exception/interrupt vector table — must be 0x400-aligned */ + . = ALIGN(0x400); + *(.text.exception_vectors) + + /* Level-1 interrupt handler */ + *(.literal._handle_level1) + *(.text._handle_level1) + + /* WiFi/BLE blob IRAM sections */ + *(.iram*) + *(.wifislprxiram*) + *(.wifiextrairam*) + *(.wifi0iram*) + *(.wifislpiram*) + *(.wifirxiram*) + *(.wifiorslpiram*) + *(.iram1*) + *(.coexiram*) + + /* Init code — reclaimed for heap after startup */ + __init_start = .; + *(.init) + __init_end = .; + . = ALIGN(4); + _iram_end = .; + } >IRAM + + /* Dummy section to put the IROM segment at the correct flash offset. */ + .text_dummy (NOLOAD): ALIGN(4) + { + . += 0x18; /* esp_image_header_t */ + . += SIZEOF(.rodata) + ((SIZEOF(.rodata) != 0) ? 0x8 : 0); /* DROM segment (optional) */ + . += SIZEOF(.data) + ((SIZEOF(.data) != 0) ? 0x8 : 0); /* DRAM segment (optional) */ + . += SIZEOF(.iram) + 0x8; /* IRAM segment */ + . += 0x8; /* IROM segment header */ + } > IROM + + /* IROM segment: main code executed from flash via cache. */ + .text : ALIGN(4) + { + *(.literal .text) + *(.literal.* .text.*) + _irom_end = .; + } >IROM + + /DISCARD/ : + { + *(.eh_frame) + } } /* For the garbage collector. + * _heap_start must be after the DRAM shadow of the IRAM section. + * IRAM and DRAM share the same physical SRAM1, with addresses offset by + * (IRAM_ORIGIN - DRAM_ORIGIN) = 0x6F0000. We use _iram_end (the actual + * end of .iram in IRAM space) converted to DRAM to avoid counting any + * alignment gaps between .iram_dummy and .iram. + * Init code at the end of .iram can be reclaimed for heap. */ -_globals_start = _sdata; -_globals_end = _ebss; -_heap_start = _ebss; +_globals_start = _sbss; +_globals_end = _edata; +_heap_start = _iram_end - 0x6F0000 - (__init_end - __init_start); _heap_end = ORIGIN(DRAM) + LENGTH(DRAM); _stack_size = 4K;