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:
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();
+ }
+ });
}
}