mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-19 20:14:04 +00:00
esp32: add interrupt support (vector table, timer alarm, GPIO SetInterrupt)
Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
@@ -5,7 +5,9 @@ package machine
|
|||||||
import (
|
import (
|
||||||
"device/esp"
|
"device/esp"
|
||||||
"errors"
|
"errors"
|
||||||
|
"runtime/interrupt"
|
||||||
"runtime/volatile"
|
"runtime/volatile"
|
||||||
|
"sync"
|
||||||
"unsafe"
|
"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 DefaultUART = UART0
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
//go:build xtensa && !esp32s3
|
//go:build xtensa && !esp32s3 && !esp32
|
||||||
|
|
||||||
package interrupt
|
package interrupt
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"device"
|
"device"
|
||||||
"device/esp"
|
"device/esp"
|
||||||
"machine"
|
"machine"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is the function called on startup right after the stack pointer has been
|
// 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.
|
// Initialize main system timer used for time.Now.
|
||||||
initTimer()
|
initTimer()
|
||||||
|
|
||||||
|
// Set up the Xtensa interrupt vector table.
|
||||||
|
interruptInit()
|
||||||
|
|
||||||
|
// Initialize timer alarm interrupt for the scheduler.
|
||||||
|
initTimerInterrupt()
|
||||||
|
|
||||||
// Initialize the heap, call main.main, etc.
|
// Initialize the heap, call main.main, etc.
|
||||||
run()
|
run()
|
||||||
|
|
||||||
@@ -65,16 +72,47 @@ var _sbss [0]byte
|
|||||||
//go:extern _ebss
|
//go:extern _ebss
|
||||||
var _ebss [0]byte
|
var _ebss [0]byte
|
||||||
|
|
||||||
// sleepTicks busy-waits until the given number of ticks have passed.
|
//go:extern _vector_table
|
||||||
func sleepTicks(d timeUnit) {
|
var _vector_table [0]uintptr
|
||||||
sleepUntil := ticks() + d
|
|
||||||
for ticks() < sleepUntil {
|
|
||||||
// TODO: suspend the CPU to not burn power here unnecessarily.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func abort() {
|
func abort() {
|
||||||
for {
|
for {
|
||||||
device.Asm("waiti 0")
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -7,12 +7,13 @@
|
|||||||
"scheduler": "tasks",
|
"scheduler": "tasks",
|
||||||
"serial": "uart",
|
"serial": "uart",
|
||||||
"linker": "ld.lld",
|
"linker": "ld.lld",
|
||||||
"default-stack-size": 2048,
|
"default-stack-size": 8192,
|
||||||
"rtlib": "compiler-rt",
|
"rtlib": "compiler-rt",
|
||||||
"libc": "picolibc",
|
"libc": "picolibc",
|
||||||
"linkerscript": "targets/esp32.ld",
|
"linkerscript": "targets/esp32.ld",
|
||||||
"extra-files": [
|
"extra-files": [
|
||||||
"src/device/esp/esp32.S",
|
"src/device/esp/esp32.S",
|
||||||
|
"targets/esp32-interrupts.S",
|
||||||
"src/internal/task/task_stack_esp32.S"
|
"src/internal/task/task_stack_esp32.S"
|
||||||
],
|
],
|
||||||
"binary-format": "esp32",
|
"binary-format": "esp32",
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ SECTIONS
|
|||||||
{
|
{
|
||||||
*(.literal.call_start_cpu0)
|
*(.literal.call_start_cpu0)
|
||||||
*(.text.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)
|
||||||
*(.literal.* .text.*)
|
*(.literal.* .text.*)
|
||||||
} >IRAM
|
} >IRAM
|
||||||
|
|||||||
Reference in New Issue
Block a user