From 209ca90fb10099a479854f9dcdea0c8a77994a1a Mon Sep 17 00:00:00 2001 From: FallenSigh Date: Wed, 24 Jun 2026 23:12:59 +0800 Subject: [PATCH] feat(poly_arith): implement synchronous PolyAdd/PolySub streaming module Phase 2.3: Polynomial modular addition and subtraction. - poly_arith_sync.v: mode=0 add (a+b mod Q), mode=1 sub (a-b mod Q) - Pure streaming (1 coeff/cycle, no BRAM needed) - Uses pipeline_reg for valid/ready handshake Verified: 10/10 vectors bit-exact vs Python reference --- sync_rtl/poly_arith/TB/tb_poly_arith.cpp | 175 ++++++++++++++++++ sync_rtl/poly_arith/poly_arith_sync.v | 61 ++++++ .../modules/poly_arith/gen_vectors.py | 108 +++++++++++ .../modules/poly_arith/test_plan.json | 24 +++ 4 files changed, 368 insertions(+) create mode 100644 sync_rtl/poly_arith/TB/tb_poly_arith.cpp create mode 100644 sync_rtl/poly_arith/poly_arith_sync.v create mode 100644 test_framework/modules/poly_arith/gen_vectors.py create mode 100644 test_framework/modules/poly_arith/test_plan.json diff --git a/sync_rtl/poly_arith/TB/tb_poly_arith.cpp b/sync_rtl/poly_arith/TB/tb_poly_arith.cpp new file mode 100644 index 0000000..e001621 --- /dev/null +++ b/sync_rtl/poly_arith/TB/tb_poly_arith.cpp @@ -0,0 +1,175 @@ +// tb_poly_arith.cpp - Verilator C++ testbench for poly_arith_sync +// +// Reads test vectors from a hex file specified by +VECTOR_FILE= plusarg. +// Each line: "MODE A0 A1 ... A255 B0 B1 ... B255" +// MODE: 'A' for add, 'S' for sub +// A0..A255: 256 hex coefficients (12-bit, 3 hex digits each) +// B0..B255: 256 hex coefficients +// +// Feeds 256 (A, B) pairs through the DUT, collects 256 output coeffs, +// prints one "RESULT: C0 C1 ... C255" line per vector. +// +// Clock: 10 ns period (100 MHz). +// Reset: 2 cycles low, then high. +// Timeout: 100,000 cycles. + +#include +#include +#include +#include +#include +#include +#include "Vpoly_arith_sync.h" +#include "verilated.h" + +#define CLK_PERIOD_NS 10.0 +#define TIMEOUT_CYCLES 100000 + +static vluint64_t main_time = 0; + +double sc_time_stamp() { + return main_time; +} + +// Helper: toggle clock (full cycle: low->high->low) with eval +static void posedge(Vpoly_arith_sync* dut) { + dut->clk = !dut->clk; + main_time += (vluint64_t)(CLK_PERIOD_NS / 2.0); + dut->eval(); + dut->clk = !dut->clk; + main_time += (vluint64_t)(CLK_PERIOD_NS / 2.0); + dut->eval(); +} + +int main(int argc, char** argv) { + Verilated::commandArgs(argc, argv); + + // Parse +VECTOR_FILE= plusarg + const char* vector_file = NULL; + for (int i = 1; i < argc; i++) { + std::string arg(argv[i]); + if (arg.rfind("+VECTOR_FILE=", 0) == 0) { + vector_file = argv[i] + 13; + } + } + + if (!vector_file) { + std::cerr << "ERROR: +VECTOR_FILE= not specified" << std::endl; + return 1; + } + + std::ifstream infile(vector_file); + if (!infile.is_open()) { + std::cerr << "ERROR: Cannot open vector file: " << vector_file << std::endl; + return 1; + } + + // Instantiate DUT + Vpoly_arith_sync* dut = new Vpoly_arith_sync; + + // Initialize + dut->clk = 0; + dut->rst_n = 0; + dut->coeff_a_in = 0; + dut->coeff_b_in = 0; + dut->mode = 0; + dut->valid_i = 0; + dut->ready_i = 0; + + // Reset: 2 cycles low + for (int i = 0; i < 4; i++) { + dut->clk = !dut->clk; + main_time += (vluint64_t)(CLK_PERIOD_NS / 2.0); + dut->eval(); + dut->clk = !dut->clk; + main_time += (vluint64_t)(CLK_PERIOD_NS / 2.0); + dut->eval(); + } + dut->rst_n = 1; + + // Always ready to receive results + dut->ready_i = 1; + + std::string line; + vluint64_t cycle = 0; + + while (std::getline(infile, line)) { + // Skip empty lines and comments + if (line.empty() || line[0] == '#') continue; + + // Parse: MODE A0..A255 B0..B255 (513 values on one line) + std::istringstream iss(line); + std::string mode_str; + if (!(iss >> mode_str)) continue; + + // Determine mode + bool mode_val; + if (mode_str == "A" || mode_str == "a") { + mode_val = false; + } else if (mode_str == "S" || mode_str == "s") { + mode_val = true; + } else { + std::cerr << "ERROR: Unknown mode: " << mode_str << std::endl; + continue; + } + + dut->mode = mode_val ? 1 : 0; + + // Read 256 A coeffs + unsigned int coeff; + std::vector a_coeffs, b_coeffs; + for (int i = 0; i < 256; i++) { + if (!(iss >> std::hex >> coeff)) { + std::cerr << "ERROR: Missing A coeff at index " << i << std::endl; + return 1; + } + a_coeffs.push_back(coeff & 0xFFF); + } + + // Read 256 B coeffs + for (int i = 0; i < 256; i++) { + if (!(iss >> std::hex >> coeff)) { + std::cerr << "ERROR: Missing B coeff at index " << i << std::endl; + return 1; + } + b_coeffs.push_back(coeff & 0xFFF); + } + + // Feed 256 coefficient pairs and collect results + std::vector results; + for (int i = 0; i < 256; i++) { + // Drive inputs + dut->coeff_a_in = a_coeffs[i]; + dut->coeff_b_in = b_coeffs[i]; + dut->valid_i = 1; + + // Posedge: DUT samples input, pipeline_reg captures data, valid_o→1 + posedge(dut); + dut->valid_i = 0; + + // Read result now (valid_o is high, coeff_out is valid) + // Posedge: pipeline_reg clears valid_o (ready_i=1), but result + // is available on coeff_out port (combinational from data_r) + posedge(dut); + + results.push_back(dut->coeff_out & 0xFFF); + + cycle += 2; + if (cycle > TIMEOUT_CYCLES) { + std::cerr << "ERROR: Timeout after " << cycle << " cycles" << std::endl; + return 1; + } + } + + // Print result as one space-separated hex line + printf("RESULT:"); + for (int i = 0; i < 256; i++) { + printf(" %03X", results[i]); + } + printf("\n"); + } + + infile.close(); + delete dut; + return 0; +} diff --git a/sync_rtl/poly_arith/poly_arith_sync.v b/sync_rtl/poly_arith/poly_arith_sync.v new file mode 100644 index 0000000..4889265 --- /dev/null +++ b/sync_rtl/poly_arith/poly_arith_sync.v @@ -0,0 +1,61 @@ +// poly_arith_sync.v - Polynomial modular add/sub for ML-KEM +// +// Performs element-wise modular addition or subtraction on two streaming +// 256-coefficient polynomials over Z_q (q=3329). +// +// mode=0: coeff_out = (coeff_a_in + coeff_b_in) mod Q +// mode=1: coeff_out = (coeff_a_in - coeff_b_in) mod Q +// +// Each cycle processes one coefficient pair. Pure streaming — no internal +// storage; the caller sequences all 256 coefficients in order. + +`include "sync_rtl/common/defines.vh" + +module poly_arith_sync ( + input clk, + input rst_n, + input [11:0] coeff_a_in, + input [11:0] coeff_b_in, + input mode, // 0=add, 1=sub + input valid_i, + output ready_o, + output [11:0] coeff_out, + output valid_o, + input ready_i +); + + //-------------------------------------------------------------- + // Combinational modular arithmetic + //-------------------------------------------------------------- + wire [12:0] add_raw; // a + b (13-bit to catch overflow) + wire [11:0] add_sub_q; // (a + b) - Q + wire [11:0] add_result; // (a + b) mod Q + + assign add_raw = {1'b0, coeff_a_in} + {1'b0, coeff_b_in}; + assign add_sub_q = add_raw[11:0] - `Q; + assign add_result = (add_raw < `Q) ? add_raw[11:0] : add_sub_q; + + wire [11:0] sub_result; // (a - b) mod Q + // When a >= b: diff = a - b (already in [0, Q-2]) + // When a < b: diff = a + Q - b (borrow correction) + assign sub_result = (coeff_a_in < coeff_b_in) + ? (coeff_a_in + `Q - coeff_b_in) + : (coeff_a_in - coeff_b_in); + + wire [11:0] mod_result = mode ? sub_result : add_result; + + //-------------------------------------------------------------- + // Pipeline the result through pipeline_reg for valid/ready + //-------------------------------------------------------------- + pipeline_reg #(.DW(12)) u_pipe ( + .clk (clk), + .rst_n (rst_n), + .data_i (mod_result), + .valid_i(valid_i), + .ready_o(ready_o), + .data_o (coeff_out), + .valid_o(valid_o), + .ready_i(ready_i) + ); + +endmodule diff --git a/test_framework/modules/poly_arith/gen_vectors.py b/test_framework/modules/poly_arith/gen_vectors.py new file mode 100644 index 0000000..6a13e57 --- /dev/null +++ b/test_framework/modules/poly_arith/gen_vectors.py @@ -0,0 +1,108 @@ +"""gen_vectors.py - Test vector generator for poly_arith module. + +Generates streaming 256-coefficient polynomial add/sub test vectors. +Each vector: "MODE A0..A255 B0..B255" (513 space-separated hex values). +Expected: "C0 C1 ... C255" (256 space-separated hex values). +""" + +import os +import random +import sys + +# Add test_framework/lib to path for VectorGenerator base class +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'lib')) + +from vector_gen import VectorGenerator + +Q = 3329 # ML-KEM prime modulus +N = 256 # polynomial degree + + +class PolyArithVectorGenerator(VectorGenerator): + """Generates test vectors for the poly_arith_sync module.""" + + def generate_one(self, params: dict) -> dict: + """Generate one polynomial add or sub vector. + + Generates 256 random coefficients for A and B, computes expected + result as (A[i] +/- B[i]) mod Q. + + Args: + params: dict with optional 'mode' key ('add' or 'sub'). + + Returns: + dict with 'input' and 'expected' keys. + """ + mode = params.get('mode', 'add') + + A = [random.randint(0, Q - 1) for _ in range(N)] + B = [random.randint(0, Q - 1) for _ in range(N)] + + if mode == 'sub': + expected = [(a - b) % Q for a, b in zip(A, B)] + else: + expected = [(a + b) % Q for a, b in zip(A, B)] + + return { + 'input': {'mode': mode, 'A': A, 'B': B}, + 'expected': {'C': expected} + } + + def write_hex_file(self, vectors: list[dict], filepath: str) -> None: + """Write input vectors as hex file. + + Format: "MODE A0 A1 ... A255 B0 B1 ... B255" + MODE is 'A' for add, 'S' for sub. + + Args: + vectors: List of vector dicts from generate_one(). + filepath: Path to write the hex file. + """ + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, 'w') as f: + for v in vectors: + inp = v['input'] + mode_char = 'A' if inp['mode'] == 'add' else 'S' + A_hex = ' '.join(f'{c:03X}' for c in inp['A']) + B_hex = ' '.join(f'{c:03X}' for c in inp['B']) + f.write(f'{mode_char} {A_hex} {B_hex}\n') + + def write_expected_file(self, vectors: list[dict], filepath: str) -> None: + """Write expected results as hex file. + + Format: "C0 C1 ... C255" (one space-separated line per vector). + + Args: + vectors: List of vector dicts from generate_one(). + filepath: Path to write the expected hex file. + """ + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, 'w') as f: + for v in vectors: + C = v['expected']['C'] + C_hex = ' '.join(f'{c:03X}' for c in C) + f.write(f'{C_hex}\n') + + def compare_results(self, got: list[str], expected_file: str) -> bool: + """Compare RTL output against expected values. + + Args: + got: List of result strings from simulation. + Each is " C0 C1 ... C255" after "RESULT:" prefix. + expected_file: Path to expected hex file. + + Returns: + bool: True if all results match. + """ + with open(expected_file, 'r') as f: + expected = [line.strip() for line in f + if line.strip() and not line.startswith('#')] + + if len(got) != len(expected): + return False + + for i, (g, e) in enumerate(zip(got, expected)): + if g.strip() != e.strip(): + return False + + return True diff --git a/test_framework/modules/poly_arith/test_plan.json b/test_framework/modules/poly_arith/test_plan.json new file mode 100644 index 0000000..4533956 --- /dev/null +++ b/test_framework/modules/poly_arith/test_plan.json @@ -0,0 +1,24 @@ +{ + "module": "poly_arith", + "rtl_top": "sync_rtl/poly_arith/poly_arith_sync.v", + "rtl_deps": ["sync_rtl/common/pipeline_reg.v"], + "tb_cpp": "sync_rtl/poly_arith/TB/tb_poly_arith.cpp", + "simulator": "verilator", + "timeout_s": 30, + "cases": [ + { + "id": "add", + "description": "Polynomial modular addition: (A[i] + B[i]) mod Q for 256 coeffs, 5 random vectors", + "params": {"mode": "add"}, + "num_vectors": 5, + "tolerance": "bit_exact" + }, + { + "id": "sub", + "description": "Polynomial modular subtraction: (A[i] - B[i]) mod Q for 256 coeffs, 5 random vectors", + "params": {"mode": "sub"}, + "num_vectors": 5, + "tolerance": "bit_exact" + } + ] +}