test(cosim): T-7 strict MF thresholds + T-8 doppler 32->48 (3 sub-frames)

T-7 (compare_mf.py): replace "energy ratio 0.001-1000" cargo-cult bounds
with strict Parseval/correlation gates — energy 0.95-1.05, mag_corr >=
0.95, peak_overlap_10 >= 0.90, corr_i/corr_q >= 0.90. All four MF cosim
scenarios still pass (energy=1.000 mag_corr=1.000 peak=1.000) but the
script now bites on any drift instead of rubber-stamping.

T-8 (doppler cosim 32->48): bump cosim/TBs/Python model to production
3-subframe / 48-bin config (PR-F). DopplerProcessor parameterised over
NUM_SUBFRAMES (default 3, legacy 2 still callable). radar_scene now uses
SHORT/MEDIUM/LONG slow-time matching chirp_scheduler.v. Goldens
regenerated; tb_doppler_cosim drops the legacy CHIRPS_PER_FRAME=32
override; all 3 doppler scenarios pass bit-exact (energy=1.0000
peak_agree=1.000 mag_corr=1.000) at production config.

tb_doppler_realdata kept on the legacy override — its goldens are
bit-exact ADI CN0566 captures (32 chirps x 64 range bins) and the
3-subframe regen needs new hardware captures + golden_reference.py
rewrite, deferred to PR-I.

Full regression: 37/41 (same 4 pre-existing T-2..T-5 failures, no new
regressions).
This commit is contained in:
Jason
2026-05-01 11:49:28 +05:45
parent 58792d0e7d
commit b7a841a32c
16 changed files with 95141 additions and 21379 deletions
+16 -17
View File
@@ -33,9 +33,9 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Configuration # Configuration
# ============================================================================= # =============================================================================
DOPPLER_FFT = 32 DOPPLER_FFT = 48 # 3 sub-frames x 16-pt FFT (PR-F)
RANGE_BINS = 512 RANGE_BINS = 512
TOTAL_OUTPUTS = RANGE_BINS * DOPPLER_FFT # 16384 TOTAL_OUTPUTS = RANGE_BINS * DOPPLER_FFT # 24576
SUBFRAME_SIZE = 16 SUBFRAME_SIZE = 16
SCENARIOS = { SCENARIOS = {
@@ -246,7 +246,7 @@ def compare_scenario(name, config, base_dir):
# ---- Pass/Fail ---- # ---- Pass/Fail ----
checks = [] checks = []
checks.append(('RTL output count == 16384', count_ok)) checks.append((f'RTL output count == {TOTAL_OUTPUTS}', count_ok))
energy_ok = (ENERGY_RATIO_MIN < energy_ratio < ENERGY_RATIO_MAX) energy_ok = (ENERGY_RATIO_MIN < energy_ratio < ENERGY_RATIO_MAX)
checks.append((f'Energy ratio in bounds ' checks.append((f'Energy ratio in bounds '
@@ -316,23 +316,22 @@ def main():
for name in run_scenarios: for name in run_scenarios:
passed, result = compare_scenario(name, SCENARIOS[name], base_dir) passed, result = compare_scenario(name, SCENARIOS[name], base_dir)
results.append((name, passed, result)) results.append((name, passed, result))
# Summary
all_pass = True
for _name, passed, result in results:
if not result: if not result:
all_pass = False print(f'[FAIL] doppler {name}: missing input/output CSV')
elif passed:
print(f'[PASS] doppler {name}: '
f'count={result["rtl_count"]} '
f'energy={result["energy_ratio"]:.4f} '
f'peak_agree={result["peak_agreement"]:.3f} '
f'mag_corr={result["avg_mag_corr"]:.3f}')
else: else:
if not passed: print(f'[FAIL] doppler {name}: '
all_pass = False f'count={result["rtl_count"]} '
f'energy={result["energy_ratio"]:.4f} '
if all_pass: f'peak_agree={result["peak_agreement"]:.3f} '
pass f'mag_corr={result["avg_mag_corr"]:.3f}')
else:
pass
all_pass = all(p and r for _, p, r in results)
sys.exit(0 if all_pass else 1) sys.exit(0 if all_pass else 1)
+47 -43
View File
@@ -61,14 +61,17 @@ SCENARIOS = {
}, },
} }
# Thresholds for pass/fail # Pass/fail thresholds (PR-Tests-1 / T-7).
# These are generous because of the fundamental twiddle arithmetic differences # Two correct FFTs of identical input must be Parseval-bounded (energy
# between the SIMULATION branch (float twiddles) and Python model (fixed twiddles) # matches), and the same dominant tones imply high spectral / I-Q correlation.
ENERGY_CORR_MIN = 0.80 # Min correlation of magnitude spectra # SIMULATION-mode runs use float twiddles vs the Python fixed-point reference,
TOP_PEAK_OVERLAP_MIN = 0.50 # At least 50% of top-N peaks must overlap # so a small gain offset is expected — but anything outside these bars
RMS_RATIO_MAX = 50.0 # Max ratio of RMS energies (generous, since gain differs) # indicates real drift between the behavioral and synthesis paths.
ENERGY_RATIO_MIN = 0.001 # Min ratio (total energy RTL / total energy Python) ENERGY_RATIO_MIN = 0.95
ENERGY_RATIO_MAX = 1000.0 # Max ratio ENERGY_RATIO_MAX = 1.05
MAG_CORR_MIN = 0.95 # Pearson correlation of L2 magnitude spectra
PEAK_OVERLAP_MIN = 0.90 # Top-10 spectral-peak overlap fraction
IQ_CORR_MIN = 0.90 # Pearson correlation on I and Q channels
# ============================================================================= # =============================================================================
@@ -221,41 +224,41 @@ def compare_scenario(scenario_name, config, base_dir):
# ---- Pass/Fail Decision ---- # ---- Pass/Fail Decision ----
# The SIMULATION branch uses floating-point twiddles ($cos/$sin) while # Strict (PR-Tests-1 / T-7). Two correct FFTs of the same input must be
# the Python model uses the fixed-point twiddle ROM (matching synthesis). # Parseval-bounded and share dominant tones; we now gate on energy ratio,
# These are fundamentally different FFT implementations. We do NOT expect # spectral correlation, top-N peak overlap, and per-channel I/Q
# structural similarity (correlation, peak overlap) between them. # correlation — not just "did the state machine reach $finish".
#
# What we CAN verify:
# 1. Both produce non-trivial output (state machine completes)
# 2. Output count is correct (1024 samples)
# 3. Energy is in a reasonable range (not wildly wrong)
#
# The true bit-accuracy comparison will happen when the synthesis branch
# is simulated (xsim on remote server) using the same fft_engine.v that
# the Python model was built to match.
checks = [] checks = []
# Check 1: Both produce output
both_have_output = py_energy > 0 and rtl_energy > 0 both_have_output = py_energy > 0 and rtl_energy > 0
checks.append(('Both produce output', both_have_output)) checks.append(('Both produce output', both_have_output))
# Check 2: RTL produced expected sample count
correct_count = len(rtl_i) == FFT_SIZE correct_count = len(rtl_i) == FFT_SIZE
checks.append(('Correct output count (2048)', correct_count)) checks.append(('Correct output count (2048)', correct_count))
# Check 3: Energy ratio within generous bounds energy_ok = ENERGY_RATIO_MIN <= energy_ratio <= ENERGY_RATIO_MAX
# Allow very wide range since twiddle differences cause large gain variation checks.append((f'Energy ratio in [{ENERGY_RATIO_MIN},{ENERGY_RATIO_MAX}] '
energy_ok = ENERGY_RATIO_MIN < energy_ratio < ENERGY_RATIO_MAX f'(actual={energy_ratio:.3f})', energy_ok))
checks.append((f'Energy ratio in bounds ({ENERGY_RATIO_MIN}-{ENERGY_RATIO_MAX})',
energy_ok)) mag_corr_ok = mag_corr >= MAG_CORR_MIN
checks.append((f'mag_corr >= {MAG_CORR_MIN} (actual={mag_corr:.3f})',
mag_corr_ok))
peak_overlap_ok = peak_overlap_10 >= PEAK_OVERLAP_MIN
checks.append((f'peak_overlap_10 >= {PEAK_OVERLAP_MIN} '
f'(actual={peak_overlap_10:.3f})', peak_overlap_ok))
iq_corr_ok = corr_i >= IQ_CORR_MIN and corr_q >= IQ_CORR_MIN
checks.append((f'corr_i,corr_q >= {IQ_CORR_MIN} '
f'(actual={corr_i:.3f}/{corr_q:.3f})', iq_corr_ok))
# Print checks
all_pass = True all_pass = True
failed_names = []
for _name, passed in checks: for _name, passed in checks:
if not passed: if not passed:
all_pass = False all_pass = False
failed_names.append(_name)
result = { result = {
'scenario': scenario_name, 'scenario': scenario_name,
@@ -271,6 +274,7 @@ def compare_scenario(scenario_name, config, base_dir):
'corr_i': corr_i, 'corr_i': corr_i,
'corr_q': corr_q, 'corr_q': corr_q,
'passed': all_pass, 'passed': all_pass,
'failed_checks': failed_names,
} }
# Write detailed comparison CSV # Write detailed comparison CSV
@@ -306,23 +310,23 @@ def main():
for name in run_scenarios: for name in run_scenarios:
passed, result = compare_scenario(name, SCENARIOS[name], base_dir) passed, result = compare_scenario(name, SCENARIOS[name], base_dir)
results.append((name, passed, result)) results.append((name, passed, result))
# Summary
all_pass = True
for _name, passed, result in results:
if not result: if not result:
all_pass = False print(f'[FAIL] mf {name}: missing input/output CSV')
elif passed:
print(f'[PASS] mf {name}: '
f'energy={result["energy_ratio"]:.3f} '
f'mag_corr={result["mag_corr"]:.3f} '
f'peak10={result["peak_overlap_10"]:.3f} '
f'corr_i={result["corr_i"]:.3f} corr_q={result["corr_q"]:.3f}')
else: else:
if not passed: print(f'[FAIL] mf {name}: '
all_pass = False f'energy={result["energy_ratio"]:.3f} '
f'mag_corr={result["mag_corr"]:.3f} '
if all_pass: f'peak10={result["peak_overlap_10"]:.3f} '
pass f'corr_i={result["corr_i"]:.3f} corr_q={result["corr_q"]:.3f} '
else: f'-> {result["failed_checks"]}')
pass
all_pass = all(p and r for _, p, r in results)
sys.exit(0 if all_pass else 1) sys.exit(0 if all_pass else 1)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+33 -21
View File
@@ -1088,29 +1088,44 @@ HAMMING_WINDOW = [
class DopplerProcessor: class DopplerProcessor:
""" """
Bit-accurate model of doppler_processor_optimized.v (dual 16-pt FFT architecture). Bit-accurate model of doppler_processor_optimized.v (per-subframe 16-pt FFT).
The staggered-PRF frame has 32 chirps total: PR-F: 48 chirps total, 3 sub-frames (SHORT/MEDIUM/LONG):
- Sub-frame 0 (long PRI): chirps 0-15 -> 16-pt Hamming -> 16-pt FFT -> bins 0-15 - Sub-frame 0 (SHORT PRI): chirps 0..15 -> 16-pt Hamming -> 16-pt FFT -> bins 0..15
- Sub-frame 1 (short PRI): chirps 16-31 -> 16-pt Hamming -> 16-pt FFT -> bins 16-31 - Sub-frame 1 (MEDIUM PRI): chirps 16..31 -> 16-pt Hamming -> 16-pt FFT -> bins 16..31
- Sub-frame 2 (LONG PRI): chirps 32..47 -> 16-pt Hamming -> 16-pt FFT -> bins 32..47
Output: doppler_bin[4:0] = {sub_frame_id, bin_in_subframe[3:0]} Output: doppler_bin[5:0] = {sub_frame_id[1:0], bin_in_subframe[3:0]}
Total output per range bin: 32 bins (16 + 16), same interface as before. Total output per range bin: 48 bins (3 x 16).
NUM_SUBFRAMES is parameterised so legacy 2-subframe call sites still work
(set CHIRPS_PER_FRAME=32, NUM_SUBFRAMES=2). Default tracks production.
""" """
DOPPLER_FFT_SIZE = 16 # Per sub-frame DOPPLER_FFT_SIZE = 16 # Per sub-frame
RANGE_BINS = 512 RANGE_BINS = 512
CHIRPS_PER_FRAME = 32
CHIRPS_PER_SUBFRAME = 16 CHIRPS_PER_SUBFRAME = 16
NUM_SUBFRAMES = 3
CHIRPS_PER_FRAME = NUM_SUBFRAMES * CHIRPS_PER_SUBFRAME # 48
def __init__(self, twiddle_file_16=None): def __init__(self, twiddle_file_16=None, num_subframes=None,
chirps_per_frame=None):
""" """
For 16-point FFT, we need the 16-point twiddle file. For 16-point FFT, we need the 16-point twiddle file.
If not provided, we generate twiddle factors mathematically If not provided, we generate twiddle factors mathematically
(cos(2*pi*k/16) for k=0..3, quarter-wave ROM with 4 entries). (cos(2*pi*k/16) for k=0..3, quarter-wave ROM with 4 entries).
num_subframes / chirps_per_frame override the class defaults for
legacy 2-subframe co-sim or future variants.
""" """
self.fft16 = None self.fft16 = None
self._twiddle_file_16 = twiddle_file_16 self._twiddle_file_16 = twiddle_file_16
if num_subframes is not None:
self.NUM_SUBFRAMES = num_subframes
self.CHIRPS_PER_FRAME = self.NUM_SUBFRAMES * self.CHIRPS_PER_SUBFRAME
if chirps_per_frame is not None:
self.CHIRPS_PER_FRAME = chirps_per_frame
self.NUM_SUBFRAMES = chirps_per_frame // self.CHIRPS_PER_SUBFRAME
@staticmethod @staticmethod
def window_multiply(data_16, window_16): def window_multiply(data_16, window_16):
@@ -1132,20 +1147,20 @@ class DopplerProcessor:
def process_frame(self, chirp_data_i, chirp_data_q): def process_frame(self, chirp_data_i, chirp_data_q):
""" """
Process one complete Doppler frame using dual 16-pt FFTs. Process one complete Doppler frame using NUM_SUBFRAMES x 16-pt FFTs.
Args: Args:
chirp_data_i: 2D array [32 chirps][512 range bins] of signed 16-bit I chirp_data_i: 2D array [CHIRPS_PER_FRAME][RANGE_BINS] of signed 16-bit I
chirp_data_q: 2D array [32 chirps][512 range bins] of signed 16-bit Q chirp_data_q: 2D array [CHIRPS_PER_FRAME][RANGE_BINS] of signed 16-bit Q
Returns: Returns:
(doppler_map_i, doppler_map_q): 2D arrays [512 range bins][32 doppler bins] (doppler_map_i, doppler_map_q): 2D arrays
of signed 16-bit [RANGE_BINS][NUM_SUBFRAMES * DOPPLER_FFT_SIZE] of signed 16-bit.
Bins 0-15 = sub-frame 0 (long PRI) Sub-frame s occupies bins [s*16 .. s*16+15].
Bins 16-31 = sub-frame 1 (short PRI)
""" """
doppler_map_i = [] doppler_map_i = []
doppler_map_q = [] doppler_map_q = []
total_bins = self.NUM_SUBFRAMES * self.DOPPLER_FFT_SIZE
# Generate 16-pt twiddle factors (quarter-wave cos, 4 entries) # Generate 16-pt twiddle factors (quarter-wave cos, 4 entries)
# cos(2*pi*k/16) for k=0..3 # cos(2*pi*k/16) for k=0..3
@@ -1164,12 +1179,11 @@ class DopplerProcessor:
fft16.mem_im = [0] * 16 fft16.mem_im = [0] * 16
for rbin in range(self.RANGE_BINS): for rbin in range(self.RANGE_BINS):
# Output bins for this range bin: 32 total (16 from each sub-frame) out_re = [0] * total_bins
out_re = [0] * 32 out_im = [0] * total_bins
out_im = [0] * 32
# Process each sub-frame independently # Process each sub-frame independently
for sf in range(2): for sf in range(self.NUM_SUBFRAMES):
chirp_start = sf * self.CHIRPS_PER_SUBFRAME chirp_start = sf * self.CHIRPS_PER_SUBFRAME
bin_offset = sf * self.DOPPLER_FFT_SIZE bin_offset = sf * self.DOPPLER_FFT_SIZE
@@ -1188,10 +1202,8 @@ class DopplerProcessor:
fft_in_re.append(win_re) fft_in_re.append(win_re)
fft_in_im.append(win_im) fft_in_im.append(win_im)
# 16-point forward FFT
fft_out_re, fft_out_im = fft16.compute(fft_in_re, fft_in_im, inverse=False) fft_out_re, fft_out_im = fft16.compute(fft_in_re, fft_in_im, inverse=False)
# Pack into output: sub-frame 0 -> bins 0-15, sub-frame 1 -> bins 16-31
for b in range(self.DOPPLER_FFT_SIZE): for b in range(self.DOPPLER_FFT_SIZE):
out_re[bin_offset + b] = fft_out_re[b] out_re[bin_offset + b] = fft_out_re[b]
out_im[bin_offset + b] = fft_out_im[b] out_im[bin_offset + b] = fft_out_im[b]
@@ -3,12 +3,14 @@
Generate Doppler processor co-simulation golden reference data. Generate Doppler processor co-simulation golden reference data.
Uses the bit-accurate Python model (fpga_model.py) to compute the expected Uses the bit-accurate Python model (fpga_model.py) to compute the expected
Doppler FFT output for the dual 16-pt FFT architecture. Also generates the Doppler FFT output for the 3-subframe / 16-pt FFT architecture (PR-F). Also
input hex files consumed by the Verilog testbench (tb_doppler_cosim.v). generates the input hex files consumed by the Verilog testbench
(tb_doppler_cosim.v).
Architecture: Architecture (matches chirp_scheduler.v ordering SHORT, MEDIUM, LONG):
Sub-frame 0 (long PRI): chirps 0-15 -> 16-pt Hamming -> 16-pt FFT -> bins 0-15 Sub-frame 0 (SHORT PRI): chirps 0..15 -> 16-pt Hamming -> 16-pt FFT -> bins 0..15
Sub-frame 1 (short PRI): chirps 16-31 -> 16-pt Hamming -> 16-pt FFT -> bins 16-31 Sub-frame 1 (MEDIUM PRI): chirps 16..31 -> 16-pt Hamming -> 16-pt FFT -> bins 16..31
Sub-frame 2 (LONG PRI): chirps 32..47 -> 16-pt Hamming -> 16-pt FFT -> bins 32..47
Usage: Usage:
cd ~/PLFM_RADAR/9_Firmware/9_2_FPGA/tb/cosim cd ~/PLFM_RADAR/9_Firmware/9_2_FPGA/tb/cosim
@@ -34,10 +36,11 @@ from radar_scene import Target, generate_doppler_frame
# ============================================================================= # =============================================================================
DOPPLER_FFT_SIZE = 16 # Per sub-frame DOPPLER_FFT_SIZE = 16 # Per sub-frame
DOPPLER_TOTAL_BINS = 32 # Total output (2 sub-frames x 16) NUM_SUBFRAMES = 3
DOPPLER_TOTAL_BINS = NUM_SUBFRAMES * DOPPLER_FFT_SIZE # 48
RANGE_BINS = 512 RANGE_BINS = 512
CHIRPS_PER_FRAME = 32 CHIRPS_PER_FRAME = NUM_SUBFRAMES * 16 # 48
TOTAL_SAMPLES = CHIRPS_PER_FRAME * RANGE_BINS # 16384 TOTAL_SAMPLES = CHIRPS_PER_FRAME * RANGE_BINS # 24576
# ============================================================================= # =============================================================================
@@ -87,11 +90,12 @@ def make_scenario_stationary():
def make_scenario_moving(): def make_scenario_moving():
"""Single target with moderate Doppler shift.""" """Single target with moderate Doppler shift."""
# v = 15 m/s fd = 2*v*fc/c 1050 Hz # v = 15 m/s -> fd = 2*v*fc/c ~= 1050 Hz
# Long PRI = 167 us → sub-frame 0 bin = fd * 16 * 167e-6 2.8 → bin ~3 # SHORT PRI 175 us: bin = fd * 16 * 175e-6 ~= 2.94 -> sf0 bin ~3
# Short PRI = 175 us → sub-frame 1 bin = fd * 16 * 175e-6 2.9 bin 16+3 = 19 # MEDIUM PRI 175 us: bin = fd * 16 * 175e-6 ~= 2.94 -> sf1 bin 16+3 = 19
# LONG PRI 167 us: bin = fd * 16 * 167e-6 ~= 2.81 -> sf2 bin 32+3 = 35
targets = [Target(range_m=500, velocity_mps=15.0, rcs_dbsm=20.0)] targets = [Target(range_m=500, velocity_mps=15.0, rcs_dbsm=20.0)]
return targets, "Single moving target v=15m/s (~1050Hz Doppler, sf0 bin~3, sf1 bin~19)" return targets, "Single moving target v=15m/s (~1050Hz Doppler, sf0~3 sf1~19 sf2~35)"
def make_scenario_two_targets(): def make_scenario_two_targets():
@@ -117,7 +121,7 @@ SCENARIOS = {
def generate_scenario(name, targets, description, base_dir): def generate_scenario(name, targets, description, base_dir):
"""Generate input hex + golden output for one scenario.""" """Generate input hex + golden output for one scenario."""
# Generate Doppler frame (32 chirps x 64 range bins) # Generate Doppler frame (48 chirps x RANGE_BINS, 3 sub-frames)
frame_i, frame_q = generate_doppler_frame(targets, seed=42) frame_i, frame_q = generate_doppler_frame(targets, seed=42)
+26 -14
View File
@@ -59,25 +59,28 @@ ADC_BITS = 8 # ADC resolution
T_LONG_CHIRP = 30e-6 # 30 us long chirp duration T_LONG_CHIRP = 30e-6 # 30 us long chirp duration
T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp
T_LISTEN_LONG = 137e-6 # 137 us listening window T_LISTEN_LONG = 137e-6 # 137 us listening window
T_PRI_LONG = 167e-6 # 30 us chirp + 137 us listen T_PRI_LONG = 167e-6 # 30 us chirp + 137 us listen (sub-frame 2: LONG)
T_PRI_SHORT = 175e-6 # staggered short-PRI sub-frame T_PRI_SHORT = 175e-6 # 1 us chirp + 174 us listen (sub-frame 0: SHORT)
T_PRI_MEDIUM = 175e-6 # 5 us chirp + 170 us listen (sub-frame 1: MEDIUM)
N_SAMPLES_LISTEN = int(T_LISTEN_LONG * FS_ADC) # 54800 samples N_SAMPLES_LISTEN = int(T_LISTEN_LONG * FS_ADC) # 54800 samples
# Processing chain # Processing chain
# Updated for 2048-pt range FFT + 4x decimation → 512 range bins per chirp. # Must stay in sync with radar_params.vh: RP_FFT_SIZE=2048, RP_NUM_RANGE_BINS=512,
# Must stay in sync with radar_params.vh: RP_FFT_SIZE=2048, RP_NUM_RANGE_BINS=512. # RP_CHIRPS_PER_FRAME=48, RP_NUM_DOPPLER_BINS=48 (PR-F: 3 sub-frames x 16).
CIC_DECIMATION = 4 CIC_DECIMATION = 4
FFT_SIZE = 2048 FFT_SIZE = 2048
RANGE_BINS = 512 RANGE_BINS = 512
DOPPLER_FFT_SIZE = 16 # Per sub-frame DOPPLER_FFT_SIZE = 16 # Per sub-frame
DOPPLER_TOTAL_BINS = 32 # Total output bins (2 sub-frames x 16) NUM_SUBFRAMES = 3 # SHORT, MEDIUM, LONG
DOPPLER_TOTAL_BINS = NUM_SUBFRAMES * DOPPLER_FFT_SIZE # 48
CHIRPS_PER_SUBFRAME = 16 CHIRPS_PER_SUBFRAME = 16
CHIRPS_PER_FRAME = 32 CHIRPS_PER_FRAME = NUM_SUBFRAMES * CHIRPS_PER_SUBFRAME # 48
# Derived # Derived
RANGE_RESOLUTION = C_LIGHT / (2 * CHIRP_BW) # 7.5 m RANGE_RESOLUTION = C_LIGHT / (2 * CHIRP_BW) # 7.5 m
MAX_UNAMBIGUOUS_RANGE = C_LIGHT * T_LISTEN_LONG / 2 # ~20.55 km MAX_UNAMBIGUOUS_RANGE = C_LIGHT * T_LISTEN_LONG / 2 # ~20.55 km
VELOCITY_RESOLUTION_LONG = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_LONG) VELOCITY_RESOLUTION_LONG = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_LONG)
VELOCITY_RESOLUTION_MEDIUM = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_MEDIUM)
VELOCITY_RESOLUTION_SHORT = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_SHORT) VELOCITY_RESOLUTION_SHORT = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_SHORT)
# Short chirp LUT (60 entries, 8-bit unsigned) # Short chirp LUT (60 entries, 8-bit unsigned)
@@ -378,15 +381,18 @@ def generate_baseband_samples(targets, n_samples_baseband, noise_stddev=0.5,
def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME, def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
n_range_bins=RANGE_BINS, noise_stddev=0.5, seed=42): n_range_bins=RANGE_BINS, noise_stddev=0.5, seed=42):
""" """
Generate a complete Doppler frame (32 chirps x 64 range bins). Generate a complete Doppler frame (48 chirps x N range bins, 3 sub-frames).
Each chirp sees a phase rotation due to target velocity: Each chirp sees a phase rotation due to target velocity:
phase_shift_per_chirp = 2*pi * doppler_hz * T_chirp_repeat phase_shift_per_chirp = 2*pi * doppler_hz * T_chirp_repeat
Sub-frame ordering matches chirp_scheduler.v: 0=SHORT, 1=MEDIUM, 2=LONG.
Slow-time accumulates each chirp's PRI from the start of the frame.
Args: Args:
targets: list of Target objects targets: list of Target objects
n_chirps: chirps per frame (32) n_chirps: chirps per frame (48)
n_range_bins: range bins per chirp (64) n_range_bins: range bins per chirp (default RANGE_BINS=512)
Returns: Returns:
(frame_i, frame_q): [n_chirps][n_range_bins] arrays of signed 16-bit (frame_i, frame_q): [n_chirps][n_range_bins] arrays of signed 16-bit
@@ -425,13 +431,19 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
amp = target.amplitude / 4.0 amp = target.amplitude / 4.0
# Doppler phase for this chirp. # Doppler phase for this chirp.
# The frame uses staggered PRF: chirps 0-15 use the long PRI, # 3-subframe staggered PRF (chirp_scheduler.v ordering):
# chirps 16-31 use the short PRI. # chirps 0..15 -> SHORT PRI
# chirps 16..31 -> MEDIUM PRI (after 16 SHORT)
# chirps 32..47 -> LONG PRI (after 16 SHORT + 16 MEDIUM)
if chirp_idx < CHIRPS_PER_SUBFRAME: if chirp_idx < CHIRPS_PER_SUBFRAME:
slow_time_s = chirp_idx * T_PRI_LONG slow_time_s = chirp_idx * T_PRI_SHORT
elif chirp_idx < 2 * CHIRPS_PER_SUBFRAME:
slow_time_s = (CHIRPS_PER_SUBFRAME * T_PRI_SHORT) + \
((chirp_idx - CHIRPS_PER_SUBFRAME) * T_PRI_MEDIUM)
else: else:
slow_time_s = (CHIRPS_PER_SUBFRAME * T_PRI_LONG) + \ slow_time_s = (CHIRPS_PER_SUBFRAME * T_PRI_SHORT) + \
((chirp_idx - CHIRPS_PER_SUBFRAME) * T_PRI_SHORT) (CHIRPS_PER_SUBFRAME * T_PRI_MEDIUM) + \
((chirp_idx - 2 * CHIRPS_PER_SUBFRAME) * T_PRI_LONG)
doppler_phase = 2 * math.pi * target.doppler_hz * slow_time_s doppler_phase = 2 * math.pi * target.doppler_hz * slow_time_s
total_phase = doppler_phase + target.phase_deg * math.pi / 180.0 total_phase = doppler_phase + target.phase_deg * math.pi / 180.0
+26 -26
View File
@@ -5,9 +5,9 @@
* Co-simulation testbench for doppler_processor_optimized (doppler_processor.v). * Co-simulation testbench for doppler_processor_optimized (doppler_processor.v).
* *
* Tests the complete Doppler processing pipeline: * Tests the complete Doppler processing pipeline:
* - Accumulates 32 chirps x 512 range bins into BRAM * - Accumulates 48 chirps x 512 range bins into BRAM (3 sub-frames, PR-F)
* - Processes each range bin: Hamming window -> dual 16-pt FFT (staggered PRF) * - Processes each range bin: Hamming window -> 3 x 16-pt FFT (staggered PRF)
* - Outputs 16384 samples (512 range bins x 32 packed Doppler bins) * - Outputs 24576 samples (512 range bins x 48 packed Doppler bins)
* *
* Validates: * Validates:
* 1. FSM state transitions (IDLE -> ACCUMULATE -> LOAD_FFT -> ... -> OUTPUT) * 1. FSM state transitions (IDLE -> ACCUMULATE -> LOAD_FFT -> ... -> OUTPUT)
@@ -39,12 +39,12 @@ module tb_doppler_cosim;
// Parameters // Parameters
// ============================================================================ // ============================================================================
localparam CLK_PERIOD = 10.0; // 100 MHz localparam CLK_PERIOD = 10.0; // 100 MHz
localparam DOPPLER_FFT = 32; // Total packed Doppler bins (2 sub-frames x 16-pt FFT) localparam DOPPLER_FFT = `RP_NUM_DOPPLER_BINS; // 48 (3 sub-frames x 16-pt FFT, PR-F)
localparam RANGE_BINS = 512; localparam RANGE_BINS = `RP_NUM_RANGE_BINS; // 512
localparam CHIRPS = 32; localparam CHIRPS = `RP_CHIRPS_PER_FRAME; // 48
localparam TOTAL_INPUTS = CHIRPS * RANGE_BINS; // 16384 localparam TOTAL_INPUTS = CHIRPS * RANGE_BINS; // 24576
localparam TOTAL_OUTPUTS = RANGE_BINS * DOPPLER_FFT; // 16384 localparam TOTAL_OUTPUTS = RANGE_BINS * DOPPLER_FFT; // 24576
localparam MAX_CYCLES = 4_000_000; // Timeout: 40 ms at 100 MHz localparam MAX_CYCLES = 6_000_000; // Timeout: 60 ms at 100 MHz (3-subframe needs longer)
// Scenario selection — input file name // Scenario selection — input file name
`ifdef SCENARIO_MOVING `ifdef SCENARIO_MOVING
@@ -81,13 +81,12 @@ wire frame_complete;
wire [3:0] dut_status; wire [3:0] dut_status;
// ============================================================================ // ============================================================================
// DUT instantiation — parameter override keeps the legacy 2-subframe golden // DUT instantiation — production defaults (3 sub-frames * 16 chirps = 48,
// vectors valid (chirp-v2 production runs 3 sub-frames at 48 chirps/frame; // PR-F). T-8: legacy 2-subframe override removed; goldens regenerated.
// this co-sim feeds CHIRPS=32 = 2 × CHIRPS_PER_SUBFRAME).
// ============================================================================ // ============================================================================
doppler_processor_optimized #( doppler_processor_optimized #(
.CHIRPS_PER_FRAME(CHIRPS), .CHIRPS_PER_FRAME(CHIRPS),
.CHIRPS_PER_SUBFRAME(16), .CHIRPS_PER_SUBFRAME(`RP_CHIRPS_PER_SUBFRAME),
.RANGE_BINS(RANGE_BINS) .RANGE_BINS(RANGE_BINS)
) dut ( ) dut (
.clk(clk), .clk(clk),
@@ -200,15 +199,16 @@ initial begin
$display("============================================================"); $display("============================================================");
$display("Doppler Processor Co-Sim Testbench"); $display("Doppler Processor Co-Sim Testbench");
$display("Scenario: %0s", SCENARIO); $display("Scenario: %0s", SCENARIO);
$display("Input samples: %0d (32 chirps x 512 range bins)", TOTAL_INPUTS); $display("Input samples: %0d (%0d chirps x %0d range bins)",
$display("Expected outputs: %0d (512 range bins x 32 packed Doppler bins, dual 16-pt FFT)", TOTAL_INPUTS, CHIRPS, RANGE_BINS);
TOTAL_OUTPUTS); $display("Expected outputs: %0d (%0d range bins x %0d packed Doppler bins, 3 x 16-pt FFT)",
TOTAL_OUTPUTS, RANGE_BINS, DOPPLER_FFT);
$display("============================================================"); $display("============================================================");
// ---- Debug: check hex file loaded ---- // ---- Debug: check hex file loaded ----
$display(" input_mem[0] = %08h", input_mem[0]); $display(" input_mem[0] = %08h", input_mem[0]);
$display(" input_mem[1] = %08h", input_mem[1]); $display(" input_mem[1] = %08h", input_mem[1]);
$display(" input_mem[16383] = %08h", input_mem[16383]); $display(" input_mem[%0d] = %08h", TOTAL_INPUTS - 1, input_mem[TOTAL_INPUTS - 1]);
// ---- Check 1: DUT starts in IDLE ---- // ---- Check 1: DUT starts in IDLE ----
check(dut_state_w == 3'b000, check(dut_state_w == 3'b000,
@@ -250,7 +250,7 @@ initial begin
#(CLK_PERIOD * 5); #(CLK_PERIOD * 5);
$display(" After wait: state=%0d", dut_state_w); $display(" After wait: state=%0d", dut_state_w);
check(dut_state_w != 3'b000 && dut_state_w != 3'b001, check(dut_state_w != 3'b000 && dut_state_w != 3'b001,
"DUT entered processing state after 16384 input samples"); "DUT entered processing state after all input samples");
check(processing_active == 1'b1, check(processing_active == 1'b1,
"processing_active asserted during Doppler FFT"); "processing_active asserted during Doppler FFT");
@@ -276,7 +276,7 @@ initial begin
// ---- Check 3: Correct output count ---- // ---- Check 3: Correct output count ----
check(out_count == TOTAL_OUTPUTS, check(out_count == TOTAL_OUTPUTS,
"Output sample count == 16384"); "Output sample count matches TOTAL_OUTPUTS");
// ---- Check 4: Did not timeout ---- // ---- Check 4: Did not timeout ----
check(cycle_count < MAX_CYCLES, check(cycle_count < MAX_CYCLES,
@@ -295,12 +295,12 @@ initial begin
"First output: range_bin=0, doppler_bin=0"); "First output: range_bin=0, doppler_bin=0");
end end
// Last output should be range_bin=511 // Last output should be range_bin=RANGE_BINS-1, doppler_bin=DOPPLER_FFT-1
if (out_count == TOTAL_OUTPUTS) begin if (out_count == TOTAL_OUTPUTS) begin
check(cap_rbin[TOTAL_OUTPUTS-1] == RANGE_BINS - 1, check(cap_rbin[TOTAL_OUTPUTS-1] == RANGE_BINS - 1,
"Last output: range_bin=511"); "Last output: range_bin=RANGE_BINS-1");
check(cap_dbin[TOTAL_OUTPUTS-1] == DOPPLER_FFT - 1, check(cap_dbin[TOTAL_OUTPUTS-1] == DOPPLER_FFT - 1,
"Last output: doppler_bin=31"); "Last output: doppler_bin=DOPPLER_FFT-1");
end end
// ---- Check 7: Range bins are monotonically non-decreasing ---- // ---- Check 7: Range bins are monotonically non-decreasing ----
@@ -338,10 +338,10 @@ initial begin
end end
end end
check(all_ok == 1, check(all_ok == 1,
"Each range bin has exactly 32 Doppler outputs"); "Each range bin has exactly DOPPLER_FFT Doppler outputs");
end end
// ---- Check 9: Doppler bins cycle 0..31 within each range bin ---- // ---- Check 9: Doppler bins cycle 0..DOPPLER_FFT-1 within each range bin ----
begin : dbin_cycle_check begin : dbin_cycle_check
integer j, expected_dbin, dbin_ok; integer j, expected_dbin, dbin_ok;
dbin_ok = 1; dbin_ok = 1;
@@ -356,7 +356,7 @@ initial begin
end end
end end
check(dbin_ok == 1, check(dbin_ok == 1,
"Doppler bins cycle 0..31 within each range bin"); "Doppler bins cycle 0..DOPPLER_FFT-1 within each range bin");
end end
// ---- Check 10: Non-trivial output (not all zeros) ---- // ---- Check 10: Non-trivial output (not all zeros) ----
@@ -406,7 +406,7 @@ initial begin
// ---- Check: FFT input count ---- // ---- Check: FFT input count ----
check(fft_in_count == TOTAL_OUTPUTS, check(fft_in_count == TOTAL_OUTPUTS,
"FFT input count == 16384"); "FFT input count == TOTAL_OUTPUTS");
// ---- Summary ---- // ---- Summary ----
$display("\n============================================================"); $display("\n============================================================");
+7 -4
View File
@@ -65,10 +65,13 @@ wire frame_complete;
wire [3:0] dut_status; wire [3:0] dut_status;
// ============================================================================ // ============================================================================
// DUT INSTANTIATION — override CHIRPS_PER_FRAME=32 to keep this TB compatible // DUT INSTANTIATION — override CHIRPS_PER_FRAME=32 (NUM_SUBFRAMES=2). This
// with the legacy 2-subframe golden vectors (chirp-v2 production runs 3 // TB is a strict bit-exact regression against pre-recorded ADI CN0566 Phaser
// sub-frames at 48 chirps/frame; the Doppler FSM is parameterised over // data (32 chirps x 64 range bins) generated by golden_reference.py against
// NUM_SUBFRAMES = CHIRPS_PER_FRAME / CHIRPS_PER_SUBFRAME). // the legacy 2-subframe architecture; production 3-subframe (48-chirp)
// coverage now lives in tb_doppler_cosim.v with synthetic stimulus.
// Regenerating realdata for 48 chirps would need new ADI captures plus a
// 3-subframe rewrite of golden_reference.py — tracked as a PR-I follow-up.
// ============================================================================ // ============================================================================
doppler_processor_optimized #( doppler_processor_optimized #(
.CHIRPS_PER_FRAME(32), .CHIRPS_PER_FRAME(32),