# Vivado XSIM Verilog Testbench Conventions ## Purpose Conventions for writing Verilog testbenches targeting Vivado XSIM simulator. These testbenches co-exist with Verilator C++ testbenches (`.cpp`) — both go in the same `TB/` directory. ## Directory Structure ``` sync_rtl// ├── .v # RTL source ├── TB/ │ ├── tb_.cpp # Verilator C++ testbench (optional) │ ├── tb__xsim.v # XSIM Verilog testbench │ ├── gen_vectors.py # Python vector generator (stdlib only) │ ├── xsim_run.tcl # Vivado compile+elaborate+simulate script │ └── vectors/ │ └── _input.hex # Test input vectors for $readmemh ``` ## TB File Template ```verilog // tb__xsim.v - Vivado XSIM testbench for // Usage: // xvlog -sv .v tb__xsim.v // xelab tb__xsim -s // xsim -R `timescale 1ns / 1ps module tb__xsim; parameter VECTOR_FILE = "sync_rtl//TB/vectors/_input.hex"; parameter TIMEOUT_CYCLES = 10000; // DUT signals (reg for inputs, wire for outputs) reg clk, rst_n; // ... module-specific ports ... // DUT instantiation u_dut ( .clk(clk), .rst_n(rst_n), // ... ); // Clock: 100 MHz (10 ns period) initial clk = 1'b0; always #5 clk = ~clk; // Vector memory (width depends on module) reg [W-1:0] vector_mem [0:MAX_VECTORS-1]; integer pass_count, fail_count; initial begin // Load vectors $readmemh(VECTOR_FILE, vector_mem); // Reset: rst_n low 3 cycles rst_n <= 1'b0; repeat (3) @(posedge clk); rst_n <= 1'b1; @(posedge clk); // Process each vector: drive → wait valid_o → capture → verify for (idx = 0; idx < vec_count; idx = idx + 1) begin // Drive DUT inputs (use <= for reg drives) // ... // Wait for valid_o with timeout cycle_count = 0; while (!valid_o && cycle_count < TIMEOUT_CYCLES) begin @(posedge clk); cycle_count = cycle_count + 1; end if (cycle_count >= TIMEOUT_CYCLES) begin $display("ERROR: Timeout on vector %0d", idx); fail_count = fail_count + 1; end else begin // Capture output, verify, write result pass_count = pass_count + 1; end end $display("PASS: %0d FAIL: %0d", pass_count, fail_count); $finish; end // Timeout watchdog initial begin #(TIMEOUT_CYCLES * 10 * 100); $display("FATAL: Global timeout"); $finish; end endmodule ``` ## Key Rules ### Clock - Always: `initial clk = 1'b0; always #5 clk = ~clk;` (100 MHz, 10 ns period) ### Reset - Active-low (`rst_n`), held low for 3 cycles minimum - Pattern: `rst_n <= 1'b0; repeat (3) @(posedge clk); rst_n <= 1'b1; @(posedge clk);` ### DUT Drive Protocol - Use **non-blocking assignment** (`<=`) for all DUT input drives in `initial` blocks - Use **blocking assignment** (`=`) for local variable initialization only - Follow valid/ready handshake: assert `valid_i`, wait `ready_o`, then de-assert ### Timeout Watchdog - Every TB MUST have a global timeout watchdog initial block - Pattern: `#(TIMEOUT_CYCLES * 10 * 100);` (gives TIMEOUT_CYCLES × 1000ns margin) - Module-specific per-vector timeouts also required (using `cycle_count < TIMEOUT_CYCLES` loop) ### Pass/Fail Tracking - Use `integer pass_count, fail_count;` - Increment on each vector completion or timeout - Print summary with `$display("PASS: %0d FAIL: %0d", pass_count, fail_count)` before `$finish` ## Vector Format (`$readmemh`) Vectors are packed as single hex numbers per line: ``` // Simple module (e.g., mod_add, 24-bit vectors) // Hex chars = ceil(W/4) 000000 0640c8 d00d00 ``` ``` // Complex module (e.g., ntt_core, 3076-bit vectors) // Hex chars = 769 00000000...0000 10000000...0001 ``` Width `W` must evenly contain all packed fields. Use padding bits to align to hex-char boundaries (multiples of 4 bits). ## Running Testbenches ### Quick Run ```bash # List available modules ./run_tb.sh --list # Run a specific module ./run_tb.sh mod_add ``` ### Manual Run ```bash source /opt/Xilinx/Vivado/2019.2/settings64.sh export LD_PRELOAD=/usr/lib64/libtinfo.so.5 # required for Vivado 2019.2 on modern Linux cd ~/Dev/mlkem vivado -mode batch -source sync_rtl/mod_add/TB/xsim_run.tcl ``` ### Vivado 2019.2 Compatibility Notes - **Include flag**: Use `-i .` (not `-include_dirs .` — that's Vivado 2020+) - **ncurses fix**: `export LD_PRELOAD=/usr/lib64/libtinfo.so.5` resolves `_nc_tiparm` symbol error from bundled LLVM 3.1 - **Timescale**: Always pass `--timescale 1ns/1ps` to `xelab` (RTL modules may lack `timescale directives) ## xsim_run.tcl ```tcl # NOTE: On some systems, you may need: # export LD_PRELOAD=/usr/lib64/libtinfo.so.5 # Compile RTL dependencies (order matters for submodule hierarchy) xvlog -sv -i . /.v xvlog -sv -i . /.v # Compile testbench xvlog -sv /tb__xsim.v # Elaborate xelab tb__xsim -s --timescale 1ns/1ps # Run xsim -R ``` ### CRITICAL: Include Directories - If ANY compiled file uses `` `include "sync_rtl/common/defines.vh" `` (or any relative include), add `-i .` to ALL `xvlog` invocations in that TCL - **Vivado 2019.2**: Use `-i .` (single dash) - **Vivado 2020+**: Use `-include_dirs .` (single or double dash) ## gen_vectors.py - **Stdlib only**: Use only Python standard library modules (`hashlib`, `os`, `sys`, `random`, `math`) - **Output**: Write hex vectors to `vectors/_input.hex`, one packed hex number per line - **Self-contained**: Should be runnable standalone (`python3 gen_vectors.py`) - **Bit ordering**: Match RTL FIPS 202 bit ordering — seed[0] = first bit into sponge. For Python hashlib, this means `bytes.fromhex(seed_hex)[::-1]` (reverse byte order) ## Common Mistakes 1. **Forgetting `-include_dirs .`**: DUTs using `` `include "sync_rtl/common/defines.vh" `` will fail compilation if the project root is not in the include path 2. **Using `=` for DUT drives**: All DUT input assignments in `initial` blocks must use `<=`, not `=`, to avoid race conditions 3. **Missing timeout watchdog**: If the DUT deadlocks, the simulation will hang forever without a watchdog 4. **Bit order mismatch**: Python hashlib and Verilog RTL may have different bit ordering for SHA3/SHAKE operations — verify with known test vectors 5. **rush_n polarity**: The reset is active-low (`rst_n`), not active-high — held to 0 for reset, 1 for normal operation