mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-16 10:43:29 +00:00
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:
@@ -112,10 +112,11 @@ func (usbdev *USB_DEVICE) ensureConfigured() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleInterrupt is called from the CPU interrupt vector when the USB
|
// handleInterrupt is called from the CPU interrupt vector when the USB
|
||||||
// peripheral raises an interrupt. For now, just clear the interrupt flag.
|
// peripheral raises an interrupt. Disable INT_ENA to prevent the
|
||||||
// The actual data drain happens in Buffered() via polling — once the ISR
|
// level-triggered interrupt from re-asserting immediately (data may
|
||||||
// mechanism is proven, we can move the drain here.
|
// still be in the FIFO). Buffered() re-enables after draining.
|
||||||
func (usbdev *USB_DEVICE) handleInterrupt() {
|
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)
|
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.
|
// 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
|
// 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).
|
// level-triggered interrupt storm).
|
||||||
func (usbdev *USB_DEVICE) Buffered() int {
|
func (usbdev *USB_DEVICE) Buffered() int {
|
||||||
usbdev.ensureConfigured()
|
usbdev.ensureConfigured()
|
||||||
@@ -171,11 +172,9 @@ func (usbdev *USB_DEVICE) Buffered() int {
|
|||||||
b := byte(usbdev.Bus.EP1.Get())
|
b := byte(usbdev.Bus.EP1.Get())
|
||||||
usbdev.Buffer.Put(b)
|
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.INT_CLR.Set(0xFFFFFFFF)
|
||||||
usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1)
|
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())
|
return int(usbdev.Buffer.Used())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,9 @@ func handleInterrupt() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Signal to sleepTicks that an interrupt has occurred.
|
||||||
|
signalInterrupt()
|
||||||
|
|
||||||
inInterrupt = false
|
inInterrupt = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +183,9 @@ func callHandler(n int) {
|
|||||||
//go:linkname callHandlers runtime/interrupt.callHandlers
|
//go:linkname callHandlers runtime/interrupt.callHandlers
|
||||||
func callHandlers(num int)
|
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")
|
var errInterruptRange = constError("interrupt for ESP32-S3 must be in range 6 through 30")
|
||||||
|
|
||||||
type constError string
|
type constError string
|
||||||
|
|||||||
@@ -181,18 +181,13 @@ _kernel_vector:
|
|||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Offset 0x340 — User exception / level-1 interrupt
|
// Offset 0x340 — User exception / level-1 interrupt
|
||||||
//
|
//
|
||||||
// Entire handler is inline — no jump, no stack access, no memory loads.
|
// Save a0 and jump to the full handler below the vector table.
|
||||||
// Just disable all CPU interrupts via INTENABLE and return.
|
|
||||||
// Buffered() re-enables INTENABLE after draining the hardware FIFO.
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
.org _vector_table + 0x340
|
.org _vector_table + 0x340
|
||||||
.global _level1_vector
|
.global _level1_vector
|
||||||
_level1_vector:
|
_level1_vector:
|
||||||
wsr a0, EXCSAVE1 // save a0
|
wsr a0, EXCSAVE1 // save a0 — only scratch register available
|
||||||
movi a0, 0
|
j _handle_level1 // jump to full handler (PC-relative, no literal pool)
|
||||||
wsr a0, INTENABLE // disable ALL CPU interrupts
|
|
||||||
rsr a0, EXCSAVE1 // restore a0
|
|
||||||
rfe // return from exception
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Offset 0x3C0 — Double exception (stub — loops forever)
|
// Offset 0x3C0 — Double exception (stub — loops forever)
|
||||||
@@ -200,3 +195,127 @@ _level1_vector:
|
|||||||
.org _vector_table + 0x3C0
|
.org _vector_table + 0x3C0
|
||||||
_double_vector:
|
_double_vector:
|
||||||
j _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
|
||||||
|
|||||||
Reference in New Issue
Block a user