feat(tb): add Vivado XSIM Verilog testbenches for all 10 sync modules

Add file-based vector testbenches ( + ) for:
- mod_add_sync, rng_sync, poly_arith_sync, comp_decomp_sync
- s_bram/sd_bram, sha3_chain_top
- ntt_core, poly_mul_sync
- sample_cbd_sync, sample_ntt_sync

Each module includes:
- tb_<module>_xsim.v: Vivado XSIM testbench
- gen_vectors.py: Python vector generator (stdlib only)
- vectors/<module>_input.hex: test input vectors
- xsim_run.tcl: compile + elaborate + simulate script
This commit is contained in:
2026-06-25 20:48:38 +08:00
parent ae5f0ca048
commit d4c3fc86fc
42 changed files with 7745 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""gen_vectors.py - Generate test vectors for comp_decomp_sync XSIM testbench.
Produces sync_rtl/comp_decomp/TB/vectors/comp_decomp_input.hex
Each line is a 32-bit hex value (8 hex chars, no spaces):
bits[31:26] = padding (0)
bits[25:14] = expected[11:0]
bits[13:2] = coeff_in[11:0]
bit[1] = d[0] (d[4:0] split across bits [6:2] and bit[1])
bit[0] = mode (0=compress, 1=decompress)
Revised layout (cleaner, 32 bits):
bits[31:20] = expected[11:0]
bits[19:8] = coeff_in[11:0]
bits[7:3] = d[4:0]
bit[2] = mode (0=compress, 1=decompress)
bits[1:0] = padding (0)
FIPS 203 formulas:
Compress_q(x, d) = round((2^d / Q) * x) mod 2^d
= ((x * 2^d + Q//2) // Q) & ((1 << d) - 1)
Decompress_q(y, d) = round((Q / 2^d) * y)
= (y * Q + (1 << (d-1))) // (1 << d)
Tests cover d in {4, 5, 10, 11} (ML-KEM standard values) plus edge d=1.
"""
import os
import sys
Q = 3329
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "vectors")
OUTPUT_FILE = os.path.join(OUTPUT_DIR, "comp_decomp_input.hex")
def compress_q(x: int, d: int) -> int:
"""Compress_q(x, d) per FIPS 203."""
if d == 0:
return 0
two_d = 1 << d
# round((2^d / Q) * x) = floor((x * 2^d + Q/2) / Q)
val = (x * two_d + Q // 2) // Q
return val & (two_d - 1)
def decompress_q(y: int, d: int) -> int:
"""Decompress_q(y, d) per FIPS 203."""
if d == 0:
return 0
two_d = 1 << d
# round((Q / 2^d) * y) = floor((y * Q + 2^(d-1)) / 2^d)
val = (y * Q + (two_d >> 1)) // two_d
return val % Q
def pack_vector(expected: int, coeff_in: int, d: int, mode: int) -> int:
"""Pack a single test vector into a 32-bit value."""
val = 0
val |= (expected & 0xFFF) << 20 # bits 31:20
val |= (coeff_in & 0xFFF) << 8 # bits 19:8
val |= (d & 0x1F) << 3 # bits 7:3
val |= (mode & 0x1) << 2 # bit 2
# bits 1:0 are padding = 0
return val
def generate() -> list[int]:
"""Generate all test vectors. Returns list of packed 32-bit values."""
vectors: list[int] = []
def add_vector(x: int, d: int, mode: int) -> None:
if mode == 0:
exp = compress_q(x, d)
else:
exp = decompress_q(x, d)
vectors.append(pack_vector(exp, x, d, mode))
# Standard ML-KEM d values
d_values = [4, 5, 10, 11]
# Test coefficient values: edges and mid-range
coeffs = [
0, # zero
1, # minimal
3328, # max (Q-1)
1000, # mid-range
2000, # mid-range
1664, # Q/2
42, # small
]
# ---- COMPRESS (mode=0) ----
for d in d_values:
for c in coeffs:
add_vector(c, d, 0)
# Compress edge: max input
add_vector(3328, d, 0)
# Compress: some systematic sweep
for c in [0, 500, 1000, 1500, 2000, 2500, 3000, 3328]:
add_vector(c, d, 0)
# ---- DECOMPRESS (mode=1) ----
# For decompress, input is in [0, 2^d-1]
for d in d_values:
two_d_mask = (1 << d) - 1
# Zero
add_vector(0, d, 1)
# Max in range
add_vector(two_d_mask, d, 1)
# Mid-range
mid = two_d_mask // 2
if mid > 0:
add_vector(mid, d, 1)
add_vector(mid - 1, d, 1) if mid > 1 else None
add_vector(mid + 1, d, 1) if mid + 1 <= two_d_mask else None
# Systematic sweep through valid range
step = max(1, two_d_mask // 8)
for y in range(0, two_d_mask + 1, step):
add_vector(y, d, 1)
# ---- Edge case: d=1 (minimum non-zero) ----
add_vector(0, 1, 0)
add_vector(3328, 1, 0)
add_vector(0, 1, 1)
add_vector(1, 1, 1)
# ---- d=12 (max for 12-bit operands, though not in ML-KEM) ----
add_vector(0, 12, 0)
add_vector(3328, 12, 0)
return vectors
def write_vectors(vectors: list[int]) -> None:
"""Write vectors to hex file."""
os.makedirs(OUTPUT_DIR, exist_ok=True)
with open(OUTPUT_FILE, "w") as f:
for v in vectors:
# 32 bits = 8 hex digits
f.write(f"{v:08X}\n")
print(f"Generated {len(vectors)} test vectors -> {OUTPUT_FILE}")
def main() -> int:
vectors = generate()
write_vectors(vectors)
# Print statistics and samples
print(f"\nTotal vectors: {len(vectors)}")
compress_count = sum(1 for v in vectors if ((v >> 2) & 1) == 0)
decompress_count = sum(1 for v in vectors if ((v >> 2) & 1) == 1)
print(f" Compress: {compress_count}")
print(f" Decompress: {decompress_count}")
print("\nSample vectors (first 5):")
for i, v in enumerate(vectors[:5]):
exp = (v >> 20) & 0xFFF
coeff = (v >> 8) & 0xFFF
d = (v >> 3) & 0x1F
mode = (v >> 2) & 0x1
op = "COMPRESS" if mode == 0 else "DECOMPRESS"
if mode == 0:
print(f" [{i}] {op} x={coeff:04d} d={d:02d} expected={exp:04d}")
else:
print(f" [{i}] {op} y={coeff:04d} d={d:02d} expected={exp:04d}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,254 @@
// tb_comp_decomp_xsim.v - Standard Verilog testbench for comp_decomp_sync targeting Vivado xsim
//
// Reads test vectors from a hex file using $readmemh.
// Each line is a 32-bit hex value (8 hex chars, no spaces):
// bits[31:20] = expected[11:0]
// bits[19:8] = coeff_in[11:0]
// bits[7:3] = d[4:0]
// bit[2] = mode (0=compress, 1=decompress)
// bits[1:0] = padding
//
// Drives comp_decomp_sync, waits for valid_o, compares coeff_out with expected,
// and reports pass/fail.
//
// Parameters:
// VECTOR_FILE - path to input hex file (default: "sync_rtl/comp_decomp/TB/vectors/comp_decomp_input.hex")
// RESULT_FILE - path to output file (default: "sync_rtl/comp_decomp/TB/vectors/comp_decomp_result.hex")
//
// Usage with xsim_run.tcl or manual:
// xvlog -sv sync_rtl/common/pipeline_reg.v sync_rtl/comp_decomp/comp_decomp_sync.v tb_comp_decomp_xsim.v
// xelab tb_comp_decomp_xsim -s tb_comp_decomp_xsim
// xsim tb_comp_decomp_xsim -R
`timescale 1ns / 1ps
module tb_comp_decomp_xsim;
// ================================================================
// Parameters
// ================================================================
parameter VECTOR_FILE = "sync_rtl/comp_decomp/TB/vectors/comp_decomp_input.hex";
parameter RESULT_FILE = "sync_rtl/comp_decomp/TB/vectors/comp_decomp_result.hex";
parameter MAX_VECTORS = 512;
parameter TIMEOUT_CYCLES = 10000;
parameter Q = 3329;
// ================================================================
// DUT signals
// ================================================================
reg clk;
reg rst_n;
reg [11:0] coeff_in;
reg [4:0] d;
reg mode;
reg valid_i;
wire ready_o;
wire [11:0] coeff_out;
wire valid_o;
reg ready_i;
// ================================================================
// DUT instantiation (named ports)
// ================================================================
comp_decomp_sync u_dut (
.clk (clk),
.rst_n (rst_n),
.coeff_in (coeff_in),
.d (d),
.mode (mode),
.valid_i (valid_i),
.ready_o (ready_o),
.coeff_out (coeff_out),
.valid_o (valid_o),
.ready_i (ready_i)
);
// ================================================================
// Clock generation: 100 MHz (10 ns period)
// ================================================================
initial clk = 1'b0;
always #5 clk = ~clk;
// ================================================================
// Vector memory (loaded by $readmemh)
// 32 bits per word:
// bits[31:20] = expected
// bits[19:8] = coeff_in
// bits[7:3] = d
// bit[2] = mode
// bits[1:0] = padding
// ================================================================
reg [31:0] vector_mem [0:MAX_VECTORS-1];
integer vec_count;
integer idx;
integer cycle_count;
integer result_fd;
// Test result tracking
integer pass_count;
integer fail_count;
// ================================================================
// Hex-to-ASCII conversion helper
// ================================================================
function [7:0] nibble_to_ascii;
input [3:0] nibble;
begin
if (nibble < 4'd10)
nibble_to_ascii = 8'h30 + {4'd0, nibble}; // '0'-'9'
else
nibble_to_ascii = 8'h41 + ({4'd0, nibble} - 4'd10); // 'A'-'F'
end
endfunction
// ================================================================
// Main test sequence
// ================================================================
initial begin
// Count loaded vectors
vec_count = 0;
// Load vectors from hex file
$readmemh(VECTOR_FILE, vector_mem);
// Count non-X/non-Z entries to determine actual vector count
begin
integer found_end;
found_end = 0;
for (idx = 0; idx < MAX_VECTORS; idx = idx + 1) begin
if (!found_end && (vector_mem[idx] === 32'hx || vector_mem[idx] === 32'hz))
found_end = 1;
else if (!found_end)
vec_count = vec_count + 1;
end
end
if (vec_count == 0) begin
$display("ERROR: No vectors loaded from %s", VECTOR_FILE);
$display(" Check that the file exists and is in the correct format.");
$display(" Each line: <8 hex chars> = {expected[11:0], coeff_in[11:0], d[4:0], mode, 2'b0}");
$finish;
end
$display("INFO: Loaded %0d test vectors from %s", vec_count, VECTOR_FILE);
// Open result file
result_fd = $fopen(RESULT_FILE, "w");
if (result_fd == 0) begin
$display("ERROR: Cannot open result file: %s", RESULT_FILE);
$finish;
end
// Initialize DUT inputs
coeff_in <= 12'd0;
d <= 5'd0;
mode <= 1'b0;
valid_i <= 1'b0;
ready_i <= 1'b1; // always ready to accept output
// Reset sequence: rst_n low for 3 cycles, then high
rst_n <= 1'b0;
repeat (3) @(posedge clk);
rst_n <= 1'b1;
@(posedge clk);
pass_count = 0;
fail_count = 0;
// ============================================================
// Process each vector
// ============================================================
for (idx = 0; idx < vec_count; idx = idx + 1) begin
begin
reg [11:0] vec_expected;
reg [11:0] vec_coeff_in;
reg [4:0] vec_d;
reg vec_mode;
reg [11:0] captured_out;
// Extract fields from vector
vec_expected = vector_mem[idx][31:20];
vec_coeff_in = vector_mem[idx][19:8];
vec_d = vector_mem[idx][7:3];
vec_mode = vector_mem[idx][2];
$display("INFO: Vector %0d - coeff=%0d d=%0d mode=%s expected=%0d",
idx, vec_coeff_in, vec_d,
vec_mode ? "DECOMPRESS" : "COMPRESS", vec_expected);
// Drive DUT inputs
coeff_in <= vec_coeff_in;
d <= vec_d;
mode <= vec_mode;
valid_i <= 1'b1;
@(posedge clk);
valid_i <= 1'b0;
// Wait for valid_o assertion (1 pipeline stage latency)
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 waiting for valid_o on vector %0d", idx);
fail_count = fail_count + 1;
$fwrite(result_fd, "RESULT: VECTOR %0d TIMEOUT\n", idx);
end else begin
// Capture output (valid_o is high, coeff_out is valid)
captured_out = coeff_out;
// Compare with expected
if (captured_out == vec_expected) begin
pass_count = pass_count + 1;
$fwrite(result_fd, "PASS: %0d - mode=%s coeff=%03X d=%0d expected=%03X got=%03X\n",
idx, vec_mode ? "DECOMPRESS" : "COMPRESS",
vec_coeff_in, vec_d, vec_expected, captured_out);
end else begin
$display("FAIL: Vector %0d - mode=%s coeff=%0d d=%0d expected=%0d got=%0d",
idx, vec_mode ? "DECOMPRESS" : "COMPRESS",
vec_coeff_in, vec_d, vec_expected, captured_out);
fail_count = fail_count + 1;
$fwrite(result_fd, "FAIL: %0d - mode=%s coeff=%03X d=%0d expected=%03X got=%03X\n",
idx, vec_mode ? "DECOMPRESS" : "COMPRESS",
vec_coeff_in, vec_d, vec_expected, captured_out);
end
end
// One extra cycle for valid_o -> ready_i handshake (pipeline clears valid_o)
@(posedge clk);
end
end
// ============================================================
// Summary
// ============================================================
$fclose(result_fd);
$display("========================================");
$display("TEST COMPLETE");
$display(" Total vectors: %0d", vec_count);
$display(" Passed: %0d", pass_count);
$display(" Failed: %0d", fail_count);
$display(" Results written to: %s", RESULT_FILE);
if (fail_count == 0) begin
$display(" STATUS: ALL TESTS PASSED");
end else begin
$display(" STATUS: %0d FAILURE(S) DETECTED", fail_count);
end
$display("========================================");
$finish;
end
// ================================================================
// Timeout watchdog (TIMEOUT_CYCLES * 10ns * vectors * margin)
// ================================================================
initial begin
#(TIMEOUT_CYCLES * 10 * MAX_VECTORS * 2);
$display("FATAL: Global simulation timeout reached");
$finish;
end
endmodule

View File

@@ -0,0 +1,135 @@
00000020
00000120
000D0020
0053E820
00A7D020
00868020
00002A20
000D0020
00000020
0021F420
0053E820
0075DC20
00A7D020
00C9C420
00EBB820
000D0020
00000028
00000128
000D0028
00A3E828
0137D028
01068028
00002A28
000D0028
00000028
0051F428
00A3E828
00E5DC28
0137D028
0189C428
01DBB828
000D0028
00000050
00000150
000D0050
1343E850
2677D050
20068050
00D02A50
000D0050
00000050
09A1F450
1343E850
1CD5DC50
2677D050
3019C450
39BBB850
000D0050
00000058
00100158
7FFD0058
2673E858
4CE7D058
40068058
01A02A58
7FFD0058
00000058
1341F458
2673E858
39B5DC58
4CE7D058
6029C458
736BB858
7FFD0058
00000024
C3100F24
5B000724
4E000624
68100824
00000024
0D000124
1A000224
27000324
34000424
41000524
4E000624
5B000724
68100824
75100924
82100A24
8F100B24
9C100C24
A9100D24
B6100E24
C3100F24
0000002C
C9901F2C
61800F2C
5B000E2C
6810102C
0000002C
1380032C
2700062C
3A80092C
4E000C2C
61800F2C
7510122C
8890152C
9C10182C
AF901B2C
C3101E2C
00000054
CFE3FF54
67D1FF54
67A1FE54
68120054
00000054
19D07F54
33A0FE54
4D717D54
6731FC54
81027B54
9AD2FA54
B4A37954
CE73F854
0000005C
CFF7FF5C
67F3FF5C
67D3FE5C
6814005C
0000005C
19E0FF5C
33D1FE5C
4DB2FD5C
67A3FC5C
8184FB5C
9B75FA5C
B556F95C
CF47F85C
00000008
000D0008
0000000C
6810010C
00000060
FFFD0060

View File

@@ -0,0 +1,60 @@
# xsim_run.tcl - Vivado xsim compilation and simulation script for comp_decomp_sync
#
# Compiles comp_decomp_sync RTL + dependencies + testbench and runs simulation.
# Run from the project root: ~/Dev/mlkem/
#
# Prerequisites:
# source /opt/Xilinx/Vivado/2019.2/settings64.sh
#
# Usage examples:
# # Step-by-step (from Tcl):
# xsim -runall xsim_run.tcl
#
# # Or via Vivado batch mode:
# vivado -mode batch -source xsim_run.tcl
#
# # Or manually:
# xvlog -sv sync_rtl/common/pipeline_reg.v sync_rtl/comp_decomp/comp_decomp_sync.v sync_rtl/comp_decomp/TB/tb_comp_decomp_xsim.v
# xelab tb_comp_decomp_xsim -s tb_comp_decomp_xsim
# xsim tb_comp_decomp_xsim -R
# ================================================================
# Configuration
# ================================================================
set RTL_DIR sync_rtl
set DUT_DIR sync_rtl/comp_decomp
set TB_DIR sync_rtl/comp_decomp/TB
# ================================================================
# Step 1: Compile all source files (xvlog)
# ================================================================
puts "=== Compiling RTL sources for comp_decomp_sync ==="
# Common dependency (pipeline register)
xvlog -sv -include_dirs . ${RTL_DIR}/common/pipeline_reg.v
# DUT (comp_decomp_sync) — uses `include "sync_rtl/common/defines.vh"
xvlog -sv -include_dirs . ${DUT_DIR}/comp_decomp_sync.v
# ================================================================
# Step 2: Compile testbench
# ================================================================
puts "=== Compiling testbench ==="
xvlog -sv ${TB_DIR}/tb_comp_decomp_xsim.v
# ================================================================
# Step 3: Elaborate snapshot (xelab)
# ================================================================
puts "=== Elaborating snapshot ==="
xelab tb_comp_decomp_xsim -s tb_comp_decomp_xsim
# ================================================================
# Step 4: Run simulation
# ================================================================
puts "=== Running comp_decomp_sync XSIM test ==="
xsim tb_comp_decomp_xsim -R
puts ""
puts "=== Simulation complete ==="