Skip to content

Commit 750fc1f

Browse files
committed
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>
1 parent b21fd85 commit 750fc1f

4 files changed

Lines changed: 489 additions & 73 deletions

File tree

src/device/esp/esp32.S

Lines changed: 239 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,49 @@
1010
.section .text.call_start_cpu0
1111
1:
1212
.long _stack_top
13+
.Lmain_addr:
14+
.long main
15+
.Lrom_mmu_init:
16+
.long 0x400095a4 // mmu_init(int cpu_no)
17+
.Lrom_cache_flash_mmu_set:
18+
.long 0x400095e0 // cache_flash_mmu_set(cpu, pid, vaddr, paddr, pgsz, pgcnt)
19+
.Lrom_Cache_Read_Enable:
20+
.long 0x40009a84 // Cache_Read_Enable(int cpu_no)
21+
.Lrom_Cache_Read_Disable:
22+
.long 0x40009ab8 // Cache_Read_Disable(int cpu_no)
23+
.Lrom_Cache_Flush:
24+
.long 0x40009a14 // Cache_Flush(int cpu_no)
25+
.Lrodata_start:
26+
.long _rodata_start
27+
.Lrodata_end:
28+
.long _rodata_end
29+
.Ltext_start:
30+
.long _text_start
31+
.Ltext_end:
32+
.long _text_end
33+
.Ldport_pro_cache_ctrl1:
34+
.long 0x3FF00044 // DPORT_PRO_CACHE_CTRL1_REG
35+
.Ldrom_paddr_ptr:
36+
.long _drom_flash_addr // pointer to builder-patched DROM flash offset
37+
.Ldrom_vaddr:
38+
.long 0x3F400000 // DROM virtual base address
39+
.Lirom_vaddr:
40+
.long 0x400D0000 // IROM virtual base address
41+
.Lmmu_table_base:
42+
.long 0x3FF10000 // PRO CPU Flash MMU table
43+
.Lrtc_wdt_protect:
44+
.long 0x3FF480A4 // RTC_CNTL_WDTWPROTECT_REG
45+
.Lrtc_wdt_key:
46+
.long 0x50D83AA1 // WDT write-protect key
47+
.Lrtc_wdt_config0:
48+
.long 0x3FF4808C // RTC_CNTL_WDTCONFIG0_REG
49+
.Ltimg0_wdt_protect:
50+
.long 0x3FF5F064 // TIMG0_WDTWPROTECT_REG
51+
.Ltimg0_wdt_config0:
52+
.long 0x3FF5F048 // TIMG0_WDTCONFIG0_REG
53+
.Lvector_table:
54+
.long _vector_table
55+
1356
.global call_start_cpu0
1457
call_start_cpu0:
1558
// We need to set the stack pointer to a different value. This is somewhat
@@ -47,11 +90,203 @@ call_start_cpu0:
4790
wsr.cpenable a2
4891
rsync
4992

50-
// Jump to the runtime start function written in Go.
51-
call4 main
93+
// Disable the RTC and TIMG0 watchdogs before configuring the flash cache.
94+
// The ROM bootloader leaves them running; a fault during cache setup would
95+
// otherwise reset the chip. The Go runtime re-disables them once it starts.
96+
l32r a2, .Lrtc_wdt_protect
97+
l32r a3, .Lrtc_wdt_key
98+
s32i a3, a2, 0 // unlock WDT write-protect
99+
memw
100+
l32r a2, .Lrtc_wdt_config0
101+
movi a3, 0
102+
s32i a3, a2, 0 // disable WDT (write 0 to config0)
103+
memw
104+
105+
// Disable TG0 WDT (Timer Group 0 Main Watchdog).
106+
// TIMG0_WDTWPROTECT_REG = 0x3FF5F064, TIMG0_WDTCONFIG0_REG = 0x3FF5F048
107+
l32r a2, .Ltimg0_wdt_protect
108+
l32r a3, .Lrtc_wdt_key // same unlock key 0x50D83AA1
109+
s32i a3, a2, 0
110+
memw
111+
l32r a2, .Ltimg0_wdt_config0
112+
movi a3, 0
113+
s32i a3, a2, 0
114+
memw
115+
116+
// Set VECBASE to our vector table. Must happen before any callx4 so that
117+
// register-window overflow exceptions route to our handlers.
118+
l32r a2, .Lvector_table
119+
wsr.vecbase a2
120+
rsync
121+
122+
// Clear PS.EXCM so window overflow exceptions work properly.
123+
rsr.ps a2
124+
movi a3, ~0x1F
125+
and a2, a2, a3
126+
movi a3, 0x20 // PS.UM = 1
127+
or a2, a2, a3
128+
wsr.ps a2
129+
rsync
130+
131+
// ---- Configure flash cache and MMU ----
132+
// The ROM bootloader only loaded IRAM/DRAM segments. We must now set up
133+
// the ICache MMU to map DROM (.rodata) and IROM (.text) from flash.
134+
135+
// 0. Disable cache and flush before reconfiguring MMU.
136+
// The ROM bootloader may leave the cache enabled.
137+
movi a6, 0 // cpu_no = 0
138+
mov a5, a1
139+
l32r a4, .Lrom_Cache_Read_Disable
140+
callx4 a4
141+
142+
movi a6, 0 // cpu_no = 0
143+
mov a5, a1
144+
l32r a4, .Lrom_Cache_Flush
145+
callx4 a4
146+
147+
// 1. Reset MMU tables for PRO CPU (clear all entries to invalid).
148+
movi a6, 0 // cpu_no = 0 (PRO)
149+
mov a5, a1
150+
l32r a4, .Lrom_mmu_init
151+
callx4 a4
152+
153+
// 2. Map DROM pages via ROM cache_flash_mmu_set:
154+
// vaddr=0x3F400000, paddr=0x10000, page_size=64KB, count=ceil(size/64KB)
155+
l32r a2, .Lrodata_end
156+
l32r a3, .Lrodata_start
157+
sub a2, a2, a3 // a2 = rodata size in bytes
158+
beqz a2, .Lskip_drom // skip if no rodata
159+
addi a2, a2, -1
160+
srli a2, a2, 16
161+
addi a2, a2, 1 // a2 = drom page count
162+
163+
// cache_flash_mmu_set(cpu=0, pid=0, vaddr, paddr, psize=64, num)
164+
movi a6, 0
165+
movi a7, 0
166+
l32r a8, .Ldrom_vaddr // 0x3F400000
167+
l32r a9, .Ldrom_paddr_ptr
168+
l32i a9, a9, 0 // a9 = DROM flash offset (builder-patched)
169+
movi a10, 64
170+
mov a11, a2
171+
mov a5, a1
172+
l32r a4, .Lrom_cache_flash_mmu_set
173+
callx4 a4
174+
.Lskip_drom:
52175

176+
// 3. Map IROM pages via ROM cache_flash_mmu_set:
177+
// vaddr=0x400D0000, paddr=0x10000 + drom_pages*64KB
178+
l32r a2, .Ltext_end
179+
l32r a3, .Ltext_start
180+
sub a2, a2, a3 // a2 = text size in bytes
181+
beqz a2, .Lskip_irom // skip if no text
182+
addi a2, a2, -1
183+
srli a2, a2, 16
184+
addi a2, a2, 1 // a2 = irom page count
185+
186+
// Compute IROM paddr into a3 = DROM flash offset + drom_pages * 64KB
187+
l32r a9, .Lrodata_end
188+
l32r a3, .Lrodata_start
189+
sub a9, a9, a3 // a9 = rodata size
190+
l32r a3, .Ldrom_paddr_ptr
191+
l32i a3, a3, 0 // a3 = DROM flash offset (builder-patched)
192+
beqz a9, .Lirom_paddr_ready
193+
addi a9, a9, -1
194+
srli a9, a9, 16
195+
addi a9, a9, 1 // a9 = drom page count
196+
slli a9, a9, 16 // a9 = drom_pages * 64KB
197+
add a3, a3, a9 // a3 = drom_flash_addr + drom_pages * 64KB
198+
.Lirom_paddr_ready:
199+
200+
movi a6, 0
201+
movi a7, 0
202+
l32r a8, .Lirom_vaddr // 0x400D0000
203+
mov a9, a3 // irom paddr
204+
movi a10, 64
205+
mov a11, a2
206+
mov a5, a1
207+
l32r a4, .Lrom_cache_flash_mmu_set
208+
callx4 a4
209+
.Lskip_irom:
210+
211+
// 4. Unmask DROM0 and IRAM0 cache buses in DPORT_PRO_CACHE_CTRL1_REG:
212+
// bit 4: PRO_CACHE_MASK_DROM0 (data flash, 0x3F400000)
213+
// bit 0: PRO_CACHE_MASK_IRAM0 (instruction flash, 0x400D0000 window)
214+
l32r a2, .Ldport_pro_cache_ctrl1
215+
l32i a3, a2, 0
216+
movi a4, ~0x11 // clear bit 0 (IRAM0) and bit 4 (DROM0)
217+
and a3, a3, a4
218+
s32i a3, a2, 0
219+
memw
220+
221+
// 5. Enable flash cache for PRO CPU.
222+
movi a6, 0 // cpu_no = 0
223+
mov a5, a1
224+
l32r a4, .Lrom_Cache_Read_Enable
225+
callx4 a4
226+
227+
isync
228+
229+
// ---- Jump to main (in IROM/flash, now accessible) ----
230+
mov a5, a1
231+
l32r a4, .Lmain_addr
232+
callx4 a4
233+
234+
// If main returns, loop forever.
235+
1: j 1b
236+
237+
// -----------------------------------------------------------------------
238+
// tinygo_scanCurrentStack — Spill all Xtensa register windows to the
239+
// stack, then call tinygo_scanstack(sp) so the conservative GC can
240+
// discover live heap pointers that are currently in physical registers.
241+
//
242+
// On RISC-V / ARM the equivalent function pushes callee-saved registers
243+
// before the call. On Xtensa windowed ABI the same effect is achieved
244+
// by forcing hardware window-overflow for every occupied pane: each
245+
// overflow saves the four registers in that pane to the stack frame
246+
// pointed to by the pane's a1 (sp). After all panes are flushed, a
247+
// scan from the current sp to stackTop covers every live value.
248+
//
249+
// Without this spill the conservative GC misses heap pointers held only
250+
// in physical registers, frees live objects, and later crashes jumping
251+
// through a freed/garbage function pointer (e.g. a goroutine trampoline).
252+
// -----------------------------------------------------------------------
53253
.section .text.tinygo_scanCurrentStack
254+
54255
.global tinygo_scanCurrentStack
55256
tinygo_scanCurrentStack:
56-
// TODO: save callee saved registers on the stack
57-
j tinygo_scanstack
257+
entry a1, 48
258+
259+
// Disable interrupts while flushing register windows.
260+
rsr a4, PS
261+
s32i a4, a1, 0 // save PS for later restore
262+
rsil a4, 3 // XCHAL_EXCM_LEVEL
263+
264+
// Flush all register windows using recursive call4.
265+
// For NAREG=64 (16 panes), 15 recursive levels cover all panes
266+
// except the current one (which is kept active).
267+
movi a6, 15
268+
call4 .Lscan_spill
269+
270+
// Restore interrupts.
271+
l32i a4, a1, 0
272+
wsr.ps a4
273+
rsync
274+
275+
// Pass current sp to tinygo_scanstack.
276+
// call4 maps caller's a5→callee's a1 (stack ptr for callee's entry)
277+
// and caller's a6→callee's a2 (first argument = sp).
278+
mov a5, a1 // callee's a1 = valid stack pointer
279+
mov a6, a1 // callee's a2 = sp argument
280+
call4 tinygo_scanstack
281+
282+
retw
283+
284+
.balign 4
285+
.Lscan_spill:
286+
entry a1, 16
287+
beqz a2, .Lscan_spill_done
288+
addi a2, a2, -1
289+
mov a6, a2
290+
call4 .Lscan_spill
291+
.Lscan_spill_done:
292+
retw

targets/esp32-interrupts.S

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -364,8 +364,75 @@ _handle_level1:
364364
rfe
365365

366366
// -----------------------------------------------------------------------
367-
// Exception halt: infinite loop for unhandled exceptions.
368-
// -----------------------------------------------------------------------
367+
// Exception halt: dump EXCCAUSE and EPC1 over UART0, then loop forever.
368+
// Helps diagnose unhandled CPU exceptions (crashes) which would otherwise
369+
// look like a silent freeze (especially with the watchdogs disabled).
370+
// -----------------------------------------------------------------------
371+
.Lexc_uart_fifo:
372+
.long 0x3FF40000 // UART0 FIFO
373+
.Lexc_uart_stat:
374+
.long 0x3FF4001C // UART0 STATUS (TXFIFO_CNT bits 23:16)
369375
.Lexception_halt:
376+
// Emit marker "\nEXC " then EXCCAUSE (8 hex), ' ', EPC1 (8 hex), '\n'.
377+
l32r a4, .Lexc_uart_fifo
378+
l32r a5, .Lexc_uart_stat
379+
movi a6, 10 // '\n'
380+
call0 .Lexc_putc
381+
movi a6, 'E'
382+
call0 .Lexc_putc
383+
movi a6, 'X'
384+
call0 .Lexc_putc
385+
movi a6, 'C'
386+
call0 .Lexc_putc
387+
movi a6, ' '
388+
call0 .Lexc_putc
389+
rsr a7, EXCCAUSE
390+
call0 .Lexc_puthex
391+
movi a6, ' '
392+
call0 .Lexc_putc
393+
rsr a7, EPC1
394+
call0 .Lexc_puthex
395+
movi a6, ' '
396+
call0 .Lexc_putc
397+
rsr a7, EXCVADDR
398+
call0 .Lexc_puthex
399+
movi a6, ' '
400+
call0 .Lexc_putc
401+
l32i a7, a1, 0 // saved a0 (return address of faulting frame)
402+
call0 .Lexc_puthex
403+
movi a6, 10 // '\n'
404+
call0 .Lexc_putc
405+
1:
370406
waiti 0
371-
j .Lexception_halt
407+
j 1b
408+
409+
// Emit char in a6 (clobbers a3). a4=fifo, a5=status. Returns via a0 (call0).
410+
.align 4
411+
.Lexc_putc:
412+
s32i a6, a4, 0
413+
2: l32i a3, a5, 0
414+
extui a3, a3, 16, 8
415+
bnez a3, 2b
416+
ret
417+
418+
// Emit 32-bit value in a7 as 8 hex chars (clobbers a6,a8,a9,a3).
419+
// Uses a10 as return save since call0 to .Lexc_putc clobbers a0.
420+
.align 4
421+
.Lexc_puthex:
422+
mov a10, a0 // save return address
423+
movi a9, 28 // shift
424+
3:
425+
ssr a9
426+
srl a8, a7
427+
extui a8, a8, 0, 4 // nibble
428+
movi a6, 10
429+
bge a8, a6, 4f
430+
addi a6, a8, '0'
431+
j 5f
432+
4: addi a6, a8, 'a' - 10
433+
5: call0 .Lexc_putc
434+
addi a9, a9, -4
435+
bgez a9, 3b
436+
mov a0, a10 // restore return address
437+
ret
438+

targets/esp32.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
"scheduler": "tasks",
88
"serial": "uart",
99
"linker": "ld.lld",
10+
"ldflags": [
11+
"--wrap=malloc",
12+
"--wrap=calloc",
13+
"--wrap=free",
14+
"--wrap=realloc",
15+
"--wrap=ppCheckTxConnTrafficIdle"
16+
],
1017
"default-stack-size": 8192,
1118
"rtlib": "compiler-rt",
1219
"libc": "picolibc",

0 commit comments

Comments
 (0)