From a4dd38de817d77cb282ddcc5a1203f75a4d50dc3 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 17 Apr 2026 16:53:49 +0200 Subject: [PATCH] machine/esp32s3: use edge-triggered CPU interrupt for GPIO pin interrupts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When SPI is configured via the GPIO Matrix, SPI signal transitions set GPIO.STATUS bits on the routed pins. With a level-triggered CPU interrupt (line 8), the ISR re-enters continuously as long as any STATUS bit is asserted — causing user GPIO callbacks to fire spuriously. Switch cpuInterruptFromPin to CPU interrupt 10, which is edge-triggered (level 1) on the Xtensa LX7. This ensures the ISR fires once per GPIO event rather than looping while SPI is active. Also move STATUS_W1TC clears to before callback dispatch so that new GPIO events arriving during handler execution generate a fresh edge, and add writeINTCLEAR(active) in handleInterrupt to properly acknowledge edge-triggered CPU interrupt pending bits via the INTCLEAR register. Fixes GPIO interrupts firing constantly when SPI and pin interrupts are used together. Signed-off-by: deadprogram --- src/examples/pininterrupt/xiao-esp32s3.go | 2 +- src/machine/machine_esp32s3.go | 22 ++++++++++++++++------ src/runtime/interrupt/interrupt_esp32s3.go | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/examples/pininterrupt/xiao-esp32s3.go b/src/examples/pininterrupt/xiao-esp32s3.go index 4173170c3..348b83f43 100644 --- a/src/examples/pininterrupt/xiao-esp32s3.go +++ b/src/examples/pininterrupt/xiao-esp32s3.go @@ -6,6 +6,6 @@ import "machine" const ( button = machine.D1 - buttonMode = machine.PinInput + buttonMode = machine.PinInputPullup buttonPinChange = machine.PinFalling ) diff --git a/src/machine/machine_esp32s3.go b/src/machine/machine_esp32s3.go index 78f5fd445..e7582688f 100644 --- a/src/machine/machine_esp32s3.go +++ b/src/machine/machine_esp32s3.go @@ -306,7 +306,12 @@ func (p Pin) pinReg() *volatile.Register32 { } const maxPin = 49 -const cpuInterruptFromPin = 8 + +// cpuInterruptFromPin selects an edge-triggered CPU interrupt line for GPIO. +// CPU interrupt 10 is edge-triggered level-1 on the Xtensa LX7, 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 @@ -370,23 +375,28 @@ var ( func setupPinInterrupt() error { esp.INTERRUPT_CORE0.SetGPIO_INTERRUPT_PRO_MAP(cpuInterruptFromPin) return interrupt.New(cpuInterruptFromPin, func(interrupt.Interrupt) { - // Check status for GPIO0-31 + // 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-48 - status1 := esp.GPIO.STATUS1.Get() 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)) } } - // Clear interrupt bits - esp.GPIO.STATUS_W1TC.SetBits(status) - esp.GPIO.STATUS1_W1TC.SetBits(status1) }).Enable() } diff --git a/src/runtime/interrupt/interrupt_esp32s3.go b/src/runtime/interrupt/interrupt_esp32s3.go index bfb55cb81..5fbaaea02 100644 --- a/src/runtime/interrupt/interrupt_esp32s3.go +++ b/src/runtime/interrupt/interrupt_esp32s3.go @@ -107,6 +107,12 @@ func handleInterrupt() { 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<