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.
This commit is contained in:
deadprogram
2026-04-02 14:09:31 +02:00
parent 3386316b9f
commit a6a4f85e61
3 changed files with 139 additions and 15 deletions
+6 -7
View File
@@ -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())
}
@@ -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
+127 -8
View File
@@ -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