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..2d3267618 --- /dev/null +++ b/hw/verilator/sim_main.cpp @@ -0,0 +1,259 @@ +#include "Vsimtop.h" +#include "Vsimtop__Syms.h" + +// If "verilator --trace" is used, include the tracing class +#if VM_TRACE +#include +#endif + +extern "C" { +// File loading support +#include "sim.h" +#include "stream.h" +#include "asm.h" +} + +#include +#include + +static const vluint64_t MAX_TIME = 100000000u; + +// 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_tsim(const Vsimtop &top) +{ + const auto &store = top.rootp->Top__DOT__tenyr__DOT__core__DOT__regs__DOT__store; + int32_t regs[16]; + for (int i = 0; i < 15; i++) + 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 +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, + // 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 + + // 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 (true) { + top.eval(); + + // 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 + if (tfp) + tfp->dump(main_time); +#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 + 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..8544b1528 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,16 @@ 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 $display("Could not load file %0s", filename); $stop(0); end +`endif end if ($value$plusargs("PERIODS=%d", temp)) periods = temp; @@ -62,7 +66,6 @@ module Top(); end #(periods * `CLOCKPERIOD) end_simulation(); end -`endif always #`CLOCKPERIOD begin if (tenyr.core.insn == 32'hffffffff)