esp32: implement flash XIP (execute-in-place) support

Add full flash XIP support for ESP32, enabling code and read-only data to
execute/load directly from flash via the MMU cache rather than consuming
precious SRAM. This increases available RAM from ~328KB to effectively
unlimited for code/rodata, while keeping ~121KB for the Go heap.

Changes:
- src/device/esp/esp32.S: Add MMU initialization in call_start_cpu0
  - Call ROM bootloader mmu_init() and cache_flash_mmu_set() to map DROM/IROM
  - Enable flash cache via ROM Cache_Read_Enable()
  - Fix tinygo_scanCurrentStack to spill all register windows for GC

- targets/esp32-interrupts.S: Add exception diagnostics

- targets/esp32.ld: Major linker script restructure for XIP
  - Add DROM (4MB @ 0x3F400000) and IROM (4MB @ 0x400D0000) regions
  - Move .rodata to DROM, main .text to IROM (both flash-mapped)
  - Keep boot code, vectors, and WiFi blob IRAM sections in SRAM0
  - Create WiFi arena in SRAM1 pool 7/6 (64KB @ 0x3FFF0000)
  - Move .bss and heap to SRAM2 (200KB @ 0x3FFAE000), avoiding ROM/MAC regions
  - Add _drom_flash_addr variable (patched by builder with flash offset)

- targets/esp32.json: Add linker wrap flags for malloc/free and WiFi functions

Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
deadprogram
2026-07-10 19:21:30 +02:00
committed by Ron Evans
parent 8727b696a5
commit 0c17b918fb
4 changed files with 496 additions and 80 deletions
+239 -4
View File
@@ -10,6 +10,49 @@
.section .text.call_start_cpu0
1:
.long _stack_top
.Lmain_addr:
.long main
.Lrom_mmu_init:
.long 0x400095a4 // mmu_init(int cpu_no)
.Lrom_cache_flash_mmu_set:
.long 0x400095e0 // cache_flash_mmu_set(cpu, pid, vaddr, paddr, pgsz, pgcnt)
.Lrom_Cache_Read_Enable:
.long 0x40009a84 // Cache_Read_Enable(int cpu_no)
.Lrom_Cache_Read_Disable:
.long 0x40009ab8 // Cache_Read_Disable(int cpu_no)
.Lrom_Cache_Flush:
.long 0x40009a14 // Cache_Flush(int cpu_no)
.Lrodata_start:
.long _rodata_start
.Lrodata_end:
.long _rodata_end
.Ltext_start:
.long _text_start
.Ltext_end:
.long _text_end
.Ldport_pro_cache_ctrl1:
.long 0x3FF00044 // DPORT_PRO_CACHE_CTRL1_REG
.Ldrom_paddr_ptr:
.long _drom_flash_addr // pointer to builder-patched DROM flash offset
.Ldrom_vaddr:
.long 0x3F400000 // DROM virtual base address
.Lirom_vaddr:
.long 0x400D0000 // IROM virtual base address
.Lmmu_table_base:
.long 0x3FF10000 // PRO CPU Flash MMU table
.Lrtc_wdt_protect:
.long 0x3FF480A4 // RTC_CNTL_WDTWPROTECT_REG
.Lrtc_wdt_key:
.long 0x50D83AA1 // WDT write-protect key
.Lrtc_wdt_config0:
.long 0x3FF4808C // RTC_CNTL_WDTCONFIG0_REG
.Ltimg0_wdt_protect:
.long 0x3FF5F064 // TIMG0_WDTWPROTECT_REG
.Ltimg0_wdt_config0:
.long 0x3FF5F048 // TIMG0_WDTCONFIG0_REG
.Lvector_table:
.long _vector_table
.global call_start_cpu0
call_start_cpu0:
// We need to set the stack pointer to a different value. This is somewhat
@@ -47,11 +90,203 @@ call_start_cpu0:
wsr.cpenable a2
rsync
// Jump to the runtime start function written in Go.
call4 main
// Disable the RTC and TIMG0 watchdogs before configuring the flash cache.
// The ROM bootloader leaves them running; a fault during cache setup would
// otherwise reset the chip. The Go runtime re-disables them once it starts.
l32r a2, .Lrtc_wdt_protect
l32r a3, .Lrtc_wdt_key
s32i a3, a2, 0 // unlock WDT write-protect
memw
l32r a2, .Lrtc_wdt_config0
movi a3, 0
s32i a3, a2, 0 // disable WDT (write 0 to config0)
memw
// Disable TG0 WDT (Timer Group 0 Main Watchdog).
// TIMG0_WDTWPROTECT_REG = 0x3FF5F064, TIMG0_WDTCONFIG0_REG = 0x3FF5F048
l32r a2, .Ltimg0_wdt_protect
l32r a3, .Lrtc_wdt_key // same unlock key 0x50D83AA1
s32i a3, a2, 0
memw
l32r a2, .Ltimg0_wdt_config0
movi a3, 0
s32i a3, a2, 0
memw
// Set VECBASE to our vector table. Must happen before any callx4 so that
// register-window overflow exceptions route to our handlers.
l32r a2, .Lvector_table
wsr.vecbase a2
rsync
// Clear PS.EXCM so window overflow exceptions work properly.
rsr.ps a2
movi a3, ~0x1F
and a2, a2, a3
movi a3, 0x20 // PS.UM = 1
or a2, a2, a3
wsr.ps a2
rsync
// ---- Configure flash cache and MMU ----
// The ROM bootloader only loaded IRAM/DRAM segments. We must now set up
// the ICache MMU to map DROM (.rodata) and IROM (.text) from flash.
// 0. Disable cache and flush before reconfiguring MMU.
// The ROM bootloader may leave the cache enabled.
movi a6, 0 // cpu_no = 0
mov a5, a1
l32r a4, .Lrom_Cache_Read_Disable
callx4 a4
movi a6, 0 // cpu_no = 0
mov a5, a1
l32r a4, .Lrom_Cache_Flush
callx4 a4
// 1. Reset MMU tables for PRO CPU (clear all entries to invalid).
movi a6, 0 // cpu_no = 0 (PRO)
mov a5, a1
l32r a4, .Lrom_mmu_init
callx4 a4
// 2. Map DROM pages via ROM cache_flash_mmu_set:
// vaddr=0x3F400000, paddr=0x10000, page_size=64KB, count=ceil(size/64KB)
l32r a2, .Lrodata_end
l32r a3, .Lrodata_start
sub a2, a2, a3 // a2 = rodata size in bytes
beqz a2, .Lskip_drom // skip if no rodata
addi a2, a2, -1
srli a2, a2, 16
addi a2, a2, 1 // a2 = drom page count
// cache_flash_mmu_set(cpu=0, pid=0, vaddr, paddr, psize=64, num)
movi a6, 0
movi a7, 0
l32r a8, .Ldrom_vaddr // 0x3F400000
l32r a9, .Ldrom_paddr_ptr
l32i a9, a9, 0 // a9 = DROM flash offset (builder-patched)
movi a10, 64
mov a11, a2
mov a5, a1
l32r a4, .Lrom_cache_flash_mmu_set
callx4 a4
.Lskip_drom:
// 3. Map IROM pages via ROM cache_flash_mmu_set:
// vaddr=0x400D0000, paddr=0x10000 + drom_pages*64KB
l32r a2, .Ltext_end
l32r a3, .Ltext_start
sub a2, a2, a3 // a2 = text size in bytes
beqz a2, .Lskip_irom // skip if no text
addi a2, a2, -1
srli a2, a2, 16
addi a2, a2, 1 // a2 = irom page count
// Compute IROM paddr into a3 = DROM flash offset + drom_pages * 64KB
l32r a9, .Lrodata_end
l32r a3, .Lrodata_start
sub a9, a9, a3 // a9 = rodata size
l32r a3, .Ldrom_paddr_ptr
l32i a3, a3, 0 // a3 = DROM flash offset (builder-patched)
beqz a9, .Lirom_paddr_ready
addi a9, a9, -1
srli a9, a9, 16
addi a9, a9, 1 // a9 = drom page count
slli a9, a9, 16 // a9 = drom_pages * 64KB
add a3, a3, a9 // a3 = drom_flash_addr + drom_pages * 64KB
.Lirom_paddr_ready:
movi a6, 0
movi a7, 0
l32r a8, .Lirom_vaddr // 0x400D0000
mov a9, a3 // irom paddr
movi a10, 64
mov a11, a2
mov a5, a1
l32r a4, .Lrom_cache_flash_mmu_set
callx4 a4
.Lskip_irom:
// 4. Unmask DROM0 and IRAM0 cache buses in DPORT_PRO_CACHE_CTRL1_REG:
// bit 4: PRO_CACHE_MASK_DROM0 (data flash, 0x3F400000)
// bit 0: PRO_CACHE_MASK_IRAM0 (instruction flash, 0x400D0000 window)
l32r a2, .Ldport_pro_cache_ctrl1
l32i a3, a2, 0
movi a4, ~0x11 // clear bit 0 (IRAM0) and bit 4 (DROM0)
and a3, a3, a4
s32i a3, a2, 0
memw
// 5. Enable flash cache for PRO CPU.
movi a6, 0 // cpu_no = 0
mov a5, a1
l32r a4, .Lrom_Cache_Read_Enable
callx4 a4
isync
// ---- Jump to main (in IROM/flash, now accessible) ----
mov a5, a1
l32r a4, .Lmain_addr
callx4 a4
// If main returns, loop forever.
1: j 1b
// -----------------------------------------------------------------------
// tinygo_scanCurrentStack Spill all Xtensa register windows to the
// stack, then call tinygo_scanstack(sp) so the conservative GC can
// discover live heap pointers that are currently in physical registers.
//
// On RISC-V / ARM the equivalent function pushes callee-saved registers
// before the call. On Xtensa windowed ABI the same effect is achieved
// by forcing hardware window-overflow for every occupied pane: each
// overflow saves the four registers in that pane to the stack frame
// pointed to by the pane's a1 (sp). After all panes are flushed, a
// scan from the current sp to stackTop covers every live value.
//
// Without this spill the conservative GC misses heap pointers held only
// in physical registers, frees live objects, and later crashes jumping
// through a freed/garbage function pointer (e.g. a goroutine trampoline).
// -----------------------------------------------------------------------
.section .text.tinygo_scanCurrentStack
.global tinygo_scanCurrentStack
tinygo_scanCurrentStack:
// TODO: save callee saved registers on the stack
j tinygo_scanstack
entry a1, 48
// Disable interrupts while flushing register windows.
rsr a4, PS
s32i a4, a1, 0 // save PS for later restore
rsil a4, 3 // XCHAL_EXCM_LEVEL
// Flush all register windows using recursive call4.
// For NAREG=64 (16 panes), 15 recursive levels cover all panes
// except the current one (which is kept active).
movi a6, 15
call4 .Lscan_spill
// Restore interrupts.
l32i a4, a1, 0
wsr.ps a4
rsync
// Pass current sp to tinygo_scanstack.
// call4 maps caller's a5callee's a1 (stack ptr for callee's entry)
// and caller's a6callee's a2 (first argument = sp).
mov a5, a1 // callee's a1 = valid stack pointer
mov a6, a1 // callee's a2 = sp argument
call4 tinygo_scanstack
retw
.balign 4
.Lscan_spill:
entry a1, 16
beqz a2, .Lscan_spill_done
addi a2, a2, -1
mov a6, a2
call4 .Lscan_spill
.Lscan_spill_done:
retw