esp32: add interrupt support (vector table, timer alarm, GPIO SetInterrupt)

Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
deadprogram
2026-07-08 23:55:46 +02:00
committed by Ron Evans
parent 9e9be09e9a
commit 94736ac0e5
8 changed files with 820 additions and 9 deletions
+97
View File
@@ -5,7 +5,9 @@ package machine
import (
"device/esp"
"errors"
"runtime/interrupt"
"runtime/volatile"
"sync"
"unsafe"
)
@@ -291,6 +293,101 @@ func (p Pin) mux() *volatile.Register32 {
}
}
const maxPin = 40
// cpuInterruptFromPin selects an edge-triggered CPU interrupt line for GPIO.
// CPU interrupt 10 is edge-triggered level-1 on the Xtensa LX6, which prevents
// the ISR from re-entering continuously when other peripherals (e.g. SPI via
// the GPIO Matrix) keep GPIO.STATUS bits asserted.
const cpuInterruptFromPin = 10
type PinChange uint8
// Pin change interrupt constants for SetInterrupt.
const (
PinRising PinChange = iota + 1
PinFalling
PinToggle
)
// SetInterrupt sets an interrupt to be executed when a particular pin changes
// state. The pin should already be configured as an input, including a pull up
// or down if no external pull is provided.
//
// You can pass a nil func to unset the pin change interrupt. If you do so,
// the change parameter is ignored and can be set to any value (such as 0).
// If the pin is already configured with a callback, you must first unset
// this pins interrupt before you can set a new callback.
func (p Pin) SetInterrupt(change PinChange, callback func(Pin)) (err error) {
if p >= maxPin {
return ErrInvalidInputPin
}
if callback == nil {
// Disable this pin interrupt
p.pinReg().ClearBits(esp.GPIO_PIN_INT_TYPE_Msk | esp.GPIO_PIN_INT_ENA_Msk)
if pinCallbacks[p] != nil {
pinCallbacks[p] = nil
}
return nil
}
if pinCallbacks[p] != nil {
// The pin was already configured.
// To properly re-configure a pin, unset it first and set a new
// configuration.
return ErrNoPinChangeChannel
}
pinCallbacks[p] = callback
onceSetupPinInterrupt.Do(func() {
err = setupPinInterrupt()
})
if err != nil {
return err
}
p.pinReg().Set(
(p.pinReg().Get() & ^uint32(esp.GPIO_PIN_INT_TYPE_Msk|esp.GPIO_PIN_INT_ENA_Msk)) |
uint32(change)<<esp.GPIO_PIN_INT_TYPE_Pos | uint32(1)<<esp.GPIO_PIN_INT_ENA_Pos)
return nil
}
var (
pinCallbacks [maxPin]func(Pin)
onceSetupPinInterrupt sync.Once
)
func setupPinInterrupt() error {
esp.DPORT.SetPRO_GPIO_INTERRUPT_MAP_PRO_GPIO_INTERRUPT_PRO_MAP(cpuInterruptFromPin)
return interrupt.New(cpuInterruptFromPin, func(interrupt.Interrupt) {
// Read and immediately clear interrupt status bits.
// Clearing before processing is critical for edge-triggered CPU
// interrupts: any new GPIO events that arrive during callback
// execution will set fresh STATUS bits, generating a new edge
// on the CPU interrupt line so they are not lost.
status := esp.GPIO.STATUS.Get()
status1 := esp.GPIO.STATUS1.Get()
esp.GPIO.STATUS_W1TC.Set(status)
esp.GPIO.STATUS1_W1TC.Set(status1)
// Check status for GPIO0-31
for i, mask := 0, uint32(1); i < 32; i, mask = i+1, mask<<1 {
if (status&mask) != 0 && pinCallbacks[i] != nil {
pinCallbacks[i](Pin(i))
}
}
// Check status for GPIO32-39
for i, mask := 32, uint32(1); i < maxPin; i, mask = i+1, mask<<1 {
if (status1&mask) != 0 && pinCallbacks[i] != nil {
pinCallbacks[i](Pin(i))
}
}
}).Enable()
}
var DefaultUART = UART0
var (
+226
View File
@@ -0,0 +1,226 @@
//go:build esp32
package interrupt
import (
"device"
)
// 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 (Xtensa LX6) interrupt model:
//
// 1. The **interrupt matrix** (DPORT) maps each peripheral source 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 0x340 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. The caller must first
// map the peripheral to a CPU interrupt line using the interrupt matrix,
// e.g.:
//
// esp.DPORT.PRO_TG_T0_LEVEL_INT_MAP.Set(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 esp32-interrupts.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
// Clear edge-triggered pending bits before dispatching handlers so that
// new edges arriving during handler execution are not lost. Writing to
// INTCLEAR is a no-op for level-triggered lines, so this is safe for all
// interrupt types.
writeINTCLEAR(active)
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)
}
}
// Signal to sleepTicks that an interrupt has occurred.
signalInterrupt()
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)
//go:linkname signalInterrupt runtime.signalInterrupt
func signalInterrupt()
var errInterruptRange = constError("interrupt for ESP32 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))
}
// writeINTCLEAR writes the INTCLEAR special register (SR 227).
// Setting bit N clears CPU interrupt N if it is edge-triggered or
// software-triggered. Bits corresponding to level-triggered interrupts
// are ignored by hardware.
func writeINTCLEAR(val uint32) {
device.AsmFull("wsr {val}, INTCLEAR", map[string]interface{}{
"val": val,
})
}
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build xtensa && !esp32s3
//go:build xtensa && !esp32s3 && !esp32
package interrupt
+45 -7
View File
@@ -6,6 +6,7 @@ import (
"device"
"device/esp"
"machine"
"unsafe"
)
// This is the function called on startup right after the stack pointer has been
@@ -52,6 +53,12 @@ func main() {
// Initialize main system timer used for time.Now.
initTimer()
// Set up the Xtensa interrupt vector table.
interruptInit()
// Initialize timer alarm interrupt for the scheduler.
initTimerInterrupt()
// Initialize the heap, call main.main, etc.
run()
@@ -65,16 +72,47 @@ var _sbss [0]byte
//go:extern _ebss
var _ebss [0]byte
// sleepTicks busy-waits until the given number of ticks have passed.
func sleepTicks(d timeUnit) {
sleepUntil := ticks() + d
for ticks() < sleepUntil {
// TODO: suspend the CPU to not burn power here unnecessarily.
}
}
//go:extern _vector_table
var _vector_table [0]uintptr
func abort() {
for {
device.Asm("waiti 0")
}
}
// 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).
// 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 a reset 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")
}
+69
View File
@@ -0,0 +1,69 @@
//go:build esp32
package runtime
import (
"device/esp"
"runtime/interrupt"
"runtime/volatile"
)
// 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
// via the DPORT interrupt matrix.
esp.DPORT.PRO_TG_T0_LEVEL_INT_MAP.Set(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) {
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_T0CONFIG_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
}
}
}
}
+371
View File
@@ -0,0 +1,371 @@
// Xtensa interrupt/exception vector table for the ESP32.
//
// The ESP32 uses an Xtensa LX6 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 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
// Writes EXCCAUSE+EPC1 to RTC STORE regs, then triggers software reset.
// ESP32 RTC_CNTL base = 0x3FF48000, STORE0 offset = 0x4C.
// -----------------------------------------------------------------------
.org _vector_table + 0x300
_kernel_vector:
j _handle_kernel_exc // jump to handler below table
// -----------------------------------------------------------------------
// Offset 0x340 User exception / level-1 interrupt
//
// 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 only scratch register available
j _handle_level1 // jump to full handler (PC-relative, no literal pool)
// -----------------------------------------------------------------------
// Offset 0x3C0 Double exception (stub halt)
// -----------------------------------------------------------------------
.org _vector_table + 0x3C0
_double_vector:
j _double_vector // halt
// -----------------------------------------------------------------------
// 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 (0x40xxx for
// IRAM), 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
.Lrtc_store0_addr:
.word 0x3FF4804C
.Lrtc_options0_addr:
.word 0x3FF48000
// -----------------------------------------------------------------------
// Kernel exception handler (out-of-table).
// Writes diagnostic info to RTC STORE regs, triggers software reset.
// -----------------------------------------------------------------------
_handle_kernel_exc:
l32r a0, .Lrtc_store0_addr // a0 = 0x3FF4804C (RTC_CNTL_STORE0)
rsr a1, EXCCAUSE
movi a2, 0x555
slli a2, a2, 20 // a2 = 0x55500000
movi a3, 6
slli a3, a3, 16 // a3 = 0x00060000
or a2, a2, a3 // a2 = 0x55560000
or a1, a2, a1 // a1 = 0x5556xxxx (magic + cause)
s32i a1, a0, 0 // STORE0
rsr a1, EPC1
s32i a1, a0, 4 // STORE1 = EPC1
// Trigger software system reset (preserves RTC STORE regs).
// RTC_CNTL_OPTIONS0 = 0x3FF48000, bit 31 = SW_SYS_RST
l32r a0, .Lrtc_options0_addr
l32i a1, a0, 0
movi a2, 1
slli a2, a2, 31
or a1, a1, a2
s32i a1, a0, 0 // trigger reset
1: j 1b // wait for reset
.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
// Check if this is an exception (not an interrupt).
// EXCCAUSE == 4 means level-1 interrupt; anything else is an exception.
rsr a2, EXCCAUSE
movi a3, 4
beq a2, a3, .Lis_interrupt
// --- It's an exception, not an interrupt ---
// Halt: loop forever (no user exception handler on ESP32 by default).
j .Lexception_halt
.Lis_interrupt:
// 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
// -----------------------------------------------------------------------
// Exception halt: infinite loop for unhandled exceptions.
// -----------------------------------------------------------------------
.Lexception_halt:
waiti 0
j .Lexception_halt
+2 -1
View File
@@ -7,12 +7,13 @@
"scheduler": "tasks",
"serial": "uart",
"linker": "ld.lld",
"default-stack-size": 2048,
"default-stack-size": 8192,
"rtlib": "compiler-rt",
"libc": "picolibc",
"linkerscript": "targets/esp32.ld",
"extra-files": [
"src/device/esp/esp32.S",
"targets/esp32-interrupts.S",
"src/internal/task/task_stack_esp32.S"
],
"binary-format": "esp32",
+9
View File
@@ -31,6 +31,15 @@ SECTIONS
{
*(.literal.call_start_cpu0)
*(.text.call_start_cpu0)
/* Xtensa exception/interrupt vector table — must be 0x400-aligned */
. = ALIGN(0x400);
*(.text.exception_vectors)
/* Level-1 interrupt handler */
*(.literal._handle_level1)
*(.text._handle_level1)
*(.literal .text)
*(.literal.* .text.*)
} >IRAM