From dcf609defb0c96db428029d6218686e8e24c252c Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Tue, 1 Apr 2025 12:38:08 +0200 Subject: [PATCH] riscv-qemu: actually sleep in time.Sleep() Instead of just incrementing the timestamp, this causes the system to actually sleep when calling time.Sleep. The direct effect is that this works as expected: $ tinygo run -target=riscv-qemu examples/serial hello world! hello world! hello world! [..etc] This commit also adds a bare bones handler for exceptions (such as invalid memory writes), since we're adding an interrupt handler anyway. While this patch doesn't add that much functionality, having interrupt support is going to be needed for multicore support on riscv-qemu. My plan is to first add this support to riscv-qemu (based on the earlier work I did for the RP2040 and demoed at FOSDEM 2025) and once the basics are in place and fully tested we can extend this support to the RP2040. Writing for QEMU first makes it much easier to debug any issues that will come up. --- src/runtime/runtime_tinygoriscv_qemu.go | 103 ++++++++++++++++++++++-- targets/riscv-qemu.json | 2 +- 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/src/runtime/runtime_tinygoriscv_qemu.go b/src/runtime/runtime_tinygoriscv_qemu.go index e68041890..e58e75351 100644 --- a/src/runtime/runtime_tinygoriscv_qemu.go +++ b/src/runtime/runtime_tinygoriscv_qemu.go @@ -11,32 +11,98 @@ import ( // This file implements the VirtIO RISC-V interface implemented in QEMU, which // is an interface designed for emulation. +// One tick is 100ns by default in QEMU. +// (This is not a standard, just the default used by QEMU). type timeUnit int64 -var timestamp timeUnit - //export main func main() { preinit() + + // Set the interrupt address. + // Note that this address must be aligned specially, otherwise the MODE bits + // of MTVEC won't be zero. + riscv.MTVEC.Set(uintptr(unsafe.Pointer(&handleInterruptASM))) + + // Enable global interrupts now that they've been set up. + // This is currently only for timer interrupts. + riscv.MSTATUS.SetBits(riscv.MSTATUS_MIE) + run() exit(0) } +//go:extern handleInterruptASM +var handleInterruptASM [0]uintptr + +//export handleInterrupt +func handleInterrupt() { + cause := riscv.MCAUSE.Get() + code := uint(cause &^ (1 << 31)) + if cause&(1<<31) != 0 { + // Topmost bit is set, which means that it is an interrupt. + switch code { + case riscv.MachineTimerInterrupt: + // Signal timeout. + timerWakeup.Set(1) + // Disable the timer, to avoid triggering the interrupt right after + // this interrupt returns. + riscv.MIE.ClearBits(riscv.MIE_MTIE) + } + } 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(code) + } + + // Zero MCAUSE so that it can later be used to see whether we're in an + // interrupt or not. + riscv.MCAUSE.Set(0) +} + func ticksToNanoseconds(ticks timeUnit) int64 { - return int64(ticks) + return int64(ticks) * 100 // one tick is 100ns } func nanosecondsToTicks(ns int64) timeUnit { - return timeUnit(ns) + return timeUnit(ns / 100) // one tick is 100ns } +var timerWakeup volatile.Register8 + func sleepTicks(d timeUnit) { - // TODO: actually sleep here for the given time. - timestamp += d + // Enable the timer. + target := uint64(ticks() + d) + aclintMTIMECMP.Set(target) + riscv.MIE.SetBits(riscv.MIE_MTIE) + + // Wait until it fires. + for { + if timerWakeup.Get() != 0 { + timerWakeup.Set(0) + // Disable timer. + break + } + riscv.Asm("wfi") + } } func ticks() timeUnit { - return timestamp + // Combining the low bits and the high bits (at a rate of 100ns per tick) + // yields a time span of over 59930 years without counter rollover. + highBits := aclintMTIME.high.Get() + for { + lowBits := aclintMTIME.low.Get() + newHighBits := aclintMTIME.high.Get() + if newHighBits == highBits { + // High bits stayed the same. + return timeUnit(lowBits) | (timeUnit(highBits) << 32) + } + // Retry, because there was a rollover in the low bits (happening every + // 429 days). + highBits = newHighBits + } } // Memory-mapped I/O as defined by QEMU. @@ -48,6 +114,15 @@ var ( stdoutWrite = (*volatile.Register8)(unsafe.Pointer(uintptr(0x10000000))) // SiFive test finisher testFinisher = (*volatile.Register32)(unsafe.Pointer(uintptr(0x100000))) + + // RISC-V Advanced Core Local Interruptor. + // It is backwards compatible with the SiFive CLINT. + // https://github.com/riscvarchive/riscv-aclint/blob/main/riscv-aclint.adoc + aclintMTIME = (*struct { + low volatile.Register32 + high volatile.Register32 + })(unsafe.Pointer(uintptr(0x0200_bff8))) + aclintMTIMECMP = (*volatile.Register64)(unsafe.Pointer(uintptr(0x0200_4000))) ) func putchar(c byte) { @@ -82,3 +157,17 @@ func exit(code int) { riscv.Asm("wfi") } } + +// handleException is called from the interrupt handler for any exception. +// Exceptions can be things like illegal instructions, invalid memory +// read/write, and similar issues. +func handleException(code uint) { + // For a list of exception codes, see: + // https://content.riscv.org/wp-content/uploads/2019/08/riscv-privileged-20190608-1.pdf#page=49 + print("fatal error: exception with mcause=") + print(code) + print(" pc=") + print(riscv.MEPC.Get()) + println() + abort() +} diff --git a/targets/riscv-qemu.json b/targets/riscv-qemu.json index 90f1c312f..8a85cf9a7 100644 --- a/targets/riscv-qemu.json +++ b/targets/riscv-qemu.json @@ -4,5 +4,5 @@ "build-tags": ["virt", "qemu"], "default-stack-size": 8192, "linkerscript": "targets/riscv-qemu.ld", - "emulator": "qemu-system-riscv32 -machine virt -nographic -bios none -device virtio-rng-device -kernel {}" + "emulator": "qemu-system-riscv32 -machine virt,aclint=on -nographic -bios none -device virtio-rng-device -kernel {}" }