Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
12 changes: 12 additions & 0 deletions hw/verilator/.gitignore
Original file line number Diff line number Diff line change
@@ -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
54 changes: 54 additions & 0 deletions hw/verilator/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
259 changes: 259 additions & 0 deletions hw/verilator/sim_main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
#include "Vsimtop.h"
#include "Vsimtop__Syms.h"

// If "verilator --trace" is used, include the tracing class
#if VM_TRACE
#include <verilated_vcd_c.h>
#endif

extern "C" {
// File loading support
#include "sim.h"
#include "stream.h"
#include "asm.h"
}

#include <cstdlib>
#include <cstring>

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<double>(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<int32_t>(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<int32_t>(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<Vsimtop *>(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=<filename> 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<int32_t>(v_d_adr);
mem_data = v_wen ? static_cast<int32_t>(v_d_to_s)
: static_cast<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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;
}
7 changes: 5 additions & 2 deletions hw/verilog/sim/simtop.v
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -62,7 +66,6 @@ module Top();
end
#(periods * `CLOCKPERIOD) end_simulation();
end
`endif

always #`CLOCKPERIOD begin
if (tenyr.core.insn == 32'hffffffff)
Expand Down
Loading