From 1e0f8c9f72f3f351a42a958a9fed4130d7b4e3eb Mon Sep 17 00:00:00 2001 From: Darren Kulp Date: Mon, 6 Jul 2026 20:33:32 -0400 Subject: [PATCH 1/5] Add Verilator-based cycle-accurate simulation framework The .gitignore excludes all Verilator-generated artifacts. --- CMakeLists.txt | 1 + hw/verilator/.gitignore | 12 +++++ hw/verilator/CMakeLists.txt | 54 ++++++++++++++++++++++ hw/verilator/sim_main.cpp | 91 +++++++++++++++++++++++++++++++++++++ hw/verilog/sim/simtop.v | 4 +- 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 hw/verilator/.gitignore create mode 100644 hw/verilator/CMakeLists.txt create mode 100644 hw/verilator/sim_main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f053abc9..1238c0ba2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,4 +45,5 @@ configure_file(src/tenyr_config.h.in tenyr_config.h) add_subdirectory(lib) add_subdirectory(hw/icarus) add_subdirectory(hw/vpi) +add_subdirectory(hw/verilator) add_subdirectory(src) diff --git a/hw/verilator/.gitignore b/hw/verilator/.gitignore new file mode 100644 index 000000000..d4bb49ba5 --- /dev/null +++ b/hw/verilator/.gitignore @@ -0,0 +1,12 @@ +*.tree +*.dot +V*top*.cpp +V*top*.h +V*top*.mk +V*top*.vpp +V*top +V*top.xml +V*top*.a +V*top__*.* +V*top_061_order_edges.txt +libtenyrsim.a diff --git a/hw/verilator/CMakeLists.txt b/hw/verilator/CMakeLists.txt new file mode 100644 index 000000000..8515991cb --- /dev/null +++ b/hw/verilator/CMakeLists.txt @@ -0,0 +1,54 @@ +find_program(VERILATOR_EXE verilator) +if(NOT VERILATOR_EXE) + return() +endif() + +# Verilog source files - order matters for top-module auto-detection. +# simtop.v must come first so Verilator detects it as the top module. +set(VERILATOR_VFILES + ${CMAKE_SOURCE_DIR}/hw/verilog/sim/simtop.v + ${CMAKE_SOURCE_DIR}/hw/verilog/top.v + ${CMAKE_SOURCE_DIR}/hw/verilog/sim/simclocks.v + ${CMAKE_SOURCE_DIR}/hw/verilog/tenyr.v + ${CMAKE_SOURCE_DIR}/hw/verilog/ram.v + ${CMAKE_SOURCE_DIR}/hw/verilog/seg7.v + ${CMAKE_SOURCE_DIR}/hw/verilog/hex2segments.v + ${CMAKE_SOURCE_DIR}/hw/verilog/gpio.v + ${CMAKE_SOURCE_DIR}/3rdparty/wb_intercon/rtl/verilog/wb_mux.v +) + +set(VERILATOR_CSRC + ${CMAKE_CURRENT_SOURCE_DIR}/sim_main.cpp + ${CMAKE_SOURCE_DIR}/src/sim.c + ${CMAKE_SOURCE_DIR}/src/asm.c + ${CMAKE_SOURCE_DIR}/src/obj.c + ${CMAKE_SOURCE_DIR}/src/common.c + ${CMAKE_SOURCE_DIR}/src/param.c + ${CMAKE_SOURCE_DIR}/src/stream.c +) + +set(VSIMTOP_BINARY ${CMAKE_CURRENT_BINARY_DIR}/Vsimtop) + +add_custom_command( + OUTPUT ${VSIMTOP_BINARY} + COMMAND ${VERILATOR_EXE} + --cc --trace --exe --timing --build + -Wall -Wno-fatal -Wno-style + -I${CMAKE_SOURCE_DIR}/hw/verilog + -I${CMAKE_SOURCE_DIR}/3rdparty/wb_intercon/rtl/verilog + -DSIMCLK=tenyr_mainclock + --Mdir ${CMAKE_CURRENT_BINARY_DIR} + -CFLAGS "-I${CMAKE_SOURCE_DIR}/src -I${CMAKE_SOURCE_DIR}/src/os/default -I${CMAKE_BINARY_DIR}/src" + -LDFLAGS -pthread + ${VERILATOR_VFILES} + ${VERILATOR_CSRC} + -o ${VSIMTOP_BINARY} + DEPENDS ${VERILATOR_VFILES} ${VERILATOR_CSRC} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Running Verilator to generate and compile Vsimtop..." + VERBATIM +) + +add_custom_target(Vsimtop ALL DEPENDS ${VSIMTOP_BINARY}) + +install(PROGRAMS ${VSIMTOP_BINARY} DESTINATION bin) diff --git a/hw/verilator/sim_main.cpp b/hw/verilator/sim_main.cpp new file mode 100644 index 000000000..0ad5bac93 --- /dev/null +++ b/hw/verilator/sim_main.cpp @@ -0,0 +1,91 @@ +#include "Vsimtop.h" +#include "Vsimtop__Syms.h" + +// If "verilator --trace" is used, include the tracing class +#if VM_TRACE +#include +#endif + +static const vluint64_t MAX_TIME = 1000000u; + +// Current simulation time (64-bit unsigned) +vluint64_t main_time = 0; + +// Called by $time in Verilog +double sc_time_stamp() +{ + return static_cast(main_time); // Note does conversion to real, to match SystemC +} + +static inline bool finished(const Vsimtop &top) +{ + return main_time >= MAX_TIME + || Verilated::gotFinish(); +} + +static void print_registers(const Vsimtop &top) +{ + const auto &store = top.rootp->Top__DOT__tenyr__DOT__core__DOT__regs__DOT__store; + uint32_t regs[16]; + for (int i = 0; i < 15; i++) + regs[i] = store[i]; + // Register P (15): hardware returns nextP when index is 15 (all 1s) + regs[15] = top.rootp->Top__DOT__tenyr__DOT__core__DOT__nextP; + for (int i = 0; i < 16; i++) { + printf("%c %08x ", 'A' + i, regs[i]); + if (i % 6 == 5) + printf("\n"); + } + printf("\n"); +} + +int main(int argc, char *argv[]) +{ + Verilated::commandArgs(argc, argv); + + Vsimtop top; + +#if VM_TRACE + VerilatedVcdC *tfp = nullptr; + // If verilator was invoked with --trace argument, + // and if at run time passed the +trace argument, turn on tracing + const char *flag = Verilated::commandArgsPlusMatch("trace"); + if (flag && 0 == strcmp(flag, "+trace")) { + Verilated::traceEverOn(true); // Verilator must compute traced signals + VL_PRINTF("Enabling waves into logs/vlt_dump.vcd...\n"); + tfp = new VerilatedVcdC; + top.trace(tfp, 99); // Trace 99 levels of hierarchy + Verilated::mkdir("logs"); + tfp->open("logs/vlt_dump.vcd"); // Open the dump file + } +#endif + + while (!finished(top)) { + top.eval(); + + // Print register state in tsim -vvvv format: + // A XXXXXXXX B XXXXXXXX ... F XXXXXXXX + // G XXXXXXXX ... L XXXXXXXX + // M XXXXXXXX ... P XXXXXXXX + if (main_time % 10000 == 0) + print_registers(top); + +#if VM_TRACE + // Dump trace data for this cycle + if (tfp) + tfp->dump(main_time); +#endif + + main_time++; + } + +#if VM_TRACE + if (tfp) + tfp->close(); +#endif + + top.final(); + VL_PRINTF("Simulation finished at time %llu\n", + (unsigned long long)main_time); + return 0; +} diff --git a/hw/verilog/sim/simtop.v b/hw/verilog/sim/simtop.v index c6d3f6b82..20d6bc6ff 100644 --- a/hw/verilog/sim/simtop.v +++ b/hw/verilog/sim/simtop.v @@ -35,7 +35,6 @@ module Top(); end endtask -`ifdef __ICARUS__ // TODO The `ifdef guard should really be controlling for VPI availability reg [8 * 4096:0] filename; reg [8 * 4096:0] logfile = "Top.vcd"; @@ -46,11 +45,13 @@ module Top(); integer failure = 0; initial #0 begin if ($value$plusargs("LOAD=%s", filename)) begin +`ifdef __ICARUS__ $tenyr_load(filename, failure); // replace with $readmemh ? if (failure) begin $display("Could not load file %0s", filename); $stop(0); end +`endif end if ($value$plusargs("PERIODS=%d", temp)) periods = temp; @@ -62,7 +63,6 @@ module Top(); end #(periods * `CLOCKPERIOD) end_simulation(); end -`endif always #`CLOCKPERIOD begin if (tenyr.core.insn == 32'hffffffff) From ba4c530455d990899ec78fb3aa117bb684f0551c Mon Sep 17 00:00:00 2001 From: Darren Kulp Date: Mon, 6 Jul 2026 20:49:04 -0400 Subject: [PATCH 2/5] Load tenyr executables into Verilator RAM at runtime Add +LOAD=filename argument support to load compiled .texe files directly into the simulated processor's RAM. The C-level sim.c loader is linked with the Verilator build, with a dispatch function writing directly to the RAM array. For Icarus Verilog, file loading uses the $tenyr_load VPI call. For Verilator, loading happens in C++ before simulation starts. --- hw/verilator/sim_main.cpp | 68 +++++++++++++++++++++++++++++++++++++++ hw/verilog/sim/simtop.v | 3 ++ 2 files changed, 71 insertions(+) diff --git a/hw/verilator/sim_main.cpp b/hw/verilator/sim_main.cpp index 0ad5bac93..b2bca11c9 100644 --- a/hw/verilator/sim_main.cpp +++ b/hw/verilator/sim_main.cpp @@ -6,6 +6,15 @@ #include #endif +extern "C" { +// File loading support +#include "sim.h" +#include "stream.h" +} + +#include +#include + static const vluint64_t MAX_TIME = 1000000u; // Current simulation time (64-bit unsigned) @@ -39,12 +48,71 @@ static void print_registers(const Vsimtop &top) printf("\n"); } +// Dispatch function for load_sim that writes directly to Verilator RAM +static int verilator_dispatch(void *ud, int op, int32_t addr, int32_t *data) +{ + Vsimtop *top = static_cast(ud); + auto &store = top->rootp->Top__DOT__tenyr__DOT__ram__DOT__store; + + switch (op) { + case OP_WRITE: + store[addr] = *data; + return 0; + case OP_INSN_READ: + case OP_DATA_READ: + *data = store[addr]; + return 0; + default: + return -1; + } +} + +// Load a file into the Verilator RAM +static int load_file(Vsimtop *top, const char *filename) +{ + FILE *file = fopen(filename, "rb"); + if (!file) { + fprintf(stderr, "Could not open file %s\n", filename); + return -1; + } + + STREAM stream_ = stream_make_from_file(file), *stream = &stream_; + + void *ud = NULL; + const struct format *f = &tenyr_asm_formats[0]; + + int rc = f->init(stream, NULL, &ud); + if (rc) { + fclose(file); + return rc; + } + + rc = load_sim(verilator_dispatch, top, f, ud, stream, 0); + + rc |= f->fini(stream, &ud); + + fclose(file); + return rc; +} + int main(int argc, char *argv[]) { Verilated::commandArgs(argc, argv); Vsimtop top; + // Load file if +LOAD= is specified + { + const char *load_arg = Verilated::commandArgsPlusMatch("LOAD="); + if (load_arg) { + const char *filename = load_arg + 6; // skip "+LOAD=" + if (load_file(&top, filename)) { + fprintf(stderr, "Failed to load file: %s\n", filename); + exit(1); + } + } + } + #if VM_TRACE VerilatedVcdC *tfp = nullptr; // If verilator was invoked with --trace argument, diff --git a/hw/verilog/sim/simtop.v b/hw/verilog/sim/simtop.v index 20d6bc6ff..8544b1528 100644 --- a/hw/verilog/sim/simtop.v +++ b/hw/verilog/sim/simtop.v @@ -45,6 +45,9 @@ module Top(); integer failure = 0; initial #0 begin if ($value$plusargs("LOAD=%s", filename)) begin + // $tenyr_load is handled in C++ for Verilator and via VPI for Icarus + // In Verilator, the C++ main loads the file before the simulation starts. + // For Verilator, filename is just stored; the actual loading happens in sim_main.cpp. `ifdef __ICARUS__ $tenyr_load(filename, failure); // replace with $readmemh ? if (failure) begin From 79404cf9935d961743e3f2c4ae459e45e8ebebdc Mon Sep 17 00:00:00 2001 From: Darren Kulp Date: Mon, 6 Jul 2026 20:50:36 -0400 Subject: [PATCH 3/5] Add tsim -vvvv compatible verbose simulation output Print memory operations, fetched instructions with disassembly, and full register state on every instruction, matching the output format of tsim run with four -v flags. Track nextP transitions to detect instruction fetches, buffer data memory operations from state s3, and print them together with the instruction fetch when the next instruction is loaded. Use the shared asm.c disassembler for consistent output. --- hw/verilator/sim_main.cpp | 94 ++++++++++++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 17 deletions(-) diff --git a/hw/verilator/sim_main.cpp b/hw/verilator/sim_main.cpp index b2bca11c9..45b70cd5a 100644 --- a/hw/verilator/sim_main.cpp +++ b/hw/verilator/sim_main.cpp @@ -10,6 +10,7 @@ extern "C" { // File loading support #include "sim.h" #include "stream.h" +#include "asm.h" } #include @@ -32,20 +33,18 @@ static inline bool finished(const Vsimtop &top) || Verilated::gotFinish(); } -static void print_registers(const Vsimtop &top) +static void print_registers_tsim(const Vsimtop &top) { const auto &store = top.rootp->Top__DOT__tenyr__DOT__core__DOT__regs__DOT__store; - uint32_t regs[16]; + int32_t regs[16]; for (int i = 0; i < 15; i++) - regs[i] = store[i]; - // Register P (15): hardware returns nextP when index is 15 (all 1s) - regs[15] = top.rootp->Top__DOT__tenyr__DOT__core__DOT__nextP; - for (int i = 0; i < 16; i++) { - printf("%c %08x ", 'A' + i, regs[i]); - if (i % 6 == 5) - printf("\n"); - } - printf("\n"); + regs[i] = static_cast(store[i]); + // Register P (15): in tsim, regs[15] = PC, and print_registers shows regs[15]+1. + // In verilator, nextP = PC+1, so regs[15] = nextP - 1 = PC. + regs[15] = static_cast(top.rootp->Top__DOT__tenyr__DOT__core__DOT__nextP - 1); + + STREAM out = stream_make_from_file(stdout); + print_registers(&out, regs); } // Dispatch function for load_sim that writes directly to Verilator RAM @@ -128,15 +127,76 @@ int main(int argc, char *argv[]) } #endif + // References to Verilator signals for tsim -vvvv style output + auto &v_nextP = top.rootp->Top__DOT__tenyr__DOT__core__DOT__nextP; + auto &v_state = top.rootp->Top__DOT__tenyr__DOT__core__DOT__state; + auto &v_ram = top.rootp->Top__DOT__tenyr__DOT__ram__DOT__store; + auto &v_d_stb = top.rootp->Top__DOT__tenyr__DOT__d_stb; + auto &v_i_stb = top.rootp->Top__DOT__tenyr__DOT__i_stb; + auto &v_wen = top.rootp->Top__DOT__tenyr__DOT__d_wen; + auto &v_d_adr = top.rootp->Top__DOT__tenyr__DOT__d_adr; + auto &v_d_to_s = top.rootp->Top__DOT__tenyr__DOT__d_to_slav; + auto &v_rdata = top.rootp->Top__DOT__tenyr__DOT__d_to_mast; + + // RESETVECTOR: RAM's OFFSET parameter; logical addr 0x1000 maps to RAM index 0 + static const uint32_t RESETVECTOR = 0x1000; + + // Core state machine states + enum { S0=0, S1=1, S2=2, S3=3, S4=4, S5=5, S6=6, S7=7 }; + + // State for buffering memory operations and tracking instruction fetches + vluint32_t prev_nextP = v_nextP; + int mem_op = -1; // -1: none, 0: read, 1: write + int32_t mem_addr = 0, mem_data = 0; + while (!finished(top)) { top.eval(); - // Print register state in tsim -vvvv format: - // A XXXXXXXX B XXXXXXXX ... F XXXXXXXX - // G XXXXXXXX ... L XXXXXXXX - // M XXXXXXXX ... P XXXXXXXX - if (main_time % 10000 == 0) - print_registers(top); + // Buffer data memory operations (s3 = data access; s5/s6 = instruction fetch) + if (v_d_stb && v_state != S5 && v_state != S6) { + mem_op = v_wen ? 1 : 0; + mem_addr = static_cast(v_d_adr); + mem_data = v_wen ? static_cast(v_d_to_s) + : static_cast(v_rdata); + } + + // Print when nextP changes (new instruction fetched) + vluint32_t cur_nextP = v_nextP; + if (cur_nextP != prev_nextP && prev_nextP != 0) { + // Print any buffered data memory operation (write or read) + if (mem_op >= 0) { + printf("%s @ 0x%08x = 0x%08x\n", + mem_op == 1 ? "write" : "read ", + mem_addr, mem_data); + mem_op = -1; + } + + // nextP = PC+1, so the executed instruction was at prev_nextP - 1 + uint32_t pc = prev_nextP - 1; + // RAM has OFFSET=RESETVECTOR, so logical addr maps to RAM index (pc - RESETVECTOR) + int32_t insn_word = static_cast(v_ram[pc - RESETVECTOR]); + + // Print instruction fetch (read from memory) + printf("read @ 0x%08x = 0x%08x\n", pc, insn_word); + + // Print IP line with disassembly + struct element elem = {}; + elem.insn.u.word = insn_word; + elem.insn.reladdr = static_cast(pc); + elem.insn.size = 1; + + STREAM out = stream_make_from_file(stdout); + printf("IP = 0x%08x\t", pc); + int len = print_disassembly(&out, &elem, ASM_AS_INSN); + printf("%*s# ", 30 - len, ""); + print_disassembly(&out, &elem, ASM_AS_DATA); + printf("\n"); + + // Print register state + print_registers_tsim(top); + printf("\n"); + } + prev_nextP = cur_nextP; #if VM_TRACE // Dump trace data for this cycle From 68277329053bbb6c667b8322a9a67f45147b7deb Mon Sep 17 00:00:00 2001 From: Darren Kulp Date: Mon, 6 Jul 2026 20:53:26 -0400 Subject: [PATCH 4/5] Increase Verilator simulation time limit to 100M cycles Raise MAX_TIME from 1,000,000 to 100,000,000 to allow longer running programs to complete without hitting the time cap. --- hw/verilator/sim_main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hw/verilator/sim_main.cpp b/hw/verilator/sim_main.cpp index 45b70cd5a..12a4f4b78 100644 --- a/hw/verilator/sim_main.cpp +++ b/hw/verilator/sim_main.cpp @@ -16,7 +16,7 @@ extern "C" { #include #include -static const vluint64_t MAX_TIME = 1000000u; +static const vluint64_t MAX_TIME = 100000000u; // Current simulation time (64-bit unsigned) vluint64_t main_time = 0; From c02c5913cdf4ac97bdba5544a95471a7f0eabdd2 Mon Sep 17 00:00:00 2001 From: Darren Kulp Date: Mon, 6 Jul 2026 20:54:49 -0400 Subject: [PATCH 5/5] Print final instruction when simulation ends with $finish Check for $finish after the loop and print its buffered memory operation, instruction fetch, disassembly, and register state, matching the output produced by tsim -vvvv. --- hw/verilator/sim_main.cpp | 42 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/hw/verilator/sim_main.cpp b/hw/verilator/sim_main.cpp index 12a4f4b78..2d3267618 100644 --- a/hw/verilator/sim_main.cpp +++ b/hw/verilator/sim_main.cpp @@ -149,7 +149,7 @@ int main(int argc, char *argv[]) int mem_op = -1; // -1: none, 0: read, 1: write int32_t mem_addr = 0, mem_data = 0; - while (!finished(top)) { + while (true) { top.eval(); // Buffer data memory operations (s3 = data access; s5/s6 = instruction fetch) @@ -205,6 +205,46 @@ int main(int argc, char *argv[]) #endif main_time++; + + // Check for finish after printing so the last instruction is emitted + if (finished(top)) + break; + } + + // If $finish was called (insn == 0xffffffff), the $finish instruction and + // its data memory operation were not printed by the normal loop logic + // (which prints when nextP changes, i.e., on the NEXT instruction fetch). + // Print them now, matching tsim -vvvv behavior. + auto &v_insn = top.rootp->Top__DOT__tenyr__DOT__core__DOT__insn; + if (Verilated::gotFinish() && v_insn == 0xffffffff) { + // Print any buffered data memory operation from the $finish instruction + if (mem_op >= 0) { + printf("%s @ 0x%08x = 0x%08x\n", + mem_op == 1 ? "write" : "read ", + mem_addr, mem_data); + } + + // $finish was fetched, so nextP = PC + 1 for $finish. + // prev_nextP was updated at end of loop, so $finish is at prev_nextP - 1. + uint32_t pc = prev_nextP - 1; + int32_t insn_word = static_cast(v_ram[pc - RESETVECTOR]); + + printf("read @ 0x%08x = 0x%08x\n", pc, insn_word); + + struct element elem = {}; + elem.insn.u.word = insn_word; + elem.insn.reladdr = static_cast(pc); + elem.insn.size = 1; + + STREAM out = stream_make_from_file(stdout); + printf("IP = 0x%08x\t", pc); + int len = print_disassembly(&out, &elem, ASM_AS_INSN); + printf("%*s# ", 30 - len, ""); + print_disassembly(&out, &elem, ASM_AS_DATA); + printf("\n"); + + print_registers_tsim(top); + printf("\n"); } #if VM_TRACE