mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-13 17:01:17 +00:00
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:
@@ -33,9 +33,9 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
# Configuration
|
||||
# =============================================================================
|
||||
|
||||
DOPPLER_FFT = 32
|
||||
DOPPLER_FFT = 48 # 3 sub-frames x 16-pt FFT (PR-F)
|
||||
RANGE_BINS = 512
|
||||
TOTAL_OUTPUTS = RANGE_BINS * DOPPLER_FFT # 16384
|
||||
TOTAL_OUTPUTS = RANGE_BINS * DOPPLER_FFT # 24576
|
||||
SUBFRAME_SIZE = 16
|
||||
|
||||
SCENARIOS = {
|
||||
@@ -246,7 +246,7 @@ def compare_scenario(name, config, base_dir):
|
||||
# ---- Pass/Fail ----
|
||||
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)
|
||||
checks.append((f'Energy ratio in bounds '
|
||||
@@ -316,23 +316,22 @@ def main():
|
||||
for name in run_scenarios:
|
||||
passed, result = compare_scenario(name, SCENARIOS[name], base_dir)
|
||||
results.append((name, passed, result))
|
||||
|
||||
# Summary
|
||||
|
||||
|
||||
all_pass = True
|
||||
for _name, passed, result in results:
|
||||
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:
|
||||
if not passed:
|
||||
all_pass = False
|
||||
|
||||
if all_pass:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
print(f'[FAIL] 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}')
|
||||
|
||||
all_pass = all(p and r for _, p, r in results)
|
||||
sys.exit(0 if all_pass else 1)
|
||||
|
||||
|
||||
|
||||
@@ -61,14 +61,17 @@ SCENARIOS = {
|
||||
},
|
||||
}
|
||||
|
||||
# Thresholds for pass/fail
|
||||
# These are generous because of the fundamental twiddle arithmetic differences
|
||||
# between the SIMULATION branch (float twiddles) and Python model (fixed twiddles)
|
||||
ENERGY_CORR_MIN = 0.80 # Min correlation of magnitude spectra
|
||||
TOP_PEAK_OVERLAP_MIN = 0.50 # At least 50% of top-N peaks must overlap
|
||||
RMS_RATIO_MAX = 50.0 # Max ratio of RMS energies (generous, since gain differs)
|
||||
ENERGY_RATIO_MIN = 0.001 # Min ratio (total energy RTL / total energy Python)
|
||||
ENERGY_RATIO_MAX = 1000.0 # Max ratio
|
||||
# Pass/fail thresholds (PR-Tests-1 / T-7).
|
||||
# Two correct FFTs of identical input must be Parseval-bounded (energy
|
||||
# matches), and the same dominant tones imply high spectral / I-Q correlation.
|
||||
# SIMULATION-mode runs use float twiddles vs the Python fixed-point reference,
|
||||
# so a small gain offset is expected — but anything outside these bars
|
||||
# indicates real drift between the behavioral and synthesis paths.
|
||||
ENERGY_RATIO_MIN = 0.95
|
||||
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 ----
|
||||
# The SIMULATION branch uses floating-point twiddles ($cos/$sin) while
|
||||
# the Python model uses the fixed-point twiddle ROM (matching synthesis).
|
||||
# These are fundamentally different FFT implementations. We do NOT expect
|
||||
# structural similarity (correlation, peak overlap) between them.
|
||||
#
|
||||
# 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.
|
||||
# Strict (PR-Tests-1 / T-7). Two correct FFTs of the same input must be
|
||||
# Parseval-bounded and share dominant tones; we now gate on energy ratio,
|
||||
# spectral correlation, top-N peak overlap, and per-channel I/Q
|
||||
# correlation — not just "did the state machine reach $finish".
|
||||
|
||||
checks = []
|
||||
|
||||
# Check 1: Both produce output
|
||||
both_have_output = py_energy > 0 and rtl_energy > 0
|
||||
checks.append(('Both produce output', both_have_output))
|
||||
|
||||
# Check 2: RTL produced expected sample count
|
||||
correct_count = len(rtl_i) == FFT_SIZE
|
||||
checks.append(('Correct output count (2048)', correct_count))
|
||||
|
||||
# Check 3: Energy ratio within generous bounds
|
||||
# Allow very wide range since twiddle differences cause large gain variation
|
||||
energy_ok = ENERGY_RATIO_MIN < energy_ratio < ENERGY_RATIO_MAX
|
||||
checks.append((f'Energy ratio in bounds ({ENERGY_RATIO_MIN}-{ENERGY_RATIO_MAX})',
|
||||
energy_ok))
|
||||
energy_ok = ENERGY_RATIO_MIN <= energy_ratio <= ENERGY_RATIO_MAX
|
||||
checks.append((f'Energy ratio in [{ENERGY_RATIO_MIN},{ENERGY_RATIO_MAX}] '
|
||||
f'(actual={energy_ratio:.3f})', 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
|
||||
failed_names = []
|
||||
for _name, passed in checks:
|
||||
if not passed:
|
||||
all_pass = False
|
||||
failed_names.append(_name)
|
||||
|
||||
result = {
|
||||
'scenario': scenario_name,
|
||||
@@ -271,6 +274,7 @@ def compare_scenario(scenario_name, config, base_dir):
|
||||
'corr_i': corr_i,
|
||||
'corr_q': corr_q,
|
||||
'passed': all_pass,
|
||||
'failed_checks': failed_names,
|
||||
}
|
||||
|
||||
# Write detailed comparison CSV
|
||||
@@ -306,23 +310,23 @@ def main():
|
||||
for name in run_scenarios:
|
||||
passed, result = compare_scenario(name, SCENARIOS[name], base_dir)
|
||||
results.append((name, passed, result))
|
||||
|
||||
# Summary
|
||||
|
||||
|
||||
all_pass = True
|
||||
for _name, passed, result in results:
|
||||
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:
|
||||
if not passed:
|
||||
all_pass = False
|
||||
|
||||
if all_pass:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
print(f'[FAIL] 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} '
|
||||
f'-> {result["failed_checks"]}')
|
||||
|
||||
all_pass = all(p and r for _, p, r in results)
|
||||
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
@@ -1088,29 +1088,44 @@ HAMMING_WINDOW = [
|
||||
|
||||
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:
|
||||
- Sub-frame 0 (long 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
|
||||
PR-F: 48 chirps total, 3 sub-frames (SHORT/MEDIUM/LONG):
|
||||
- Sub-frame 0 (SHORT PRI): chirps 0..15 -> 16-pt Hamming -> 16-pt FFT -> bins 0..15
|
||||
- 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]}
|
||||
Total output per range bin: 32 bins (16 + 16), same interface as before.
|
||||
Output: doppler_bin[5:0] = {sub_frame_id[1:0], bin_in_subframe[3:0]}
|
||||
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
|
||||
RANGE_BINS = 512
|
||||
CHIRPS_PER_FRAME = 32
|
||||
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.
|
||||
If not provided, we generate twiddle factors mathematically
|
||||
(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._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
|
||||
def window_multiply(data_16, window_16):
|
||||
@@ -1132,20 +1147,20 @@ class DopplerProcessor:
|
||||
|
||||
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:
|
||||
chirp_data_i: 2D array [32 chirps][512 range bins] of signed 16-bit I
|
||||
chirp_data_q: 2D array [32 chirps][512 range bins] of signed 16-bit Q
|
||||
chirp_data_i: 2D array [CHIRPS_PER_FRAME][RANGE_BINS] of signed 16-bit I
|
||||
chirp_data_q: 2D array [CHIRPS_PER_FRAME][RANGE_BINS] of signed 16-bit Q
|
||||
|
||||
Returns:
|
||||
(doppler_map_i, doppler_map_q): 2D arrays [512 range bins][32 doppler bins]
|
||||
of signed 16-bit
|
||||
Bins 0-15 = sub-frame 0 (long PRI)
|
||||
Bins 16-31 = sub-frame 1 (short PRI)
|
||||
(doppler_map_i, doppler_map_q): 2D arrays
|
||||
[RANGE_BINS][NUM_SUBFRAMES * DOPPLER_FFT_SIZE] of signed 16-bit.
|
||||
Sub-frame s occupies bins [s*16 .. s*16+15].
|
||||
"""
|
||||
doppler_map_i = []
|
||||
doppler_map_q = []
|
||||
total_bins = self.NUM_SUBFRAMES * self.DOPPLER_FFT_SIZE
|
||||
|
||||
# Generate 16-pt twiddle factors (quarter-wave cos, 4 entries)
|
||||
# cos(2*pi*k/16) for k=0..3
|
||||
@@ -1164,12 +1179,11 @@ class DopplerProcessor:
|
||||
fft16.mem_im = [0] * 16
|
||||
|
||||
for rbin in range(self.RANGE_BINS):
|
||||
# Output bins for this range bin: 32 total (16 from each sub-frame)
|
||||
out_re = [0] * 32
|
||||
out_im = [0] * 32
|
||||
out_re = [0] * total_bins
|
||||
out_im = [0] * total_bins
|
||||
|
||||
# Process each sub-frame independently
|
||||
for sf in range(2):
|
||||
for sf in range(self.NUM_SUBFRAMES):
|
||||
chirp_start = sf * self.CHIRPS_PER_SUBFRAME
|
||||
bin_offset = sf * self.DOPPLER_FFT_SIZE
|
||||
|
||||
@@ -1188,10 +1202,8 @@ class DopplerProcessor:
|
||||
fft_in_re.append(win_re)
|
||||
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)
|
||||
|
||||
# Pack into output: sub-frame 0 -> bins 0-15, sub-frame 1 -> bins 16-31
|
||||
for b in range(self.DOPPLER_FFT_SIZE):
|
||||
out_re[bin_offset + b] = fft_out_re[b]
|
||||
out_im[bin_offset + b] = fft_out_im[b]
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
Generate Doppler processor co-simulation golden reference data.
|
||||
|
||||
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
|
||||
input hex files consumed by the Verilog testbench (tb_doppler_cosim.v).
|
||||
Doppler FFT output for the 3-subframe / 16-pt FFT architecture (PR-F). Also
|
||||
generates the input hex files consumed by the Verilog testbench
|
||||
(tb_doppler_cosim.v).
|
||||
|
||||
Architecture:
|
||||
Sub-frame 0 (long 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
|
||||
Architecture (matches chirp_scheduler.v ordering — SHORT, MEDIUM, LONG):
|
||||
Sub-frame 0 (SHORT PRI): chirps 0..15 -> 16-pt Hamming -> 16-pt FFT -> bins 0..15
|
||||
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:
|
||||
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_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
|
||||
CHIRPS_PER_FRAME = 32
|
||||
TOTAL_SAMPLES = CHIRPS_PER_FRAME * RANGE_BINS # 16384
|
||||
CHIRPS_PER_FRAME = NUM_SUBFRAMES * 16 # 48
|
||||
TOTAL_SAMPLES = CHIRPS_PER_FRAME * RANGE_BINS # 24576
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -87,11 +90,12 @@ def make_scenario_stationary():
|
||||
|
||||
def make_scenario_moving():
|
||||
"""Single target with moderate Doppler shift."""
|
||||
# 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 → sub-frame 1 bin = fd * 16 * 175e-6 ≈ 2.9 → bin 16+3 = 19
|
||||
# v = 15 m/s -> fd = 2*v*fc/c ~= 1050 Hz
|
||||
# SHORT PRI 175 us: bin = fd * 16 * 175e-6 ~= 2.94 -> sf0 bin ~3
|
||||
# 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)]
|
||||
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():
|
||||
@@ -117,7 +121,7 @@ SCENARIOS = {
|
||||
def generate_scenario(name, targets, description, base_dir):
|
||||
"""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)
|
||||
|
||||
|
||||
|
||||
@@ -59,25 +59,28 @@ ADC_BITS = 8 # ADC resolution
|
||||
T_LONG_CHIRP = 30e-6 # 30 us long chirp duration
|
||||
T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp
|
||||
T_LISTEN_LONG = 137e-6 # 137 us listening window
|
||||
T_PRI_LONG = 167e-6 # 30 us chirp + 137 us listen
|
||||
T_PRI_SHORT = 175e-6 # staggered short-PRI sub-frame
|
||||
T_PRI_LONG = 167e-6 # 30 us chirp + 137 us listen (sub-frame 2: LONG)
|
||||
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
|
||||
|
||||
# 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
|
||||
FFT_SIZE = 2048
|
||||
RANGE_BINS = 512
|
||||
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_FRAME = 32
|
||||
CHIRPS_PER_FRAME = NUM_SUBFRAMES * CHIRPS_PER_SUBFRAME # 48
|
||||
|
||||
# Derived
|
||||
RANGE_RESOLUTION = C_LIGHT / (2 * CHIRP_BW) # 7.5 m
|
||||
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_MEDIUM = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_MEDIUM)
|
||||
VELOCITY_RESOLUTION_SHORT = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_SHORT)
|
||||
|
||||
# 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,
|
||||
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:
|
||||
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:
|
||||
targets: list of Target objects
|
||||
n_chirps: chirps per frame (32)
|
||||
n_range_bins: range bins per chirp (64)
|
||||
n_chirps: chirps per frame (48)
|
||||
n_range_bins: range bins per chirp (default RANGE_BINS=512)
|
||||
|
||||
Returns:
|
||||
(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
|
||||
|
||||
# Doppler phase for this chirp.
|
||||
# The frame uses staggered PRF: chirps 0-15 use the long PRI,
|
||||
# chirps 16-31 use the short PRI.
|
||||
# 3-subframe staggered PRF (chirp_scheduler.v ordering):
|
||||
# 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:
|
||||
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:
|
||||
slow_time_s = (CHIRPS_PER_SUBFRAME * T_PRI_LONG) + \
|
||||
((chirp_idx - CHIRPS_PER_SUBFRAME) * T_PRI_SHORT)
|
||||
slow_time_s = (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
|
||||
total_phase = doppler_phase + target.phase_deg * math.pi / 180.0
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
* Co-simulation testbench for doppler_processor_optimized (doppler_processor.v).
|
||||
*
|
||||
* Tests the complete Doppler processing pipeline:
|
||||
* - Accumulates 32 chirps x 512 range bins into BRAM
|
||||
* - Processes each range bin: Hamming window -> dual 16-pt FFT (staggered PRF)
|
||||
* - Outputs 16384 samples (512 range bins x 32 packed Doppler bins)
|
||||
* - Accumulates 48 chirps x 512 range bins into BRAM (3 sub-frames, PR-F)
|
||||
* - Processes each range bin: Hamming window -> 3 x 16-pt FFT (staggered PRF)
|
||||
* - Outputs 24576 samples (512 range bins x 48 packed Doppler bins)
|
||||
*
|
||||
* Validates:
|
||||
* 1. FSM state transitions (IDLE -> ACCUMULATE -> LOAD_FFT -> ... -> OUTPUT)
|
||||
@@ -39,12 +39,12 @@ module tb_doppler_cosim;
|
||||
// Parameters
|
||||
// ============================================================================
|
||||
localparam CLK_PERIOD = 10.0; // 100 MHz
|
||||
localparam DOPPLER_FFT = 32; // Total packed Doppler bins (2 sub-frames x 16-pt FFT)
|
||||
localparam RANGE_BINS = 512;
|
||||
localparam CHIRPS = 32;
|
||||
localparam TOTAL_INPUTS = CHIRPS * RANGE_BINS; // 16384
|
||||
localparam TOTAL_OUTPUTS = RANGE_BINS * DOPPLER_FFT; // 16384
|
||||
localparam MAX_CYCLES = 4_000_000; // Timeout: 40 ms at 100 MHz
|
||||
localparam DOPPLER_FFT = `RP_NUM_DOPPLER_BINS; // 48 (3 sub-frames x 16-pt FFT, PR-F)
|
||||
localparam RANGE_BINS = `RP_NUM_RANGE_BINS; // 512
|
||||
localparam CHIRPS = `RP_CHIRPS_PER_FRAME; // 48
|
||||
localparam TOTAL_INPUTS = CHIRPS * RANGE_BINS; // 24576
|
||||
localparam TOTAL_OUTPUTS = RANGE_BINS * DOPPLER_FFT; // 24576
|
||||
localparam MAX_CYCLES = 6_000_000; // Timeout: 60 ms at 100 MHz (3-subframe needs longer)
|
||||
|
||||
// Scenario selection — input file name
|
||||
`ifdef SCENARIO_MOVING
|
||||
@@ -81,13 +81,12 @@ wire frame_complete;
|
||||
wire [3:0] dut_status;
|
||||
|
||||
// ============================================================================
|
||||
// DUT instantiation — parameter override keeps the legacy 2-subframe golden
|
||||
// vectors valid (chirp-v2 production runs 3 sub-frames at 48 chirps/frame;
|
||||
// this co-sim feeds CHIRPS=32 = 2 × CHIRPS_PER_SUBFRAME).
|
||||
// DUT instantiation — production defaults (3 sub-frames * 16 chirps = 48,
|
||||
// PR-F). T-8: legacy 2-subframe override removed; goldens regenerated.
|
||||
// ============================================================================
|
||||
doppler_processor_optimized #(
|
||||
.CHIRPS_PER_FRAME(CHIRPS),
|
||||
.CHIRPS_PER_SUBFRAME(16),
|
||||
.CHIRPS_PER_SUBFRAME(`RP_CHIRPS_PER_SUBFRAME),
|
||||
.RANGE_BINS(RANGE_BINS)
|
||||
) dut (
|
||||
.clk(clk),
|
||||
@@ -200,15 +199,16 @@ initial begin
|
||||
$display("============================================================");
|
||||
$display("Doppler Processor Co-Sim Testbench");
|
||||
$display("Scenario: %0s", SCENARIO);
|
||||
$display("Input samples: %0d (32 chirps x 512 range bins)", TOTAL_INPUTS);
|
||||
$display("Expected outputs: %0d (512 range bins x 32 packed Doppler bins, dual 16-pt FFT)",
|
||||
TOTAL_OUTPUTS);
|
||||
$display("Input samples: %0d (%0d chirps x %0d range bins)",
|
||||
TOTAL_INPUTS, CHIRPS, RANGE_BINS);
|
||||
$display("Expected outputs: %0d (%0d range bins x %0d packed Doppler bins, 3 x 16-pt FFT)",
|
||||
TOTAL_OUTPUTS, RANGE_BINS, DOPPLER_FFT);
|
||||
$display("============================================================");
|
||||
|
||||
// ---- Debug: check hex file loaded ----
|
||||
$display(" input_mem[0] = %08h", input_mem[0]);
|
||||
$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(dut_state_w == 3'b000,
|
||||
@@ -250,7 +250,7 @@ initial begin
|
||||
#(CLK_PERIOD * 5);
|
||||
$display(" After wait: state=%0d", dut_state_w);
|
||||
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,
|
||||
"processing_active asserted during Doppler FFT");
|
||||
|
||||
@@ -276,7 +276,7 @@ initial begin
|
||||
|
||||
// ---- Check 3: Correct output count ----
|
||||
check(out_count == TOTAL_OUTPUTS,
|
||||
"Output sample count == 16384");
|
||||
"Output sample count matches TOTAL_OUTPUTS");
|
||||
|
||||
// ---- Check 4: Did not timeout ----
|
||||
check(cycle_count < MAX_CYCLES,
|
||||
@@ -295,12 +295,12 @@ initial begin
|
||||
"First output: range_bin=0, doppler_bin=0");
|
||||
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
|
||||
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,
|
||||
"Last output: doppler_bin=31");
|
||||
"Last output: doppler_bin=DOPPLER_FFT-1");
|
||||
end
|
||||
|
||||
// ---- Check 7: Range bins are monotonically non-decreasing ----
|
||||
@@ -338,10 +338,10 @@ initial begin
|
||||
end
|
||||
end
|
||||
check(all_ok == 1,
|
||||
"Each range bin has exactly 32 Doppler outputs");
|
||||
"Each range bin has exactly DOPPLER_FFT Doppler outputs");
|
||||
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
|
||||
integer j, expected_dbin, dbin_ok;
|
||||
dbin_ok = 1;
|
||||
@@ -356,7 +356,7 @@ initial begin
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
// ---- Check 10: Non-trivial output (not all zeros) ----
|
||||
@@ -406,7 +406,7 @@ initial begin
|
||||
|
||||
// ---- Check: FFT input count ----
|
||||
check(fft_in_count == TOTAL_OUTPUTS,
|
||||
"FFT input count == 16384");
|
||||
"FFT input count == TOTAL_OUTPUTS");
|
||||
|
||||
// ---- Summary ----
|
||||
$display("\n============================================================");
|
||||
|
||||
@@ -65,10 +65,13 @@ wire frame_complete;
|
||||
wire [3:0] dut_status;
|
||||
|
||||
// ============================================================================
|
||||
// DUT INSTANTIATION — override CHIRPS_PER_FRAME=32 to keep this TB compatible
|
||||
// with the legacy 2-subframe golden vectors (chirp-v2 production runs 3
|
||||
// sub-frames at 48 chirps/frame; the Doppler FSM is parameterised over
|
||||
// NUM_SUBFRAMES = CHIRPS_PER_FRAME / CHIRPS_PER_SUBFRAME).
|
||||
// DUT INSTANTIATION — override CHIRPS_PER_FRAME=32 (NUM_SUBFRAMES=2). This
|
||||
// TB is a strict bit-exact regression against pre-recorded ADI CN0566 Phaser
|
||||
// data (32 chirps x 64 range bins) generated by golden_reference.py against
|
||||
// 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 #(
|
||||
.CHIRPS_PER_FRAME(32),
|
||||
|
||||
Reference in New Issue
Block a user