Skip to content

Commit a6a4f85

Browse files
committed
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.
1 parent 3386316 commit a6a4f85

File tree

3 files changed

+139
-15
lines changed

3 files changed

+139
-15
lines changed

src/machine/machine_esp32xx_usb.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,11 @@ func (usbdev *USB_DEVICE) ensureConfigured() {
112112
}
113113

114114
// handleInterrupt is called from the CPU interrupt vector when the USB
115-
// peripheral raises an interrupt. For now, just clear the interrupt flag.
116-
// The actual data drain happens in Buffered() via polling — once the ISR
117-
// mechanism is proven, we can move the drain here.
115+
// peripheral raises an interrupt. Disable INT_ENA to prevent the
116+
// level-triggered interrupt from re-asserting immediately (data may
117+
// still be in the FIFO). Buffered() re-enables after draining.
118118
func (usbdev *USB_DEVICE) handleInterrupt() {
119+
usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(0)
119120
usbdev.Bus.SetINT_CLR_SERIAL_OUT_RECV_PKT_INT_CLR(1)
120121
}
121122

@@ -162,7 +163,7 @@ func (usbdev *USB_DEVICE) Write(data []byte) (n int, err error) {
162163

163164
// Buffered returns the number of bytes waiting in the receive ring buffer.
164165
// It drains any data sitting in the hardware FIFO and re-enables the
165-
// USB interrupt (which the ISR disables via INTENABLE to prevent a
166+
// peripheral-level USB interrupt (which the ISR disables to prevent a
166167
// level-triggered interrupt storm).
167168
func (usbdev *USB_DEVICE) Buffered() int {
168169
usbdev.ensureConfigured()
@@ -171,11 +172,9 @@ func (usbdev *USB_DEVICE) Buffered() int {
171172
b := byte(usbdev.Bus.EP1.Get())
172173
usbdev.Buffer.Put(b)
173174
}
174-
// Clear pending flags and re-enable the RX interrupt.
175+
// Clear pending flags and re-enable the RX interrupt at the peripheral level.
175176
usbdev.Bus.INT_CLR.Set(0xFFFFFFFF)
176177
usbdev.Bus.SetINT_ENA_SERIAL_OUT_RECV_PKT_INT_ENA(1)
177-
// Re-enable CPU interrupt 8 in INTENABLE (the ISR clears all bits).
178-
interrupt.New(cpuInterruptFromUSB, usbHandleInterrupt).Enable()
179178
return int(usbdev.Buffer.Used())
180179
}
181180

src/runtime/interrupt/interrupt_esp32s3.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ func handleInterrupt() {
115115
}
116116
}
117117

118+
// Signal to sleepTicks that an interrupt has occurred.
119+
signalInterrupt()
120+
118121
inInterrupt = false
119122
}
120123

@@ -180,6 +183,9 @@ func callHandler(n int) {
180183
//go:linkname callHandlers runtime/interrupt.callHandlers
181184
func callHandlers(num int)
182185

186+
//go:linkname signalInterrupt runtime.signalInterrupt
187+
func signalInterrupt()
188+
183189
var errInterruptRange = constError("interrupt for ESP32-S3 must be in range 6 through 30")
184190

185191
type constError string

targets/esp32s3-interrupts.S

Lines changed: 127 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -181,22 +181,141 @@ _kernel_vector:
181181
// -----------------------------------------------------------------------
182182
// Offset 0x340 — User exception / level-1 interrupt
183183
//
184-
// Entire handler is inline — no jump, no stack access, no memory loads.
185-
// Just disable all CPU interrupts via INTENABLE and return.
186-
// Buffered() re-enables INTENABLE after draining the hardware FIFO.
184+
// Save a0 and jump to the full handler below the vector table.
187185
// -----------------------------------------------------------------------
188186
.org _vector_table + 0x340
189187
.global _level1_vector
190188
_level1_vector:
191-
wsr a0, EXCSAVE1 // save a0
192-
movi a0, 0
193-
wsr a0, INTENABLE // disable ALL CPU interrupts
194-
rsr a0, EXCSAVE1 // restore a0
195-
rfe // return from exception
189+
wsr a0, EXCSAVE1 // save a0 — only scratch register available
190+
j _handle_level1 // jump to full handler (PC-relative, no literal pool)
196191

197192
// -----------------------------------------------------------------------
198193
// Offset 0x3C0 — Double exception (stub — loops forever)
199194
// -----------------------------------------------------------------------
200195
.org _vector_table + 0x3C0
201196
_double_vector:
202197
j _double_vector
198+
199+
// -----------------------------------------------------------------------
200+
// Level-1 interrupt handler — lives outside the vector table so there
201+
// is no 64-byte size constraint.
202+
//
203+
// Saves the interrupted context on the current stack, clears PS.EXCM
204+
// (so window overflow/underflow work), calls the Go handleInterrupt
205+
// dispatcher, restores context, and returns via rfe.
206+
//
207+
// We call handleInterrupt via callx4 (window rotation by 4). This is
208+
// required because:
209+
// - callx0 does not set PS.CALLINC, so the Go function's "entry"
210+
// instruction would use whatever CALLINC the interrupted code left,
211+
// causing incorrect window rotation and a garbage stack pointer.
212+
// - callx0 puts the return address in a0 with the raw PC (0x42xxx for
213+
// flash), whose top 2 bits (01) cause retw to decrement WindowBase
214+
// by 1 even though nothing was incremented.
215+
//
216+
// With callx4, CALLINC is explicitly set to 1 and the return address
217+
// in a4 has the top 2 bits set to 01 — matching the window rotation
218+
// that entry performs. After retw, WindowBase is correctly restored.
219+
// Our a0..a3 (including a1, the frame pointer) are NOT in the callee's
220+
// register window (callee uses physical regs +4..+19), so a1 is
221+
// preserved across the call without needing EXCSAVE1.
222+
// -----------------------------------------------------------------------
223+
// Literal data for l32r (must be at a lower address than the l32r).
224+
.balign 4
225+
.LhandleInterrupt_addr:
226+
.word handleInterrupt
227+
228+
.global _handle_level1
229+
_handle_level1:
230+
// --- allocate 96-byte exception frame on the interrupted stack ---
231+
// Layout (offsets from a1 after adjustment):
232+
// 0: a0 4: a1(orig) 8: a2 12: a3 16: a4 20: a5
233+
// 24: a6 28: a7 32: a8 36: a9 40: a10 44: a11
234+
// 48: a12 52: a13 56: a14 60: a15
235+
// 64: SAR 68: EPC1 72: PS
236+
addi a0, a1, -96 // a0 = new frame pointer
237+
s32i a1, a0, 4 // save original a1 (SP)
238+
mov a1, a0 // a1 = frame pointer
239+
240+
rsr a0, EXCSAVE1 // recover original a0
241+
s32i a0, a1, 0 // save original a0
242+
243+
// Save general registers a2..a15.
244+
s32i a2, a1, 8
245+
s32i a3, a1, 12
246+
s32i a4, a1, 16
247+
s32i a5, a1, 20
248+
s32i a6, a1, 24
249+
s32i a7, a1, 28
250+
s32i a8, a1, 32
251+
s32i a9, a1, 36
252+
s32i a10, a1, 40
253+
s32i a11, a1, 44
254+
s32i a12, a1, 48
255+
s32i a13, a1, 52
256+
s32i a14, a1, 56
257+
s32i a15, a1, 60
258+
259+
// Save special registers.
260+
rsr a2, SAR
261+
s32i a2, a1, 64
262+
rsr a2, EPC1
263+
s32i a2, a1, 68
264+
265+
// Clear PS.EXCM (bit 4) so window overflow/underflow exceptions work
266+
// during the Go call. Set PS.INTLEVEL=1 to prevent re-entry of
267+
// level-1 interrupts.
268+
rsr a2, PS
269+
s32i a2, a1, 72 // save PS (with EXCM=1 set by hardware)
270+
movi a3, ~0x1F // mask: clear INTLEVEL (bits 0-3) + EXCM (bit 4)
271+
and a2, a2, a3
272+
movi a3, 1 // INTLEVEL = 1
273+
or a2, a2, a3
274+
wsr a2, PS
275+
rsync
276+
277+
// Call the Go interrupt dispatcher via callx4.
278+
// callx4 explicitly sets PS.CALLINC=1 and puts the return address
279+
// (with top 2 bits = 01) in a4. After entry rotates the window by
280+
// 4, the callee sees: a0 = our a4 (return addr), a1 = our a5 - N.
281+
// We set a5 = our frame pointer so the callee gets a valid stack.
282+
mov a5, a1
283+
l32r a2, .LhandleInterrupt_addr
284+
callx4 a2
285+
// After retw, WindowBase is restored. a0..a3 are preserved because
286+
// they are outside the callee's register window.
287+
288+
// --- restore context ---
289+
290+
// Restore PS (restores EXCM=1).
291+
l32i a2, a1, 72
292+
wsr a2, PS
293+
rsync
294+
295+
// Restore special registers.
296+
l32i a2, a1, 64
297+
wsr a2, SAR
298+
l32i a2, a1, 68
299+
wsr a2, EPC1
300+
301+
// Restore general registers a15..a2.
302+
l32i a15, a1, 60
303+
l32i a14, a1, 56
304+
l32i a13, a1, 52
305+
l32i a12, a1, 48
306+
l32i a11, a1, 44
307+
l32i a10, a1, 40
308+
l32i a9, a1, 36
309+
l32i a8, a1, 32
310+
l32i a7, a1, 28
311+
l32i a6, a1, 24
312+
l32i a5, a1, 20
313+
l32i a4, a1, 16
314+
l32i a3, a1, 12
315+
l32i a2, a1, 8
316+
317+
// Restore a0 and a1 (a1 must be last since it is the frame pointer).
318+
l32i a0, a1, 0
319+
l32i a1, a1, 4 // restores original SP (deallocates frame)
320+
321+
rfe

0 commit comments

Comments
 (0)