Skip to content

Commit dcf609d

Browse files
aykevldeadprogram
authored andcommitted
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.
1 parent 4bce85d commit dcf609d

File tree

2 files changed

+97
-8
lines changed

2 files changed

+97
-8
lines changed

src/runtime/runtime_tinygoriscv_qemu.go

+96-7
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,98 @@ import (
1111
// This file implements the VirtIO RISC-V interface implemented in QEMU, which
1212
// is an interface designed for emulation.
1313

14+
// One tick is 100ns by default in QEMU.
15+
// (This is not a standard, just the default used by QEMU).
1416
type timeUnit int64
1517

16-
var timestamp timeUnit
17-
1818
//export main
1919
func main() {
2020
preinit()
21+
22+
// Set the interrupt address.
23+
// Note that this address must be aligned specially, otherwise the MODE bits
24+
// of MTVEC won't be zero.
25+
riscv.MTVEC.Set(uintptr(unsafe.Pointer(&handleInterruptASM)))
26+
27+
// Enable global interrupts now that they've been set up.
28+
// This is currently only for timer interrupts.
29+
riscv.MSTATUS.SetBits(riscv.MSTATUS_MIE)
30+
2131
run()
2232
exit(0)
2333
}
2434

35+
//go:extern handleInterruptASM
36+
var handleInterruptASM [0]uintptr
37+
38+
//export handleInterrupt
39+
func handleInterrupt() {
40+
cause := riscv.MCAUSE.Get()
41+
code := uint(cause &^ (1 << 31))
42+
if cause&(1<<31) != 0 {
43+
// Topmost bit is set, which means that it is an interrupt.
44+
switch code {
45+
case riscv.MachineTimerInterrupt:
46+
// Signal timeout.
47+
timerWakeup.Set(1)
48+
// Disable the timer, to avoid triggering the interrupt right after
49+
// this interrupt returns.
50+
riscv.MIE.ClearBits(riscv.MIE_MTIE)
51+
}
52+
} else {
53+
// Topmost bit is clear, so it is an exception of some sort.
54+
// We could implement support for unsupported instructions here (such as
55+
// misaligned loads). However, for now we'll just print a fatal error.
56+
handleException(code)
57+
}
58+
59+
// Zero MCAUSE so that it can later be used to see whether we're in an
60+
// interrupt or not.
61+
riscv.MCAUSE.Set(0)
62+
}
63+
2564
func ticksToNanoseconds(ticks timeUnit) int64 {
26-
return int64(ticks)
65+
return int64(ticks) * 100 // one tick is 100ns
2766
}
2867

2968
func nanosecondsToTicks(ns int64) timeUnit {
30-
return timeUnit(ns)
69+
return timeUnit(ns / 100) // one tick is 100ns
3170
}
3271

72+
var timerWakeup volatile.Register8
73+
3374
func sleepTicks(d timeUnit) {
34-
// TODO: actually sleep here for the given time.
35-
timestamp += d
75+
// Enable the timer.
76+
target := uint64(ticks() + d)
77+
aclintMTIMECMP.Set(target)
78+
riscv.MIE.SetBits(riscv.MIE_MTIE)
79+
80+
// Wait until it fires.
81+
for {
82+
if timerWakeup.Get() != 0 {
83+
timerWakeup.Set(0)
84+
// Disable timer.
85+
break
86+
}
87+
riscv.Asm("wfi")
88+
}
3689
}
3790

3891
func ticks() timeUnit {
39-
return timestamp
92+
// Combining the low bits and the high bits (at a rate of 100ns per tick)
93+
// yields a time span of over 59930 years without counter rollover.
94+
highBits := aclintMTIME.high.Get()
95+
for {
96+
lowBits := aclintMTIME.low.Get()
97+
newHighBits := aclintMTIME.high.Get()
98+
if newHighBits == highBits {
99+
// High bits stayed the same.
100+
return timeUnit(lowBits) | (timeUnit(highBits) << 32)
101+
}
102+
// Retry, because there was a rollover in the low bits (happening every
103+
// 429 days).
104+
highBits = newHighBits
105+
}
40106
}
41107

42108
// Memory-mapped I/O as defined by QEMU.
@@ -48,6 +114,15 @@ var (
48114
stdoutWrite = (*volatile.Register8)(unsafe.Pointer(uintptr(0x10000000)))
49115
// SiFive test finisher
50116
testFinisher = (*volatile.Register32)(unsafe.Pointer(uintptr(0x100000)))
117+
118+
// RISC-V Advanced Core Local Interruptor.
119+
// It is backwards compatible with the SiFive CLINT.
120+
// https://github.com/riscvarchive/riscv-aclint/blob/main/riscv-aclint.adoc
121+
aclintMTIME = (*struct {
122+
low volatile.Register32
123+
high volatile.Register32
124+
})(unsafe.Pointer(uintptr(0x0200_bff8)))
125+
aclintMTIMECMP = (*volatile.Register64)(unsafe.Pointer(uintptr(0x0200_4000)))
51126
)
52127

53128
func putchar(c byte) {
@@ -82,3 +157,17 @@ func exit(code int) {
82157
riscv.Asm("wfi")
83158
}
84159
}
160+
161+
// handleException is called from the interrupt handler for any exception.
162+
// Exceptions can be things like illegal instructions, invalid memory
163+
// read/write, and similar issues.
164+
func handleException(code uint) {
165+
// For a list of exception codes, see:
166+
// https://content.riscv.org/wp-content/uploads/2019/08/riscv-privileged-20190608-1.pdf#page=49
167+
print("fatal error: exception with mcause=")
168+
print(code)
169+
print(" pc=")
170+
print(riscv.MEPC.Get())
171+
println()
172+
abort()
173+
}

targets/riscv-qemu.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
"build-tags": ["virt", "qemu"],
55
"default-stack-size": 8192,
66
"linkerscript": "targets/riscv-qemu.ld",
7-
"emulator": "qemu-system-riscv32 -machine virt -nographic -bios none -device virtio-rng-device -kernel {}"
7+
"emulator": "qemu-system-riscv32 -machine virt,aclint=on -nographic -bios none -device virtio-rng-device -kernel {}"
88
}

0 commit comments

Comments
 (0)