mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-10 23:41:18 +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
|
||||
|
||||
Reference in New Issue
Block a user