esp32s3: add interrupt support (#5244)

* esp32s3: add interrupt support

This finally adds the long awaited support for interrupts on the
Xtensa arch. Initially just for the ESP32-S3 but then others.

Signed-off-by: deadprogram <ron@hybridgroup.com>

* esp32s3: get interrupts working correctly

There were a number of needed changes in order to get interrupts correctly working
on the esp32s3 processor:

- PS.UM=1 in interruptInit() - routed interrupts to user exception vector (0x340)
instead of kernel (0x300)
- Inline ISR in the vector slot - external handlers via j/call0 crashed (likely
clang Xtensa literal pool issue with large movi constants in separate sections)
- Disable INTENABLE (not just INT_CLR) - the USB RX interrupt is level-triggered;
clearing INT_CLR alone causes infinite re-entry since data is still in the FIFO
- Buffered() re-enables INTENABLE after draining the hardware FIFO

Signed-off-by: deadprogram <ron@hybridgroup.com>

---------

Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
Ron Evans
2026-04-03 16:32:06 +02:00
committed by deadprogram
parent b29edadf48
commit 9b1a3a2236
6 changed files with 481 additions and 1 deletions
+223
View File
@@ -0,0 +1,223 @@
//go:build esp32s3
package interrupt
import (
"device"
"device/esp"
"runtime/volatile"
"unsafe"
)
// State represents the previous global interrupt state.
type State uintptr
// Disable disables all interrupts and returns the previous interrupt state. It
// can be used in a critical section like this:
//
// state := interrupt.Disable()
// // critical section
// interrupt.Restore(state)
//
// Critical sections can be nested. Make sure to call Restore in the same order
// as you called Disable (this happens naturally with the pattern above).
func Disable() (state State) {
return State(device.AsmFull("rsil {}, 15", nil))
}
// Restore restores interrupts to what they were before. Give the previous state
// returned by Disable as a parameter. If interrupts were disabled before
// calling Disable, this will not re-enable interrupts, allowing for nested
// critical sections.
func Restore(state State) {
device.AsmFull("wsr {state}, PS", map[string]interface{}{
"state": state,
})
}
// The ESP32-S3 (Xtensa LX7) interrupt model:
//
// 1. The **interrupt matrix** (INTERRUPT_CORE0) maps each peripheral source
// (0-98) to one of 32 CPU interrupt lines via a 5-bit mapping register.
// 2. The CPU's INTENABLE special register (SR 228) enables/disables each of
// the 32 CPU interrupt lines independently.
// 3. When an enabled CPU interrupt fires, the processor vectors to the
// level-1 exception vector (offset 0x180 from VECBASE).
// 4. The INTERRUPT special register (SR 226) shows which CPU interrupts are
// currently pending.
//
// We allocate CPU interrupt lines 6..30 for use by peripherals via
// interrupt.New(). Lines 0-5 are reserved (timer, software, etc.) and
// line 31 is avoided because some hardware treats it specially.
const (
// First / last allocatable CPU interrupt for peripherals.
firstCPUInt = 6
lastCPUInt = 30
)
// cpuIntUsed tracks which CPU interrupt lines have been allocated.
var cpuIntUsed [32]bool
// cpuIntToPeripheral maps CPU interrupt number → peripheral IRQ source,
// so that handleInterrupt can dispatch to the correct Go handler.
var cpuIntToPeripheral [32]int
// inInterrupt is set while we're inside the interrupt handler so that
// interrupt.In() returns the correct value.
var inInterrupt bool
// Enable enables a CPU interrupt for the ESP32-S3. The caller must first
// map the peripheral to a CPU interrupt line using the interrupt matrix,
// e.g.:
//
// esp.INTERRUPT_CORE0.SetGPIO_INTERRUPT_PRO_MAP(cpuInt)
// interrupt.New(cpuInt, handler).Enable()
func (i Interrupt) Enable() error {
if i.num < firstCPUInt || i.num > lastCPUInt {
return errInterruptRange
}
// Mark as used.
cpuIntUsed[i.num] = true
// Read current INTENABLE, set the bit for this CPU interrupt.
cur := readINTENABLE()
cur |= 1 << uint(i.num)
writeINTENABLE(cur)
return nil
}
// In returns whether the CPU is currently inside an interrupt handler.
func In() bool {
return inInterrupt
}
// handleInterrupt is called from the assembly vector code in esp32s3.S.
// It determines which CPU interrupt(s) fired and dispatches to the
// registered Go handlers.
//
//export handleInterrupt
func handleInterrupt() {
inInterrupt = true
// INTERRUPT register shows pending + enabled CPU interrupts.
pending := readINTERRUPT()
enabled := readINTENABLE()
active := pending & enabled
for i := firstCPUInt; i <= lastCPUInt; i++ {
if active&(1<<uint(i)) != 0 {
// callHandlers requires a compile-time constant, so we
// dispatch through a switch.
callHandler(i)
}
}
inInterrupt = false
}
//go:inline
func callHandler(n int) {
switch n {
case 6:
callHandlers(6)
case 7:
callHandlers(7)
case 8:
callHandlers(8)
case 9:
callHandlers(9)
case 10:
callHandlers(10)
case 11:
callHandlers(11)
case 12:
callHandlers(12)
case 13:
callHandlers(13)
case 14:
callHandlers(14)
case 15:
callHandlers(15)
case 16:
callHandlers(16)
case 17:
callHandlers(17)
case 18:
callHandlers(18)
case 19:
callHandlers(19)
case 20:
callHandlers(20)
case 21:
callHandlers(21)
case 22:
callHandlers(22)
case 23:
callHandlers(23)
case 24:
callHandlers(24)
case 25:
callHandlers(25)
case 26:
callHandlers(26)
case 27:
callHandlers(27)
case 28:
callHandlers(28)
case 29:
callHandlers(29)
case 30:
callHandlers(30)
}
}
// callHandlers dispatches to registered interrupt handlers for a given
// interrupt number.
//
//go:linkname callHandlers runtime/interrupt.callHandlers
func callHandlers(num int)
var errInterruptRange = constError("interrupt for ESP32-S3 must be in range 6 through 30")
type constError string
func (e constError) Error() string {
return string(e)
}
// readINTENABLE reads the INTENABLE special register (SR 228).
func readINTENABLE() uint32 {
return uint32(device.AsmFull("rsr {}, INTENABLE", nil))
}
// writeINTENABLE writes the INTENABLE special register (SR 228).
func writeINTENABLE(val uint32) {
device.AsmFull("wsr {val}, INTENABLE", map[string]interface{}{
"val": val,
})
}
// readINTERRUPT reads the INTERRUPT special register (SR 226), which
// reflects the currently pending CPU interrupts.
func readINTERRUPT() uint32 {
return uint32(device.AsmFull("rsr {}, INTERRUPT", nil))
}
// -- Interrupt matrix helpers -----------------------------------------------
// The ESP32-S3 interrupt matrix has one mapping register per peripheral
// source. These are memory-mapped in the INTERRUPT_CORE0 peripheral.
// The mapping register for peripheral source N is at:
// base + N*4 (where base = &INTERRUPT_CORE0.PRO_MAC_INTR_MAP)
//
// We provide helpers to set/get the mapping for any source number.
// mapPeripheralToInt routes peripheral IRQ source `src` to CPU interrupt
// `cpuInt` via the interrupt matrix.
func mapPeripheralToInt(src int, cpuInt int) {
base := unsafe.Pointer(&esp.INTERRUPT_CORE0.PRO_MAC_INTR_MAP)
reg := (*volatile.Register32)(unsafe.Add(base, uintptr(src)*4))
reg.Set(uint32(cpuInt))
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build xtensa
//go:build xtensa && !esp32s3
package interrupt
+41
View File
@@ -3,8 +3,10 @@
package runtime
import (
"device"
"device/esp"
"machine"
"unsafe"
)
// This is the function called on startup after the flash (IROM/DROM) is
@@ -75,6 +77,9 @@ func main() {
// Initialize main system timer used for time.Now.
initTimer()
// Set up the Xtensa interrupt vector table.
interruptInit()
// Initialize the heap, call main.main, etc.
run()
@@ -92,6 +97,42 @@ func abort() {
print("abort called\n")
}
// interruptInit installs the Xtensa vector table by writing its address
// to the VECBASE special register and ensures all CPU interrupts are
// initially disabled.
func interruptInit() {
// Disable all CPU interrupts while we configure.
device.AsmFull("wsr {zero}, INTENABLE", map[string]interface{}{
"zero": uintptr(0),
})
// Write the vector table address to VECBASE (SR 231).
vecbase := uintptr(unsafe.Pointer(&_vector_table))
device.AsmFull("wsr {vecbase}, VECBASE", map[string]interface{}{
"vecbase": vecbase,
})
// Clear PS.EXCM and PS.INTLEVEL so that level-1 interrupts can fire.
// The ROM bootloader leaves PS.EXCM=1 (exception mode), which masks
// all interrupts at level ≤ EXCMLEVEL (level 1 on ESP32-S3).
// PS.INTLEVEL may also be non-zero. Both must be 0 for peripheral
// interrupts to trigger.
//
// We also set PS.UM=1 (bit 5) so that level-1 interrupts route to
// the User exception vector at VECBASE+0x340, where our handler lives.
// With PS.UM=0 (the ROM default), they would go to the Kernel exception
// vector at VECBASE+0x300 which is an infinite-loop stub.
ps := uintptr(device.AsmFull("rsr {}, PS", nil))
ps &^= 0x1F // clear INTLEVEL (bits 0-3) and EXCM (bit 4)
ps |= 0x20 // set PS.UM (bit 5) — use User exception vector
device.AsmFull("wsr {ps}, PS", map[string]interface{}{
"ps": ps,
})
// Synchronize pipeline after writing special registers.
device.Asm("rsync")
}
//go:extern _vector_table
var _vector_table [0]uintptr
+202
View File
@@ -0,0 +1,202 @@
// Xtensa interrupt/exception vector table for the ESP32-S3.
//
// The ESP32-S3 uses an Xtensa LX7 core with the windowed register ABI.
// Interrupt vectors are placed at fixed offsets from the VECBASE special
// register. We only handle level-1 (user) interrupts for now.
//
// Vector offsets (from ESP32-S3 core-isa.h XCHAL definitions):
// 0x000 Window overflow 4
// 0x040 Window underflow 4
// 0x080 Window overflow 8
// 0x0C0 Window underflow 8
// 0x100 Window overflow 12
// 0x140 Window underflow 12
// 0x180 Level-2 interrupt
// 0x1C0 Level-3 interrupt
// 0x200 Level-4 interrupt
// 0x240 Level-5 interrupt
// 0x280 Debug exception (level-6)
// 0x2C0 NMI (level-7)
// 0x300 Kernel exception
// 0x340 User exception (level-1 interrupt)
// 0x3C0 Double exception
// PS register field definitions.
#define PS_WOE 0x00040000
#define PS_EXCM 0x00000010
#define PS_INTLEVEL_MASK 0x0000000F
// -----------------------------------------------------------------------
// Vector table must be aligned to 0x400 (1024 bytes).
// -----------------------------------------------------------------------
.section .text.exception_vectors,"ax"
.global _vector_table
.balign 0x400
_vector_table:
// -----------------------------------------------------------------------
// Offset 0x000 Window overflow 4
// -----------------------------------------------------------------------
.org _vector_table + 0x000
_window_overflow4:
s32e a0, a5, -16
s32e a1, a5, -12
s32e a2, a5, -8
s32e a3, a5, -4
rfwo
// -----------------------------------------------------------------------
// Offset 0x040 Window underflow 4
// -----------------------------------------------------------------------
.org _vector_table + 0x040
_window_underflow4:
l32e a0, a5, -16
l32e a1, a5, -12
l32e a2, a5, -8
l32e a3, a5, -4
rfwu
// -----------------------------------------------------------------------
// Offset 0x080 Window overflow 8
// -----------------------------------------------------------------------
.org _vector_table + 0x080
_window_overflow8:
s32e a0, a9, -16
l32e a0, a1, -12
s32e a1, a9, -12
s32e a2, a9, -8
s32e a3, a9, -4
s32e a4, a0, -32
s32e a5, a0, -28
s32e a6, a0, -24
s32e a7, a0, -20
rfwo
// -----------------------------------------------------------------------
// Offset 0x0C0 Window underflow 8
// -----------------------------------------------------------------------
.org _vector_table + 0x0C0
_window_underflow8:
l32e a0, a9, -16
l32e a1, a9, -12
l32e a2, a9, -8
l32e a7, a1, -12
l32e a3, a9, -4
l32e a4, a7, -32
l32e a5, a7, -28
l32e a6, a7, -24
l32e a7, a7, -20
rfwu
// -----------------------------------------------------------------------
// Offset 0x100 Window overflow 12
// -----------------------------------------------------------------------
.org _vector_table + 0x100
_window_overflow12:
s32e a0, a13, -16
l32e a0, a1, -12
s32e a1, a13, -12
s32e a2, a13, -8
s32e a3, a13, -4
s32e a4, a0, -48
s32e a5, a0, -44
s32e a6, a0, -40
s32e a7, a0, -36
s32e a8, a0, -32
s32e a9, a0, -28
s32e a10, a0, -24
s32e a11, a0, -20
rfwo
// -----------------------------------------------------------------------
// Offset 0x140 Window underflow 12
// -----------------------------------------------------------------------
.org _vector_table + 0x140
_window_underflow12:
l32e a0, a13, -16
l32e a1, a13, -12
l32e a2, a13, -8
l32e a11, a1, -12
l32e a3, a13, -4
l32e a4, a11, -48
l32e a5, a11, -44
l32e a6, a11, -40
l32e a7, a11, -36
l32e a8, a11, -32
l32e a9, a11, -28
l32e a10, a11, -24
l32e a11, a11, -20
rfwu
// -----------------------------------------------------------------------
// Offset 0x180 Level-2 interrupt (stub loops forever)
// -----------------------------------------------------------------------
.org _vector_table + 0x180
_level2_vector:
j _level2_vector
// -----------------------------------------------------------------------
// Offset 0x1C0 Level-3 interrupt (stub loops forever)
// -----------------------------------------------------------------------
.org _vector_table + 0x1C0
_level3_vector:
j _level3_vector
// -----------------------------------------------------------------------
// Offset 0x200 Level-4 interrupt (stub loops forever)
// -----------------------------------------------------------------------
.org _vector_table + 0x200
_level4_vector:
j _level4_vector
// -----------------------------------------------------------------------
// Offset 0x240 Level-5 interrupt (stub loops forever)
// -----------------------------------------------------------------------
.org _vector_table + 0x240
_level5_vector:
j _level5_vector
// -----------------------------------------------------------------------
// Offset 0x280 Debug exception / level-6 (stub loops forever)
// -----------------------------------------------------------------------
.org _vector_table + 0x280
_debug_vector:
j _debug_vector
// -----------------------------------------------------------------------
// Offset 0x2C0 NMI / level-7 (stub loops forever)
// -----------------------------------------------------------------------
.org _vector_table + 0x2C0
_nmi_vector:
j _nmi_vector
// -----------------------------------------------------------------------
// Offset 0x300 Kernel exception (stub loops forever)
// -----------------------------------------------------------------------
.org _vector_table + 0x300
_kernel_vector:
j _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.
// -----------------------------------------------------------------------
.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
// -----------------------------------------------------------------------
// Offset 0x3C0 Double exception (stub loops forever)
// -----------------------------------------------------------------------
.org _vector_table + 0x3C0
_double_vector:
j _double_vector
+1
View File
@@ -12,6 +12,7 @@
"linkerscript": "targets/esp32s3.ld",
"extra-files": [
"src/device/esp/esp32.S",
"targets/esp32s3-interrupts.S",
"src/internal/task/task_stack_esp32.S"
],
"binary-format": "esp32s3",
+13
View File
@@ -39,6 +39,19 @@ SECTIONS
*(.text.call_start_cpu0)
} >IRAM AT >DRAM
/* Xtensa exception/interrupt vector table — must be 0x400-aligned. */
.text.exception_vectors : ALIGN(0x400)
{
*(.text.exception_vectors)
} >IRAM AT >DRAM
/* Level-1 interrupt handler (called from the vector stub). */
.text._handle_level1 : ALIGN(4)
{
*(.literal._handle_level1)
*(.text._handle_level1)
} >IRAM AT >DRAM
/* All other code and literals */
.text : ALIGN(4)
{