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.
This commit is contained in:
Ayke van Laethem
2025-04-01 12:38:08 +02:00
committed by Ron Evans
parent 4bce85dc09
commit dcf609defb
2 changed files with 97 additions and 8 deletions
+96 -7
View File
@@ -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()
}
+1 -1
View File
@@ -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 {}"
}