feat: add test vector generation for RTL verification
Improve generate_test_vectors() to use mixed SNR levels (high SNR for first half, nominal for second half) ensuring a mix of converged and non-converged test cases. Add gen_firmware_vectors.py converter that reads test_vectors.json and produces packed LLR data matching the RTL wishbone interface format (5 LLRs per 32-bit word, 6-bit two's complement). Generated 20 vectors: 11 converged, 9 non-converged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
22162
data/test_vectors.json
Normal file
22162
data/test_vectors.json
Normal file
File diff suppressed because it is too large
Load Diff
288
model/gen_firmware_vectors.py
Normal file
288
model/gen_firmware_vectors.py
Normal file
@@ -0,0 +1,288 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate test vector files for cocotb and firmware from the Python model output.
|
||||
|
||||
Reads data/test_vectors.json and produces:
|
||||
1. chip_ignite/verilog/dv/cocotb/ldpc_tests/test_data.py (cocotb Python module)
|
||||
2. chip_ignite/firmware/ldpc_demo/test_vectors.h (C header for PicoRV32)
|
||||
|
||||
LLR packing format (matches wishbone_interface.sv):
|
||||
Each 32-bit word holds 5 LLRs, 6 bits each, in two's complement.
|
||||
Word[i] bits [5:0] = LLR[5*i+0]
|
||||
Word[i] bits [11:6] = LLR[5*i+1]
|
||||
Word[i] bits [17:12] = LLR[5*i+2]
|
||||
Word[i] bits [23:18] = LLR[5*i+3]
|
||||
Word[i] bits [29:24] = LLR[5*i+4]
|
||||
52 words cover 260 LLRs (256 used, last 4 are zero-padded).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Paths relative to this script's directory
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
|
||||
INPUT_FILE = os.path.join(PROJECT_DIR, 'data', 'test_vectors.json')
|
||||
COCOTB_OUTPUT = os.path.join(PROJECT_DIR, 'chip_ignite', 'verilog', 'dv', 'cocotb',
|
||||
'ldpc_tests', 'test_data.py')
|
||||
FIRMWARE_OUTPUT = os.path.join(PROJECT_DIR, 'chip_ignite', 'firmware', 'ldpc_demo',
|
||||
'test_vectors.h')
|
||||
|
||||
Q_BITS = 6
|
||||
LLRS_PER_WORD = 5
|
||||
N_LLR = 256
|
||||
N_WORDS = (N_LLR + LLRS_PER_WORD - 1) // LLRS_PER_WORD # 52
|
||||
K = 32
|
||||
|
||||
|
||||
def signed_to_twos_complement(val, bits=Q_BITS):
|
||||
"""Convert signed integer to two's complement unsigned representation."""
|
||||
if val < 0:
|
||||
return val + (1 << bits)
|
||||
return val & ((1 << bits) - 1)
|
||||
|
||||
|
||||
def pack_llr_words(llr_quantized):
|
||||
"""
|
||||
Pack 256 signed LLRs into 52 uint32 words.
|
||||
|
||||
Each word contains 5 LLRs, 6 bits each:
|
||||
bits[5:0] = LLR[5*word + 0]
|
||||
bits[11:6] = LLR[5*word + 1]
|
||||
bits[17:12] = LLR[5*word + 2]
|
||||
bits[23:18] = LLR[5*word + 3]
|
||||
bits[29:24] = LLR[5*word + 4]
|
||||
"""
|
||||
# Pad to 260 entries (52 * 5)
|
||||
padded = list(llr_quantized) + [0] * (N_WORDS * LLRS_PER_WORD - N_LLR)
|
||||
|
||||
words = []
|
||||
for w in range(N_WORDS):
|
||||
word = 0
|
||||
for p in range(LLRS_PER_WORD):
|
||||
llr_idx = w * LLRS_PER_WORD + p
|
||||
tc = signed_to_twos_complement(padded[llr_idx])
|
||||
word |= (tc & 0x3F) << (p * Q_BITS)
|
||||
words.append(word)
|
||||
return words
|
||||
|
||||
|
||||
def bits_to_uint32(bits):
|
||||
"""Convert a list of 32 binary values to a single uint32 (bit 0 = LSB)."""
|
||||
val = 0
|
||||
for i, b in enumerate(bits):
|
||||
if b:
|
||||
val |= (1 << i)
|
||||
return val
|
||||
|
||||
|
||||
def generate_cocotb_test_data(vectors):
|
||||
"""Generate Python module for cocotb tests."""
|
||||
lines = []
|
||||
lines.append('"""')
|
||||
lines.append('Auto-generated test vector data for LDPC decoder cocotb tests.')
|
||||
lines.append('Generated by model/gen_firmware_vectors.py')
|
||||
lines.append('')
|
||||
lines.append('LLR packing: 5 LLRs per 32-bit word, 6 bits each (two\'s complement)')
|
||||
lines.append(' Word bits [5:0] = LLR[5*i+0]')
|
||||
lines.append(' Word bits [11:6] = LLR[5*i+1]')
|
||||
lines.append(' Word bits [17:12] = LLR[5*i+2]')
|
||||
lines.append(' Word bits [23:18] = LLR[5*i+3]')
|
||||
lines.append(' Word bits [29:24] = LLR[5*i+4]')
|
||||
lines.append('"""')
|
||||
lines.append('')
|
||||
lines.append(f'# Number of test vectors')
|
||||
lines.append(f'NUM_VECTORS = {len(vectors)}')
|
||||
lines.append(f'LLR_WORDS_PER_VECTOR = {N_WORDS}')
|
||||
lines.append('')
|
||||
lines.append('# Wishbone register offsets (byte-addressed)')
|
||||
lines.append('REG_CTRL = 0x00')
|
||||
lines.append('REG_STATUS = 0x04')
|
||||
lines.append('REG_LLR_BASE = 0x10 # 52 words: 0x10, 0x14, ..., 0xDC')
|
||||
lines.append('REG_DECODED = 0x50')
|
||||
lines.append('REG_VERSION = 0x54')
|
||||
lines.append('')
|
||||
lines.append('')
|
||||
lines.append('TEST_VECTORS = [')
|
||||
|
||||
for vec in vectors:
|
||||
llr_words = pack_llr_words(vec['llr_quantized'])
|
||||
decoded_word = bits_to_uint32(vec['decoded_bits'])
|
||||
|
||||
lines.append(' {')
|
||||
lines.append(f' \'index\': {vec["index"]},')
|
||||
|
||||
# Format LLR words as hex, 8 per line
|
||||
lines.append(f' \'llr_words\': [')
|
||||
for chunk_start in range(0, len(llr_words), 8):
|
||||
chunk = llr_words[chunk_start:chunk_start + 8]
|
||||
hex_str = ', '.join(f'0x{w:08X}' for w in chunk)
|
||||
comma = ',' if chunk_start + 8 < len(llr_words) else ''
|
||||
lines.append(f' {hex_str}{comma}')
|
||||
lines.append(f' ],')
|
||||
|
||||
lines.append(f' \'decoded_word\': 0x{decoded_word:08X},')
|
||||
lines.append(f' \'info_bits\': {vec["info_bits"]},')
|
||||
lines.append(f' \'converged\': {vec["converged"]},')
|
||||
lines.append(f' \'iterations\': {vec["iterations"]},')
|
||||
lines.append(f' \'syndrome_weight\': {vec["syndrome_weight"]},')
|
||||
lines.append(f' \'bit_errors\': {vec["bit_errors"]},')
|
||||
lines.append(' },')
|
||||
|
||||
lines.append(']')
|
||||
lines.append('')
|
||||
lines.append('')
|
||||
lines.append('def get_converged_vectors():')
|
||||
lines.append(' """Return only vectors that converged (for positive testing)."""')
|
||||
lines.append(' return [v for v in TEST_VECTORS if v[\'converged\']]')
|
||||
lines.append('')
|
||||
lines.append('')
|
||||
lines.append('def get_failed_vectors():')
|
||||
lines.append(' """Return only vectors that did not converge (for negative testing)."""')
|
||||
lines.append(' return [v for v in TEST_VECTORS if not v[\'converged\']]')
|
||||
lines.append('')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def generate_firmware_header(vectors):
|
||||
"""Generate C header for PicoRV32 firmware."""
|
||||
lines = []
|
||||
lines.append('/*')
|
||||
lines.append(' * Auto-generated test vectors for LDPC decoder firmware')
|
||||
lines.append(' * Generated by model/gen_firmware_vectors.py')
|
||||
lines.append(' *')
|
||||
lines.append(' * LLR packing: 5 LLRs per 32-bit word, 6 bits each (two\'s complement)')
|
||||
lines.append(' * Word bits [5:0] = LLR[5*i+0]')
|
||||
lines.append(' * Word bits [11:6] = LLR[5*i+1]')
|
||||
lines.append(' * Word bits [17:12] = LLR[5*i+2]')
|
||||
lines.append(' * Word bits [23:18] = LLR[5*i+3]')
|
||||
lines.append(' * Word bits [29:24] = LLR[5*i+4]')
|
||||
lines.append(' */')
|
||||
lines.append('')
|
||||
lines.append('#ifndef TEST_VECTORS_H')
|
||||
lines.append('#define TEST_VECTORS_H')
|
||||
lines.append('')
|
||||
lines.append('#include <stdint.h>')
|
||||
lines.append('')
|
||||
lines.append(f'#define NUM_TEST_VECTORS {len(vectors)}')
|
||||
lines.append(f'#define LLR_WORDS_PER_VECTOR {N_WORDS}')
|
||||
lines.append('')
|
||||
|
||||
# Generate per-vector arrays
|
||||
for vec in vectors:
|
||||
idx = vec['index']
|
||||
llr_words = pack_llr_words(vec['llr_quantized'])
|
||||
decoded_word = bits_to_uint32(vec['decoded_bits'])
|
||||
|
||||
lines.append(f'/* Vector {idx}: converged={vec["converged"]}, '
|
||||
f'iterations={vec["iterations"]}, '
|
||||
f'syndrome_weight={vec["syndrome_weight"]}, '
|
||||
f'bit_errors={vec["bit_errors"]} */')
|
||||
lines.append(f'static const uint32_t tv{idx}_llr[{N_WORDS}] = {{')
|
||||
for chunk_start in range(0, len(llr_words), 4):
|
||||
chunk = llr_words[chunk_start:chunk_start + 4]
|
||||
hex_str = ', '.join(f'0x{w:08X}' for w in chunk)
|
||||
comma = ',' if chunk_start + 4 < len(llr_words) else ''
|
||||
lines.append(f' {hex_str}{comma}')
|
||||
lines.append('};')
|
||||
lines.append(f'static const uint32_t tv{idx}_decoded = 0x{decoded_word:08X};')
|
||||
lines.append(f'static const int tv{idx}_converged = {1 if vec["converged"] else 0};')
|
||||
lines.append(f'static const int tv{idx}_iterations = {vec["iterations"]};')
|
||||
lines.append(f'static const int tv{idx}_syndrome_weight = {vec["syndrome_weight"]};')
|
||||
lines.append('')
|
||||
|
||||
# Generate array-of-pointers for easy iteration
|
||||
lines.append('/* Array of LLR pointers for iteration */')
|
||||
lines.append(f'static const uint32_t * const tv_llr[NUM_TEST_VECTORS] = {{')
|
||||
for i, vec in enumerate(vectors):
|
||||
comma = ',' if i < len(vectors) - 1 else ''
|
||||
lines.append(f' tv{vec["index"]}_llr{comma}')
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
|
||||
lines.append(f'static const uint32_t tv_decoded[NUM_TEST_VECTORS] = {{')
|
||||
for i, vec in enumerate(vectors):
|
||||
decoded_word = bits_to_uint32(vec['decoded_bits'])
|
||||
comma = ',' if i < len(vectors) - 1 else ''
|
||||
lines.append(f' 0x{decoded_word:08X}{comma} /* tv{vec["index"]} */')
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
|
||||
lines.append(f'static const int tv_converged[NUM_TEST_VECTORS] = {{')
|
||||
vals = ', '.join(str(1 if v['converged'] else 0) for v in vectors)
|
||||
lines.append(f' {vals}')
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
|
||||
lines.append(f'static const int tv_iterations[NUM_TEST_VECTORS] = {{')
|
||||
vals = ', '.join(str(v['iterations']) for v in vectors)
|
||||
lines.append(f' {vals}')
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
|
||||
lines.append(f'static const int tv_syndrome_weight[NUM_TEST_VECTORS] = {{')
|
||||
vals = ', '.join(str(v['syndrome_weight']) for v in vectors)
|
||||
lines.append(f' {vals}')
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
|
||||
lines.append('#endif /* TEST_VECTORS_H */')
|
||||
lines.append('')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
# Load test vectors
|
||||
print(f'Reading {INPUT_FILE}...')
|
||||
with open(INPUT_FILE) as f:
|
||||
vectors = json.load(f)
|
||||
print(f' Loaded {len(vectors)} vectors')
|
||||
converged = sum(1 for v in vectors if v['converged'])
|
||||
print(f' Converged: {converged}/{len(vectors)}')
|
||||
|
||||
# Generate cocotb test data
|
||||
cocotb_content = generate_cocotb_test_data(vectors)
|
||||
os.makedirs(os.path.dirname(COCOTB_OUTPUT), exist_ok=True)
|
||||
with open(COCOTB_OUTPUT, 'w') as f:
|
||||
f.write(cocotb_content)
|
||||
print(f' Wrote {COCOTB_OUTPUT}')
|
||||
|
||||
# Generate firmware header
|
||||
firmware_content = generate_firmware_header(vectors)
|
||||
os.makedirs(os.path.dirname(FIRMWARE_OUTPUT), exist_ok=True)
|
||||
with open(FIRMWARE_OUTPUT, 'w') as f:
|
||||
f.write(firmware_content)
|
||||
print(f' Wrote {FIRMWARE_OUTPUT}')
|
||||
|
||||
# Verify: check roundtrip of LLR packing
|
||||
print('\nVerifying LLR packing roundtrip...')
|
||||
for vec in vectors:
|
||||
llr_q = vec['llr_quantized']
|
||||
words = pack_llr_words(llr_q)
|
||||
# Unpack and compare
|
||||
for w_idx, word in enumerate(words):
|
||||
for p in range(LLRS_PER_WORD):
|
||||
llr_idx = w_idx * LLRS_PER_WORD + p
|
||||
if llr_idx >= N_LLR:
|
||||
break
|
||||
tc_val = (word >> (p * Q_BITS)) & 0x3F
|
||||
# Convert back to signed
|
||||
if tc_val >= 32:
|
||||
signed_val = tc_val - 64
|
||||
else:
|
||||
signed_val = tc_val
|
||||
expected = llr_q[llr_idx]
|
||||
assert signed_val == expected, (
|
||||
f'Vec {vec["index"]}, LLR[{llr_idx}]: '
|
||||
f'packed={signed_val}, expected={expected}'
|
||||
)
|
||||
print(' LLR packing roundtrip OK for all vectors')
|
||||
|
||||
print('\nDone.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -397,14 +397,26 @@ def run_ber_simulation(lam_s_db_range, lam_b=0.1, n_frames=1000, max_iter=30):
|
||||
|
||||
|
||||
def generate_test_vectors(n_vectors=10, lam_s=2.0, lam_b=0.1, max_iter=30):
|
||||
"""Generate test vectors for RTL verification."""
|
||||
"""
|
||||
Generate test vectors for RTL verification.
|
||||
|
||||
Uses a mix of signal levels to ensure we get both converged and
|
||||
non-converged vectors. First half uses high SNR (lam_s * 3) for
|
||||
reliable convergence, then uses the specified lam_s for realistic
|
||||
channel conditions.
|
||||
"""
|
||||
H = build_full_h_matrix()
|
||||
vectors = []
|
||||
|
||||
# Use high SNR for first half to guarantee converged vectors
|
||||
n_high_snr = n_vectors // 2
|
||||
lam_schedule = [lam_s * 3.0] * n_high_snr + [lam_s] * (n_vectors - n_high_snr)
|
||||
|
||||
for i in range(n_vectors):
|
||||
info = np.random.randint(0, 2, K)
|
||||
codeword = ldpc_encode(info, H)
|
||||
llr_float, photons = poisson_channel(codeword, lam_s, lam_b)
|
||||
cur_lam_s = lam_schedule[i]
|
||||
llr_float, photons = poisson_channel(codeword, cur_lam_s, lam_b)
|
||||
llr_q = quantize_llr(llr_float)
|
||||
decoded, converged, iters, syn_wt = decode_layered_min_sum(llr_q, max_iter)
|
||||
|
||||
@@ -420,10 +432,11 @@ def generate_test_vectors(n_vectors=10, lam_s=2.0, lam_b=0.1, max_iter=30):
|
||||
'iterations': iters,
|
||||
'syndrome_weight': syn_wt,
|
||||
'bit_errors': int(np.sum(decoded != info)),
|
||||
'lam_s': cur_lam_s,
|
||||
}
|
||||
vectors.append(vec)
|
||||
status = "PASS" if np.array_equal(decoded, info) else f"FAIL ({vec['bit_errors']} errs)"
|
||||
print(f" Vector {i}: {status} (iter={iters}, converged={converged})")
|
||||
print(f" Vector {i}: {status} (lam_s={cur_lam_s:.1f}, iter={iters}, converged={converged})")
|
||||
|
||||
return vectors
|
||||
|
||||
|
||||
Reference in New Issue
Block a user