mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-07 04:23:41 +00:00
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:
@@ -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,4 +1,4 @@
|
||||
//go:build xtensa
|
||||
//go:build xtensa && !esp32s3
|
||||
|
||||
package interrupt
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user