From a6a4f85e61f7fe6ea54d4b91d93b0fe0ce31a08c Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 2 Apr 2026 14:09:31 +0200 Subject: [PATCH] esp32s3: replace inline ISR with full interrupt vector handler Replace the minimal inline ISR (which only disabled INTENABLE) with a full level-1 interrupt handler that saves/restores the interrupted context and dispatches to Go's handleInterrupt. The handler uses callx4 (not callx0) to call into Go code because: - callx0 does not set PS.CALLINC, so the Go function's entry instruction uses stale CALLINC from the interrupted code, causing wrong window rotation and a garbage stack pointer. - callx4 explicitly sets CALLINC=1, and our frame pointer (a1) is outside the callee's register window so it is preserved. Also updates the USB Serial/JTAG ISR to disable INT_ENA (peripheral level) instead of relying on INTENABLE, and adds signalInterrupt to the dispatcher so sleepTicks can be woken by any interrupt. --- src/machine/machine_esp32xx_usb.go | 13 +- src/runtime/interrupt/interrupt_esp32s3.go | 6 + targets/esp32s3-interrupts.S | 135 +++++++++++++++++++-- 3 files changed, 139 insertions(+), 15 deletions(-) diff --git a/src/machine/machine_esp32xx_usb.go b/src/machine/machine_esp32xx_usb.go index 2e36b9b2c..5066fab67 100644 --- a/src/machine/machine_esp32xx_usb.go +++ b/src/machine/machine_esp32xx_usb.go @@ -112,10 +112,11 @@ func (usbdev *USB_DEVICE) ensureConfigured() { } // handleInterrupt is called from the CPU interrupt vector when the USB -// peripheral raises an interrupt. For now, just clear the interrupt flag. -// The actual data drain happens in Buffered() via polling — once the ISR -// mechanism is proven, we can move the drain here. +// peripheral raises an interrupt. Disable INT_ENA to prevent the +// level-triggered interrupt from re-asserting immediately (data may +// still be in the FIFO). Buffered() re-enables after draining. func (usbdev *USB_DEVICE) handleInterrupt() { + usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(0) usbdev.Bus.SetINT_CLR_SERIAL_OUT_RECV_PKT_INT_CLR(1) } @@ -162,7 +163,7 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) { // Buffered returns the number of bytes waiting in the receive ring buffer. // It drains any data sitting in the hardware FIFO and re-enables the -// USB interrupt (which the ISR disables via INTENABLE to prevent a +// peripheral-level USB interrupt (which the ISR disables to prevent a // level-triggered interrupt storm). func (usbdev *USB_DEVICE) Buffered() int { usbdev.ensureConfigured() @@ -171,11 +172,9 @@ func (usbdev *USB_DEVICE) Buffered() int { b := byte(usbdev.Bus.EP1.Get()) usbdev.Buffer.Put(b) } - // Clear pending flags and re-enable the RX interrupt. + // Clear pending flags and re-enable the RX interrupt at the peripheral level. usbdev.Bus.INT_CLR.Set(0xFFFFFFFF) usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1) - // Re-enable CPU interrupt 8 in INTENABLE (the ISR clears all bits). - interrupt.New(cpuInterruptFromUSB, usbHandleInterrupt).Enable() return int(usbdev.Buffer.Used()) } diff --git a/src/runtime/interrupt/interrupt_esp32s3.go b/src/runtime/interrupt/interrupt_esp32s3.go index 4f7b8b111..bfb55cb81 100644 --- a/src/runtime/interrupt/interrupt_esp32s3.go +++ b/src/runtime/interrupt/interrupt_esp32s3.go @@ -115,6 +115,9 @@ func handleInterrupt() { } } + // Signal to sleepTicks that an interrupt has occurred. + signalInterrupt() + inInterrupt = false } @@ -180,6 +183,9 @@ func callHandler(n int) { //go:linkname callHandlers runtime/interrupt.callHandlers func callHandlers(num int) +//go:linkname signalInterrupt runtime.signalInterrupt +func signalInterrupt() + var errInterruptRange = constError("interrupt for ESP32-S3 must be in range 6 through 30") type constError string diff --git a/targets/esp32s3-interrupts.S b/targets/esp32s3-interrupts.S index 9236b5e97..bdc731fd1 100644 --- a/targets/esp32s3-interrupts.S +++ b/targets/esp32s3-interrupts.S @@ -181,18 +181,13 @@ _kernel_vector: // ----------------------------------------------------------------------- // Offset 0x340 — User exception / level-1 interrupt // -// Entire handler is inline — no jump, no stack access, no memory loads. -// Just disable all CPU interrupts via INTENABLE and return. -// Buffered() re-enables INTENABLE after draining the hardware FIFO. +// 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 - movi a0, 0 - wsr a0, INTENABLE // disable ALL CPU interrupts - rsr a0, EXCSAVE1 // restore a0 - rfe // return from exception + 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 — loops forever) @@ -200,3 +195,127 @@ _level1_vector: .org _vector_table + 0x3C0 _double_vector: j _double_vector + +// ----------------------------------------------------------------------- +// 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 (0x42xxx for +// flash), 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 + + .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 + + // 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