From 68ccd55a46ad460e61f2d3d8a4e6d9b18d476903 Mon Sep 17 00:00:00 2001 From: luoliwoshang <2643523683@qq.com> Date: Wed, 26 Nov 2025 18:15:25 +0800 Subject: [PATCH] feat: Add runtime/interrupt package and update volatile package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete runtime/interrupt package with platform-specific implementations: - interrupt.go: Core interrupt handling interface - checkpoint.go: Checkpoint mechanism for interrupt state management - Platform-specific implementations for AVR, Cortex-M, ESP32-C3, GBA, K210, SiFive, RISC-V, Xtensa Update runtime/volatile package: - Change LLGoPackage from "decl" to "link" for proper linking This change provides a cross-platform interrupt handling abstraction that supports multiple embedded architectures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- runtime/interrupt/checkpoint.go | 67 +++++ runtime/interrupt/interrupt.go | 39 +++ runtime/interrupt/interrupt_avr.go | 45 ++++ runtime/interrupt/interrupt_cortexm.go | 65 +++++ runtime/interrupt/interrupt_esp32c3.go | 239 ++++++++++++++++++ runtime/interrupt/interrupt_gameboyadvance.go | 104 ++++++++ runtime/interrupt/interrupt_k210.go | 27 ++ runtime/interrupt/interrupt_none.go | 31 +++ runtime/interrupt/interrupt_sifive.go | 18 ++ runtime/interrupt/interrupt_tinygoriscv.go | 38 +++ runtime/interrupt/interrupt_xtensa.go | 39 +++ runtime/volatile/volatile.go | 2 +- 12 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 runtime/interrupt/checkpoint.go create mode 100644 runtime/interrupt/interrupt.go create mode 100644 runtime/interrupt/interrupt_avr.go create mode 100644 runtime/interrupt/interrupt_cortexm.go create mode 100644 runtime/interrupt/interrupt_esp32c3.go create mode 100644 runtime/interrupt/interrupt_gameboyadvance.go create mode 100644 runtime/interrupt/interrupt_k210.go create mode 100644 runtime/interrupt/interrupt_none.go create mode 100644 runtime/interrupt/interrupt_sifive.go create mode 100644 runtime/interrupt/interrupt_tinygoriscv.go create mode 100644 runtime/interrupt/interrupt_xtensa.go diff --git a/runtime/interrupt/checkpoint.go b/runtime/interrupt/checkpoint.go new file mode 100644 index 0000000..2ce0b31 --- /dev/null +++ b/runtime/interrupt/checkpoint.go @@ -0,0 +1,67 @@ +package interrupt + +import _ "unsafe" + +// A checkpoint is a setjmp like buffer, that can be used as a flag for +// interrupts. +// +// It can be used as follows: +// +// // global var +// var c Checkpoint +// +// // to set up the checkpoint and wait for it +// if c.Save() { +// setupInterrupt() +// for { +// waitForInterrupt() +// } +// } +// +// // Inside the interrupt handler: +// if c.Saved() { +// c.Jump() +// } +type Checkpoint struct { + jumpSP uintptr + jumpPC uintptr +} + +// Save the execution state in the given checkpoint, overwriting a previous +// saved checkpoint. +// +// This function returns twice: once the normal way after saving (returning +// true) and once after jumping (returning false). +// +// This function is a compiler intrinsic, it is not implemented in Go. +// TODO(zzy): implement Save +func (c *Checkpoint) Save() bool { + panic("todo:Checkpoint.Save") +} + +// Returns whether a jump point was saved (and not erased due to a jump). +func (c *Checkpoint) Saved() bool { + return c.jumpPC != 0 +} + +// Jump to the point where the execution state was saved, and erase the saved +// jump point. This must *only* be called from inside an interrupt. +// +// This method does not return in the conventional way, it resumes execution at +// the last point a checkpoint was saved. +func (c *Checkpoint) Jump() { + if !c.Saved() { + panic("runtime/interrupt: no checkpoint was saved") + } + jumpPC := c.jumpPC + jumpSP := c.jumpSP + c.jumpPC = 0 + c.jumpSP = 0 + if jumpPC == 0 { + panic("jumping to 0") + } + checkpointJump(jumpSP, jumpPC) +} + +//go:linkname checkpointJump __llgo_checkpointJump +func checkpointJump(jumpSP, jumpPC uintptr) diff --git a/runtime/interrupt/interrupt.go b/runtime/interrupt/interrupt.go new file mode 100644 index 0000000..aa3f149 --- /dev/null +++ b/runtime/interrupt/interrupt.go @@ -0,0 +1,39 @@ +// Package interrupt provides access to hardware interrupts. It provides a way +// to define interrupts and to enable/disable them. +package interrupt + +import "unsafe" + +// Interrupt provides direct access to hardware interrupts. You can configure +// this interrupt through this interface. +// +// Do not use the zero value of an Interrupt object. Instead, call New to obtain +// an interrupt handle. +type Interrupt struct { + // Make this number unexported so it cannot be set directly. This provides + // some encapsulation. + num int +} + +// New is a compiler intrinsic that creates a new Interrupt object. You may call +// it only once, and must pass constant parameters to it. That means that the +// interrupt ID must be a Go constant and that the handler must be a simple +// function: closures are not supported. +// TODO(zzy): implement New +func New(id int, handler func(Interrupt)) Interrupt { + panic("todo:interrupt.New") +} + +// handle is used internally, between IR generation and interrupt lowering. The +// frontend will create runtime/interrupt.handle objects, cast them to an int, +// and use that in an Interrupt object. That way the compiler will be able to +// optimize away all interrupt handles that are never used in a program. +// This system only works when interrupts need to be enabled before use and this +// is done only through calling Enable() on this object. If interrupts cannot +// individually be enabled/disabled, the compiler should create a pseudo-call +// (like runtime/interrupt.use()) that keeps the interrupt alive. +type handle struct { + context unsafe.Pointer + funcPtr uintptr + Interrupt +} diff --git a/runtime/interrupt/interrupt_avr.go b/runtime/interrupt/interrupt_avr.go new file mode 100644 index 0000000..9e75cf7 --- /dev/null +++ b/runtime/interrupt/interrupt_avr.go @@ -0,0 +1,45 @@ +//go:build avr + +package interrupt + +import "github.com/goplus/emb/device" + +// State represents the previous global interrupt state. +type State uint8 + +// 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) { + // SREG is at I/O address 0x3f. + return State(device.AsmFull(` + in {}, 0x3f + cli + `, 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) { + // SREG is at I/O address 0x3f. + device.AsmFull("out 0x3f, {state}", map[string]interface{}{ + "state": state, + }) +} + +// In returns whether the system is currently in an interrupt. +// +// Warning: this always returns false on AVR, as there does not appear to be a +// reliable way to determine whether we're currently running inside an interrupt +// handler. +func In() bool { + return false +} diff --git a/runtime/interrupt/interrupt_cortexm.go b/runtime/interrupt/interrupt_cortexm.go new file mode 100644 index 0000000..467d08a --- /dev/null +++ b/runtime/interrupt/interrupt_cortexm.go @@ -0,0 +1,65 @@ +//go:build cortexm + +package interrupt + +import ( + "github.com/goplus/emb/device/arm" +) + +// Enable enables this interrupt. Right after calling this function, the +// interrupt may be invoked if it was already pending. +func (irq Interrupt) Enable() { + // Clear the ARM pending bit, an asserting device may still + // trigger the interrupt once enabled. + arm.ClearPendingIRQ(uint32(irq.num)) + arm.EnableIRQ(uint32(irq.num)) +} + +// Disable disables this interrupt. +func (irq Interrupt) Disable() { + arm.DisableIRQ(uint32(irq.num)) +} + +// SetPriority sets the interrupt priority for this interrupt. A lower number +// means a higher priority. Additionally, most hardware doesn't implement all +// priority bits (only the uppoer bits). +// +// Examples: 0xff (lowest priority), 0xc0 (low priority), 0x00 (highest possible +// priority). +func (irq Interrupt) SetPriority(priority uint8) { + arm.SetPriority(uint32(irq.num), uint32(priority)) +} + +// 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(arm.DisableInterrupts()) +} + +// 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) { + arm.EnableInterrupts(uintptr(state)) +} + +// In returns whether the system is currently in an interrupt. +func In() bool { + // The VECTACTIVE field gives the instruction vector that is currently + // active (in handler mode), or 0 if not in an interrupt. + // Documentation: + // https://developer.arm.com/documentation/dui0497/a/cortex-m0-peripherals/system-control-block/interrupt-control-and-state-register + vectactive := uint8(arm.SCB.ICSR.Get()) + return vectactive != 0 +} diff --git a/runtime/interrupt/interrupt_esp32c3.go b/runtime/interrupt/interrupt_esp32c3.go new file mode 100644 index 0000000..359c980 --- /dev/null +++ b/runtime/interrupt/interrupt_esp32c3.go @@ -0,0 +1,239 @@ +//go:build esp32c3 + +package interrupt + +import ( + "errors" + "unsafe" + + "github.com/goplus/emb/device/esp" + "github.com/goplus/emb/device/riscv" + "github.com/goplus/emb/runtime/volatile" +) + +// Enable register CPU interrupt with interrupt.Interrupt. +// The ESP32-C3 has 31 CPU independent interrupts. +// The Interrupt.New(x, f) (x = [1..31]) attaches CPU interrupt to function f. +// Caller must map the selected interrupt using following sequence (for example using id 5): +// +// // map interrupt 5 to my XXXX module +// esp.INTERRUPT_CORE0.XXXX_INTERRUPT_PRO_MAP.Set( 5 ) +// _ = Interrupt.New(5, func(interrupt.Interrupt) { +// ... +// }).Enable() +func (i Interrupt) Enable() error { + if i.num < 1 && i.num > 31 { + return errors.New("interrupt for ESP32-C3 must be in range of 1 through 31") + } + + // TODO(zzy):https://github.com/goplus/llgo/issues/1290 + // mask := riscv.DisableInterrupts() + // defer riscv.EnableInterrupts(mask) + + // enable CPU interrupt number i.num + esp.INTERRUPT_CORE0.CPU_INT_ENABLE.SetBits(1 << i.num) + + // Set pulse interrupt type (rising edge detection) + esp.INTERRUPT_CORE0.CPU_INT_TYPE.SetBits(1 << i.num) + + // Set default threshold to defaultThreshold + reg := (*volatile.Register32)(unsafe.Add(unsafe.Pointer(&esp.INTERRUPT_CORE0.CPU_INT_PRI_0), i.num*4)) + reg.Set(defaultThreshold) + + // Reset interrupt before reenabling + esp.INTERRUPT_CORE0.CPU_INT_CLEAR.SetBits(1 << i.num) + esp.INTERRUPT_CORE0.CPU_INT_CLEAR.ClearBits(1 << i.num) + + // we must wait for any pending write operations to complete + riscv.Asm("fence") + return nil +} + +// Adding pseudo function calls that is replaced by the compiler with the actual +// functions registered through interrupt.New. +// +//go:linkname callHandlers runtime/interrupt.callHandlers +func callHandlers(num int) + +const ( + IRQNUM_1 = 1 + iota + IRQNUM_2 + IRQNUM_3 + IRQNUM_4 + IRQNUM_5 + IRQNUM_6 + IRQNUM_7 + IRQNUM_8 + IRQNUM_9 + IRQNUM_10 + IRQNUM_11 + IRQNUM_12 + IRQNUM_13 + IRQNUM_14 + IRQNUM_15 + IRQNUM_16 + IRQNUM_17 + IRQNUM_18 + IRQNUM_19 + IRQNUM_20 + IRQNUM_21 + IRQNUM_22 + IRQNUM_23 + IRQNUM_24 + IRQNUM_25 + IRQNUM_26 + IRQNUM_27 + IRQNUM_28 + IRQNUM_29 + IRQNUM_30 + IRQNUM_31 +) + +const ( + defaultThreshold = 5 + disableThreshold = 10 +) + +//go:inline +func callHandler(n int) { + switch n { + case IRQNUM_1: + callHandlers(IRQNUM_1) + case IRQNUM_2: + callHandlers(IRQNUM_2) + case IRQNUM_3: + callHandlers(IRQNUM_3) + case IRQNUM_4: + callHandlers(IRQNUM_4) + case IRQNUM_5: + callHandlers(IRQNUM_5) + case IRQNUM_6: + callHandlers(IRQNUM_6) + case IRQNUM_7: + callHandlers(IRQNUM_7) + case IRQNUM_8: + callHandlers(IRQNUM_8) + case IRQNUM_9: + callHandlers(IRQNUM_9) + case IRQNUM_10: + callHandlers(IRQNUM_10) + case IRQNUM_11: + callHandlers(IRQNUM_11) + case IRQNUM_12: + callHandlers(IRQNUM_12) + case IRQNUM_13: + callHandlers(IRQNUM_13) + case IRQNUM_14: + callHandlers(IRQNUM_14) + case IRQNUM_15: + callHandlers(IRQNUM_15) + case IRQNUM_16: + callHandlers(IRQNUM_16) + case IRQNUM_17: + callHandlers(IRQNUM_17) + case IRQNUM_18: + callHandlers(IRQNUM_18) + case IRQNUM_19: + callHandlers(IRQNUM_19) + case IRQNUM_20: + callHandlers(IRQNUM_20) + case IRQNUM_21: + callHandlers(IRQNUM_21) + case IRQNUM_22: + callHandlers(IRQNUM_22) + case IRQNUM_23: + callHandlers(IRQNUM_23) + case IRQNUM_24: + callHandlers(IRQNUM_24) + case IRQNUM_25: + callHandlers(IRQNUM_25) + case IRQNUM_26: + callHandlers(IRQNUM_26) + case IRQNUM_27: + callHandlers(IRQNUM_27) + case IRQNUM_28: + callHandlers(IRQNUM_28) + case IRQNUM_29: + callHandlers(IRQNUM_29) + case IRQNUM_30: + callHandlers(IRQNUM_30) + case IRQNUM_31: + callHandlers(IRQNUM_31) + } +} + +//TODO(zzy): origin with export handleInterrupt +// func handleInterrupt() { +// mcause := riscv.MCAUSE.Get() +// exception := mcause&(1<<31) == 0 +// interruptNumber := uint32(mcause & 0x1f) + +// if !exception && interruptNumber > 0 { +// // save MSTATUS & MEPC, which could be overwritten by another CPU interrupt +// mstatus := riscv.MSTATUS.Get() +// mepc := riscv.MEPC.Get() +// // Using threshold to temporary disable this interrupts. +// // FYI: using CPU interrupt enable bit make runtime to loose interrupts. +// reg := (*volatile.Register32)(unsafe.Add(unsafe.Pointer(&esp.INTERRUPT_CORE0.CPU_INT_PRI_0), interruptNumber*4)) +// thresholdSave := reg.Get() +// reg.Set(disableThreshold) +// riscv.Asm("fence") + +// interruptBit := uint32(1 << interruptNumber) + +// // reset pending status interrupt +// if esp.INTERRUPT_CORE0.CPU_INT_TYPE.Get()&interruptBit != 0 { +// // this is edge type interrupt +// esp.INTERRUPT_CORE0.CPU_INT_CLEAR.SetBits(interruptBit) +// esp.INTERRUPT_CORE0.CPU_INT_CLEAR.ClearBits(interruptBit) +// } else { +// // this is level type interrupt +// esp.INTERRUPT_CORE0.CPU_INT_CLEAR.ClearBits(interruptBit) +// } + +// // enable CPU interrupts +// riscv.MSTATUS.SetBits(riscv.MSTATUS_MIE) + +// // Call registered interrupt handler(s) +// callHandler(int(interruptNumber)) + +// // disable CPU interrupts +// riscv.MSTATUS.ClearBits(riscv.MSTATUS_MIE) + +// // restore interrupt threshold to enable interrupt again +// reg.Set(thresholdSave) +// riscv.Asm("fence") + +// // restore MSTATUS & MEPC +// riscv.MSTATUS.Set(mstatus) +// riscv.MEPC.Set(mepc) + +// // do not enable CPU interrupts now +// // the 'MRET' in src/device/riscv/handleinterrupt.S will copies the state of MPIE back into MIE, and subsequently clears MPIE. +// // riscv.MSTATUS.SetBits(riscv.MSTATUS_MIE) +// } else { +// // Topmost bit is clear, so it is an exception of some sort. +// // We could implement support for unsupported instructions here (such as +// // misaligned loads). However, for now we'll just print a fatal error. +// handleException(mcause) +// } +// } + +func handleException(mcause uintptr) { + println("*** Exception: pc:", riscv.MEPC.Get()) + println("*** Exception: code:", uint32(mcause&0x1f)) + println("*** Exception: mcause:", mcause) + switch uint32(mcause & 0x1f) { + case riscv.InstructionAccessFault: + println("*** virtual address:", riscv.MTVAL.Get()) + case riscv.IllegalInstruction: + println("*** opcode:", riscv.MTVAL.Get()) + case riscv.LoadAccessFault: + println("*** read address:", riscv.MTVAL.Get()) + case riscv.StoreOrAMOAccessFault: + println("*** write address:", riscv.MTVAL.Get()) + } + for { + riscv.Asm("wfi") + } +} diff --git a/runtime/interrupt/interrupt_gameboyadvance.go b/runtime/interrupt/interrupt_gameboyadvance.go new file mode 100644 index 0000000..ca3da76 --- /dev/null +++ b/runtime/interrupt/interrupt_gameboyadvance.go @@ -0,0 +1,104 @@ +//go:build gameboyadvance + +package interrupt + +// This is good documentation of the GBA: https://www.akkit.org/info/gbatek.htm + +import ( + "github.com/goplus/emb/device/gba" +) + +// Enable enables this interrupt. Right after calling this function, the +// interrupt may be invoked if it was already pending. +func (irq Interrupt) Enable() { + gba.INTERRUPT.IE.SetBits(1 << uint(irq.num)) +} + +var inInterrupt bool + +//TODO(zzy): origin with export handleInterrupt +// func handleInterrupt() { +// inInterrupt = true +// flags := gba.INTERRUPT.IF.Get() +// for i := 0; i < 14; i++ { +// if flags&(1<