diff --git a/src/machine/machine_esp32xx_usb.go b/src/machine/machine_esp32xx_usb.go index 2e36b9b2c9..5066fab677 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 4f7b8b1111..bfb55cb81f 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/src/runtime/runtime_esp32s3.go b/src/runtime/runtime_esp32s3.go index 56b1283fca..474c5a8fad 100644 --- a/src/runtime/runtime_esp32s3.go +++ b/src/runtime/runtime_esp32s3.go @@ -80,6 +80,9 @@ func main() { // Set up the Xtensa interrupt vector table. interruptInit() + // Initialize timer alarm interrupt for the scheduler. + initTimerInterrupt() + // Initialize the heap, call main.main, etc. run() diff --git a/src/runtime/runtime_esp32sx.go b/src/runtime/runtime_esp32sx.go index b30dc37e97..2a92783559 100644 --- a/src/runtime/runtime_esp32sx.go +++ b/src/runtime/runtime_esp32sx.go @@ -5,6 +5,8 @@ package runtime import ( "device/esp" "machine" + "runtime/interrupt" + "runtime/volatile" "unsafe" ) @@ -73,11 +75,62 @@ func ticksToNanoseconds(ticks timeUnit) int64 { return int64(ticks) * 25 } -// sleepTicks busy-waits until the given number of ticks have passed. +// CPU interrupt number used for the TIMG0 timer alarm. +const timerAlarmCPUInterrupt = 9 + +var interruptPending volatile.Register8 + +func signalInterrupt() { + interruptPending.Set(1) +} + +var timerAlarmInterrupt interrupt.Interrupt + +// timerAlarmHandler clears the timer interrupt at the peripheral level +// and disables INT_ENA to prevent level-triggered re-assertion. +func timerAlarmHandler(interrupt.Interrupt) { + esp.TIMG0.INT_ENA_TIMERS.ClearBits(1) + esp.TIMG0.INT_CLR_TIMERS.Set(1) +} + +// initTimerInterrupt routes the TIMG0 timer 0 alarm interrupt to a CPU +// interrupt and registers a handler that clears the alarm flag. +func initTimerInterrupt() { + // Clear any stale timer interrupt before enabling. + esp.TIMG0.INT_CLR_TIMERS.Set(1) + + // Map the TIMG0 T0 peripheral interrupt to a CPU interrupt line. + esp.INTERRUPT_CORE0.SetTG_T0_INT_MAP(timerAlarmCPUInterrupt) + + // Register the interrupt handler and enable it once. + timerAlarmInterrupt = interrupt.New(timerAlarmCPUInterrupt, timerAlarmHandler) + timerAlarmInterrupt.Enable() +} + +// sleepTicks spins until the given number of ticks have elapsed, using the +// TIMG0 alarm interrupt to avoid busy-waiting for the entire duration. func sleepTicks(d timeUnit) { - sleepUntil := ticks() + d - for ticks() < sleepUntil { - // TODO: suspend the CPU to not burn power here unnecessarily. + target := ticks() + d + for ticks() < target { + // Set the alarm to fire at the target tick count. + interruptPending.Set(0) + + esp.TIMG0.T0ALARMLO.Set(uint32(target)) + esp.TIMG0.T0ALARMHI.Set(uint32(target >> 32)) + + // Enable the alarm (auto-clears when alarm fires). + esp.TIMG0.T0CONFIG.SetBits(esp.TIMG_TCONFIG_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/esp32s3-interrupts.S b/targets/esp32s3-interrupts.S index 9236b5e970..bdc731fd12 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