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

210 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```verilog
// 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
```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 . <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