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
+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
}
}
}
}