Compare commits

...

11 Commits

18 changed files with 779 additions and 237 deletions

57
Cargo.lock generated
View File

@@ -122,6 +122,62 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "crossbeam"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "goblin"
version = "0.10.4"
@@ -281,6 +337,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"clap",
"crossbeam",
"goblin",
"int-enum",
"memmap2",

View File

@@ -6,6 +6,7 @@ edition = "2024"
[dependencies]
anyhow = "1.0.100"
clap = { version = "4.5.53", features = ["derive"] }
crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] }
goblin = "0.10.4"
int-enum = "1.2.0"
memmap2 = "0.9.8"

View File

@@ -1,4 +1,4 @@
Copyright 2025 taitep
Copyright 2025-2026 taitep
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

View File

@@ -4,7 +4,7 @@ taitep's RISC-V Emulator.
The goal is to support at least RV64GC and be able to run Linux,
potentially more. No plans for RV32I or RV32/64E.
Currently implemented RISC-V ISA: `RV64IM`
Currently implemented RISC-V ISA: `RV64IM-Zalrsc`
## Current Use
Currently, the emulator is nowhere near complete,

18
echo.S
View File

@@ -1,18 +0,0 @@
.section .text
.globl _start
.equ UART_DATA, 0
.equ UART_STATUS, 1
.equ UART_RX_READY, 0b10
.equ UART_TX_READY, 0b01
_start:
li a0, 0x10000
loop:
lbu t0, UART_STATUS(a0)
andi t0, t0, UART_RX_READY
beqz t0, loop
lbu t0, UART_DATA(a0)
sb t0, UART_DATA(a0)
j loop

View File

@@ -1,96 +0,0 @@
// Copyright (c) 2025 taitep
// SPDX-License-Identifier: BSD-2-Clause
//
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
// See LICENSE file in the project root for full license text.
use std::collections::VecDeque;
use std::io;
use std::os::fd::AsFd;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use nix::fcntl::fcntl;
use nix::fcntl::{FcntlArg, OFlag};
use trve::exceptions::MemoryExceptionType;
use trve::mem::MemDeviceInterface;
/// byte 0: rx/tx
/// byte 1: status (------rt, r=rxready, t=txready)/none
pub struct BasicUart {
rx_buf: Mutex<VecDeque<u8>>,
}
impl BasicUart {
pub fn new() -> Self {
// Make sure stdin is nonblocking
let stdin = io::stdin();
let fd = stdin.as_fd();
let flags = fcntl(fd, FcntlArg::F_GETFL).unwrap();
fcntl(
fd,
FcntlArg::F_SETFL(OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK),
)
.unwrap();
BasicUart {
rx_buf: Mutex::new(VecDeque::new()),
}
}
pub fn spawn_poller(self, poll_interval: Duration) -> Arc<Self> {
let shared = Arc::new(self);
let uart_clone = shared.clone();
thread::spawn(move || {
loop {
uart_clone.poll();
thread::sleep(poll_interval);
}
});
shared
}
fn write(&self, byte: u8) {
print!("{}", byte as char);
}
fn read(&self) -> u8 {
self.rx_buf.lock().unwrap().pop_front().unwrap_or(0)
}
fn can_read(&self) -> bool {
!self.rx_buf.lock().unwrap().is_empty()
}
pub fn poll(&self) {
let mut rx_buf = self.rx_buf.lock().unwrap();
let mut buffer = [0u8; 1];
while let Ok(1) = nix::unistd::read(io::stdin().as_fd(), &mut buffer) {
rx_buf.push_back(buffer[0]);
}
}
}
impl MemDeviceInterface for BasicUart {
fn write_byte(&self, addr: u64, value: u8) -> Result<(), MemoryExceptionType> {
match addr {
0 => {
self.write(value);
Ok(())
}
_ => Err(MemoryExceptionType::AccessFault),
}
}
fn read_byte(&self, addr: u64) -> Result<u8, MemoryExceptionType> {
match addr {
0 => Ok(self.read()),
1 => Ok(1 | (self.can_read() as u8) << 1),
_ => Err(MemoryExceptionType::AccessFault),
}
}
}

View File

@@ -1,10 +1,10 @@
// Copyright (c) 2025 taitep
// Copyright (c) 2025-2026 taitep
// SPDX-License-Identifier: BSD-2-Clause
//
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
// See LICENSE file in the project root for full license text.
use std::{collections::HashSet, sync::mpsc};
use std::collections::HashSet;
use crate::{
core::commands::CoreCmd,
@@ -19,18 +19,21 @@ pub struct Core {
pub(crate) x_regs: [u64; 32],
pub(crate) pc: u64,
pub(crate) mem: MemConfig,
command_stream: mpsc::Receiver<CoreCmd>,
command_stream: crossbeam::channel::Receiver<CoreCmd>,
// LR/SC reservation set. Pair of the RAM version block index and expected version.
pub(crate) reservation: Option<(usize, u32)>,
}
pub mod commands;
impl Core {
pub fn new(mem: MemConfig, command_stream: mpsc::Receiver<CoreCmd>) -> Self {
pub fn new(mem: MemConfig, command_stream: crossbeam::channel::Receiver<CoreCmd>) -> Self {
Self {
x_regs: [0; 32],
pc: 0,
mem,
command_stream,
reservation: None,
}
}
@@ -103,13 +106,16 @@ impl Core {
};
}
fn debug_loop(&mut self, dbg_stream: mpsc::Receiver<gdb::DebugCommand>) -> anyhow::Result<()> {
fn debug_loop(
&mut self,
dbg_stream: crossbeam::channel::Receiver<gdb::DebugCommand>,
) -> anyhow::Result<()> {
let mut breakpoints = HashSet::new();
loop {
match dbg_stream.recv()? {
DebugCommand::GetRegs(sender) => sender.send(gdb::RegsResponse {
x_regs: self.x_regs.clone(),
x_regs: self.x_regs,
pc: self.pc,
})?,
DebugCommand::ReadMem {
@@ -160,7 +166,7 @@ impl Core {
return StopReason::Exception(ExceptionType::Breakpoint);
}
if let Ok(_) = stopper.try_recv() {
if stopper.try_recv().is_ok() {
return StopReason::Interrupted;
}

View File

@@ -4,6 +4,8 @@
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
// See LICENSE file in the project root for full license text.
use std::sync::atomic::Ordering;
const MASK_REGISTER: u32 = 0x1f;
#[derive(Clone, Copy)]
@@ -51,7 +53,12 @@ impl Instruction {
#[inline]
pub fn funct7(self) -> u8 {
(self.0 >> 25 & 0x7f) as u8
(self.0 >> 25) as u8
}
#[inline]
pub fn funct5(self) -> u8 {
(self.0 >> 27) as u8
}
#[inline]
@@ -95,11 +102,26 @@ impl Instruction {
/// 32bit ones use funct7 in this way
#[inline]
pub fn funct6(self) -> u8 {
(self.0 >> 26 & 0x3f) as u8
(self.0 >> 26) as u8
}
/// Mostly/only used for the SYSTEM opcode
#[inline]
pub fn funct12(self) -> u16 {
(self.0 >> 20) as u16
}
/// Looks at the aq/rl bits of atomic instructions and converts to an Ordering
#[inline]
pub fn amo_ordering(self) -> Ordering {
let aq = self.0 >> 26 & 1 != 0;
let rl = self.0 >> 25 & 1 != 0;
match (aq, rl) {
(false, false) => Ordering::Relaxed,
(false, true) => Ordering::Release,
(true, false) => Ordering::Acquire,
(true, true) => Ordering::AcqRel,
}
}
}

1
src/devices.rs Normal file
View File

@@ -0,0 +1 @@
pub mod serial;

140
src/devices/serial.rs Normal file
View File

@@ -0,0 +1,140 @@
// Copyright (c) 2026 taitep
// SPDX-License-Identifier: BSD-2-Clause
//
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
// See LICENSE file in the project root for full license text.
use std::{
io::{Read, Write},
sync::{Arc, Mutex},
time::Duration,
};
mod fifo;
use fifo::UartFifo;
use crate::{exceptions::MemoryExceptionType, mem::MemDeviceInterface};
pub struct SifiveUart {
rx: Mutex<(UartFifo<2048>, bool)>,
tx: Mutex<(UartFifo<2048>, bool)>,
}
impl SifiveUart {
pub fn new_arc() -> Arc<Self> {
Arc::new(Self {
rx: Mutex::new((UartFifo::default(), false)),
tx: Mutex::new((UartFifo::default(), false)),
})
}
pub fn spawn_io_thread<R: Read + Send + 'static, T: Write + Send + Sync + 'static>(
self: Arc<Self>,
mut rx_backend: R,
mut tx_backend: T,
interval: Duration,
) {
std::thread::spawn(move || {
loop {
{
// Read data
let mut rx_guard = self.rx.lock().expect("could not lock uart RX half");
let (rx_buf, rx_en) = &mut *rx_guard;
if *rx_en {
let _ = rx_buf.read_from(&mut rx_backend);
}
}
{
// Write data
let mut tx_guard = self.tx.lock().expect("could not lock uart RX half");
let (tx_buf, tx_en) = &mut *tx_guard;
if *tx_en {
let _ = tx_buf.write_to(&mut tx_backend);
let _ = tx_backend.flush();
}
}
std::thread::sleep(interval);
}
});
}
}
impl MemDeviceInterface for SifiveUart {
fn write_word(&self, addr: u64, value: u32) -> Result<(), MemoryExceptionType> {
// dbg!(addr, value);
match addr {
0x00 => {
// TXDATA
let (ref mut tx_buf, _) = *self.tx.lock().expect("could not lock uart TX half");
tx_buf.push_single_byte(value as u8);
Ok(())
}
0x08 => {
// TXCTRL
let (_, ref mut tx_en) = *self.tx.lock().expect("could not lock uart TX half");
*tx_en = value & 1 != 0;
Ok(())
}
0x04 => Ok(()), // RXDATA
0x0c => {
// RXCTRL
let (_, ref mut rx_en) = *self.rx.lock().expect("could not lock uart RX half");
*rx_en = value & 1 != 0;
Ok(())
}
0x10 => Ok(()), // IE
0x14 => Ok(()), // IP
0x18 => Ok(()), // DIV
_ => {
if addr < 0x1c {
Err(MemoryExceptionType::AddressMisaligned)
} else {
Err(MemoryExceptionType::AccessFault)
}
}
}
}
fn read_word(&self, addr: u64) -> Result<u32, MemoryExceptionType> {
// dbg!(addr);
match addr {
0x00 => {
// TXDATA
let (ref tx_buf, _) = *self.tx.lock().expect("could not lock uart TX half");
Ok(if tx_buf.is_full() { 0x80000000 } else { 0 })
}
0x08 => {
// TXCTRL
let (_, tx_en) = *self.tx.lock().expect("could not lock uart TX half");
Ok(if tx_en { 1 } else { 0 })
}
0x04 => {
// RXDATA
let (ref mut rx_buf, _) = *self.rx.lock().expect("could not lock uart RX half");
Ok(match rx_buf.pop_single_byte() {
None => 0x80000000,
Some(b) => b as u32,
})
}
0x0c => {
// RXCTRL
let (_, rx_en) = *self.rx.lock().expect("could not lock uart RX half");
Ok(if rx_en { 1 } else { 0 })
}
0x10 => Ok(0), // IE
0x14 => Ok(0), // IP
0x18 => Ok(1), // DIV
_ => {
if addr < 0x1c {
Err(MemoryExceptionType::AddressMisaligned)
} else {
Err(MemoryExceptionType::AccessFault)
}
}
}
}
}

113
src/devices/serial/fifo.rs Normal file
View File

@@ -0,0 +1,113 @@
// Copyright (c) 2026 taitep
// SPDX-License-Identifier: BSD-2-Clause
//
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
// See LICENSE file in the project root for full license text.
use std::io::{self, Read, Write};
pub struct UartFifo<const CAP: usize> {
buf: [u8; CAP],
head: usize,
tail: usize,
len: usize,
}
impl<const CAP: usize> UartFifo<CAP> {
pub fn pop_single_byte(&mut self) -> Option<u8> {
if self.is_empty() {
return None;
}
let value = self.buf[self.tail];
self.advance_read(1);
Some(value)
}
pub fn push_single_byte(&mut self, value: u8) -> bool {
if self.is_full() {
return false;
}
self.buf[self.head] = value;
self.advance_write(1);
true
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn is_full(&self) -> bool {
self.len == CAP
}
fn write_slice(&mut self) -> &mut [u8] {
if self.is_full() {
return &mut [];
}
if self.head >= self.tail {
&mut self.buf[self.head..]
} else {
&mut self.buf[self.head..self.tail]
}
}
fn advance_write(&mut self, n: usize) {
debug_assert!(n <= CAP - self.len);
self.head = (self.head + n) % CAP;
self.len += n;
}
fn read_slice(&self) -> &[u8] {
if self.is_empty() {
return &[];
}
if self.tail < self.head {
&self.buf[self.tail..self.head]
} else {
&self.buf[self.tail..]
}
}
fn advance_read(&mut self, n: usize) {
debug_assert!(n <= self.len);
self.tail = (self.tail + n) % CAP;
self.len -= n;
}
pub fn read_from<R: Read>(&mut self, reader: &mut R) -> io::Result<usize> {
let slice = self.write_slice();
if slice.is_empty() {
return Ok(0);
}
let n = reader.read(slice)?;
self.advance_write(n);
Ok(n)
}
pub fn write_to<W: Write>(&mut self, writer: &mut W) -> io::Result<usize> {
let slice = self.read_slice();
if slice.is_empty() {
return Ok(0);
}
let n = writer.write(slice)?;
self.advance_read(n);
Ok(n)
}
}
impl<const SIZE: usize> Default for UartFifo<SIZE> {
fn default() -> Self {
UartFifo {
buf: [0; SIZE],
head: 0,
tail: 0,
len: 0,
}
}
}

View File

@@ -98,14 +98,14 @@ impl MemoryException {
}
}
impl Into<MemoryExceptionType> for MemoryException {
fn into(self) -> MemoryExceptionType {
self.type_
impl From<MemoryException> for MemoryExceptionType {
fn from(val: MemoryException) -> Self {
val.type_
}
}
impl Into<ExceptionType> for Exception {
fn into(self) -> ExceptionType {
self.type_
impl From<Exception> for ExceptionType {
fn from(val: Exception) -> Self {
val.type_
}
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2025 taitep
// Copyright (c) 2025-2026 taitep
// SPDX-License-Identifier: BSD-2-Clause
//
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
@@ -7,7 +7,6 @@
use std::{
io::{self, BufRead, ErrorKind, Write},
net::{TcpListener, TcpStream},
sync::mpsc,
};
use crate::{
@@ -29,7 +28,7 @@ pub(crate) enum DebugCommand {
ExitDebugMode,
}
pub struct DebugStream(pub(crate) mpsc::Receiver<DebugCommand>);
pub struct DebugStream(pub(crate) crossbeam::channel::Receiver<DebugCommand>);
#[derive(Clone, Copy, Debug)]
pub(crate) enum StopReason {
@@ -68,13 +67,12 @@ pub(crate) struct RegsResponse {
pub pc: u64,
}
pub fn run_stub(cmd_sender: mpsc::Sender<CoreCmd>) {
pub fn run_stub(cmd_sender: crossbeam::channel::Sender<CoreCmd>) {
std::thread::spawn(move || {
let listener = TcpListener::bind("127.0.0.1:1234").expect("couldnt start tcp listener");
for stream_res in listener.incoming() {
if let Ok(stream) = stream_res {
let (dbg_tx, dbg_rx) = mpsc::channel();
for stream in listener.incoming().flatten() {
let (dbg_tx, dbg_rx) = crossbeam::channel::bounded(16);
stream
.set_nonblocking(true)
@@ -86,13 +84,12 @@ pub fn run_stub(cmd_sender: mpsc::Sender<CoreCmd>) {
handle_gdb_connection(stream, dbg_tx).expect("failure during connection");
}
}
});
}
fn handle_gdb_connection(
gdb_stream: TcpStream,
dbg_tx: mpsc::Sender<DebugCommand>,
dbg_tx: crossbeam::channel::Sender<DebugCommand>,
) -> io::Result<()> {
eprintln!("gdb connected");
let mut reader = io::BufReader::new(gdb_stream.try_clone()?);
@@ -100,18 +97,19 @@ fn handle_gdb_connection(
loop {
match read_rsp_packet(&mut reader) {
Ok(packet) => match handle_packet(
Ok(packet) => {
if handle_packet(
&packet[..packet.len() - 1],
&mut writer,
&dbg_tx,
&mut reader,
) {
Err(_) => {
)
.is_err()
{
let _ = dbg_tx.send(DebugCommand::ExitDebugMode);
break;
}
_ => {}
},
}
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
std::thread::yield_now();
}
@@ -152,7 +150,7 @@ fn read_rsp_packet<R: BufRead>(reader: &mut R) -> io::Result<String> {
fn handle_packet<W: Write, R: BufRead>(
packet: &str,
writer: &mut W,
dbg_tx: &mpsc::Sender<DebugCommand>,
dbg_tx: &crossbeam::channel::Sender<DebugCommand>,
reader: &mut R,
) -> io::Result<()> {
writer.write_all(b"+")?;
@@ -225,7 +223,7 @@ fn handle_packet<W: Write, R: BufRead>(
let (responder, stop_reason_rx) = oneshot::channel();
dbg_tx.send(DebugCommand::Step(responder)).unwrap();
let stop_reason = stop_reason_rx.recv().unwrap();
send_packet(&stop_reason.to_rsp(), writer)?;
send_packet(stop_reason.to_rsp(), writer)?;
}
"c" => {
@@ -254,7 +252,7 @@ fn handle_packet<W: Write, R: BufRead>(
}
if let Ok(stop_reason) = stop_reason_rx.try_recv() {
send_packet(&stop_reason.to_rsp(), writer)?;
send_packet(stop_reason.to_rsp(), writer)?;
return Ok(());
}
@@ -266,11 +264,12 @@ fn handle_packet<W: Write, R: BufRead>(
}
"Z" if packet.chars().nth(1) == Some('0') => {
if let Some((addr_str, size_str)) = packet[3..].split_once(',') {
if let (Ok(addr), Ok(size)) = (
if let Some((addr_str, size_str)) = packet[3..].split_once(',')
&& let (Ok(addr), Ok(size)) = (
u64::from_str_radix(addr_str, 16),
u64::from_str_radix(size_str, 16),
) {
)
{
if size != 4 {
send_packet("", writer)?;
return Ok(());
@@ -280,16 +279,16 @@ fn handle_packet<W: Write, R: BufRead>(
send_packet("OK", writer)?;
return Ok(());
}
}
send_packet("", writer)?;
}
"z" if packet.chars().nth(1) == Some('0') => {
if let Some((addr_str, size_str)) = packet[3..].split_once(',') {
if let (Ok(addr), Ok(size)) = (
if let Some((addr_str, size_str)) = packet[3..].split_once(',')
&& let (Ok(addr), Ok(size)) = (
u64::from_str_radix(addr_str, 16),
u64::from_str_radix(size_str, 16),
) {
)
{
if size != 4 {
send_packet("", writer)?;
return Ok(());
@@ -299,7 +298,6 @@ fn handle_packet<W: Write, R: BufRead>(
send_packet("OK", writer)?;
return Ok(());
}
}
send_packet("", writer)?;
}

View File

@@ -7,6 +7,7 @@
#[macro_use]
mod macros;
mod rva;
mod rvi;
mod rvm;
@@ -166,6 +167,7 @@ pub(crate) fn find_and_exec(instr: Instruction, core: &mut Core) -> Result<(), E
Ok(())
}
},
0b01011 => rva::find_and_exec(instr, core),
_ => illegal(instr),
}
}

247
src/instructions/rva.rs Normal file
View File

@@ -0,0 +1,247 @@
// Copyright (c) 2026 taitep
// SPDX-License-Identifier: BSD-2-Clause
//
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
// See LICENSE file in the project root for full license text.
use std::sync::atomic::{AtomicU32, AtomicU64};
use super::illegal;
use crate::{
core::Core,
decode::Instruction,
exceptions::{Exception, ExceptionType},
mem::{RAM_START, Ram},
};
pub(super) fn find_and_exec(instr: Instruction, core: &mut Core) -> Result<(), Exception> {
match (instr.funct5(), instr.funct3()) {
(0b00010, 0b010) if instr.rs2() == 0 => lr_w(core, instr),
(0b00010, 0b011) if instr.rs2() == 0 => lr_d(core, instr),
(0b00011, 0b010) => sc_w(core, instr),
(0b00011, 0b011) => sc_d(core, instr),
_ => illegal(instr),
}
}
fn lr_d(core: &mut Core, instr: Instruction) -> Result<(), Exception> {
core.reservation = None;
let addr = core.reg_read(instr.rs1());
if !addr.is_multiple_of(8) {
return Err(Exception {
type_: ExceptionType::LoadAddressMisaligned,
value: addr,
});
}
if addr < RAM_START {
return Err(Exception {
type_: ExceptionType::LoadAccessFault,
value: addr,
});
}
let ram_addr = addr - RAM_START;
let reservation_data = core
.mem
.ram
.wait_for_even_version(ram_addr)
.ok_or_else(|| Exception {
type_: ExceptionType::LoadAccessFault,
value: addr,
})?;
core.reg_write(instr.rd(), unsafe {
let index = ram_addr as usize / 8;
core.mem
.ram
.buf_transmuted::<AtomicU64>()
.get(index)
.ok_or_else(|| Exception {
type_: ExceptionType::LoadAccessFault,
value: addr,
})?
.load(instr.amo_ordering())
});
core.reservation = Some(reservation_data);
core.advance_pc();
Ok(())
}
fn sc_d(core: &mut Core, instr: Instruction) -> Result<(), Exception> {
let res = if let Some((reserved_chunk_id, reserved_version)) = core.reservation {
let addr = core.reg_read(instr.rs1());
if !addr.is_multiple_of(8) {
return Err(Exception {
type_: ExceptionType::StoreAmoAddressMisaligned,
value: addr,
});
}
if addr < RAM_START {
return Err(Exception {
type_: ExceptionType::StoreAmoAccessFault,
value: addr,
});
}
let ram_addr = addr - RAM_START;
let chunk_id = ram_addr as usize / Ram::VERSION_CHUNK_SIZE;
if chunk_id != reserved_chunk_id {
// Mismatched reservation location and address
core.reg_write(instr.rd(), 1);
core.reservation = None;
return Ok(());
}
let claim_res = core
.mem
.ram
.claim_expected(chunk_id, reserved_version)
.ok_or_else(|| Exception {
type_: ExceptionType::StoreAmoAccessFault,
value: addr,
})?;
if claim_res.is_some() {
core.reservation = None;
let value = core.reg_read(instr.rs2());
unsafe {
let index = ram_addr as usize / 8;
core.mem
.ram
.buf_transmuted::<AtomicU64>()
.get(index)
.ok_or_else(|| Exception {
type_: ExceptionType::StoreAmoAccessFault,
value: addr,
})?
.store(value, instr.amo_ordering());
Ok(0)
}
} else {
core.reservation = None;
Ok(1)
}
} else {
core.reservation = None;
Ok(1)
}
.map(|s| core.reg_write(instr.rd(), s));
core.advance_pc();
res
}
fn lr_w(core: &mut Core, instr: Instruction) -> Result<(), Exception> {
core.reservation = None;
let addr = core.reg_read(instr.rs1());
if !addr.is_multiple_of(4) {
return Err(Exception {
type_: ExceptionType::LoadAddressMisaligned,
value: addr,
});
}
if addr < RAM_START {
return Err(Exception {
type_: ExceptionType::LoadAccessFault,
value: addr,
});
}
let ram_addr = addr - RAM_START;
let reservation_data = core
.mem
.ram
.wait_for_even_version(ram_addr)
.ok_or_else(|| Exception {
type_: ExceptionType::LoadAccessFault,
value: addr,
})?;
core.reg_write(instr.rd(), unsafe {
let index = ram_addr as usize / 4;
core.mem
.ram
.buf_transmuted::<AtomicU32>()
.get(index)
.ok_or_else(|| Exception {
type_: ExceptionType::LoadAccessFault,
value: addr,
})?
.load(instr.amo_ordering())
} as i32 as i64 as u64);
core.reservation = Some(reservation_data);
core.advance_pc();
Ok(())
}
fn sc_w(core: &mut Core, instr: Instruction) -> Result<(), Exception> {
let res = if let Some((reserved_chunk_id, reserved_version)) = core.reservation {
let addr = core.reg_read(instr.rs1());
if !addr.is_multiple_of(4) {
return Err(Exception {
type_: ExceptionType::StoreAmoAddressMisaligned,
value: addr,
});
}
if addr < RAM_START {
return Err(Exception {
type_: ExceptionType::StoreAmoAccessFault,
value: addr,
});
}
let ram_addr = addr - RAM_START;
let chunk_id = ram_addr as usize / Ram::VERSION_CHUNK_SIZE;
if chunk_id != reserved_chunk_id {
// Mismatched reservation location and address
core.reg_write(instr.rd(), 1);
core.reservation = None;
return Ok(());
}
let claim_res = core
.mem
.ram
.claim_expected(chunk_id, reserved_version)
.ok_or_else(|| Exception {
type_: ExceptionType::StoreAmoAccessFault,
value: addr,
})?;
if claim_res.is_some() {
core.reservation = None;
let value = core.reg_read(instr.rs2());
unsafe {
let index = ram_addr as usize / 4;
core.mem
.ram
.buf_transmuted::<AtomicU32>()
.get(index)
.ok_or_else(|| Exception {
type_: ExceptionType::StoreAmoAccessFault,
value: addr,
})?
.store(value as u32, instr.amo_ordering());
Ok(0)
}
} else {
core.reservation = None;
Ok(1)
}
} else {
core.reservation = None;
Ok(1)
}
.map(|s| core.reg_write(instr.rd(), s));
core.advance_pc();
res
}

View File

@@ -1,5 +1,6 @@
pub mod core;
mod decode;
pub mod devices;
pub mod exceptions;
pub mod gdb;
mod instructions;

View File

@@ -1,15 +1,17 @@
// Copyright (c) 2025 taitep
// Copyright (c) 2025-2026 taitep
// SPDX-License-Identifier: BSD-2-Clause
//
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
// See LICENSE file in the project root for full license text.
use std::{path::PathBuf, sync::Arc, time::Duration};
use std::{io, path::PathBuf, sync::Arc, time::Duration};
use clap::Parser;
use nix::fcntl::{FcntlArg, OFlag, fcntl};
use trve::{
core::Core,
devices,
exceptions::MemoryExceptionType,
gdb,
mem::{MemConfig, MemDeviceInterface, MmioRoot, Ram},
@@ -17,13 +19,14 @@ use trve::{
use anyhow::Result;
use crate::basic_uart::BasicUart;
mod execload;
/// Taitep's RISC-V Emulator
#[derive(Parser)]
struct Args {
/// Path to ELF or raw binary executable to load
executable: PathBuf,
/// Make CPU wait for a GDB connection before starting execution
#[arg(long)]
wait: bool,
}
@@ -39,8 +42,13 @@ fn main() -> Result<()> {
let mut mmio_root = MmioRoot::default();
mmio_root.insert(0, Arc::new(DbgOut));
let uart = BasicUart::new();
let uart = uart.spawn_poller(Duration::from_millis(10));
if let Err(e) = fcntl(io::stdin(), FcntlArg::F_SETFL(OFlag::O_NONBLOCK)) {
eprintln!("Could not make stdout nonblocking, skipping. Error: {e}");
}
let uart = devices::serial::SifiveUart::new_arc();
uart.clone()
.spawn_io_thread(io::stdin(), io::stdout(), Duration::from_millis(10));
mmio_root.insert(0x10000, uart);
let mem_cfg = MemConfig {
@@ -48,7 +56,7 @@ fn main() -> Result<()> {
mmio_root,
};
let (cmd_sender, cmd_reciever) = std::sync::mpsc::channel();
let (cmd_sender, cmd_reciever) = crossbeam::channel::bounded(16);
gdb::run_stub(cmd_sender.clone());
@@ -64,8 +72,6 @@ fn main() -> Result<()> {
Ok(())
}
mod basic_uart;
struct DbgOut;
impl MemDeviceInterface for DbgOut {

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2025 taitep
// Copyright (c) 2025-2026 taitep
// SPDX-License-Identifier: BSD-2-Clause
//
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
@@ -6,7 +6,10 @@
use std::sync::{
Arc,
atomic::{AtomicU8, AtomicU16, AtomicU32, AtomicU64, Ordering::Relaxed},
atomic::{
AtomicU8, AtomicU16, AtomicU32, AtomicU64,
Ordering::{self, Relaxed},
},
};
use memmap2::MmapMut;
@@ -169,52 +172,6 @@ impl MemConfig {
.map_err(|e| e.with_addr(addr))
}
}
pub fn get_atomic_dword(&self, addr: u64) -> Result<&AtomicU64, MemoryException> {
if !addr.is_multiple_of(8) {
return Err(MemoryException {
type_: MemoryExceptionType::AddressMisaligned,
addr,
});
}
let index = ((addr - RAM_START) / 8) as usize;
unsafe {
self.ram
.buf_transmuted::<AtomicU64>()
.get(index)
.ok_or(MemoryException {
type_: MemoryExceptionType::AccessFault,
addr,
})
}
}
pub fn get_atomic_word(&self, addr: u64) -> Result<&AtomicU32, MemoryException> {
if !addr.is_multiple_of(4) {
return Err(MemoryException {
type_: MemoryExceptionType::AddressMisaligned,
addr,
});
}
if addr < RAM_START {
return Err(MemoryException {
type_: MemoryExceptionType::AccessFault,
addr,
});
}
let index = ((addr - RAM_START) / 4) as usize;
unsafe {
self.ram
.buf_transmuted::<AtomicU32>()
.get(index)
.ok_or(MemoryException {
type_: MemoryExceptionType::AccessFault,
addr,
})
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -225,6 +182,7 @@ pub enum MemoryMappingType {
pub struct Ram {
buf: MmapMut,
version_counters: Arc<[AtomicU32]>,
}
#[cfg(target_endian = "big")]
@@ -237,9 +195,16 @@ impl Ram {
}
Ok(Self {
buf: MmapMut::map_anon(size)?,
// SAFETY: We do not care about the initial version counts. Wrapping is fine. Only
// equality is ever checked for, not magnitude.
version_counters: unsafe {
Arc::new_uninit_slice(size.div_ceil(Self::VERSION_CHUNK_SIZE)).assume_init()
},
})
}
pub const VERSION_CHUNK_SIZE: usize = 64;
pub fn buf_mut(&mut self) -> &mut [u8] {
self.buf.as_mut()
}
@@ -251,7 +216,7 @@ impl Ram {
/// It must also be known that the contents of RAM are made up of naturally
/// aligned valid instances of T.
#[inline]
unsafe fn buf_transmuted<T>(&self) -> &[T] {
pub(crate) unsafe fn buf_transmuted<T>(&self) -> &[T] {
debug_assert!(self.buf.len().is_multiple_of(std::mem::size_of::<T>()));
unsafe {
std::slice::from_raw_parts(
@@ -357,6 +322,9 @@ impl Ram {
return Ok(());
}
self.claim_addr_even(addr)
.ok_or_else(|| MemoryExceptionType::AccessFault.with_addr(addr))?;
let index = (addr / 8) as usize;
unsafe {
self.buf_transmuted::<AtomicU64>()
@@ -382,6 +350,9 @@ impl Ram {
return Ok(());
}
self.claim_addr_even(addr)
.ok_or_else(|| MemoryExceptionType::AccessFault.with_addr(addr))?;
let index = (addr / 4) as usize;
unsafe {
self.buf_transmuted::<AtomicU32>()
@@ -407,6 +378,9 @@ impl Ram {
return Ok(());
}
self.claim_addr_even(addr)
.ok_or_else(|| MemoryExceptionType::AccessFault.with_addr(addr))?;
let index = (addr / 2) as usize;
unsafe {
self.buf_transmuted::<AtomicU16>()
@@ -421,6 +395,8 @@ impl Ram {
}
#[inline]
pub fn write_byte(&self, addr: u64, value: u8) -> Result<(), MemoryException> {
self.claim_addr_even(addr)
.ok_or_else(|| MemoryExceptionType::AccessFault.with_addr(addr))?;
self.buf_atomic()
.get(addr as usize)
.ok_or(MemoryException {
@@ -430,6 +406,92 @@ impl Ram {
.store(value, Relaxed);
Ok(())
}
pub fn claim_addr_even<'a>(&'a self, addr: u64) -> Option<RamVersionClaim<'a>> {
let chunk_id = addr as usize / Self::VERSION_CHUNK_SIZE;
let chunk_counter = self.version_counters.get(chunk_id)?;
Some(RamVersionClaim::claim_even(&chunk_counter))
}
// Tries to create a claim for a specified chunk id with a specific version
// Outer Option represents whether the chunk id exists
// Inner Option represents whether the claim succeeded
pub fn claim_expected<'a>(
&'a self,
chunk_id: usize,
expected: u32,
) -> Option<Option<RamVersionClaim<'a>>> {
self.version_counters
.get(chunk_id)
.map(|chunk_counter| RamVersionClaim::claim_expected(chunk_counter, expected))
}
/// Waits for a specific address to have an even (ready) version
/// number and returns the version chunk id and version
pub fn wait_for_even_version(&self, addr: u64) -> Option<(usize, u32)> {
let chunk_id = addr as usize / Self::VERSION_CHUNK_SIZE;
let chunk_counter = self.version_counters.get(chunk_id)?;
loop {
let current_version = chunk_counter.load(Ordering::Acquire);
if current_version.is_multiple_of(2) {
return Some((chunk_id, current_version));
}
}
}
}
pub struct RamVersionClaim<'a> {
version_counter: &'a AtomicU32,
initial_version: u32,
}
impl<'a> RamVersionClaim<'a> {
pub fn claim_even(counter: &'a AtomicU32) -> RamVersionClaim<'a> {
loop {
let current_version = counter.load(Ordering::Acquire);
if !current_version.is_multiple_of(2) {
continue;
}
// Attempt to increment and therefore successfully claim the version
let res = counter.compare_exchange(
current_version,
current_version.wrapping_add(1),
Ordering::AcqRel,
Ordering::Acquire,
);
if let Ok(initial_version) = res {
return RamVersionClaim {
version_counter: counter,
initial_version,
};
}
}
}
pub fn claim_expected(counter: &'a AtomicU32, expected: u32) -> Option<RamVersionClaim<'a>> {
counter
.compare_exchange(
expected,
expected.wrapping_add(1),
Ordering::AcqRel,
Ordering::Acquire,
)
.ok()
.map(|initial_version| RamVersionClaim {
version_counter: counter,
initial_version,
})
}
}
impl<'a> Drop for RamVersionClaim<'a> {
fn drop(&mut self) {
self.version_counter
.store(self.initial_version.wrapping_add(2), Ordering::Release);
}
}
pub const MMIO_SECOND_LEVEL_PAGE_SIZE: usize = 64 * 1024;