From 17a2fbfe81d5a4019f694d838e54707a08561266 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 8 Jul 2026 23:55:46 +0200 Subject: [PATCH 1/3] esp32: add interrupt support (vector table, timer alarm, GPIO SetInterrupt) Signed-off-by: deadprogram --- src/machine/machine_esp32.go | 97 ++++++ src/runtime/interrupt/interrupt_esp32.go | 226 +++++++++++++ src/runtime/interrupt/interrupt_xtensa.go | 2 +- src/runtime/runtime_esp32.go | 52 ++- src/runtime/runtime_esp32_timer.go | 69 ++++ targets/esp32-interrupts.S | 371 ++++++++++++++++++++++ targets/esp32.json | 3 +- targets/esp32.ld | 9 + 8 files changed, 820 insertions(+), 9 deletions(-) create mode 100644 src/runtime/interrupt/interrupt_esp32.go create mode 100644 src/runtime/runtime_esp32_timer.go create mode 100644 targets/esp32-interrupts.S diff --git a/src/machine/machine_esp32.go b/src/machine/machine_esp32.go index 3f715e6de5..9335a04cef 100644 --- a/src/machine/machine_esp32.go +++ b/src/machine/machine_esp32.go @@ -5,7 +5,9 @@ package machine import ( "device/esp" "errors" + "runtime/interrupt" "runtime/volatile" + "sync" "unsafe" ) @@ -291,6 +293,101 @@ func (p Pin) mux() *volatile.Register32 { } } +const maxPin = 40 + +// cpuInterruptFromPin selects an edge-triggered CPU interrupt line for GPIO. +// CPU interrupt 10 is edge-triggered level-1 on the Xtensa LX6, which prevents +// the ISR from re-entering continuously when other peripherals (e.g. SPI via +// the GPIO Matrix) keep GPIO.STATUS bits asserted. +const cpuInterruptFromPin = 10 + +type PinChange uint8 + +// Pin change interrupt constants for SetInterrupt. +const ( + PinRising PinChange = iota + 1 + PinFalling + PinToggle +) + +// SetInterrupt sets an interrupt to be executed when a particular pin changes +// state. The pin should already be configured as an input, including a pull up +// or down if no external pull is provided. +// +// You can pass a nil func to unset the pin change interrupt. If you do so, +// the change parameter is ignored and can be set to any value (such as 0). +// If the pin is already configured with a callback, you must first unset +// this pins interrupt before you can set a new callback. +func (p Pin) SetInterrupt(change PinChange, callback func(Pin)) (err error) { + if p >= maxPin { + return ErrInvalidInputPin + } + + if callback == nil { + // Disable this pin interrupt + p.pinReg().ClearBits(esp.GPIO_PIN_INT_TYPE_Msk | esp.GPIO_PIN_INT_ENA_Msk) + + if pinCallbacks[p] != nil { + pinCallbacks[p] = nil + } + return nil + } + + if pinCallbacks[p] != nil { + // The pin was already configured. + // To properly re-configure a pin, unset it first and set a new + // configuration. + return ErrNoPinChangeChannel + } + pinCallbacks[p] = callback + + onceSetupPinInterrupt.Do(func() { + err = setupPinInterrupt() + }) + if err != nil { + return err + } + + p.pinReg().Set( + (p.pinReg().Get() & ^uint32(esp.GPIO_PIN_INT_TYPE_Msk|esp.GPIO_PIN_INT_ENA_Msk)) | + uint32(change)< lastCPUInt { + return errInterruptRange + } + + // Mark as used. + cpuIntUsed[i.num] = true + + // Read current INTENABLE, set the bit for this CPU interrupt. + cur := readINTENABLE() + cur |= 1 << uint(i.num) + writeINTENABLE(cur) + + return nil +} + +// In returns whether the CPU is currently inside an interrupt handler. +func In() bool { + return inInterrupt +} + +// handleInterrupt is called from the assembly vector code in esp32-interrupts.S. +// It determines which CPU interrupt(s) fired and dispatches to the +// registered Go handlers. +// +//export handleInterrupt +func handleInterrupt() { + inInterrupt = true + + // INTERRUPT register shows pending + enabled CPU interrupts. + pending := readINTERRUPT() + enabled := readINTENABLE() + active := pending & enabled + + // Clear edge-triggered pending bits before dispatching handlers so that + // new edges arriving during handler execution are not lost. Writing to + // INTCLEAR is a no-op for level-triggered lines, so this is safe for all + // interrupt types. + writeINTCLEAR(active) + + for i := firstCPUInt; i <= lastCPUInt; i++ { + if active&(1<> 32)) + + // Enable the alarm (auto-clears when alarm fires). + esp.TIMG0.T0CONFIG.SetBits(esp.TIMG_T0CONFIG_ALARM_EN) + + // Re-enable the timer interrupt (handler disables INT_ENA). + esp.TIMG0.INT_CLR_TIMERS.Set(1) + esp.TIMG0.INT_ENA_TIMERS.SetBits(1) + + // Wait for any interrupt (timer alarm or other) or timeout. + for interruptPending.Get() == 0 { + if ticks() >= target { + return + } + } + } +} diff --git a/targets/esp32-interrupts.S b/targets/esp32-interrupts.S new file mode 100644 index 0000000000..f172b7d098 --- /dev/null +++ b/targets/esp32-interrupts.S @@ -0,0 +1,371 @@ +// Xtensa interrupt/exception vector table for the ESP32. +// +// The ESP32 uses an Xtensa LX6 core with the windowed register ABI. +// Interrupt vectors are placed at fixed offsets from the VECBASE special +// register. We only handle level-1 (user) interrupts for now. +// +// Vector offsets (from ESP32 core-isa.h XCHAL definitions): +// 0x000 Window overflow 4 +// 0x040 Window underflow 4 +// 0x080 Window overflow 8 +// 0x0C0 Window underflow 8 +// 0x100 Window overflow 12 +// 0x140 Window underflow 12 +// 0x180 Level-2 interrupt +// 0x1C0 Level-3 interrupt +// 0x200 Level-4 interrupt +// 0x240 Level-5 interrupt +// 0x280 Debug exception (level-6) +// 0x2C0 NMI (level-7) +// 0x300 Kernel exception +// 0x340 User exception (level-1 interrupt) +// 0x3C0 Double exception + +// PS register field definitions. +#define PS_WOE 0x00040000 +#define PS_EXCM 0x00000010 +#define PS_INTLEVEL_MASK 0x0000000F + +// ----------------------------------------------------------------------- +// Vector table — must be aligned to 0x400 (1024 bytes). +// ----------------------------------------------------------------------- + .section .text.exception_vectors,"ax" + .global _vector_table + .balign 0x400 +_vector_table: + +// ----------------------------------------------------------------------- +// Offset 0x000 — Window overflow 4 +// ----------------------------------------------------------------------- + .org _vector_table + 0x000 +_window_overflow4: + s32e a0, a5, -16 + s32e a1, a5, -12 + s32e a2, a5, -8 + s32e a3, a5, -4 + rfwo + +// ----------------------------------------------------------------------- +// Offset 0x040 — Window underflow 4 +// ----------------------------------------------------------------------- + .org _vector_table + 0x040 +_window_underflow4: + l32e a0, a5, -16 + l32e a1, a5, -12 + l32e a2, a5, -8 + l32e a3, a5, -4 + rfwu + +// ----------------------------------------------------------------------- +// Offset 0x080 — Window overflow 8 +// ----------------------------------------------------------------------- + .org _vector_table + 0x080 +_window_overflow8: + s32e a0, a9, -16 + l32e a0, a1, -12 + s32e a1, a9, -12 + s32e a2, a9, -8 + s32e a3, a9, -4 + s32e a4, a0, -32 + s32e a5, a0, -28 + s32e a6, a0, -24 + s32e a7, a0, -20 + rfwo + +// ----------------------------------------------------------------------- +// Offset 0x0C0 — Window underflow 8 +// ----------------------------------------------------------------------- + .org _vector_table + 0x0C0 +_window_underflow8: + l32e a0, a9, -16 + l32e a1, a9, -12 + l32e a2, a9, -8 + l32e a7, a1, -12 + l32e a3, a9, -4 + l32e a4, a7, -32 + l32e a5, a7, -28 + l32e a6, a7, -24 + l32e a7, a7, -20 + rfwu + +// ----------------------------------------------------------------------- +// Offset 0x100 — Window overflow 12 +// ----------------------------------------------------------------------- + .org _vector_table + 0x100 +_window_overflow12: + s32e a0, a13, -16 + l32e a0, a1, -12 + s32e a1, a13, -12 + s32e a2, a13, -8 + s32e a3, a13, -4 + s32e a4, a0, -48 + s32e a5, a0, -44 + s32e a6, a0, -40 + s32e a7, a0, -36 + s32e a8, a0, -32 + s32e a9, a0, -28 + s32e a10, a0, -24 + s32e a11, a0, -20 + rfwo + +// ----------------------------------------------------------------------- +// Offset 0x140 — Window underflow 12 +// ----------------------------------------------------------------------- + .org _vector_table + 0x140 +_window_underflow12: + l32e a0, a13, -16 + l32e a1, a13, -12 + l32e a2, a13, -8 + l32e a11, a1, -12 + l32e a3, a13, -4 + l32e a4, a11, -48 + l32e a5, a11, -44 + l32e a6, a11, -40 + l32e a7, a11, -36 + l32e a8, a11, -32 + l32e a9, a11, -28 + l32e a10, a11, -24 + l32e a11, a11, -20 + rfwu + +// ----------------------------------------------------------------------- +// Offset 0x180 — Level-2 interrupt (stub — loops forever) +// ----------------------------------------------------------------------- + .org _vector_table + 0x180 +_level2_vector: + j _level2_vector + +// ----------------------------------------------------------------------- +// Offset 0x1C0 — Level-3 interrupt (stub — loops forever) +// ----------------------------------------------------------------------- + .org _vector_table + 0x1C0 +_level3_vector: + j _level3_vector + +// ----------------------------------------------------------------------- +// Offset 0x200 — Level-4 interrupt (stub — loops forever) +// ----------------------------------------------------------------------- + .org _vector_table + 0x200 +_level4_vector: + j _level4_vector + +// ----------------------------------------------------------------------- +// Offset 0x240 — Level-5 interrupt (stub — loops forever) +// ----------------------------------------------------------------------- + .org _vector_table + 0x240 +_level5_vector: + j _level5_vector + +// ----------------------------------------------------------------------- +// Offset 0x280 — Debug exception / level-6 (stub — loops forever) +// ----------------------------------------------------------------------- + .org _vector_table + 0x280 +_debug_vector: + j _debug_vector + +// ----------------------------------------------------------------------- +// Offset 0x2C0 — NMI / level-7 (stub — loops forever) +// ----------------------------------------------------------------------- + .org _vector_table + 0x2C0 +_nmi_vector: + j _nmi_vector + +// ----------------------------------------------------------------------- +// Offset 0x300 — Kernel exception +// Writes EXCCAUSE+EPC1 to RTC STORE regs, then triggers software reset. +// ESP32 RTC_CNTL base = 0x3FF48000, STORE0 offset = 0x4C. +// ----------------------------------------------------------------------- + .org _vector_table + 0x300 +_kernel_vector: + j _handle_kernel_exc // jump to handler below table + +// ----------------------------------------------------------------------- +// Offset 0x340 — User exception / level-1 interrupt +// +// Save a0 and jump to the full handler below the vector table. +// ----------------------------------------------------------------------- + .org _vector_table + 0x340 + .global _level1_vector +_level1_vector: + wsr a0, EXCSAVE1 // save a0 — only scratch register available + j _handle_level1 // jump to full handler (PC-relative, no literal pool) + +// ----------------------------------------------------------------------- +// Offset 0x3C0 — Double exception (stub — halt) +// ----------------------------------------------------------------------- + .org _vector_table + 0x3C0 +_double_vector: + j _double_vector // halt + +// ----------------------------------------------------------------------- +// Level-1 interrupt handler — lives outside the vector table so there +// is no 64-byte size constraint. +// +// Saves the interrupted context on the current stack, clears PS.EXCM +// (so window overflow/underflow work), calls the Go handleInterrupt +// dispatcher, restores context, and returns via rfe. +// +// We call handleInterrupt via callx4 (window rotation by 4). This is +// required because: +// - callx0 does not set PS.CALLINC, so the Go function's "entry" +// instruction would use whatever CALLINC the interrupted code left, +// causing incorrect window rotation and a garbage stack pointer. +// - callx0 puts the return address in a0 with the raw PC (0x40xxx for +// IRAM), whose top 2 bits (01) cause retw to decrement WindowBase +// by 1 even though nothing was incremented. +// +// With callx4, CALLINC is explicitly set to 1 and the return address +// in a4 has the top 2 bits set to 01 — matching the window rotation +// that entry performs. After retw, WindowBase is correctly restored. +// Our a0..a3 (including a1, the frame pointer) are NOT in the callee's +// register window (callee uses physical regs +4..+19), so a1 is +// preserved across the call without needing EXCSAVE1. +// ----------------------------------------------------------------------- +// Literal data for l32r (must be at a lower address than the l32r). + .balign 4 +.LhandleInterrupt_addr: + .word handleInterrupt +.Lrtc_store0_addr: + .word 0x3FF4804C +.Lrtc_options0_addr: + .word 0x3FF48000 + +// ----------------------------------------------------------------------- +// Kernel exception handler (out-of-table). +// Writes diagnostic info to RTC STORE regs, triggers software reset. +// ----------------------------------------------------------------------- +_handle_kernel_exc: + l32r a0, .Lrtc_store0_addr // a0 = 0x3FF4804C (RTC_CNTL_STORE0) + rsr a1, EXCCAUSE + movi a2, 0x555 + slli a2, a2, 20 // a2 = 0x55500000 + movi a3, 6 + slli a3, a3, 16 // a3 = 0x00060000 + or a2, a2, a3 // a2 = 0x55560000 + or a1, a2, a1 // a1 = 0x5556xxxx (magic + cause) + s32i a1, a0, 0 // STORE0 + rsr a1, EPC1 + s32i a1, a0, 4 // STORE1 = EPC1 + // Trigger software system reset (preserves RTC STORE regs). + // RTC_CNTL_OPTIONS0 = 0x3FF48000, bit 31 = SW_SYS_RST + l32r a0, .Lrtc_options0_addr + l32i a1, a0, 0 + movi a2, 1 + slli a2, a2, 31 + or a1, a1, a2 + s32i a1, a0, 0 // trigger reset +1: j 1b // wait for reset + + .global _handle_level1 +_handle_level1: + // --- allocate 96-byte exception frame on the interrupted stack --- + // Layout (offsets from a1 after adjustment): + // 0: a0 4: a1(orig) 8: a2 12: a3 16: a4 20: a5 + // 24: a6 28: a7 32: a8 36: a9 40: a10 44: a11 + // 48: a12 52: a13 56: a14 60: a15 + // 64: SAR 68: EPC1 72: PS + addi a0, a1, -96 // a0 = new frame pointer + s32i a1, a0, 4 // save original a1 (SP) + mov a1, a0 // a1 = frame pointer + + rsr a0, EXCSAVE1 // recover original a0 + s32i a0, a1, 0 // save original a0 + + // Save general registers a2..a15. + s32i a2, a1, 8 + s32i a3, a1, 12 + s32i a4, a1, 16 + s32i a5, a1, 20 + s32i a6, a1, 24 + s32i a7, a1, 28 + s32i a8, a1, 32 + s32i a9, a1, 36 + s32i a10, a1, 40 + s32i a11, a1, 44 + s32i a12, a1, 48 + s32i a13, a1, 52 + s32i a14, a1, 56 + s32i a15, a1, 60 + + // Save special registers. + rsr a2, SAR + s32i a2, a1, 64 + rsr a2, EPC1 + s32i a2, a1, 68 + + // Clear PS.EXCM (bit 4) so window overflow/underflow exceptions work + // during the Go call. Set PS.INTLEVEL=1 to prevent re-entry of + // level-1 interrupts. + rsr a2, PS + s32i a2, a1, 72 // save PS (with EXCM=1 set by hardware) + movi a3, ~0x1F // mask: clear INTLEVEL (bits 0-3) + EXCM (bit 4) + and a2, a2, a3 + movi a3, 1 // INTLEVEL = 1 + or a2, a2, a3 + wsr a2, PS + rsync + + // Check if this is an exception (not an interrupt). + // EXCCAUSE == 4 means level-1 interrupt; anything else is an exception. + rsr a2, EXCCAUSE + movi a3, 4 + beq a2, a3, .Lis_interrupt + + // --- It's an exception, not an interrupt --- + // Halt: loop forever (no user exception handler on ESP32 by default). + j .Lexception_halt + +.Lis_interrupt: + + // Call the Go interrupt dispatcher via callx4. + // callx4 explicitly sets PS.CALLINC=1 and puts the return address + // (with top 2 bits = 01) in a4. After entry rotates the window by + // 4, the callee sees: a0 = our a4 (return addr), a1 = our a5 - N. + // We set a5 = our frame pointer so the callee gets a valid stack. + mov a5, a1 + l32r a2, .LhandleInterrupt_addr + callx4 a2 + // After retw, WindowBase is restored. a0..a3 are preserved because + // they are outside the callee's register window. + + // --- restore context --- + + // Restore PS (restores EXCM=1). + l32i a2, a1, 72 + wsr a2, PS + rsync + + // Restore special registers. + l32i a2, a1, 64 + wsr a2, SAR + l32i a2, a1, 68 + wsr a2, EPC1 + + // Restore general registers a15..a2. + l32i a15, a1, 60 + l32i a14, a1, 56 + l32i a13, a1, 52 + l32i a12, a1, 48 + l32i a11, a1, 44 + l32i a10, a1, 40 + l32i a9, a1, 36 + l32i a8, a1, 32 + l32i a7, a1, 28 + l32i a6, a1, 24 + l32i a5, a1, 20 + l32i a4, a1, 16 + l32i a3, a1, 12 + l32i a2, a1, 8 + + // Restore a0 and a1 (a1 must be last since it is the frame pointer). + l32i a0, a1, 0 + l32i a1, a1, 4 // restores original SP (deallocates frame) + + rfe + +// ----------------------------------------------------------------------- +// Exception halt: infinite loop for unhandled exceptions. +// ----------------------------------------------------------------------- +.Lexception_halt: + waiti 0 + j .Lexception_halt diff --git a/targets/esp32.json b/targets/esp32.json index 2c7abd6993..73215a96a9 100644 --- a/targets/esp32.json +++ b/targets/esp32.json @@ -7,12 +7,13 @@ "scheduler": "tasks", "serial": "uart", "linker": "ld.lld", - "default-stack-size": 2048, + "default-stack-size": 8192, "rtlib": "compiler-rt", "libc": "picolibc", "linkerscript": "targets/esp32.ld", "extra-files": [ "src/device/esp/esp32.S", + "targets/esp32-interrupts.S", "src/internal/task/task_stack_esp32.S" ], "binary-format": "esp32", diff --git a/targets/esp32.ld b/targets/esp32.ld index 6818ce3190..2d71a162d5 100644 --- a/targets/esp32.ld +++ b/targets/esp32.ld @@ -31,6 +31,15 @@ SECTIONS { *(.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) + *(.literal .text) *(.literal.* .text.*) } >IRAM From b21fd854561aebcbb6903a7244f59503977b35c3 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 9 Jul 2026 00:45:31 +0200 Subject: [PATCH 2/3] esp32: add interrupt-based UART RX and fix init order Signed-off-by: deadprogram --- src/machine/machine_esp32.go | 149 ++++++++++++++++++++++++++++++++++- src/runtime/runtime_esp32.go | 11 ++- 2 files changed, 152 insertions(+), 8 deletions(-) diff --git a/src/machine/machine_esp32.go b/src/machine/machine_esp32.go index 9335a04cef..9f7fe86107 100644 --- a/src/machine/machine_esp32.go +++ b/src/machine/machine_esp32.go @@ -412,19 +412,65 @@ var ( TXRXSignal: 198, RTSCTSSignal: 199, } + + onceUart = sync.Once{} ) +// CPU interrupt line used for all UART peripherals. +// +// On the ESP32 (Xtensa LX6) the 32 CPU interrupt lines have fixed hardware +// roles. Lines 6, 7, 11, 14, 15, 16 and 29 are internal (Xtensa timers, +// software and profiling/NMI) and are NOT wired to the peripheral interrupt +// matrix, so a peripheral routed to one of them via DPORT never fires. +// The usable level-1 peripheral lines are 2, 3, 5, 8, 9, 10 (edge), 12, 13, +// 17 and 18. We use line 8 for UART (9 is the timer alarm, 10 is GPIO). +const cpuInterruptFromUART = 8 + +// uartInterrupts is the set of UART interrupt flags we care about for RX. +const uartInterrupts = esp.UART_INT_ENA_RXFIFO_FULL_INT_ENA | + esp.UART_INT_ENA_RXFIFO_TOUT_INT_ENA | + esp.UART_INT_ENA_PARITY_ERR_INT_ENA | + esp.UART_INT_ENA_FRM_ERR_INT_ENA | + esp.UART_INT_ENA_RXFIFO_OVF_INT_ENA | + esp.UART_INT_ENA_GLITCH_DET_INT_ENA + type UART struct { - Bus *esp.UART_Type - Buffer *RingBuffer - TXRXSignal uint32 - RTSCTSSignal uint32 + Bus *esp.UART_Type + Buffer *RingBuffer + TXRXSignal uint32 + RTSCTSSignal uint32 + ParityErrorDetected bool + DataErrorDetected bool + DataOverflowDetected bool } func (uart *UART) Configure(config UARTConfig) { if config.BaudRate == 0 { config.BaudRate = 115200 } + + // If no pins are specified (the zero value is GPIO0, which is never a + // sensible default for both TX and RX), pick sensible defaults per UART. + // + // For UART0 (the console) we deliberately leave the pins untouched: the + // ROM bootloader has already wired GPIO1 (TX) and GPIO3 (RX) directly via + // the IO MUX to the USB-serial bridge. Re-routing them through the GPIO + // matrix is unnecessary and can break RX, so we keep the bootloader setup + // which is exactly what makes the boot log and greeting appear. + if config.TX == 0 && config.RX == 0 { + switch uart.Bus { + case esp.UART0: + config.TX = NoPin + config.RX = NoPin + case esp.UART1: + config.TX = 10 + config.RX = 9 + case esp.UART2: + config.TX = 17 + config.RX = 16 + } + } + uart.Bus.CLKDIV.Set(peripheralClock / config.BaudRate) if config.RX != NoPin { @@ -442,6 +488,101 @@ func (uart *UART) Configure(config UARTConfig) { if config.CTS != NoPin { config.CTS.configure(PinConfig{Mode: PinInputPullup}, uart.RTSCTSSignal) } + + uart.configureInterrupt() + uart.enableReceiver() +} + +func (uart *UART) configureInterrupt() { + // Disable all UART interrupts while configuring. + uart.Bus.INT_ENA.ClearBits(0x0ffff) + + // Map this UART's peripheral interrupt to a CPU interrupt line via DPORT. + switch uart.Bus { + case esp.UART0: + esp.DPORT.SetPRO_UART_INTR_MAP(cpuInterruptFromUART) + case esp.UART1: + esp.DPORT.SetPRO_UART1_INTR_MAP(cpuInterruptFromUART) + case esp.UART2: + esp.DPORT.SetPRO_UART2_INTR_MAP(cpuInterruptFromUART) + } + + // Register the ISR only once (shared across all UARTs on the same CPU int). + // interrupt.New is a compiler intrinsic and requires a plain (non-capturing) + // handler function, so we use a named package-level function. + onceUart.Do(func() { + _ = interrupt.New(cpuInterruptFromUART, handleUARTInterrupt).Enable() + }) +} + +// handleUARTInterrupt is the shared UART interrupt handler. It must be a plain +// function (not a closure) because interrupt.New is a compiler intrinsic that +// does not support closures. +func handleUARTInterrupt(interrupt.Interrupt) { + UART0.serveInterrupt() + UART1.serveInterrupt() + UART2.serveInterrupt() +} + +func (uart *UART) serveInterrupt() { + // Check masked interrupt status. + interruptFlag := uart.Bus.INT_ST.Get() + if (interruptFlag & uartInterrupts) == 0 { + return + } + + // Block UART interrupts while processing. + uart.Bus.INT_ENA.ClearBits(uartInterrupts) + + if interruptFlag&(esp.UART_INT_ENA_RXFIFO_FULL_INT_ENA|esp.UART_INT_ENA_RXFIFO_TOUT_INT_ENA) != 0 { + for uart.Bus.GetSTATUS_RXFIFO_CNT() > 0 { + // The ESP32 UART FIFO must be accessed through the AHB address + // (base + 0x200C0000 == 0x60000000 for UART0), not the APB FIFO + // register, due to a silicon erratum. This mirrors writeByte. + b := (*volatile.Register8)(unsafe.Add(unsafe.Pointer(uart.Bus), 0x200C0000)).Get() + if !uart.Buffer.Put(b) { + uart.DataOverflowDetected = true + } + } + } + if interruptFlag&esp.UART_INT_ENA_PARITY_ERR_INT_ENA > 0 { + uart.ParityErrorDetected = true + } + if interruptFlag&esp.UART_INT_ENA_FRM_ERR_INT_ENA != 0 { + uart.DataErrorDetected = true + } + if interruptFlag&esp.UART_INT_ENA_RXFIFO_OVF_INT_ENA != 0 { + uart.DataOverflowDetected = true + } + if interruptFlag&esp.UART_INT_ENA_GLITCH_DET_INT_ENA != 0 { + uart.DataErrorDetected = true + } + + // Clear the interrupt status bits. + uart.Bus.INT_CLR.SetBits(interruptFlag) + uart.Bus.INT_CLR.ClearBits(interruptFlag) + // Re-enable UART interrupts. + uart.Bus.INT_ENA.Set(uartInterrupts) +} + +func (uart *UART) enableReceiver() { + // Reset the RX FIFO. + uart.Bus.SetCONF0_RXFIFO_RST(1) + uart.Bus.SetCONF0_RXFIFO_RST(0) + // Trigger interrupt when 1 byte is available (low latency). + uart.Bus.SetCONF1_RXFIFO_FULL_THRHD(1) + // Enable the RX timeout so that a single byte still generates an interrupt + // once the line has been idle for the given number of bit periods. Without + // this, RXFIFO_FULL only fires once more than the threshold has arrived. + uart.Bus.SetCONF1_RX_TOUT_THRHD(2) + uart.Bus.SetCONF1_RX_TOUT_EN(1) + // Enable RX-related interrupts. + uart.Bus.SetINT_ENA_RXFIFO_FULL_INT_ENA(1) + uart.Bus.SetINT_ENA_RXFIFO_TOUT_INT_ENA(1) + uart.Bus.SetINT_ENA_FRM_ERR_INT_ENA(1) + uart.Bus.SetINT_ENA_PARITY_ERR_INT_ENA(1) + uart.Bus.SetINT_ENA_GLITCH_DET_INT_ENA(1) + uart.Bus.SetINT_ENA_RXFIFO_OVF_INT_ENA(1) } func (uart *UART) writeByte(b byte) error { diff --git a/src/runtime/runtime_esp32.go b/src/runtime/runtime_esp32.go index cc2888e054..74e5c6af0c 100644 --- a/src/runtime/runtime_esp32.go +++ b/src/runtime/runtime_esp32.go @@ -47,18 +47,21 @@ func main() { // faster. clearbss() - // Initialize UART. - machine.InitSerial() - // Initialize main system timer used for time.Now. initTimer() - // Set up the Xtensa interrupt vector table. + // Set up the Xtensa interrupt vector table. This zeroes INTENABLE, so it + // must run before any peripheral (UART, timer, etc) enables its own CPU + // interrupt line - otherwise that enable would be wiped out here. interruptInit() // Initialize timer alarm interrupt for the scheduler. initTimerInterrupt() + // Initialize UART. This enables the UART RX interrupt, which must happen + // after interruptInit so the INTENABLE bit is not cleared again. + machine.InitSerial() + // Initialize the heap, call main.main, etc. run() From 750fc1f79bbebf7709c53fbc89c2dfd181266e4e Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 10 Jul 2026 19:21:30 +0200 Subject: [PATCH 3/3] 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 --- src/device/esp/esp32.S | 243 ++++++++++++++++++++++++++++++++++++- targets/esp32-interrupts.S | 73 ++++++++++- targets/esp32.json | 7 ++ targets/esp32.ld | 239 ++++++++++++++++++++++++++---------- 4 files changed, 489 insertions(+), 73 deletions(-) diff --git a/src/device/esp/esp32.S b/src/device/esp/esp32.S index 1179a2daa1..69febb81ee 100644 --- a/src/device/esp/esp32.S +++ b/src/device/esp/esp32.S @@ -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 a5→callee's a1 (stack ptr for callee's entry) + // and caller's a6→callee'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 diff --git a/targets/esp32-interrupts.S b/targets/esp32-interrupts.S index f172b7d098..e215b05f98 100644 --- a/targets/esp32-interrupts.S +++ b/targets/esp32-interrupts.S @@ -364,8 +364,75 @@ _handle_level1: rfe // ----------------------------------------------------------------------- -// Exception halt: infinite loop for unhandled exceptions. -// ----------------------------------------------------------------------- +// Exception halt: dump EXCCAUSE and EPC1 over UART0, then loop forever. +// Helps diagnose unhandled CPU exceptions (crashes) which would otherwise +// look like a silent freeze (especially with the watchdogs disabled). +// ----------------------------------------------------------------------- +.Lexc_uart_fifo: + .long 0x3FF40000 // UART0 FIFO +.Lexc_uart_stat: + .long 0x3FF4001C // UART0 STATUS (TXFIFO_CNT bits 23:16) .Lexception_halt: + // Emit marker "\nEXC " then EXCCAUSE (8 hex), ' ', EPC1 (8 hex), '\n'. + l32r a4, .Lexc_uart_fifo + l32r a5, .Lexc_uart_stat + movi a6, 10 // '\n' + call0 .Lexc_putc + movi a6, 'E' + call0 .Lexc_putc + movi a6, 'X' + call0 .Lexc_putc + movi a6, 'C' + call0 .Lexc_putc + movi a6, ' ' + call0 .Lexc_putc + rsr a7, EXCCAUSE + call0 .Lexc_puthex + movi a6, ' ' + call0 .Lexc_putc + rsr a7, EPC1 + call0 .Lexc_puthex + movi a6, ' ' + call0 .Lexc_putc + rsr a7, EXCVADDR + call0 .Lexc_puthex + movi a6, ' ' + call0 .Lexc_putc + l32i a7, a1, 0 // saved a0 (return address of faulting frame) + call0 .Lexc_puthex + movi a6, 10 // '\n' + call0 .Lexc_putc +1: waiti 0 - j .Lexception_halt + j 1b + +// Emit char in a6 (clobbers a3). a4=fifo, a5=status. Returns via a0 (call0). + .align 4 +.Lexc_putc: + s32i a6, a4, 0 +2: l32i a3, a5, 0 + extui a3, a3, 16, 8 + bnez a3, 2b + ret + +// Emit 32-bit value in a7 as 8 hex chars (clobbers a6,a8,a9,a3). +// Uses a10 as return save since call0 to .Lexc_putc clobbers a0. + .align 4 +.Lexc_puthex: + mov a10, a0 // save return address + movi a9, 28 // shift +3: + ssr a9 + srl a8, a7 + extui a8, a8, 0, 4 // nibble + movi a6, 10 + bge a8, a6, 4f + addi a6, a8, '0' + j 5f +4: addi a6, a8, 'a' - 10 +5: call0 .Lexc_putc + addi a9, a9, -4 + bgez a9, 3b + mov a0, a10 // restore return address + ret + diff --git a/targets/esp32.json b/targets/esp32.json index 73215a96a9..fcc969e0ed 100644 --- a/targets/esp32.json +++ b/targets/esp32.json @@ -7,6 +7,13 @@ "scheduler": "tasks", "serial": "uart", "linker": "ld.lld", + "ldflags": [ + "--wrap=malloc", + "--wrap=calloc", + "--wrap=free", + "--wrap=realloc", + "--wrap=ppCheckTxConnTrafficIdle" + ], "default-stack-size": 8192, "rtlib": "compiler-rt", "libc": "picolibc", diff --git a/targets/esp32.ld b/targets/esp32.ld index 2d71a162d5..388cc81d41 100644 --- a/targets/esp32.ld +++ b/targets/esp32.ld @@ -1,52 +1,61 @@ -/* Linker script for the ESP32 */ +/* Linker script for the ESP32 (flash XIP) + * + * The ESP32 has 520KB of internal SRAM: + * - SRAM0 (192KB): 0x40070000-0x4009FFFF — upper 64KB used by ICache when + * flash XIP is active. Usable IRAM: 0x40080000 (128KB). + * - SRAM1 (128KB): 0x3FFE0000-0x3FFFFFFF — data only. + * - SRAM2 (200KB): 0x3FFAE000-0x3FFDFFFF — data only. + * + * Flash is memory-mapped via the cache: + * - DROM (read-only data): 0x3F400000, up to 4MB (64KB pages via MMU) + * - IROM (executable code): 0x400D0000, up to 4MB (64KB pages via MMU) + * The startup code in IRAM configures the MMU to map flash pages. + * The TinyGo builder places DROM/IROM content at page-aligned flash offsets. + */ MEMORY { - /* Data RAM. Allows byte access. - * There are various data RAM regions: - * SRAM2: 0x3FFA_E000..0x3FFD_FFFF (72 + 128 = 200K) - * SRAM1: 0x3FFE_0000..0x3FFF_FFFF (128K) - * This gives us 328K of contiguous RAM, which is the largest span possible. - * SRAM1 has other addresses as well but the datasheet seems to indicate - * these are aliases. - */ - DRAM (rw) : ORIGIN = 0x3FFAE000, LENGTH = 200K + 128K /* Internal SRAM 1 + 2 */ + /* SRAM2 (0x3FFAE000-0x3FFE0000, 200K): a single safe contiguous block used + * for the stack, initialized .data, .bss, and the whole Go GC heap. */ + DRAM (rw) : ORIGIN = 0x3FFAE000, LENGTH = 200K + + /* Top of SRAM1 (0x3FFF0000-0x40000000, 64K): pool 7 + pool 6. This is the + * ONLY part of SRAM1 that is safe for us to use: + * - 0x3FFE0000-0x3FFE0440 : ROM PRO data (ROM writes it) — avoid + * - 0x3FFE0440-0x3FFE8000 : ROM boot-stack area — NOT free during WiFi; + * putting the arena here corrupts DMA and breaks + * association ("auth expired") — avoid + * - 0x3FFE8000-0x3FFF0000 : pool 8, "used for MAC dump" by the WiFi MAC — + * corrupts anything placed here — avoid + * - 0x3FFF0000-0x40000000 : pool 7/6 — safe (verified: arena + .bss both + * work here). Used for the WiFi arena. + * The arena (DMA buffers) owns this whole 64K block; .bss stays in SRAM2. */ + DRAM1 (rw) : ORIGIN = 0x3FFF0000, LENGTH = 64K - /* Instruction RAM. */ - IRAM (x) : ORIGIN = 0x40080000, LENGTH = 128K /* Internal SRAM 0 */ + IRAM (x) : ORIGIN = 0x40080000, LENGTH = 128K /* SRAM0 (usable portion) */ + + DROM (r) : ORIGIN = 0x3F400000, LENGTH = 4M /* Flash data bus (page-aligned) */ + IROM (rx) : ORIGIN = 0x400D0000, LENGTH = 4M /* Flash instruction bus (page-aligned) */ } -/* The entry point. It is set in the image flashed to the chip, so must be - * defined. - */ ENTRY(call_start_cpu0) SECTIONS { - /* 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. + /* Constant global variables, stored in flash (DROM). + * The builder places this at a page-aligned flash offset. */ - .text : ALIGN(4) + .rodata : ALIGN(4) { - *(.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) - - *(.literal .text) - *(.literal.* .text.*) - } >IRAM + _rodata_start = ABSOLUTE(.); + *(.rodata*) + . = ALIGN (4); + _rodata_end = ABSOLUTE(.); + } >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); @@ -54,55 +63,133 @@ SECTIONS _stack_top = .; } >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) + /* Global variables that are mutable and zero-initialized. Kept in SRAM2 + * (DRAM): the WiFi blob's .bss holds structures the MAC/DMA touches, and + * SRAM1 has no room for both .bss and an adequately-sized arena in its only + * safe region (pool 7/6, 64K) — so .bss stays in SRAM2 and the arena owns + * the SRAM1 block. */ + .bss (NOLOAD) : ALIGN(4) { - *(.rodata) - *(.rodata.*) + . = ALIGN (4); + _sbss = ABSOLUTE(.); + *(.bss .bss.*) + . = ALIGN (4); + _ebss = ABSOLUTE(.); } >DRAM - /* Mutable global variables. - */ + /* Reserved zero-initialized storage for WiFi blob global variables. + * On ESP32-S3 these live at fixed ROM DRAM addresses; on ESP32 the blob + * doesn't export them so we allocate space here. */ + .wifi_bss (NOLOAD) : ALIGN(4) + { + _wifi_bss_start = ABSOLUTE(.); + /* Function pointers patched by netif.c (6 × 4 bytes) */ + PROVIDE(g_config_func = ABSOLUTE(.)); . += 4; + PROVIDE(g_net80211_tx_func = ABSOLUTE(.)); . += 4; + PROVIDE(g_timer_func = ABSOLUTE(.)); . += 4; + PROVIDE(s_michael_mic_failure_cb = ABSOLUTE(.)); . += 4; + PROVIDE(g_tx_done_cb_func = ABSOLUTE(.)); . += 4; + PROVIDE(s_encap_amsdu_func = ABSOLUTE(.)); . += 4; + /* Pointer-sized globals used by pp/lmac (3 × 4 bytes) */ + PROVIDE(pp_wdev_funcs = ABSOLUTE(.)); . += 4; + PROVIDE(our_tx_eb = ABSOLUTE(.)); . += 4; + PROVIDE(our_wait_eb = ABSOLUTE(.)); . += 4; + PROVIDE(lmacConfMib_ptr = ABSOLUTE(.)); . += 4; + . = ALIGN(4); + _wifi_bss_end = ABSOLUTE(.); + } >DRAM + + /* WiFi blob arena (DMA buffers): owns all of DRAM1 (SRAM1 pool 7/6, + * 0x3FFF0000-0x40000000, 64K) — the only SRAM1 region safe from ROM/MAC + * writes. Outside the Go GC heap; DMA-capable; isolated from GC objects. */ + .arena (NOLOAD) : ALIGN(16) + { + _arena_start = ABSOLUTE(.); + } >DRAM1 + _arena_end = ORIGIN(DRAM1) + LENGTH(DRAM1); + ASSERT((_arena_end - _arena_start) >= 32K, "arena region < 32K") + + /* Mutable global variables, initialized by the ROM bootloader. */ .data : ALIGN(4) { + . = ALIGN (4); _sdata = ABSOLUTE(.); - *(.data) - *(.data.*) + *(.data .data.*) + *(.dram*) + . = ALIGN (4); + /* DROM flash physical address, patched by the TinyGo ESP image builder + * with the flash offset where the .rodata (DROM) segment was placed. + * The startup code reads this to program the flash cache MMU. */ + _drom_flash_addr = ABSOLUTE(.); + LONG(0x10000); + . = ALIGN (4); _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. + * data that is loaded by the boot ROM. */ 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). + /* IRAM segment: boot code, interrupt vectors, and WiFi/BLE blob code + * that must run from RAM. */ + .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*) + + . = ALIGN(4); + _iram_end = .; + } >IRAM + + /* IROM segment: main code executed from flash via MMU cache. + * The builder places this at a page-aligned flash offset. */ - .bss (NOLOAD) : ALIGN(4) + .text : ALIGN(4) { - . = ALIGN (4); - _sbss = ABSOLUTE(.); - *(.bss) - *(.bss.*) - . = ALIGN (4); - _ebss = ABSOLUTE(.); - } >DRAM + _text_start = ABSOLUTE(.); + *(.literal .text) + *(.literal.* .text.*) + *(.phyiram*) + . = ALIGN(4); + _text_end = ABSOLUTE(.); + } >IROM + + /DISCARD/ : + { + *(.eh_frame) + } } -/* For the garbage collector. - */ -_globals_start = _sdata; -_globals_end = _ebss; -_heap_start = _ebss; -_heap_end = ORIGIN(DRAM) + LENGTH(DRAM); +/* For the garbage collector. */ +_globals_start = _sbss; +_globals_end = _wifi_bss_end; +_heap_start = _edata; +/* Go GC heap = the rest of SRAM2 above .bss (~121KB), a single safe contiguous + * block. The WiFi arena lives in DRAM1 (SRAM1 pool 7/6), so its ~64KB does not + * consume Go heap and its DMA is isolated from GC objects. The heap never + * touches SRAM1's ROM/MAC-dump regions (0x3FFE0000-0x3FFF0000, left unused). */ +_heap_end = 0x3FFE0000; _stack_size = 4K; @@ -209,3 +296,23 @@ __umodsi3 = 0x4000c7d0; __umulsidi3 = 0x4000c7d8; __unorddf2 = 0x400637f4; __unordsf2 = 0x40063478; + +/* From ESP-IDF: + * components/esp_rom/esp32/ld/esp32.rom.ld + * ROM functions needed for WiFi/PHY support. + */ +PROVIDE(ets_delay_us = 0x40008534); +PROVIDE(ets_printf = 0x40007d54); +PROVIDE(ets_install_putc1 = 0x40007d18); +PROVIDE(ets_install_uart_printf = 0x40007d28); +PROVIDE(intr_matrix_set = 0x4000681c); +PROVIDE(rtc_get_reset_reason = 0x400081d4); +PROVIDE(phy_get_romfuncs = 0x40004100); +PROVIDE(uart_tx_one_char = 0x40009200); +PROVIDE(uart_rx_one_char = 0x400092d0); +PROVIDE(ets_intr_lock = 0x400067b0); +PROVIDE(ets_intr_unlock = 0x400067c4); +PROVIDE(ets_isr_attach = 0x400067ec); +PROVIDE(ets_isr_mask = 0x400067fc); +PROVIDE(ets_isr_unmask = 0x40006808); +PROVIDE(roundup2 = 0x4000ab7c);