Files
tinygo/src/runtime/runtime_tinygoriscv_qemu.go
T
Ayke van Laethem 41e501aaf4 runtime: move timeUnit to a single place
The timeUnit is now the same type everywhere. Move it to a single place
and add some documentation to it.
2025-04-15 09:23:23 +02:00

172 lines
4.4 KiB
Go

//go:build tinygo.riscv && virt && qemu
package runtime
import (
"device/riscv"
"runtime/volatile"
"unsafe"
)
// This file implements the VirtIO RISC-V interface implemented in QEMU, which
// is an interface designed for emulation.
//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)
}
// One tick is 100ns by default in QEMU.
// (This is not a standard, just the default used by QEMU).
func ticksToNanoseconds(ticks timeUnit) int64 {
return int64(ticks) * 100 // one tick is 100ns
}
func nanosecondsToTicks(ns int64) timeUnit {
return timeUnit(ns / 100) // one tick is 100ns
}
var timerWakeup volatile.Register8
func sleepTicks(d timeUnit) {
// 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 {
// 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.
// Source: https://github.com/qemu/qemu/blob/master/hw/riscv/virt.c
// Technically this is an implementation detail but hopefully they won't change
// the memory-mapped I/O registers.
var (
// UART0 output register.
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) {
stdoutWrite.Set(uint8(c))
}
func getchar() byte {
// dummy, TODO
return 0
}
func buffered() int {
// dummy, TODO
return 0
}
func abort() {
exit(1)
}
func exit(code int) {
// Make sure the QEMU process exits.
if code == 0 {
testFinisher.Set(0x5555) // FINISHER_PASS
} else {
// Exit code is stored in the upper 16 bits of the 32 bit value.
testFinisher.Set(uint32(code)<<16 | 0x3333) // FINISHER_FAIL
}
// Lock up forever (as a fallback).
for {
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()
}