machine,targets: add minimal esp32c6 implementation

This adds a minimal esp32c6 implementation, currently only
supporting the examples/serial and examples/blinky1 programs.
It does correctly output the expected "Hello, World" via the
serial port, as well as blink the onboard LED.

In addition, it adds support for the PLIC based IRQ handling
as used on the ESP32C6 processor.

Some parts of this code are loosely based on PR #5252 and #5248

Signed-off-by: deadprogram <ron@hybridgroup.com>
This commit is contained in:
deadprogram
2026-06-25 12:25:03 +02:00
committed by Ron Evans
parent ba95eb3e1b
commit 5f66260c3a
14 changed files with 1616 additions and 5 deletions
+250
View File
@@ -0,0 +1,250 @@
//go:build esp32c6
package interrupt
import (
"device/riscv"
"errors"
"runtime/volatile"
"unsafe"
)
//go:extern tinygo_saved_ra
var tinygo_saved_ra uintptr
// plicType maps the ESP32-C6 PLIC (Platform-Level Interrupt Controller)
// machine-mode registers. The C6 uses the PLIC — not the INTPRI/INTC block
// that the ESP32-C3 uses — as its CPU interrupt controller
// (SOC_INT_PLIC_SUPPORTED). The INTPRI registers at 0x600c5000 are vestigial
// backward-compatibility aliases on the C6 and are not wired to the CPU, so
// writing them never delivers an interrupt.
type plicType struct {
MXINT_ENABLE volatile.Register32 // 0x00 bit N enables CPU interrupt line N
MXINT_TYPE volatile.Register32 // 0x04 bit N: 1=edge, 0=level
MXINT_CLEAR volatile.Register32 // 0x08 edge acknowledge
MXINT_EIP_STATUS volatile.Register32 // 0x0C pending status (read-only)
MXINT_PRI [32]volatile.Register32 // 0x10..0x8C per-line priority (4 bits)
MXINT_THRESH volatile.Register32 // 0x90 priority threshold (8 bits)
}
// plic points at the PLIC machine-mode register block (DR_REG_PLIC_MX_BASE).
var plic = (*plicType)(unsafe.Pointer(uintptr(0x20001000)))
// Enable registers a CPU interrupt.
// The ESP32-C6 has 31 CPU independent interrupts (1..31).
func (i Interrupt) Enable() error {
if i.num < 1 || i.num > 31 {
return errors.New("interrupt for ESP32-C6 must be in range of 1 through 31")
}
mask := riscv.DisableInterrupts()
defer riscv.EnableInterrupts(mask)
// Enable CPU interrupt number i.num via the PLIC.
plic.MXINT_ENABLE.SetBits(1 << i.num)
// Set pulse interrupt type (rising edge detection).
plic.MXINT_TYPE.SetBits(1 << i.num)
// Set default priority (must be >= threshold to be delivered).
plic.MXINT_PRI[i.num].Set(defaultThreshold)
// Reset interrupt before re-enabling.
plic.MXINT_CLEAR.SetBits(1 << i.num)
plic.MXINT_CLEAR.ClearBits(1 << i.num)
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)
//go:linkname signalInterrupt runtime.signalInterrupt
func signalInterrupt()
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
// Priority 0 disables an interrupt on ESP32-C6.
disableThreshold = 0
)
//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)
}
}
//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()
// Temporarily disable this interrupt by lowering its PLIC priority
// below the threshold.
thresholdSave := plic.MXINT_PRI[interruptNumber].Get()
plic.MXINT_PRI[interruptNumber].Set(disableThreshold)
riscv.Asm("fence")
interruptBit := uint32(1 << interruptNumber)
// Reset pending status interrupt.
if plic.MXINT_TYPE.Get()&interruptBit != 0 {
// Edge type interrupt.
plic.MXINT_CLEAR.SetBits(interruptBit)
plic.MXINT_CLEAR.ClearBits(interruptBit)
} else {
// Level type interrupt.
plic.MXINT_CLEAR.ClearBits(interruptBit)
}
// Enable CPU interrupts.
riscv.MSTATUS.SetBits(riscv.MSTATUS_MIE)
// Call registered interrupt handler(s).
callHandler(int(interruptNumber))
// Signal to sleepTicks that an interrupt has occurred.
signalInterrupt()
// Disable CPU interrupts.
riscv.MSTATUS.ClearBits(riscv.MSTATUS_MIE)
// Restore interrupt priority to enable interrupt again.
plic.MXINT_PRI[interruptNumber].Set(thresholdSave)
riscv.Asm("fence")
// Zero MCAUSE so that interrupt.In() returns false once we
// return to normal (non-interrupt) code.
riscv.MCAUSE.Set(0)
// Restore MSTATUS & MEPC.
riscv.MSTATUS.Set(mstatus)
riscv.MEPC.Set(mepc)
} else {
handleException(mcause)
}
}
func handleException(mcause uintptr) {
println("*** Exception: pc:", riscv.MEPC.Get())
println("*** Exception: code:", uint32(mcause&0x1f))
println("*** Exception: mcause:", mcause)
println("*** Exception: ra:", tinygo_saved_ra)
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")
}
}
+211
View File
@@ -0,0 +1,211 @@
//go:build esp32c6
package runtime
import (
"device/esp"
"device/riscv"
"machine"
"runtime/interrupt"
"runtime/volatile"
"unsafe"
)
// This is the function called on startup after the flash (IROM/DROM) is
// initialized and the stack pointer has been set.
//
//export main
func main() {
// This initialization configures the following things:
// * It disables all watchdog timers. They might be useful at some point in
// the future, but will need integration into the scheduler. For now,
// they're all disabled.
// * It sets the CPU frequency to 160MHz, which is the maximum speed allowed
// for this CPU. Lower frequencies might be possible in the future, but
// running fast and sleeping quickly is often also a good strategy to save
// power.
// Disable Timer Group 0 watchdog (unlock first).
esp.TIMG0.WDTWPROTECT.Set(0x50D83AA1)
esp.TIMG0.WDTCONFIG0.Set(0)
// Disable Timer Group 1 watchdog (unlock first).
esp.TIMG1.WDTWPROTECT.Set(0x50D83AA1)
esp.TIMG1.WDTCONFIG0.Set(0)
// Disable LP watchdog (write-protect key first).
esp.LP_WDT.WDTWPROTECT.Set(0x50D83AA1)
esp.LP_WDT.WDTCONFIG0.Set(0)
// Disable super watchdog.
esp.LP_WDT.SWD_WPROTECT.Set(0x50D83AA1)
esp.LP_WDT.SWD_CONF.SetBits(1 << 30) // SWD_DISABLE bit
// Change CPU frequency to 160MHz from SPLL (480MHz).
//
// Clock tree: SPLL (480MHz) → HP root → CPU / AHB / APB
//
// Set dividers BEFORE switching the clock source so the first PLL
// cycle already arrives divided:
// HP root = SPLL / (HS_DIV_NUM+1) = 480 / 3 = 160 MHz
// CPU = HP root / (CPU_HS_DIV_NUM+1) = 160 / 1 = 160 MHz
// AHB = HP root / (AHB_HS_DIV_NUM+1) = 160 / 4 = 40 MHz
// APB = AHB / (APB_HS_DIV_NUM+1) = 40 / 1 = 40 MHz
esp.PCR.CPU_FREQ_CONF.Set(0 << 8) // CPU_HS_DIV_NUM = 0 (div1)
esp.PCR.AHB_FREQ_CONF.Set(3 << 8) // AHB_HS_DIV_NUM = 3 (div4)
esp.PCR.APB_FREQ_CONF.Set(0 << 8) // APB_HS_DIV_NUM = 0 (div1)
// Switch to PLL: SOC_CLK_SEL = 1 (SPLL), HS_DIV_NUM = 2 (div3).
esp.PCR.SYSCLK_CONF.Set(1<<16 | 2<<8)
clearbss()
// Configure interrupt handler
interruptInit()
// Initialize main system timer used for time.Now.
initTimer()
// Initialize timer alarm interrupt for the scheduler.
initTimerInterrupt()
// Initialize the heap, call main.main, etc.
run()
// Fallback: if main ever returns, hang the CPU.
exit(0)
}
func init() {
machine.InitSerial()
}
func abort() {
for {
riscv.Asm("wfi")
}
}
// interruptInit initializes the interrupt controller.
func interruptInit() {
mie := riscv.DisableInterrupts()
// Reset all interrupt priorities to zero in the PLIC.
for i := 1; i < 32; i++ {
plic.MXINT_PRI[i].Set(0)
}
// Default threshold for interrupts is 5.
plic.MXINT_THRESH.Set(5)
// Set the interrupt address.
// Set MODE field to 1 - a vector base address.
// Note that this address must be aligned to 256 bytes.
riscv.MTVEC.Set((uintptr(unsafe.Pointer(&_vector_table))) | 1)
// On the ESP32-C6 the CPU receives PLIC interrupts through the standard
// RISC-V mie CSR (machine interrupt-enable, 0x304). Unlike the ESP32-C3's
// INTC, the PLIC's per-line interrupts are gated by mie, so it must be
// enabled for any interrupt to reach the CPU. esp-hal does the same thing
// for the PLIC: `csrw mie, 0xffffffff`.
riscv.MIE.Set(0xffffffff)
// Globally enable machine-mode interrupts (MSTATUS.MIE).
//
// Unlike the ESP32-C3, whose ROM bootloader hands control to the
// application with MSTATUS.MIE already set, the ESP32-C6 ROM leaves it
// cleared. Restoring the previous state (riscv.EnableInterrupts(mie))
// would therefore leave interrupts globally disabled, so no interrupt is
// ever delivered to the CPU: timer alarms silently fall back to the
// polling loop in sleepTicks and peripheral RX interrupts (such as the
// USB Serial/JTAG controller) never fire. Set the bit explicitly instead.
_ = mie
riscv.MSTATUS.SetBits(riscv.MSTATUS_MIE)
}
// CPU interrupt number used for the TIMG0 timer alarm.
const timerAlarmCPUInterrupt = 9
var interruptPending volatile.Register8
func signalInterrupt() {
interruptPending.Set(1)
}
// initTimerInterrupt routes the TIMG0 timer 0 alarm interrupt to a CPU
// interrupt and registers a handler.
func initTimerInterrupt() {
// Map the TIMG0 T0 peripheral interrupt to a CPU interrupt line.
// On C6, INTERRUPT_CORE0 is for peripheral→CPU interrupt mapping.
esp.INTERRUPT_CORE0.TG0_T0_INTR_MAP.Set(timerAlarmCPUInterrupt)
// Enable T0 interrupt at the timer group level.
esp.TIMG0.INT_ENA_TIMERS.SetBits(1)
// Register the interrupt handler.
interrupt.New(timerAlarmCPUInterrupt, func(interrupt.Interrupt) {
esp.TIMG0.INT_CLR_TIMERS.Set(1)
})
// Enable the CPU interrupt:
mie := riscv.DisableInterrupts()
// Clear any stale pending bit.
plic.MXINT_CLEAR.SetBits(1 << timerAlarmCPUInterrupt)
plic.MXINT_CLEAR.ClearBits(1 << timerAlarmCPUInterrupt)
// Set edge-triggered.
plic.MXINT_TYPE.SetBits(1 << timerAlarmCPUInterrupt)
// Set priority above threshold.
plic.MXINT_PRI[timerAlarmCPUInterrupt].Set(10)
riscv.Asm("fence")
plic.MXINT_ENABLE.SetBits(1 << timerAlarmCPUInterrupt)
riscv.EnableInterrupts(mie)
}
// 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) {
machine.FlushSerial()
target := ticks() + d
for ticks() < target {
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)
for interruptPending.Get() == 0 {
if ticks() >= target {
return
}
}
}
}
//go:extern _vector_table
var _vector_table [0]uintptr
// plicType maps the ESP32-C6 PLIC (Platform-Level Interrupt Controller)
// machine-mode registers. The C6 uses the PLIC — not the INTPRI/INTC block
// that the ESP32-C3 uses — as its CPU interrupt controller
// (SOC_INT_PLIC_SUPPORTED). The INTPRI registers at 0x600c5000 are vestigial
// backward-compatibility aliases on the C6 and are not wired to the CPU, so
// writing them never delivers an interrupt.
type plicType struct {
MXINT_ENABLE volatile.Register32 // 0x00 bit N enables CPU interrupt line N
MXINT_TYPE volatile.Register32 // 0x04 bit N: 1=edge, 0=level
MXINT_CLEAR volatile.Register32 // 0x08 edge acknowledge
MXINT_EIP_STATUS volatile.Register32 // 0x0C pending status (read-only)
MXINT_PRI [32]volatile.Register32 // 0x10..0x8C per-line priority (4 bits)
MXINT_THRESH volatile.Register32 // 0x90 priority threshold (8 bits)
}
// plic points at the PLIC machine-mode register block (DR_REG_PLIC_MX_BASE).
var plic = (*plicType)(unsafe.Pointer(uintptr(0x20001000)))
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build esp32 || esp32c3
//go:build esp32 || esp32c3 || esp32c6
package runtime