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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 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]] [[package]]
name = "goblin" name = "goblin"
version = "0.10.4" version = "0.10.4"
@@ -281,6 +337,7 @@ version = "0.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"crossbeam",
"goblin", "goblin",
"int-enum", "int-enum",
"memmap2", "memmap2",

View File

@@ -6,6 +6,7 @@ edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.100" anyhow = "1.0.100"
clap = { version = "4.5.53", features = ["derive"] } clap = { version = "4.5.53", features = ["derive"] }
crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] }
goblin = "0.10.4" goblin = "0.10.4"
int-enum = "1.2.0" int-enum = "1.2.0"
memmap2 = "0.9.8" 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: 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, The goal is to support at least RV64GC and be able to run Linux,
potentially more. No plans for RV32I or RV32/64E. potentially more. No plans for RV32I or RV32/64E.
Currently implemented RISC-V ISA: `RV64IM` Currently implemented RISC-V ISA: `RV64IM-Zalrsc`
## Current Use ## Current Use
Currently, the emulator is nowhere near complete, 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 // SPDX-License-Identifier: BSD-2-Clause
// //
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve) // This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
// See LICENSE file in the project root for full license text. // See LICENSE file in the project root for full license text.
use std::{collections::HashSet, sync::mpsc}; use std::collections::HashSet;
use crate::{ use crate::{
core::commands::CoreCmd, core::commands::CoreCmd,
@@ -19,18 +19,21 @@ pub struct Core {
pub(crate) x_regs: [u64; 32], pub(crate) x_regs: [u64; 32],
pub(crate) pc: u64, pub(crate) pc: u64,
pub(crate) mem: MemConfig, 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; pub mod commands;
impl Core { 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 { Self {
x_regs: [0; 32], x_regs: [0; 32],
pc: 0, pc: 0,
mem, mem,
command_stream, 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(); let mut breakpoints = HashSet::new();
loop { loop {
match dbg_stream.recv()? { match dbg_stream.recv()? {
DebugCommand::GetRegs(sender) => sender.send(gdb::RegsResponse { DebugCommand::GetRegs(sender) => sender.send(gdb::RegsResponse {
x_regs: self.x_regs.clone(), x_regs: self.x_regs,
pc: self.pc, pc: self.pc,
})?, })?,
DebugCommand::ReadMem { DebugCommand::ReadMem {
@@ -160,7 +166,7 @@ impl Core {
return StopReason::Exception(ExceptionType::Breakpoint); return StopReason::Exception(ExceptionType::Breakpoint);
} }
if let Ok(_) = stopper.try_recv() { if stopper.try_recv().is_ok() {
return StopReason::Interrupted; return StopReason::Interrupted;
} }

View File

@@ -4,6 +4,8 @@
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve) // This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
// See LICENSE file in the project root for full license text. // See LICENSE file in the project root for full license text.
use std::sync::atomic::Ordering;
const MASK_REGISTER: u32 = 0x1f; const MASK_REGISTER: u32 = 0x1f;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@@ -51,7 +53,12 @@ impl Instruction {
#[inline] #[inline]
pub fn funct7(self) -> u8 { 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] #[inline]
@@ -95,11 +102,26 @@ impl Instruction {
/// 32bit ones use funct7 in this way /// 32bit ones use funct7 in this way
#[inline] #[inline]
pub fn funct6(self) -> u8 { pub fn funct6(self) -> u8 {
(self.0 >> 26 & 0x3f) as u8 (self.0 >> 26) as u8
} }
/// Mostly/only used for the SYSTEM opcode /// Mostly/only used for the SYSTEM opcode
#[inline]
pub fn funct12(self) -> u16 { pub fn funct12(self) -> u16 {
(self.0 >> 20) as 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 { impl From<MemoryException> for MemoryExceptionType {
fn into(self) -> MemoryExceptionType { fn from(val: MemoryException) -> Self {
self.type_ val.type_
} }
} }
impl Into<ExceptionType> for Exception { impl From<Exception> for ExceptionType {
fn into(self) -> ExceptionType { fn from(val: Exception) -> Self {
self.type_ val.type_
} }
} }

View File

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

View File

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

View File

@@ -1,15 +1,17 @@
// Copyright (c) 2025 taitep // Copyright (c) 2025-2026 taitep
// SPDX-License-Identifier: BSD-2-Clause // SPDX-License-Identifier: BSD-2-Clause
// //
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve) // This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
// See LICENSE file in the project root for full license text. // 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 clap::Parser;
use nix::fcntl::{FcntlArg, OFlag, fcntl};
use trve::{ use trve::{
core::Core, core::Core,
devices,
exceptions::MemoryExceptionType, exceptions::MemoryExceptionType,
gdb, gdb,
mem::{MemConfig, MemDeviceInterface, MmioRoot, Ram}, mem::{MemConfig, MemDeviceInterface, MmioRoot, Ram},
@@ -17,13 +19,14 @@ use trve::{
use anyhow::Result; use anyhow::Result;
use crate::basic_uart::BasicUart;
mod execload; mod execload;
/// Taitep's RISC-V Emulator
#[derive(Parser)] #[derive(Parser)]
struct Args { struct Args {
/// Path to ELF or raw binary executable to load
executable: PathBuf, executable: PathBuf,
/// Make CPU wait for a GDB connection before starting execution
#[arg(long)] #[arg(long)]
wait: bool, wait: bool,
} }
@@ -39,8 +42,13 @@ fn main() -> Result<()> {
let mut mmio_root = MmioRoot::default(); let mut mmio_root = MmioRoot::default();
mmio_root.insert(0, Arc::new(DbgOut)); mmio_root.insert(0, Arc::new(DbgOut));
let uart = BasicUart::new(); if let Err(e) = fcntl(io::stdin(), FcntlArg::F_SETFL(OFlag::O_NONBLOCK)) {
let uart = uart.spawn_poller(Duration::from_millis(10)); 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); mmio_root.insert(0x10000, uart);
let mem_cfg = MemConfig { let mem_cfg = MemConfig {
@@ -48,7 +56,7 @@ fn main() -> Result<()> {
mmio_root, 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()); gdb::run_stub(cmd_sender.clone());
@@ -64,8 +72,6 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
mod basic_uart;
struct DbgOut; struct DbgOut;
impl MemDeviceInterface for 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 // SPDX-License-Identifier: BSD-2-Clause
// //
// This file is part of TRVE (https://gitea.taitep.se/taitep/trve) // This file is part of TRVE (https://gitea.taitep.se/taitep/trve)
@@ -6,7 +6,10 @@
use std::sync::{ use std::sync::{
Arc, Arc,
atomic::{AtomicU8, AtomicU16, AtomicU32, AtomicU64, Ordering::Relaxed}, atomic::{
AtomicU8, AtomicU16, AtomicU32, AtomicU64,
Ordering::{self, Relaxed},
},
}; };
use memmap2::MmapMut; use memmap2::MmapMut;
@@ -169,52 +172,6 @@ impl MemConfig {
.map_err(|e| e.with_addr(addr)) .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)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -225,6 +182,7 @@ pub enum MemoryMappingType {
pub struct Ram { pub struct Ram {
buf: MmapMut, buf: MmapMut,
version_counters: Arc<[AtomicU32]>,
} }
#[cfg(target_endian = "big")] #[cfg(target_endian = "big")]
@@ -237,9 +195,16 @@ impl Ram {
} }
Ok(Self { Ok(Self {
buf: MmapMut::map_anon(size)?, 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] { pub fn buf_mut(&mut self) -> &mut [u8] {
self.buf.as_mut() 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 /// It must also be known that the contents of RAM are made up of naturally
/// aligned valid instances of T. /// aligned valid instances of T.
#[inline] #[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>())); debug_assert!(self.buf.len().is_multiple_of(std::mem::size_of::<T>()));
unsafe { unsafe {
std::slice::from_raw_parts( std::slice::from_raw_parts(
@@ -357,6 +322,9 @@ impl Ram {
return Ok(()); return Ok(());
} }
self.claim_addr_even(addr)
.ok_or_else(|| MemoryExceptionType::AccessFault.with_addr(addr))?;
let index = (addr / 8) as usize; let index = (addr / 8) as usize;
unsafe { unsafe {
self.buf_transmuted::<AtomicU64>() self.buf_transmuted::<AtomicU64>()
@@ -382,6 +350,9 @@ impl Ram {
return Ok(()); return Ok(());
} }
self.claim_addr_even(addr)
.ok_or_else(|| MemoryExceptionType::AccessFault.with_addr(addr))?;
let index = (addr / 4) as usize; let index = (addr / 4) as usize;
unsafe { unsafe {
self.buf_transmuted::<AtomicU32>() self.buf_transmuted::<AtomicU32>()
@@ -407,6 +378,9 @@ impl Ram {
return Ok(()); return Ok(());
} }
self.claim_addr_even(addr)
.ok_or_else(|| MemoryExceptionType::AccessFault.with_addr(addr))?;
let index = (addr / 2) as usize; let index = (addr / 2) as usize;
unsafe { unsafe {
self.buf_transmuted::<AtomicU16>() self.buf_transmuted::<AtomicU16>()
@@ -421,6 +395,8 @@ impl Ram {
} }
#[inline] #[inline]
pub fn write_byte(&self, addr: u64, value: u8) -> Result<(), MemoryException> { 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() self.buf_atomic()
.get(addr as usize) .get(addr as usize)
.ok_or(MemoryException { .ok_or(MemoryException {
@@ -430,6 +406,92 @@ impl Ram {
.store(value, Relaxed); .store(value, Relaxed);
Ok(()) 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; pub const MMIO_SECOND_LEVEL_PAGE_SIZE: usize = 64 * 1024;