perilus

RISC-V processor built with Chisel
git clone https://git.bradleygannon.com/perilus.git
Log | Files | Refs | Submodules | README

commit 94894e970b27a28cbc3b81f11a396e3921a3d185
parent 796b36980503b2175fbc2ba7a94924ddecca181b
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Sun,  4 Oct 2026 12:31:14 -0400

Add VCD recording to simulator

Diffstat:
Mjustfile | 2+-
Msim/core/build.rs | 1+
Msim/core/src/perilus.rs | 21++++++++++++++++++++-
Msim/core/src/wrapper.cpp | 22++++++++++++++++++++++
Msim/gui/src/app.rs | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msim/gui/src/ui/controls.rs | 6++++++
6 files changed, 113 insertions(+), 3 deletions(-)

diff --git a/justfile b/justfile @@ -33,7 +33,7 @@ chisel force='': [arg("force", long="force", value="1")] verilate force='': if [ -n "{{force}}" ] || [ $(find generated/ -name "**.sv" -newer "verilated/.timestamp" 2>&1 | wc -l) != 0 ]; then - verilator --build --public-flat-rw --Mdir verilated --cc \ + verilator --build --trace-vcd --public-flat-rw --Mdir verilated --cc \ generated/Perilus.sv \ generated/Memory.sv \ generated/RegisterFile.sv \ diff --git a/sim/core/build.rs b/sim/core/build.rs @@ -30,6 +30,7 @@ fn main() -> Result<(), Box<dyn Error>> { .include(&verilated_dir) .warnings(false) .file("src/wrapper.cpp") + .file(format!("{verilator_root}/include/verilated_vcd_c.cpp")) .compile("wrapper"); println!("cargo:rustc-link-search=native={}", verilated_dir.display()); diff --git a/sim/core/src/perilus.rs b/sim/core/src/perilus.rs @@ -1,5 +1,5 @@ use bounded_integer::BoundedU32; -use std::{io, path::PathBuf, slice}; +use std::{ffi::CString, io, path::PathBuf, slice}; unsafe extern "C" { fn perilus_init(); @@ -15,6 +15,8 @@ unsafe extern "C" { fn perilus_set_register(reg: u32, value: u32); fn perilus_get_memory() -> *const u32; fn perilus_set_memory(address: u32, value: u32); + unsafe fn start_vcd(filename: *const std::ffi::c_char); + unsafe fn stop_vcd(); } pub type Register = BoundedU32<0, 31>; @@ -104,6 +106,23 @@ impl Perilus { pub fn save_file(&self, file: PathBuf) -> io::Result<()> { std::fs::write(file, self.memory_bytes()) } + pub fn start_trace(&self, file: PathBuf) -> io::Result<()> { + // Ensure the parent directory exists (VCD file will be created by Verilator) + if let Some(parent) = file.parent() { + std::fs::create_dir_all(parent)?; + } + let c_string = CString::new(file.to_string_lossy().as_bytes()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; + unsafe { + start_vcd(c_string.as_ptr()); + } + Ok(()) + } + pub fn stop_trace(&self) { + unsafe { + stop_vcd(); + } + } } impl Drop for Perilus { diff --git a/sim/core/src/wrapper.cpp b/sim/core/src/wrapper.cpp @@ -1,8 +1,10 @@ #include <VPerilus.h> #include <VPerilus___024root.h> #include <verilated.h> +#include <verilated_vcd_c.h> static VerilatedContext* ctx = nullptr; +static VerilatedVcdC* vcd = nullptr; static VPerilus* perilus = nullptr; extern "C" { @@ -17,17 +19,37 @@ extern "C" { } void perilus_eval() { perilus->eval(); + if (vcd != nullptr) { + vcd->dump(ctx->time()); + } } void perilus_drop() { perilus->final(); + if (vcd != nullptr) { + vcd->close(); + } delete perilus; delete ctx; + delete vcd; perilus = nullptr; ctx = nullptr; + vcd = nullptr; } void perilus_increment_time() { ctx->timeInc(1); } + void start_vcd(const char* filename) { + Verilated::traceEverOn(true); + vcd = new VerilatedVcdC; + perilus->trace(vcd, 99); + vcd->open(filename); + } + void stop_vcd() { + if (vcd != nullptr) { + vcd->close(); + } + vcd = nullptr; + } // clock void perilus_set_clock(uint32_t new_clock) { diff --git a/sim/gui/src/app.rs b/sim/gui/src/app.rs @@ -1,5 +1,6 @@ use std::ops::Range; -use std::time::{Duration, Instant}; +use std::path::PathBuf; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use eframe::egui; use egui::{Color32, FontId, Key, Modifiers}; @@ -84,6 +85,7 @@ struct Actions { up: bool, down: bool, backspace: bool, + trace: bool, nibble: Option<u8>, } @@ -107,6 +109,8 @@ pub(crate) struct SimApp { pub(crate) scroll_to_row: Option<usize>, pub(crate) mem_viewport: Range<usize>, pub(crate) status: String, + pub(crate) tracing: bool, + pub(crate) trace_file: PathBuf, run_cycles: u64, } @@ -132,6 +136,8 @@ impl SimApp { scroll_to_row: None, mem_viewport: 0..0, status: "Ready".into(), + tracing: false, + trace_file: PathBuf::new(), run_cycles: 0, } } @@ -199,6 +205,31 @@ impl SimApp { self.status = format!("Follow PC {}", if self.follow_pc { "on" } else { "off" }); } + pub(crate) fn toggle_trace(&mut self) { + if self.tracing { + self.perilus.stop_trace(); + self.status = format!("Closed trace file {}", self.trace_file.display()); + self.tracing = false; + } else { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + let iso = chrono_lite(timestamp); + let trace_file = std::env::temp_dir().join(format!("perilus-{iso}.vcd")); + match self.perilus.start_trace(trace_file.clone()) { + Ok(()) => { + self.status = format!("Opened trace file {}", trace_file.display()); + self.tracing = true; + self.trace_file = trace_file; + } + Err(e) => { + self.status = format!("Trace start failed: {e}"); + } + } + } + } + pub(crate) fn stop(&mut self) { if self.run_state != RunState::Idle { self.status = "Stopped".into(); @@ -472,6 +503,7 @@ impl SimApp { focus_address: i.consume_key(Modifiers::NONE, Key::G), names: i.consume_key(Modifiers::NONE, Key::X), view: i.consume_key(Modifiers::NONE, Key::V), + trace: i.consume_key(Modifiers::NONE, Key::T), clear: i.consume_key(Modifiers::SHIFT, Key::C), follow: i.consume_key(Modifiers::SHIFT, Key::P), set_pc_cursor: i.consume_key(Modifiers::NONE, Key::P), @@ -548,6 +580,9 @@ impl SimApp { ViewMode::Byte => ViewMode::Word, }; } + if actions.trace { + self.toggle_trace(); + } if actions.clear { self.clear_cursor(); } @@ -670,3 +705,30 @@ fn parse_hex(text: &str) -> Option<u32> { } u32::from_str_radix(digits, 16).ok() } + +/// Format Unix seconds as an ISO-8601 UTC string like `2026-10-04T14:28:19Z`. +fn chrono_lite(secs: u64) -> String { + let days = secs / 86_400; + let mut rem = secs % 86_400; + let hours = rem / 3600; + rem %= 3600; + let minutes = rem / 60; + let seconds = rem % 60; + + // Days since epoch → year/month/day (algorithm from Howard Hinnant) + let z = days + 719_468; + let era = z / 146_097; + let doe = z - era * 146_097; + let yoe = (doe - doe / 1461 + doe / 36_524 - doe / 146_096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + let year = if m <= 2 { y + 1 } else { y }; + + format!( + "{year:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + m, d, hours, minutes, seconds + ) +} diff --git a/sim/gui/src/ui/controls.rs b/sim/gui/src/ui/controls.rs @@ -43,5 +43,11 @@ impl SimApp { self.save_memory(); } }); + ui.horizontal_wrapped(|ui| { + let mut tracing = self.tracing; + if ui.toggle_value(&mut tracing, "Trace [t]").changed() { + self.toggle_trace(); + } + }); } }