Files
mlkem-sync/.trellis/spec/rtl/xsim-tb-conventions.md
FallenSigh 79653ac3a5 fix(tb): fix Vivado 2019.2 compatibility and add run_tb.sh
- Replace -include_dirs . with -i . (Vivado 2019.2 syntax)
- Add --timescale 1ns/1ps to all xelab commands
- Add LD_PRELOAD comment for ncurses compatibility
- Add run_tb.sh convenience script
  Usage: ./run_tb.sh mod_add
         ./run_tb.sh --list
- Update spec with Vivado 2019.2 compatibility notes
2026-06-25 20:53:47 +08:00

6.7 KiB
Raw Blame History

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/<module>/
├── <module>.v                # RTL source
├── TB/
│   ├── tb_<module>.cpp       # Verilator C++ testbench (optional)
│   ├── tb_<module>_xsim.v    # XSIM Verilog testbench
│   ├── gen_vectors.py        # Python vector generator (stdlib only)
│   ├── xsim_run.tcl          # Vivado compile+elaborate+simulate script
│   └── vectors/
│       └── <module>_input.hex  # Test input vectors for $readmemh

TB File Template

// tb_<module>_xsim.v - Vivado XSIM testbench for <module>
// Usage:
//   xvlog -sv <deps> <module>.v tb_<module>_xsim.v
//   xelab tb_<module>_xsim -s <snapshot>
//   xsim <snapshot> -R

`timescale 1ns / 1ps

module tb_<module>_xsim;

    parameter VECTOR_FILE = "sync_rtl/<module>/TB/vectors/<module>_input.hex";
    parameter TIMEOUT_CYCLES = 10000;

    // DUT signals (reg for inputs, wire for outputs)
    reg clk, rst_n;
    // ... module-specific ports ...

    // DUT instantiation
    <module> 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

# List available modules
./run_tb.sh --list

# Run a specific module
./run_tb.sh mod_add

Manual Run

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

# 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 . <rtl_dir>/<submodule>.v
xvlog -sv -i . <rtl_dir>/<dut>.v

# Compile testbench
xvlog -sv <tb_dir>/tb_<module>_xsim.v

# Elaborate
xelab tb_<module>_xsim -s <snapshot> --timescale 1ns/1ps

# Run
xsim <snapshot> -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/<module>_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