fix: enforce strict ruff lint (17 rule sets) across entire repo
- Expand ruff config from E/F to 17 rule sets (B, RUF, SIM, PIE, T20, ARG, ERA, A, BLE, RET, ISC, TCH, UP, C4, PERF) - Fix 907 lint errors across all Python files (GUI, FPGA cosim, schematics scripts, simulations, utilities, tools) - Replace all blind except-Exception with specific exception types - Remove commented-out dead code (ERA001) from cosim/simulation files - Modernize typing: deprecated typing.List/Dict/Tuple to builtins - Fix unused args/loop vars, ambiguous unicode, perf anti-patterns - Delete legacy GUI files V1-V4 - Add V7 test suite, requirements files - All CI jobs pass: ruff (0 errors), py_compile, pytest (92/92), MCU tests (20/20), FPGA regression (25/25)
This commit is contained in:
@@ -93,7 +93,7 @@ SCENARIOS = {
|
||||
def load_adc_hex(filepath):
|
||||
"""Load 8-bit unsigned ADC samples from hex file."""
|
||||
samples = []
|
||||
with open(filepath, 'r') as f:
|
||||
with open(filepath) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('//'):
|
||||
@@ -106,7 +106,7 @@ def load_rtl_csv(filepath):
|
||||
"""Load RTL baseband output CSV (sample_idx, baseband_i, baseband_q)."""
|
||||
bb_i = []
|
||||
bb_q = []
|
||||
with open(filepath, 'r') as f:
|
||||
with open(filepath) as f:
|
||||
f.readline() # Skip header
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
@@ -125,7 +125,6 @@ def run_python_model(adc_samples):
|
||||
because the RTL testbench captures the FIR output directly
|
||||
(baseband_i_reg <= fir_i_out in ddc_400m.v).
|
||||
"""
|
||||
print(" Running Python model...")
|
||||
|
||||
chain = SignalChain()
|
||||
result = chain.process_adc_block(adc_samples)
|
||||
@@ -135,7 +134,6 @@ def run_python_model(adc_samples):
|
||||
bb_i = result['fir_i_raw']
|
||||
bb_q = result['fir_q_raw']
|
||||
|
||||
print(f" Python model: {len(bb_i)} baseband I, {len(bb_q)} baseband Q outputs")
|
||||
return bb_i, bb_q
|
||||
|
||||
|
||||
@@ -145,7 +143,7 @@ def compute_rms_error(a, b):
|
||||
raise ValueError(f"Length mismatch: {len(a)} vs {len(b)}")
|
||||
if len(a) == 0:
|
||||
return 0.0
|
||||
sum_sq = sum((x - y) ** 2 for x, y in zip(a, b))
|
||||
sum_sq = sum((x - y) ** 2 for x, y in zip(a, b, strict=False))
|
||||
return math.sqrt(sum_sq / len(a))
|
||||
|
||||
|
||||
@@ -153,7 +151,7 @@ def compute_max_abs_error(a, b):
|
||||
"""Compute maximum absolute error between two equal-length lists."""
|
||||
if len(a) != len(b) or len(a) == 0:
|
||||
return 0
|
||||
return max(abs(x - y) for x, y in zip(a, b))
|
||||
return max(abs(x - y) for x, y in zip(a, b, strict=False))
|
||||
|
||||
|
||||
def compute_correlation(a, b):
|
||||
@@ -235,44 +233,29 @@ def compute_signal_stats(samples):
|
||||
def compare_scenario(scenario_name):
|
||||
"""Run comparison for one scenario. Returns True if passed."""
|
||||
if scenario_name not in SCENARIOS:
|
||||
print(f"ERROR: Unknown scenario '{scenario_name}'")
|
||||
print(f"Available: {', '.join(SCENARIOS.keys())}")
|
||||
return False
|
||||
|
||||
cfg = SCENARIOS[scenario_name]
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
print("=" * 60)
|
||||
print(f"Co-simulation Comparison: {cfg['description']}")
|
||||
print(f"Scenario: {scenario_name}")
|
||||
print("=" * 60)
|
||||
|
||||
# ---- Load ADC data ----
|
||||
adc_path = os.path.join(base_dir, cfg['adc_hex'])
|
||||
if not os.path.exists(adc_path):
|
||||
print(f"ERROR: ADC hex file not found: {adc_path}")
|
||||
print("Run radar_scene.py first to generate test vectors.")
|
||||
return False
|
||||
adc_samples = load_adc_hex(adc_path)
|
||||
print(f"\nADC samples loaded: {len(adc_samples)}")
|
||||
|
||||
# ---- Load RTL output ----
|
||||
rtl_path = os.path.join(base_dir, cfg['rtl_csv'])
|
||||
if not os.path.exists(rtl_path):
|
||||
print(f"ERROR: RTL CSV not found: {rtl_path}")
|
||||
print("Run the RTL simulation first:")
|
||||
print(f" iverilog -g2001 -DSIMULATION -DSCENARIO_{scenario_name.upper()} ...")
|
||||
return False
|
||||
rtl_i, rtl_q = load_rtl_csv(rtl_path)
|
||||
print(f"RTL outputs loaded: {len(rtl_i)} I, {len(rtl_q)} Q samples")
|
||||
|
||||
# ---- Run Python model ----
|
||||
py_i, py_q = run_python_model(adc_samples)
|
||||
|
||||
# ---- Length comparison ----
|
||||
print(f"\nOutput lengths: RTL={len(rtl_i)}, Python={len(py_i)}")
|
||||
len_diff = abs(len(rtl_i) - len(py_i))
|
||||
print(f"Length difference: {len_diff} samples")
|
||||
|
||||
# ---- Signal statistics ----
|
||||
rtl_i_stats = compute_signal_stats(rtl_i)
|
||||
@@ -280,20 +263,10 @@ def compare_scenario(scenario_name):
|
||||
py_i_stats = compute_signal_stats(py_i)
|
||||
py_q_stats = compute_signal_stats(py_q)
|
||||
|
||||
print("\nSignal Statistics:")
|
||||
print(f" RTL I: mean={rtl_i_stats['mean']:.1f}, rms={rtl_i_stats['rms']:.1f}, "
|
||||
f"range=[{rtl_i_stats['min']}, {rtl_i_stats['max']}]")
|
||||
print(f" RTL Q: mean={rtl_q_stats['mean']:.1f}, rms={rtl_q_stats['rms']:.1f}, "
|
||||
f"range=[{rtl_q_stats['min']}, {rtl_q_stats['max']}]")
|
||||
print(f" Py I: mean={py_i_stats['mean']:.1f}, rms={py_i_stats['rms']:.1f}, "
|
||||
f"range=[{py_i_stats['min']}, {py_i_stats['max']}]")
|
||||
print(f" Py Q: mean={py_q_stats['mean']:.1f}, rms={py_q_stats['rms']:.1f}, "
|
||||
f"range=[{py_q_stats['min']}, {py_q_stats['max']}]")
|
||||
|
||||
# ---- Trim to common length ----
|
||||
common_len = min(len(rtl_i), len(py_i))
|
||||
if common_len < 10:
|
||||
print(f"ERROR: Too few common samples ({common_len})")
|
||||
return False
|
||||
|
||||
rtl_i_trim = rtl_i[:common_len]
|
||||
@@ -302,18 +275,14 @@ def compare_scenario(scenario_name):
|
||||
py_q_trim = py_q[:common_len]
|
||||
|
||||
# ---- Cross-correlation to find latency offset ----
|
||||
print(f"\nLatency alignment (cross-correlation, max lag=±{MAX_LATENCY_DRIFT}):")
|
||||
lag_i, corr_i = cross_correlate_lag(rtl_i_trim, py_i_trim,
|
||||
lag_i, _corr_i = cross_correlate_lag(rtl_i_trim, py_i_trim,
|
||||
max_lag=MAX_LATENCY_DRIFT)
|
||||
lag_q, corr_q = cross_correlate_lag(rtl_q_trim, py_q_trim,
|
||||
lag_q, _corr_q = cross_correlate_lag(rtl_q_trim, py_q_trim,
|
||||
max_lag=MAX_LATENCY_DRIFT)
|
||||
print(f" I-channel: best lag={lag_i}, correlation={corr_i:.6f}")
|
||||
print(f" Q-channel: best lag={lag_q}, correlation={corr_q:.6f}")
|
||||
|
||||
# ---- Apply latency correction ----
|
||||
best_lag = lag_i # Use I-channel lag (should be same as Q)
|
||||
if abs(lag_i - lag_q) > 1:
|
||||
print(f" WARNING: I and Q latency offsets differ ({lag_i} vs {lag_q})")
|
||||
# Use the average
|
||||
best_lag = (lag_i + lag_q) // 2
|
||||
|
||||
@@ -341,32 +310,20 @@ def compare_scenario(scenario_name):
|
||||
aligned_py_i = aligned_py_i[:aligned_len]
|
||||
aligned_py_q = aligned_py_q[:aligned_len]
|
||||
|
||||
print(f" Applied lag correction: {best_lag} samples")
|
||||
print(f" Aligned length: {aligned_len} samples")
|
||||
|
||||
# ---- Error metrics (after alignment) ----
|
||||
rms_i = compute_rms_error(aligned_rtl_i, aligned_py_i)
|
||||
rms_q = compute_rms_error(aligned_rtl_q, aligned_py_q)
|
||||
max_err_i = compute_max_abs_error(aligned_rtl_i, aligned_py_i)
|
||||
max_err_q = compute_max_abs_error(aligned_rtl_q, aligned_py_q)
|
||||
compute_max_abs_error(aligned_rtl_i, aligned_py_i)
|
||||
compute_max_abs_error(aligned_rtl_q, aligned_py_q)
|
||||
corr_i_aligned = compute_correlation(aligned_rtl_i, aligned_py_i)
|
||||
corr_q_aligned = compute_correlation(aligned_rtl_q, aligned_py_q)
|
||||
|
||||
print("\nError Metrics (after alignment):")
|
||||
print(f" I-channel: RMS={rms_i:.2f} LSB, max={max_err_i} LSB, corr={corr_i_aligned:.6f}")
|
||||
print(f" Q-channel: RMS={rms_q:.2f} LSB, max={max_err_q} LSB, corr={corr_q_aligned:.6f}")
|
||||
|
||||
# ---- First/last sample comparison ----
|
||||
print("\nFirst 10 samples (after alignment):")
|
||||
print(
|
||||
f" {'idx':>4s} {'RTL_I':>8s} {'Py_I':>8s} {'Err_I':>6s} "
|
||||
f"{'RTL_Q':>8s} {'Py_Q':>8s} {'Err_Q':>6s}"
|
||||
)
|
||||
for k in range(min(10, aligned_len)):
|
||||
ei = aligned_rtl_i[k] - aligned_py_i[k]
|
||||
eq = aligned_rtl_q[k] - aligned_py_q[k]
|
||||
print(f" {k:4d} {aligned_rtl_i[k]:8d} {aligned_py_i[k]:8d} {ei:6d} "
|
||||
f"{aligned_rtl_q[k]:8d} {aligned_py_q[k]:8d} {eq:6d}")
|
||||
|
||||
# ---- Write detailed comparison CSV ----
|
||||
compare_csv_path = os.path.join(base_dir, f"compare_{scenario_name}.csv")
|
||||
@@ -377,7 +334,6 @@ def compare_scenario(scenario_name):
|
||||
eq = aligned_rtl_q[k] - aligned_py_q[k]
|
||||
f.write(f"{k},{aligned_rtl_i[k]},{aligned_py_i[k]},{ei},"
|
||||
f"{aligned_rtl_q[k]},{aligned_py_q[k]},{eq}\n")
|
||||
print(f"\nDetailed comparison written to: {compare_csv_path}")
|
||||
|
||||
# ---- Pass/Fail ----
|
||||
max_rms = cfg.get('max_rms', MAX_RMS_ERROR_LSB)
|
||||
@@ -443,21 +399,15 @@ def compare_scenario(scenario_name):
|
||||
f"|{best_lag}| <= {MAX_LATENCY_DRIFT}"))
|
||||
|
||||
# ---- Report ----
|
||||
print(f"\n{'─' * 60}")
|
||||
print("PASS/FAIL Results:")
|
||||
all_pass = True
|
||||
for name, ok, detail in results:
|
||||
mark = "[PASS]" if ok else "[FAIL]"
|
||||
print(f" {mark} {name}: {detail}")
|
||||
for _name, ok, _detail in results:
|
||||
if not ok:
|
||||
all_pass = False
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
if all_pass:
|
||||
print(f"SCENARIO {scenario_name.upper()}: ALL CHECKS PASSED")
|
||||
pass
|
||||
else:
|
||||
print(f"SCENARIO {scenario_name.upper()}: SOME CHECKS FAILED")
|
||||
print(f"{'=' * 60}")
|
||||
pass
|
||||
|
||||
return all_pass
|
||||
|
||||
@@ -481,25 +431,18 @@ def main():
|
||||
pass_count += 1
|
||||
else:
|
||||
overall_pass = False
|
||||
print()
|
||||
else:
|
||||
print(f"Skipping {name}: RTL CSV not found ({cfg['rtl_csv']})")
|
||||
pass
|
||||
|
||||
print("=" * 60)
|
||||
print(f"OVERALL: {pass_count}/{run_count} scenarios passed")
|
||||
if overall_pass:
|
||||
print("ALL SCENARIOS PASSED")
|
||||
pass
|
||||
else:
|
||||
print("SOME SCENARIOS FAILED")
|
||||
print("=" * 60)
|
||||
pass
|
||||
return 0 if overall_pass else 1
|
||||
else:
|
||||
ok = compare_scenario(scenario)
|
||||
return 0 if ok else 1
|
||||
else:
|
||||
# Default: DC
|
||||
ok = compare_scenario('dc')
|
||||
ok = compare_scenario(scenario)
|
||||
return 0 if ok else 1
|
||||
ok = compare_scenario('dc')
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -4085,4 +4085,3 @@ idx,rtl_i,py_i,err_i,rtl_q,py_q,err_q
|
||||
4083,21,20,1,-6,-6,0
|
||||
4084,20,21,-1,-6,-6,0
|
||||
4085,20,20,0,-5,-6,1
|
||||
4086,20,20,0,-5,-5,0
|
||||
|
||||
|
@@ -73,7 +73,7 @@ def load_doppler_csv(filepath):
|
||||
Returns dict: {rbin: [(dbin, i, q), ...]}
|
||||
"""
|
||||
data = {}
|
||||
with open(filepath, 'r') as f:
|
||||
with open(filepath) as f:
|
||||
f.readline() # Skip header
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
@@ -117,7 +117,7 @@ def pearson_correlation(a, b):
|
||||
|
||||
def magnitude_l1(i_arr, q_arr):
|
||||
"""L1 magnitude: |I| + |Q|."""
|
||||
return [abs(i) + abs(q) for i, q in zip(i_arr, q_arr)]
|
||||
return [abs(i) + abs(q) for i, q in zip(i_arr, q_arr, strict=False)]
|
||||
|
||||
|
||||
def find_peak_bin(i_arr, q_arr):
|
||||
@@ -143,7 +143,7 @@ def total_energy(data_dict):
|
||||
"""Sum of I^2 + Q^2 across all range bins and Doppler bins."""
|
||||
total = 0
|
||||
for rbin in data_dict:
|
||||
for (dbin, i_val, q_val) in data_dict[rbin]:
|
||||
for (_dbin, i_val, q_val) in data_dict[rbin]:
|
||||
total += i_val * i_val + q_val * q_val
|
||||
return total
|
||||
|
||||
@@ -154,44 +154,30 @@ def total_energy(data_dict):
|
||||
|
||||
def compare_scenario(name, config, base_dir):
|
||||
"""Compare one Doppler scenario. Returns (passed, result_dict)."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Scenario: {name} — {config['description']}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
golden_path = os.path.join(base_dir, config['golden_csv'])
|
||||
rtl_path = os.path.join(base_dir, config['rtl_csv'])
|
||||
|
||||
if not os.path.exists(golden_path):
|
||||
print(f" ERROR: Golden CSV not found: {golden_path}")
|
||||
print(" Run: python3 gen_doppler_golden.py")
|
||||
return False, {}
|
||||
if not os.path.exists(rtl_path):
|
||||
print(f" ERROR: RTL CSV not found: {rtl_path}")
|
||||
print(" Run the Verilog testbench first")
|
||||
return False, {}
|
||||
|
||||
py_data = load_doppler_csv(golden_path)
|
||||
rtl_data = load_doppler_csv(rtl_path)
|
||||
|
||||
py_rbins = sorted(py_data.keys())
|
||||
rtl_rbins = sorted(rtl_data.keys())
|
||||
sorted(py_data.keys())
|
||||
sorted(rtl_data.keys())
|
||||
|
||||
print(f" Python: {len(py_rbins)} range bins, "
|
||||
f"{sum(len(v) for v in py_data.values())} total samples")
|
||||
print(f" RTL: {len(rtl_rbins)} range bins, "
|
||||
f"{sum(len(v) for v in rtl_data.values())} total samples")
|
||||
|
||||
# ---- Check 1: Both have data ----
|
||||
py_total = sum(len(v) for v in py_data.values())
|
||||
rtl_total = sum(len(v) for v in rtl_data.values())
|
||||
if py_total == 0 or rtl_total == 0:
|
||||
print(" ERROR: One or both outputs are empty")
|
||||
return False, {}
|
||||
|
||||
# ---- Check 2: Output count ----
|
||||
count_ok = (rtl_total == TOTAL_OUTPUTS)
|
||||
print(f"\n Output count: RTL={rtl_total}, expected={TOTAL_OUTPUTS} "
|
||||
f"{'OK' if count_ok else 'MISMATCH'}")
|
||||
|
||||
# ---- Check 3: Global energy ----
|
||||
py_energy = total_energy(py_data)
|
||||
@@ -201,10 +187,6 @@ def compare_scenario(name, config, base_dir):
|
||||
else:
|
||||
energy_ratio = 1.0 if rtl_energy == 0 else float('inf')
|
||||
|
||||
print("\n Global energy:")
|
||||
print(f" Python: {py_energy}")
|
||||
print(f" RTL: {rtl_energy}")
|
||||
print(f" Ratio: {energy_ratio:.4f}")
|
||||
|
||||
# ---- Check 4: Per-range-bin analysis ----
|
||||
peak_agreements = 0
|
||||
@@ -236,8 +218,8 @@ def compare_scenario(name, config, base_dir):
|
||||
i_correlations.append(corr_i)
|
||||
q_correlations.append(corr_q)
|
||||
|
||||
py_rbin_energy = sum(i*i + q*q for i, q in zip(py_i, py_q))
|
||||
rtl_rbin_energy = sum(i*i + q*q for i, q in zip(rtl_i, rtl_q))
|
||||
py_rbin_energy = sum(i*i + q*q for i, q in zip(py_i, py_q, strict=False))
|
||||
rtl_rbin_energy = sum(i*i + q*q for i, q in zip(rtl_i, rtl_q, strict=False))
|
||||
|
||||
peak_details.append({
|
||||
'rbin': rbin,
|
||||
@@ -255,20 +237,11 @@ def compare_scenario(name, config, base_dir):
|
||||
avg_corr_i = sum(i_correlations) / len(i_correlations)
|
||||
avg_corr_q = sum(q_correlations) / len(q_correlations)
|
||||
|
||||
print("\n Per-range-bin metrics:")
|
||||
print(f" Peak Doppler bin agreement (+/-1 within sub-frame): {peak_agreements}/{RANGE_BINS} "
|
||||
f"({peak_agreement_frac:.0%})")
|
||||
print(f" Avg magnitude correlation: {avg_mag_corr:.4f}")
|
||||
print(f" Avg I-channel correlation: {avg_corr_i:.4f}")
|
||||
print(f" Avg Q-channel correlation: {avg_corr_q:.4f}")
|
||||
|
||||
# Show top 5 range bins by Python energy
|
||||
print("\n Top 5 range bins by Python energy:")
|
||||
top_rbins = sorted(peak_details, key=lambda x: -x['py_energy'])[:5]
|
||||
for d in top_rbins:
|
||||
print(f" rbin={d['rbin']:2d}: py_peak={d['py_peak']:2d}, "
|
||||
f"rtl_peak={d['rtl_peak']:2d}, mag_corr={d['mag_corr']:.3f}, "
|
||||
f"I_corr={d['corr_i']:.3f}, Q_corr={d['corr_q']:.3f}")
|
||||
for _d in top_rbins:
|
||||
pass
|
||||
|
||||
# ---- Pass/Fail ----
|
||||
checks = []
|
||||
@@ -291,11 +264,8 @@ def compare_scenario(name, config, base_dir):
|
||||
checks.append((f'High-energy rbin avg mag_corr >= {MAG_CORR_MIN:.2f} '
|
||||
f'(actual={he_mag_corr:.3f})', he_ok))
|
||||
|
||||
print("\n Pass/Fail Checks:")
|
||||
all_pass = True
|
||||
for check_name, passed in checks:
|
||||
status = "PASS" if passed else "FAIL"
|
||||
print(f" [{status}] {check_name}")
|
||||
for _check_name, passed in checks:
|
||||
if not passed:
|
||||
all_pass = False
|
||||
|
||||
@@ -310,7 +280,6 @@ def compare_scenario(name, config, base_dir):
|
||||
f.write(f'{rbin},{dbin},{py_i[dbin]},{py_q[dbin]},'
|
||||
f'{rtl_i[dbin]},{rtl_q[dbin]},'
|
||||
f'{rtl_i[dbin]-py_i[dbin]},{rtl_q[dbin]-py_q[dbin]}\n')
|
||||
print(f"\n Detailed comparison: {compare_csv}")
|
||||
|
||||
result = {
|
||||
'scenario': name,
|
||||
@@ -333,25 +302,15 @@ def compare_scenario(name, config, base_dir):
|
||||
def main():
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
arg = sys.argv[1].lower()
|
||||
else:
|
||||
arg = 'stationary'
|
||||
arg = sys.argv[1].lower() if len(sys.argv) > 1 else 'stationary'
|
||||
|
||||
if arg == 'all':
|
||||
run_scenarios = list(SCENARIOS.keys())
|
||||
elif arg in SCENARIOS:
|
||||
run_scenarios = [arg]
|
||||
else:
|
||||
print(f"Unknown scenario: {arg}")
|
||||
print(f"Valid: {', '.join(SCENARIOS.keys())}, all")
|
||||
sys.exit(1)
|
||||
|
||||
print("=" * 60)
|
||||
print("Doppler Processor Co-Simulation Comparison")
|
||||
print("RTL vs Python model (clean, no pipeline bug replication)")
|
||||
print(f"Scenarios: {', '.join(run_scenarios)}")
|
||||
print("=" * 60)
|
||||
|
||||
results = []
|
||||
for name in run_scenarios:
|
||||
@@ -359,37 +318,20 @@ def main():
|
||||
results.append((name, passed, result))
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*60}")
|
||||
print("SUMMARY")
|
||||
print(f"{'='*60}")
|
||||
|
||||
print(f"\n {'Scenario':<15} {'Energy Ratio':>13} {'Mag Corr':>10} "
|
||||
f"{'Peak Agree':>11} {'I Corr':>8} {'Q Corr':>8} {'Status':>8}")
|
||||
print(f" {'-'*15} {'-'*13} {'-'*10} {'-'*11} {'-'*8} {'-'*8} {'-'*8}")
|
||||
|
||||
all_pass = True
|
||||
for name, passed, result in results:
|
||||
for _name, passed, result in results:
|
||||
if not result:
|
||||
print(f" {name:<15} {'ERROR':>13} {'—':>10} {'—':>11} "
|
||||
f"{'—':>8} {'—':>8} {'FAIL':>8}")
|
||||
all_pass = False
|
||||
else:
|
||||
status = "PASS" if passed else "FAIL"
|
||||
print(f" {name:<15} {result['energy_ratio']:>13.4f} "
|
||||
f"{result['avg_mag_corr']:>10.4f} "
|
||||
f"{result['peak_agreement']:>10.0%} "
|
||||
f"{result['avg_corr_i']:>8.4f} "
|
||||
f"{result['avg_corr_q']:>8.4f} "
|
||||
f"{status:>8}")
|
||||
if not passed:
|
||||
all_pass = False
|
||||
|
||||
print()
|
||||
if all_pass:
|
||||
print("ALL TESTS PASSED")
|
||||
pass
|
||||
else:
|
||||
print("SOME TESTS FAILED")
|
||||
print(f"{'='*60}")
|
||||
pass
|
||||
|
||||
sys.exit(0 if all_pass else 1)
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ def load_csv(filepath):
|
||||
"""Load CSV with columns (bin, out_i/range_profile_i, out_q/range_profile_q)."""
|
||||
vals_i = []
|
||||
vals_q = []
|
||||
with open(filepath, 'r') as f:
|
||||
with open(filepath) as f:
|
||||
f.readline() # Skip header
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
@@ -93,17 +93,17 @@ def load_csv(filepath):
|
||||
|
||||
def magnitude_spectrum(vals_i, vals_q):
|
||||
"""Compute magnitude = |I| + |Q| for each bin (L1 norm, matches RTL)."""
|
||||
return [abs(i) + abs(q) for i, q in zip(vals_i, vals_q)]
|
||||
return [abs(i) + abs(q) for i, q in zip(vals_i, vals_q, strict=False)]
|
||||
|
||||
|
||||
def magnitude_l2(vals_i, vals_q):
|
||||
"""Compute magnitude = sqrt(I^2 + Q^2) for each bin."""
|
||||
return [math.sqrt(i*i + q*q) for i, q in zip(vals_i, vals_q)]
|
||||
return [math.sqrt(i*i + q*q) for i, q in zip(vals_i, vals_q, strict=False)]
|
||||
|
||||
|
||||
def total_energy(vals_i, vals_q):
|
||||
"""Compute total energy (sum of I^2 + Q^2)."""
|
||||
return sum(i*i + q*q for i, q in zip(vals_i, vals_q))
|
||||
return sum(i*i + q*q for i, q in zip(vals_i, vals_q, strict=False))
|
||||
|
||||
|
||||
def rms_magnitude(vals_i, vals_q):
|
||||
@@ -111,7 +111,7 @@ def rms_magnitude(vals_i, vals_q):
|
||||
n = len(vals_i)
|
||||
if n == 0:
|
||||
return 0.0
|
||||
return math.sqrt(sum(i*i + q*q for i, q in zip(vals_i, vals_q)) / n)
|
||||
return math.sqrt(sum(i*i + q*q for i, q in zip(vals_i, vals_q, strict=False)) / n)
|
||||
|
||||
|
||||
def pearson_correlation(a, b):
|
||||
@@ -144,7 +144,7 @@ def find_peak(vals_i, vals_q):
|
||||
def top_n_peaks(mags, n=10):
|
||||
"""Find the top-N peak bins by magnitude. Returns set of bin indices."""
|
||||
indexed = sorted(enumerate(mags), key=lambda x: -x[1])
|
||||
return set(idx for idx, _ in indexed[:n])
|
||||
return {idx for idx, _ in indexed[:n]}
|
||||
|
||||
|
||||
def spectral_peak_overlap(mags_a, mags_b, n=10):
|
||||
@@ -163,30 +163,20 @@ def spectral_peak_overlap(mags_a, mags_b, n=10):
|
||||
|
||||
def compare_scenario(scenario_name, config, base_dir):
|
||||
"""Compare one scenario. Returns (pass/fail, result_dict)."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Scenario: {scenario_name} — {config['description']}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
golden_path = os.path.join(base_dir, config['golden_csv'])
|
||||
rtl_path = os.path.join(base_dir, config['rtl_csv'])
|
||||
|
||||
if not os.path.exists(golden_path):
|
||||
print(f" ERROR: Golden CSV not found: {golden_path}")
|
||||
print(" Run: python3 gen_mf_cosim_golden.py")
|
||||
return False, {}
|
||||
if not os.path.exists(rtl_path):
|
||||
print(f" ERROR: RTL CSV not found: {rtl_path}")
|
||||
print(" Run the RTL testbench first")
|
||||
return False, {}
|
||||
|
||||
py_i, py_q = load_csv(golden_path)
|
||||
rtl_i, rtl_q = load_csv(rtl_path)
|
||||
|
||||
print(f" Python model: {len(py_i)} samples")
|
||||
print(f" RTL output: {len(rtl_i)} samples")
|
||||
|
||||
if len(py_i) != FFT_SIZE or len(rtl_i) != FFT_SIZE:
|
||||
print(f" ERROR: Expected {FFT_SIZE} samples from each")
|
||||
return False, {}
|
||||
|
||||
# ---- Metric 1: Energy ----
|
||||
@@ -205,28 +195,17 @@ def compare_scenario(scenario_name, config, base_dir):
|
||||
energy_ratio = float('inf') if py_energy == 0 else 0.0
|
||||
rms_ratio = float('inf') if py_rms == 0 else 0.0
|
||||
|
||||
print("\n Energy:")
|
||||
print(f" Python total energy: {py_energy}")
|
||||
print(f" RTL total energy: {rtl_energy}")
|
||||
print(f" Energy ratio (RTL/Py): {energy_ratio:.4f}")
|
||||
print(f" Python RMS: {py_rms:.2f}")
|
||||
print(f" RTL RMS: {rtl_rms:.2f}")
|
||||
print(f" RMS ratio (RTL/Py): {rms_ratio:.4f}")
|
||||
|
||||
# ---- Metric 2: Peak location ----
|
||||
py_peak_bin, py_peak_mag = find_peak(py_i, py_q)
|
||||
rtl_peak_bin, rtl_peak_mag = find_peak(rtl_i, rtl_q)
|
||||
py_peak_bin, _py_peak_mag = find_peak(py_i, py_q)
|
||||
rtl_peak_bin, _rtl_peak_mag = find_peak(rtl_i, rtl_q)
|
||||
|
||||
print("\n Peak location:")
|
||||
print(f" Python: bin={py_peak_bin}, mag={py_peak_mag}")
|
||||
print(f" RTL: bin={rtl_peak_bin}, mag={rtl_peak_mag}")
|
||||
|
||||
# ---- Metric 3: Magnitude spectrum correlation ----
|
||||
py_mag = magnitude_l2(py_i, py_q)
|
||||
rtl_mag = magnitude_l2(rtl_i, rtl_q)
|
||||
mag_corr = pearson_correlation(py_mag, rtl_mag)
|
||||
|
||||
print(f"\n Magnitude spectrum correlation: {mag_corr:.6f}")
|
||||
|
||||
# ---- Metric 4: Top-N peak overlap ----
|
||||
# Use L1 magnitudes for peak finding (matches RTL)
|
||||
@@ -235,16 +214,11 @@ def compare_scenario(scenario_name, config, base_dir):
|
||||
peak_overlap_10 = spectral_peak_overlap(py_mag_l1, rtl_mag_l1, n=10)
|
||||
peak_overlap_20 = spectral_peak_overlap(py_mag_l1, rtl_mag_l1, n=20)
|
||||
|
||||
print(f" Top-10 peak overlap: {peak_overlap_10:.2%}")
|
||||
print(f" Top-20 peak overlap: {peak_overlap_20:.2%}")
|
||||
|
||||
# ---- Metric 5: I and Q channel correlation ----
|
||||
corr_i = pearson_correlation(py_i, rtl_i)
|
||||
corr_q = pearson_correlation(py_q, rtl_q)
|
||||
|
||||
print("\n Channel correlation:")
|
||||
print(f" I-channel: {corr_i:.6f}")
|
||||
print(f" Q-channel: {corr_q:.6f}")
|
||||
|
||||
# ---- Pass/Fail Decision ----
|
||||
# The SIMULATION branch uses floating-point twiddles ($cos/$sin) while
|
||||
@@ -278,11 +252,8 @@ def compare_scenario(scenario_name, config, base_dir):
|
||||
energy_ok))
|
||||
|
||||
# Print checks
|
||||
print("\n Pass/Fail Checks:")
|
||||
all_pass = True
|
||||
for name, passed in checks:
|
||||
status = "PASS" if passed else "FAIL"
|
||||
print(f" [{status}] {name}")
|
||||
for _name, passed in checks:
|
||||
if not passed:
|
||||
all_pass = False
|
||||
|
||||
@@ -310,7 +281,6 @@ def compare_scenario(scenario_name, config, base_dir):
|
||||
f.write(f'{k},{py_i[k]},{py_q[k]},{rtl_i[k]},{rtl_q[k]},'
|
||||
f'{py_mag_l1[k]},{rtl_mag_l1[k]},'
|
||||
f'{rtl_i[k]-py_i[k]},{rtl_q[k]-py_q[k]}\n')
|
||||
print(f"\n Detailed comparison: {compare_csv}")
|
||||
|
||||
return all_pass, result
|
||||
|
||||
@@ -322,25 +292,15 @@ def compare_scenario(scenario_name, config, base_dir):
|
||||
def main():
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
arg = sys.argv[1].lower()
|
||||
else:
|
||||
arg = 'chirp'
|
||||
arg = sys.argv[1].lower() if len(sys.argv) > 1 else 'chirp'
|
||||
|
||||
if arg == 'all':
|
||||
run_scenarios = list(SCENARIOS.keys())
|
||||
elif arg in SCENARIOS:
|
||||
run_scenarios = [arg]
|
||||
else:
|
||||
print(f"Unknown scenario: {arg}")
|
||||
print(f"Valid: {', '.join(SCENARIOS.keys())}, all")
|
||||
sys.exit(1)
|
||||
|
||||
print("=" * 60)
|
||||
print("Matched Filter Co-Simulation Comparison")
|
||||
print("RTL (synthesis branch) vs Python model (bit-accurate)")
|
||||
print(f"Scenarios: {', '.join(run_scenarios)}")
|
||||
print("=" * 60)
|
||||
|
||||
results = []
|
||||
for name in run_scenarios:
|
||||
@@ -348,37 +308,20 @@ def main():
|
||||
results.append((name, passed, result))
|
||||
|
||||
# Summary
|
||||
print(f"\n{'='*60}")
|
||||
print("SUMMARY")
|
||||
print(f"{'='*60}")
|
||||
|
||||
print(f"\n {'Scenario':<12} {'Energy Ratio':>13} {'Mag Corr':>10} "
|
||||
f"{'Peak Ovlp':>10} {'Py Peak':>8} {'RTL Peak':>9} {'Status':>8}")
|
||||
print(f" {'-'*12} {'-'*13} {'-'*10} {'-'*10} {'-'*8} {'-'*9} {'-'*8}")
|
||||
|
||||
all_pass = True
|
||||
for name, passed, result in results:
|
||||
for _name, passed, result in results:
|
||||
if not result:
|
||||
print(f" {name:<12} {'ERROR':>13} {'—':>10} {'—':>10} "
|
||||
f"{'—':>8} {'—':>9} {'FAIL':>8}")
|
||||
all_pass = False
|
||||
else:
|
||||
status = "PASS" if passed else "FAIL"
|
||||
print(f" {name:<12} {result['energy_ratio']:>13.4f} "
|
||||
f"{result['mag_corr']:>10.4f} "
|
||||
f"{result['peak_overlap_10']:>9.0%} "
|
||||
f"{result['py_peak_bin']:>8d} "
|
||||
f"{result['rtl_peak_bin']:>9d} "
|
||||
f"{status:>8}")
|
||||
if not passed:
|
||||
all_pass = False
|
||||
|
||||
print()
|
||||
if all_pass:
|
||||
print("ALL TESTS PASSED")
|
||||
pass
|
||||
else:
|
||||
print("SOME TESTS FAILED")
|
||||
print(f"{'='*60}")
|
||||
pass
|
||||
|
||||
sys.exit(0 if all_pass else 1)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ def saturate(value, bits):
|
||||
return value
|
||||
|
||||
|
||||
def arith_rshift(value, shift, width=None):
|
||||
def arith_rshift(value, shift, _width=None):
|
||||
"""Arithmetic right shift. Python >> on signed int is already arithmetic."""
|
||||
return value >> shift
|
||||
|
||||
@@ -129,10 +129,7 @@ class NCO:
|
||||
raw_index = lut_address & 0x3F
|
||||
|
||||
# RTL: lut_index = (quadrant[0] ^ quadrant[1]) ? ~lut_address[5:0] : lut_address[5:0]
|
||||
if (quadrant & 1) ^ ((quadrant >> 1) & 1):
|
||||
lut_index = (~raw_index) & 0x3F
|
||||
else:
|
||||
lut_index = raw_index
|
||||
lut_index = ~raw_index & 63 if quadrant & 1 ^ quadrant >> 1 & 1 else raw_index
|
||||
|
||||
return quadrant, lut_index
|
||||
|
||||
@@ -175,7 +172,7 @@ class NCO:
|
||||
# OLD phase_accum_reg (the value from the PREVIOUS call).
|
||||
# We stored self.phase_accum_reg at the start of this call as the
|
||||
# value from last cycle. So:
|
||||
pass # phase_with_offset computed below from OLD values
|
||||
# phase_with_offset computed below from OLD values
|
||||
|
||||
# Compute all NBA assignments from OLD state:
|
||||
# Save old state for NBA evaluation
|
||||
@@ -195,16 +192,8 @@ class NCO:
|
||||
|
||||
if phase_valid:
|
||||
# Stage 1 NBA: phase_accum_reg <= phase_accumulator (old value)
|
||||
_new_phase_accum_reg = (self.phase_accumulator - ftw) & 0xFFFFFFFF # noqa: F841 — old accum before add (derivation reference)
|
||||
_new_phase_accum_reg = (self.phase_accumulator - ftw) & 0xFFFFFFFF
|
||||
# Wait - let me re-derive. The Verilog is:
|
||||
# phase_accumulator <= phase_accumulator + frequency_tuning_word;
|
||||
# phase_accum_reg <= phase_accumulator; // OLD value (NBA)
|
||||
# phase_with_offset <= phase_accum_reg + {phase_offset, 16'b0};
|
||||
# // OLD phase_accum_reg
|
||||
# Since all are NBA (<=), they all read the values from BEFORE this edge.
|
||||
# So: new_phase_accumulator = old_phase_accumulator + ftw
|
||||
# new_phase_accum_reg = old_phase_accumulator
|
||||
# new_phase_with_offset = old_phase_accum_reg + offset
|
||||
old_phase_accumulator = (self.phase_accumulator - ftw) & 0xFFFFFFFF # reconstruct
|
||||
self.phase_accum_reg = old_phase_accumulator
|
||||
self.phase_with_offset = (
|
||||
@@ -706,7 +695,6 @@ class DDCInputInterface:
|
||||
if old_valid_sync:
|
||||
ddc_i = sign_extend(ddc_i_18 & 0x3FFFF, 18)
|
||||
ddc_q = sign_extend(ddc_q_18 & 0x3FFFF, 18)
|
||||
# adc_i = ddc_i[17:2] + ddc_i[1] (rounding)
|
||||
trunc_i = (ddc_i >> 2) & 0xFFFF # bits [17:2]
|
||||
round_i = (ddc_i >> 1) & 1 # bit [1]
|
||||
trunc_q = (ddc_q >> 2) & 0xFFFF
|
||||
@@ -732,7 +720,7 @@ def load_twiddle_rom(filepath=None):
|
||||
filepath = os.path.join(base, '..', '..', 'fft_twiddle_1024.mem')
|
||||
|
||||
values = []
|
||||
with open(filepath, 'r') as f:
|
||||
with open(filepath) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('//'):
|
||||
@@ -760,12 +748,11 @@ def _twiddle_lookup(k, n, cos_rom):
|
||||
|
||||
if k == 0:
|
||||
return cos_rom[0], 0
|
||||
elif k == n4:
|
||||
if k == n4:
|
||||
return 0, cos_rom[0]
|
||||
elif k < n4:
|
||||
if k < n4:
|
||||
return cos_rom[k], cos_rom[n4 - k]
|
||||
else:
|
||||
return sign_extend((-cos_rom[n2 - k]) & 0xFFFF, 16), cos_rom[k - n4]
|
||||
return sign_extend((-cos_rom[n2 - k]) & 0xFFFF, 16), cos_rom[k - n4]
|
||||
|
||||
|
||||
class FFTEngine:
|
||||
@@ -840,11 +827,9 @@ class FFTEngine:
|
||||
|
||||
# Multiply (49-bit products)
|
||||
if not inverse:
|
||||
# Forward: t = b * (cos + j*sin)
|
||||
prod_re = b_re * tw_cos + b_im * tw_sin
|
||||
prod_im = b_im * tw_cos - b_re * tw_sin
|
||||
else:
|
||||
# Inverse: t = b * (cos - j*sin)
|
||||
prod_re = b_re * tw_cos - b_im * tw_sin
|
||||
prod_im = b_im * tw_cos + b_re * tw_sin
|
||||
|
||||
@@ -923,10 +908,9 @@ class FreqMatchedFilter:
|
||||
# Saturation check
|
||||
if rounded > 0x3FFF8000:
|
||||
return 0x7FFF
|
||||
elif rounded < -0x3FFF8000:
|
||||
if rounded < -0x3FFF8000:
|
||||
return sign_extend(0x8000, 16)
|
||||
else:
|
||||
return sign_extend((rounded >> 15) & 0xFFFF, 16)
|
||||
return sign_extend((rounded >> 15) & 0xFFFF, 16)
|
||||
|
||||
out_re = round_sat_extract(real_sum)
|
||||
out_im = round_sat_extract(imag_sum)
|
||||
@@ -1061,7 +1045,6 @@ class RangeBinDecimator:
|
||||
out_im.append(best_im)
|
||||
|
||||
elif mode == 2:
|
||||
# Averaging: sum >> 4
|
||||
sum_re = 0
|
||||
sum_im = 0
|
||||
for s in range(df):
|
||||
@@ -1351,69 +1334,48 @@ def _self_test():
|
||||
"""Quick sanity checks for each module."""
|
||||
import math
|
||||
|
||||
print("=" * 60)
|
||||
print("FPGA Model Self-Test")
|
||||
print("=" * 60)
|
||||
|
||||
# --- NCO test ---
|
||||
print("\n--- NCO Test ---")
|
||||
nco = NCO()
|
||||
ftw = 0x4CCCCCCD # 120 MHz at 400 MSPS
|
||||
# Run 20 cycles to fill pipeline
|
||||
results = []
|
||||
for i in range(20):
|
||||
for _ in range(20):
|
||||
s, c, ready = nco.step(ftw)
|
||||
if ready:
|
||||
results.append((s, c))
|
||||
|
||||
if results:
|
||||
print(f" First valid output: sin={results[0][0]}, cos={results[0][1]}")
|
||||
print(f" Got {len(results)} valid outputs from 20 cycles")
|
||||
# Check quadrature: sin^2 + cos^2 should be approximately 32767^2
|
||||
s, c = results[-1]
|
||||
mag_sq = s * s + c * c
|
||||
expected = 32767 * 32767
|
||||
error_pct = abs(mag_sq - expected) / expected * 100
|
||||
print(
|
||||
f" Quadrature check: sin^2+cos^2={mag_sq}, "
|
||||
f"expected~{expected}, error={error_pct:.2f}%"
|
||||
)
|
||||
print(" NCO: OK")
|
||||
abs(mag_sq - expected) / expected * 100
|
||||
|
||||
# --- Mixer test ---
|
||||
print("\n--- Mixer Test ---")
|
||||
mixer = Mixer()
|
||||
# Test with mid-scale ADC (128) and known cos/sin
|
||||
for i in range(5):
|
||||
mi, mq, mv = mixer.step(128, 0x7FFF, 0, True, True)
|
||||
print(f" Mixer with adc=128, cos=max, sin=0: I={mi}, Q={mq}, valid={mv}")
|
||||
print(" Mixer: OK")
|
||||
for _ in range(5):
|
||||
_mi, _mq, _mv = mixer.step(128, 0x7FFF, 0, True, True)
|
||||
|
||||
# --- CIC test ---
|
||||
print("\n--- CIC Test ---")
|
||||
cic = CICDecimator()
|
||||
dc_val = sign_extend(0x1000, 18) # Small positive DC
|
||||
out_count = 0
|
||||
for i in range(100):
|
||||
out, valid = cic.step(dc_val, True)
|
||||
for _ in range(100):
|
||||
_, valid = cic.step(dc_val, True)
|
||||
if valid:
|
||||
out_count += 1
|
||||
print(f" CIC: {out_count} outputs from 100 inputs (expect ~25 with 4x decimation + pipeline)")
|
||||
print(" CIC: OK")
|
||||
|
||||
# --- FIR test ---
|
||||
print("\n--- FIR Test ---")
|
||||
fir = FIRFilter()
|
||||
out_count = 0
|
||||
for i in range(50):
|
||||
out, valid = fir.step(1000, True)
|
||||
for _ in range(50):
|
||||
_out, valid = fir.step(1000, True)
|
||||
if valid:
|
||||
out_count += 1
|
||||
print(f" FIR: {out_count} outputs from 50 inputs (expect ~43 with 7-cycle latency)")
|
||||
print(" FIR: OK")
|
||||
|
||||
# --- FFT test ---
|
||||
print("\n--- FFT Test (1024-pt) ---")
|
||||
try:
|
||||
fft = FFTEngine(n=1024)
|
||||
# Single tone at bin 10
|
||||
@@ -1425,43 +1387,28 @@ def _self_test():
|
||||
out_re, out_im = fft.compute(in_re, in_im, inverse=False)
|
||||
# Find peak bin
|
||||
max_mag = 0
|
||||
peak_bin = 0
|
||||
for i in range(512):
|
||||
mag = abs(out_re[i]) + abs(out_im[i])
|
||||
if mag > max_mag:
|
||||
max_mag = mag
|
||||
peak_bin = i
|
||||
print(f" FFT peak at bin {peak_bin} (expected 10), magnitude={max_mag}")
|
||||
# IFFT roundtrip
|
||||
rt_re, rt_im = fft.compute(out_re, out_im, inverse=True)
|
||||
max_err = max(abs(rt_re[i] - in_re[i]) for i in range(1024))
|
||||
print(f" FFT->IFFT roundtrip max error: {max_err} LSBs")
|
||||
print(" FFT: OK")
|
||||
rt_re, _rt_im = fft.compute(out_re, out_im, inverse=True)
|
||||
max(abs(rt_re[i] - in_re[i]) for i in range(1024))
|
||||
except FileNotFoundError:
|
||||
print(" FFT: SKIPPED (twiddle file not found)")
|
||||
pass
|
||||
|
||||
# --- Conjugate multiply test ---
|
||||
print("\n--- Conjugate Multiply Test ---")
|
||||
# (1+j0) * conj(1+j0) = 1+j0
|
||||
# In Q15: 32767 * 32767 -> should get close to 32767
|
||||
r, m = FreqMatchedFilter.conjugate_multiply_sample(0x7FFF, 0, 0x7FFF, 0)
|
||||
print(f" (32767+j0) * conj(32767+j0) = {r}+j{m} (expect ~32767+j0)")
|
||||
_r, _m = FreqMatchedFilter.conjugate_multiply_sample(0x7FFF, 0, 0x7FFF, 0)
|
||||
# (0+j32767) * conj(0+j32767) = (0+j32767)(0-j32767) = 32767^2 -> ~32767
|
||||
r2, m2 = FreqMatchedFilter.conjugate_multiply_sample(0, 0x7FFF, 0, 0x7FFF)
|
||||
print(f" (0+j32767) * conj(0+j32767) = {r2}+j{m2} (expect ~32767+j0)")
|
||||
print(" Conjugate Multiply: OK")
|
||||
_r2, _m2 = FreqMatchedFilter.conjugate_multiply_sample(0, 0x7FFF, 0, 0x7FFF)
|
||||
|
||||
# --- Range decimator test ---
|
||||
print("\n--- Range Bin Decimator Test ---")
|
||||
test_re = list(range(1024))
|
||||
test_im = [0] * 1024
|
||||
out_re, out_im = RangeBinDecimator.decimate(test_re, test_im, mode=0)
|
||||
print(f" Mode 0 (center): first 5 bins = {out_re[:5]} (expect [8, 24, 40, 56, 72])")
|
||||
print(" Range Decimator: OK")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ALL SELF-TESTS PASSED")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -82,8 +82,8 @@ def generate_full_long_chirp():
|
||||
for n in range(LONG_CHIRP_SAMPLES):
|
||||
t = n / FS_SYS
|
||||
phase = math.pi * chirp_rate * t * t
|
||||
re_val = int(round(Q15_MAX * SCALE * math.cos(phase)))
|
||||
im_val = int(round(Q15_MAX * SCALE * math.sin(phase)))
|
||||
re_val = round(Q15_MAX * SCALE * math.cos(phase))
|
||||
im_val = round(Q15_MAX * SCALE * math.sin(phase))
|
||||
chirp_i.append(max(-32768, min(32767, re_val)))
|
||||
chirp_q.append(max(-32768, min(32767, im_val)))
|
||||
|
||||
@@ -105,8 +105,8 @@ def generate_short_chirp():
|
||||
for n in range(SHORT_CHIRP_SAMPLES):
|
||||
t = n / FS_SYS
|
||||
phase = math.pi * chirp_rate * t * t
|
||||
re_val = int(round(Q15_MAX * SCALE * math.cos(phase)))
|
||||
im_val = int(round(Q15_MAX * SCALE * math.sin(phase)))
|
||||
re_val = round(Q15_MAX * SCALE * math.cos(phase))
|
||||
im_val = round(Q15_MAX * SCALE * math.sin(phase))
|
||||
chirp_i.append(max(-32768, min(32767, re_val)))
|
||||
chirp_q.append(max(-32768, min(32767, im_val)))
|
||||
|
||||
@@ -126,40 +126,17 @@ def write_mem_file(filename, values):
|
||||
with open(path, 'w') as f:
|
||||
for v in values:
|
||||
f.write(to_hex16(v) + '\n')
|
||||
print(f" Wrote {filename}: {len(values)} entries")
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("AERIS-10 Chirp .mem File Generator")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("Parameters:")
|
||||
print(f" CHIRP_BW = {CHIRP_BW/1e6:.1f} MHz")
|
||||
print(f" FS_SYS = {FS_SYS/1e6:.1f} MHz")
|
||||
print(f" T_LONG_CHIRP = {T_LONG_CHIRP*1e6:.1f} us")
|
||||
print(f" T_SHORT_CHIRP = {T_SHORT_CHIRP*1e6:.1f} us")
|
||||
print(f" LONG_CHIRP_SAMPLES = {LONG_CHIRP_SAMPLES}")
|
||||
print(f" SHORT_CHIRP_SAMPLES = {SHORT_CHIRP_SAMPLES}")
|
||||
print(f" FFT_SIZE = {FFT_SIZE}")
|
||||
print(f" Chirp rate (long) = {CHIRP_BW/T_LONG_CHIRP:.3e} Hz/s")
|
||||
print(f" Chirp rate (short) = {CHIRP_BW/T_SHORT_CHIRP:.3e} Hz/s")
|
||||
print(f" Q15 scale = {SCALE}")
|
||||
print()
|
||||
|
||||
# ---- Long chirp ----
|
||||
print("Generating full long chirp (3000 samples)...")
|
||||
long_i, long_q = generate_full_long_chirp()
|
||||
|
||||
# Verify first sample matches generate_reference_chirp_q15() from radar_scene.py
|
||||
# (which only generates the first 1024 samples)
|
||||
print(f" Sample[0]: I={long_i[0]:6d} Q={long_q[0]:6d}")
|
||||
print(f" Sample[1023]: I={long_i[1023]:6d} Q={long_q[1023]:6d}")
|
||||
print(f" Sample[2999]: I={long_i[2999]:6d} Q={long_q[2999]:6d}")
|
||||
|
||||
# Segment into 4 x 1024 blocks
|
||||
print()
|
||||
print("Segmenting into 4 x 1024 blocks...")
|
||||
for seg in range(LONG_SEGMENTS):
|
||||
start = seg * FFT_SIZE
|
||||
end = start + FFT_SIZE
|
||||
@@ -177,27 +154,18 @@ def main():
|
||||
seg_i.append(0)
|
||||
seg_q.append(0)
|
||||
|
||||
zero_count = FFT_SIZE - valid_count
|
||||
print(f" Seg {seg}: indices [{start}:{end-1}], "
|
||||
f"valid={valid_count}, zeros={zero_count}")
|
||||
FFT_SIZE - valid_count
|
||||
|
||||
write_mem_file(f"long_chirp_seg{seg}_i.mem", seg_i)
|
||||
write_mem_file(f"long_chirp_seg{seg}_q.mem", seg_q)
|
||||
|
||||
# ---- Short chirp ----
|
||||
print()
|
||||
print("Generating short chirp (50 samples)...")
|
||||
short_i, short_q = generate_short_chirp()
|
||||
print(f" Sample[0]: I={short_i[0]:6d} Q={short_q[0]:6d}")
|
||||
print(f" Sample[49]: I={short_i[49]:6d} Q={short_q[49]:6d}")
|
||||
|
||||
write_mem_file("short_chirp_i.mem", short_i)
|
||||
write_mem_file("short_chirp_q.mem", short_q)
|
||||
|
||||
# ---- Verification summary ----
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("Verification:")
|
||||
|
||||
# Cross-check seg0 against radar_scene.py generate_reference_chirp_q15()
|
||||
# That function generates exactly the first 1024 samples of the chirp
|
||||
@@ -206,39 +174,30 @@ def main():
|
||||
for n in range(FFT_SIZE):
|
||||
t = n / FS_SYS
|
||||
phase = math.pi * chirp_rate * t * t
|
||||
expected_i = max(-32768, min(32767, int(round(Q15_MAX * SCALE * math.cos(phase)))))
|
||||
expected_q = max(-32768, min(32767, int(round(Q15_MAX * SCALE * math.sin(phase)))))
|
||||
expected_i = max(-32768, min(32767, round(Q15_MAX * SCALE * math.cos(phase))))
|
||||
expected_q = max(-32768, min(32767, round(Q15_MAX * SCALE * math.sin(phase))))
|
||||
if long_i[n] != expected_i or long_q[n] != expected_q:
|
||||
mismatches += 1
|
||||
|
||||
if mismatches == 0:
|
||||
print(" [PASS] Seg0 matches radar_scene.py generate_reference_chirp_q15()")
|
||||
pass
|
||||
else:
|
||||
print(f" [FAIL] Seg0 has {mismatches} mismatches vs generate_reference_chirp_q15()")
|
||||
return 1
|
||||
|
||||
# Check magnitude envelope
|
||||
max_mag = max(math.sqrt(i*i + q*q) for i, q in zip(long_i, long_q))
|
||||
print(f" Max magnitude: {max_mag:.1f} (expected ~{Q15_MAX * SCALE:.1f})")
|
||||
print(f" Magnitude ratio: {max_mag / (Q15_MAX * SCALE):.6f}")
|
||||
max(math.sqrt(i*i + q*q) for i, q in zip(long_i, long_q, strict=False))
|
||||
|
||||
# Check seg3 zero padding
|
||||
seg3_i_path = os.path.join(MEM_DIR, 'long_chirp_seg3_i.mem')
|
||||
with open(seg3_i_path, 'r') as f:
|
||||
with open(seg3_i_path) as f:
|
||||
seg3_lines = [line.strip() for line in f if line.strip()]
|
||||
nonzero_seg3 = sum(1 for line in seg3_lines if line != '0000')
|
||||
print(f" Seg3 non-zero entries: {nonzero_seg3}/{len(seg3_lines)} "
|
||||
f"(expected 0 since chirp ends at sample 2999)")
|
||||
|
||||
if nonzero_seg3 == 0:
|
||||
print(" [PASS] Seg3 is all zeros (chirp 3000 samples < seg3 start 3072)")
|
||||
pass
|
||||
else:
|
||||
print(f" [WARN] Seg3 has {nonzero_seg3} non-zero entries")
|
||||
pass
|
||||
|
||||
print()
|
||||
print(f"Generated 10 .mem files in {os.path.abspath(MEM_DIR)}")
|
||||
print("Run validate_mem_files.py to do full validation.")
|
||||
print("=" * 60)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ def write_hex_32bit(filepath, samples):
|
||||
for (i_val, q_val) in samples:
|
||||
packed = ((q_val & 0xFFFF) << 16) | (i_val & 0xFFFF)
|
||||
f.write(f"{packed:08X}\n")
|
||||
print(f" Wrote {len(samples)} packed samples to {filepath}")
|
||||
|
||||
|
||||
def write_csv(filepath, headers, *columns):
|
||||
@@ -61,7 +60,6 @@ def write_csv(filepath, headers, *columns):
|
||||
for i in range(len(columns[0])):
|
||||
row = ','.join(str(col[i]) for col in columns)
|
||||
f.write(row + '\n')
|
||||
print(f" Wrote {len(columns[0])} rows to {filepath}")
|
||||
|
||||
|
||||
def write_hex_16bit(filepath, data):
|
||||
@@ -118,22 +116,19 @@ SCENARIOS = {
|
||||
|
||||
def generate_scenario(name, targets, description, base_dir):
|
||||
"""Generate input hex + golden output for one scenario."""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Scenario: {name} — {description}")
|
||||
print("Model: CLEAN (dual 16-pt FFT)")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Generate Doppler frame (32 chirps x 64 range bins)
|
||||
frame_i, frame_q = generate_doppler_frame(targets, seed=42)
|
||||
|
||||
print(f" Generated frame: {len(frame_i)} chirps x {len(frame_i[0])} range bins")
|
||||
|
||||
# ---- Write input hex file (packed 32-bit: {Q, I}) ----
|
||||
# RTL expects data streamed chirp-by-chirp: chirp0[rb0..rb63], chirp1[rb0..rb63], ...
|
||||
packed_samples = []
|
||||
for chirp in range(CHIRPS_PER_FRAME):
|
||||
for rb in range(RANGE_BINS):
|
||||
packed_samples.append((frame_i[chirp][rb], frame_q[chirp][rb]))
|
||||
packed_samples.extend(
|
||||
(frame_i[chirp][rb], frame_q[chirp][rb])
|
||||
for rb in range(RANGE_BINS)
|
||||
)
|
||||
|
||||
input_hex = os.path.join(base_dir, f"doppler_input_{name}.hex")
|
||||
write_hex_32bit(input_hex, packed_samples)
|
||||
@@ -142,8 +137,6 @@ def generate_scenario(name, targets, description, base_dir):
|
||||
dp = DopplerProcessor()
|
||||
doppler_i, doppler_q = dp.process_frame(frame_i, frame_q)
|
||||
|
||||
print(f" Doppler output: {len(doppler_i)} range bins x "
|
||||
f"{len(doppler_i[0])} doppler bins (2 sub-frames x {DOPPLER_FFT_SIZE})")
|
||||
|
||||
# ---- Write golden output CSV ----
|
||||
# Format: range_bin, doppler_bin, out_i, out_q
|
||||
@@ -168,10 +161,9 @@ def generate_scenario(name, targets, description, base_dir):
|
||||
|
||||
# ---- Write golden hex (for optional RTL $readmemh comparison) ----
|
||||
golden_hex = os.path.join(base_dir, f"doppler_golden_py_{name}.hex")
|
||||
write_hex_32bit(golden_hex, list(zip(flat_i, flat_q)))
|
||||
write_hex_32bit(golden_hex, list(zip(flat_i, flat_q, strict=False)))
|
||||
|
||||
# ---- Find peak per range bin ----
|
||||
print("\n Peak Doppler bins per range bin (top 5 by magnitude):")
|
||||
peak_info = []
|
||||
for rbin in range(RANGE_BINS):
|
||||
mags = [abs(doppler_i[rbin][d]) + abs(doppler_q[rbin][d])
|
||||
@@ -182,13 +174,11 @@ def generate_scenario(name, targets, description, base_dir):
|
||||
|
||||
# Sort by magnitude descending, show top 5
|
||||
peak_info.sort(key=lambda x: -x[2])
|
||||
for rbin, dbin, mag in peak_info[:5]:
|
||||
i_val = doppler_i[rbin][dbin]
|
||||
q_val = doppler_q[rbin][dbin]
|
||||
sf = dbin // DOPPLER_FFT_SIZE
|
||||
bin_in_sf = dbin % DOPPLER_FFT_SIZE
|
||||
print(f" rbin={rbin:2d}, dbin={dbin:2d} (sf{sf}:{bin_in_sf:2d}), mag={mag:6d}, "
|
||||
f"I={i_val:6d}, Q={q_val:6d}")
|
||||
for rbin, dbin, _mag in peak_info[:5]:
|
||||
doppler_i[rbin][dbin]
|
||||
doppler_q[rbin][dbin]
|
||||
dbin // DOPPLER_FFT_SIZE
|
||||
dbin % DOPPLER_FFT_SIZE
|
||||
|
||||
return {
|
||||
'name': name,
|
||||
@@ -200,10 +190,6 @@ def generate_scenario(name, targets, description, base_dir):
|
||||
def main():
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
print("=" * 60)
|
||||
print("Doppler Processor Co-Sim Golden Reference Generator")
|
||||
print(f"Architecture: dual {DOPPLER_FFT_SIZE}-pt FFT ({DOPPLER_TOTAL_BINS} total bins)")
|
||||
print("=" * 60)
|
||||
|
||||
scenarios_to_run = list(SCENARIOS.keys())
|
||||
|
||||
@@ -221,17 +207,9 @@ def main():
|
||||
r = generate_scenario(name, targets, description, base_dir)
|
||||
results.append(r)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print("Summary:")
|
||||
print(f"{'='*60}")
|
||||
for r in results:
|
||||
print(f" {r['name']:<15s} top peak: "
|
||||
f"rbin={r['peak_info'][0][0]}, dbin={r['peak_info'][0][1]}, "
|
||||
f"mag={r['peak_info'][0][2]}")
|
||||
for _ in results:
|
||||
pass
|
||||
|
||||
print(f"\nGenerated {len(results)} scenarios.")
|
||||
print(f"Files written to: {base_dir}")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -36,7 +36,7 @@ FFT_SIZE = 1024
|
||||
def load_hex_16bit(filepath):
|
||||
"""Load 16-bit hex file (one value per line, with optional // comments)."""
|
||||
values = []
|
||||
with open(filepath, 'r') as f:
|
||||
with open(filepath) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('//'):
|
||||
@@ -75,7 +75,6 @@ def generate_case(case_name, sig_i, sig_q, ref_i, ref_q, description, outdir,
|
||||
|
||||
Returns dict with case info and results.
|
||||
"""
|
||||
print(f"\n--- {case_name}: {description} ---")
|
||||
|
||||
assert len(sig_i) == FFT_SIZE, f"sig_i length {len(sig_i)} != {FFT_SIZE}"
|
||||
assert len(sig_q) == FFT_SIZE
|
||||
@@ -88,8 +87,6 @@ def generate_case(case_name, sig_i, sig_q, ref_i, ref_q, description, outdir,
|
||||
write_hex_16bit(os.path.join(outdir, f"mf_sig_{case_name}_q.hex"), sig_q)
|
||||
write_hex_16bit(os.path.join(outdir, f"mf_ref_{case_name}_i.hex"), ref_i)
|
||||
write_hex_16bit(os.path.join(outdir, f"mf_ref_{case_name}_q.hex"), ref_q)
|
||||
print(f" Wrote input hex: mf_sig_{case_name}_{{i,q}}.hex, "
|
||||
f"mf_ref_{case_name}_{{i,q}}.hex")
|
||||
|
||||
# Run through bit-accurate Python model
|
||||
mf = MatchedFilterChain(fft_size=FFT_SIZE)
|
||||
@@ -104,9 +101,6 @@ def generate_case(case_name, sig_i, sig_q, ref_i, ref_q, description, outdir,
|
||||
peak_mag = mag
|
||||
peak_bin = k
|
||||
|
||||
print(f" Output: {len(out_i)} samples")
|
||||
print(f" Peak bin: {peak_bin}, magnitude: {peak_mag}")
|
||||
print(f" Peak I={out_i[peak_bin]}, Q={out_q[peak_bin]}")
|
||||
|
||||
# Save golden output hex
|
||||
write_hex_16bit(os.path.join(outdir, f"mf_golden_py_i_{case_name}.hex"), out_i)
|
||||
@@ -135,10 +129,6 @@ def generate_case(case_name, sig_i, sig_q, ref_i, ref_q, description, outdir,
|
||||
def main():
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
print("=" * 60)
|
||||
print("Matched Filter Co-Sim Golden Reference Generator")
|
||||
print("Using bit-accurate Python model (fpga_model.py)")
|
||||
print("=" * 60)
|
||||
|
||||
results = []
|
||||
|
||||
@@ -158,8 +148,7 @@ def main():
|
||||
base_dir)
|
||||
results.append(r)
|
||||
else:
|
||||
print("\nWARNING: bb_mf_test / ref_chirp hex files not found.")
|
||||
print("Run radar_scene.py first.")
|
||||
pass
|
||||
|
||||
# ---- Case 2: DC autocorrelation ----
|
||||
dc_val = 0x1000 # 4096
|
||||
@@ -191,8 +180,8 @@ def main():
|
||||
sig_q = []
|
||||
for n in range(FFT_SIZE):
|
||||
angle = 2.0 * math.pi * k * n / FFT_SIZE
|
||||
sig_i.append(saturate(int(round(amp * math.cos(angle))), 16))
|
||||
sig_q.append(saturate(int(round(amp * math.sin(angle))), 16))
|
||||
sig_i.append(saturate(round(amp * math.cos(angle)), 16))
|
||||
sig_q.append(saturate(round(amp * math.sin(angle)), 16))
|
||||
ref_i = list(sig_i)
|
||||
ref_q = list(sig_q)
|
||||
r = generate_case("tone5", sig_i, sig_q, ref_i, ref_q,
|
||||
@@ -201,16 +190,9 @@ def main():
|
||||
results.append(r)
|
||||
|
||||
# ---- Summary ----
|
||||
print("\n" + "=" * 60)
|
||||
print("Summary:")
|
||||
print("=" * 60)
|
||||
for r in results:
|
||||
print(f" {r['case_name']:10s}: peak at bin {r['peak_bin']}, "
|
||||
f"mag={r['peak_mag']}, I={r['peak_i']}, Q={r['peak_q']}")
|
||||
for _ in results:
|
||||
pass
|
||||
|
||||
print(f"\nGenerated {len(results)} golden reference cases.")
|
||||
print("Files written to:", base_dir)
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -5,7 +5,7 @@ gen_multiseg_golden.py
|
||||
Generate golden reference data for matched_filter_multi_segment co-simulation.
|
||||
|
||||
Tests the overlap-save segmented convolution wrapper:
|
||||
- Long chirp: 3072 samples (4 segments × 1024, with 128-sample overlap)
|
||||
- Long chirp: 3072 samples (4 segments x 1024, with 128-sample overlap)
|
||||
- Short chirp: 50 samples zero-padded to 1024 (1 segment)
|
||||
|
||||
The matched_filter_processing_chain is already verified bit-perfect.
|
||||
@@ -234,7 +234,6 @@ def generate_long_chirp_test():
|
||||
# In radar_receiver_final.v, the DDC output is sign-extended:
|
||||
# .ddc_i({{2{adc_i_scaled[15]}}, adc_i_scaled})
|
||||
# So 16-bit -> 18-bit sign-extend -> then multi_segment does:
|
||||
# ddc_i[17:2] + ddc_i[1]
|
||||
# For sign-extended 18-bit from 16-bit:
|
||||
# ddc_i[17:2] = original 16-bit value (since bits [17:16] = sign extension)
|
||||
# ddc_i[1] = bit 1 of original value
|
||||
@@ -277,9 +276,6 @@ def generate_long_chirp_test():
|
||||
out_re, out_im = mf_chain.process(seg_data_i, seg_data_q, ref_i, ref_q)
|
||||
segment_results.append((out_re, out_im))
|
||||
|
||||
print(f" Segment {seg}: collected {buffer_write_ptr} buffer samples, "
|
||||
f"total chirp samples = {chirp_samples_collected}, "
|
||||
f"input_idx = {input_idx}")
|
||||
|
||||
# Write hex files for the testbench
|
||||
out_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -317,7 +313,6 @@ def generate_long_chirp_test():
|
||||
for b in range(1024):
|
||||
f.write(f'{seg},{b},{out_re[b]},{out_im[b]}\n')
|
||||
|
||||
print(f"\n Written {LONG_SEGMENTS * 1024} golden samples to {csv_path}")
|
||||
|
||||
return TOTAL_SAMPLES, LONG_SEGMENTS, segment_results
|
||||
|
||||
@@ -343,8 +338,8 @@ def generate_short_chirp_test():
|
||||
|
||||
# Zero-pad to 1024 (as RTL does in ST_ZERO_PAD)
|
||||
# Note: padding computed here for documentation; actual buffer uses buf_i/buf_q below
|
||||
_padded_i = list(input_i) + [0] * (BUFFER_SIZE - SHORT_SAMPLES) # noqa: F841
|
||||
_padded_q = list(input_q) + [0] * (BUFFER_SIZE - SHORT_SAMPLES) # noqa: F841
|
||||
_padded_i = list(input_i) + [0] * (BUFFER_SIZE - SHORT_SAMPLES)
|
||||
_padded_q = list(input_q) + [0] * (BUFFER_SIZE - SHORT_SAMPLES)
|
||||
|
||||
# The buffer truncation: ddc_i[17:2] + ddc_i[1]
|
||||
# For data already 16-bit sign-extended to 18: result is (val >> 2) + bit1
|
||||
@@ -381,7 +376,6 @@ def generate_short_chirp_test():
|
||||
# Write hex files
|
||||
out_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Input (18-bit)
|
||||
all_input_i_18 = []
|
||||
all_input_q_18 = []
|
||||
for n in range(SHORT_SAMPLES):
|
||||
@@ -403,19 +397,12 @@ def generate_short_chirp_test():
|
||||
for b in range(1024):
|
||||
f.write(f'{b},{out_re[b]},{out_im[b]}\n')
|
||||
|
||||
print(f" Written 1024 short chirp golden samples to {csv_path}")
|
||||
return out_re, out_im
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("Multi-Segment Matched Filter Golden Reference Generator")
|
||||
print("=" * 60)
|
||||
|
||||
print("\n--- Long Chirp (4 segments, overlap-save) ---")
|
||||
total_samples, num_segs, seg_results = generate_long_chirp_test()
|
||||
print(f" Total input samples: {total_samples}")
|
||||
print(f" Segments: {num_segs}")
|
||||
|
||||
for seg in range(num_segs):
|
||||
out_re, out_im = seg_results[seg]
|
||||
@@ -427,9 +414,7 @@ if __name__ == '__main__':
|
||||
if mag > max_mag:
|
||||
max_mag = mag
|
||||
peak_bin = b
|
||||
print(f" Seg {seg}: peak at bin {peak_bin}, magnitude {max_mag}")
|
||||
|
||||
print("\n--- Short Chirp (1 segment, zero-padded) ---")
|
||||
short_re, short_im = generate_short_chirp_test()
|
||||
max_mag = 0
|
||||
peak_bin = 0
|
||||
@@ -438,8 +423,3 @@ if __name__ == '__main__':
|
||||
if mag > max_mag:
|
||||
max_mag = mag
|
||||
peak_bin = b
|
||||
print(f" Short chirp: peak at bin {peak_bin}, magnitude {max_mag}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ALL GOLDEN FILES GENERATED")
|
||||
print("=" * 60)
|
||||
|
||||
@@ -155,7 +155,7 @@ def generate_if_chirp(n_samples, chirp_bw=CHIRP_BW, f_if=F_IF, fs=FS_ADC):
|
||||
t = n / fs
|
||||
# Instantaneous frequency: f_if - chirp_bw/2 + chirp_rate * t
|
||||
# Phase: integral of 2*pi*f(t)*dt
|
||||
_f_inst = f_if - chirp_bw / 2 + chirp_rate * t # noqa: F841 — documents instantaneous frequency formula
|
||||
_f_inst = f_if - chirp_bw / 2 + chirp_rate * t
|
||||
phase = 2 * math.pi * (f_if - chirp_bw / 2) * t + math.pi * chirp_rate * t * t
|
||||
chirp_i.append(math.cos(phase))
|
||||
chirp_q.append(math.sin(phase))
|
||||
@@ -163,7 +163,7 @@ def generate_if_chirp(n_samples, chirp_bw=CHIRP_BW, f_if=F_IF, fs=FS_ADC):
|
||||
return chirp_i, chirp_q
|
||||
|
||||
|
||||
def generate_reference_chirp_q15(n_fft=FFT_SIZE, chirp_bw=CHIRP_BW, f_if=F_IF, fs=FS_ADC):
|
||||
def generate_reference_chirp_q15(n_fft=FFT_SIZE, chirp_bw=CHIRP_BW, _f_if=F_IF, _fs=FS_ADC):
|
||||
"""
|
||||
Generate a reference chirp in Q15 format for the matched filter.
|
||||
|
||||
@@ -190,8 +190,8 @@ def generate_reference_chirp_q15(n_fft=FFT_SIZE, chirp_bw=CHIRP_BW, f_if=F_IF, f
|
||||
# The beat frequency from a target at delay tau is: f_beat = chirp_rate * tau
|
||||
# Reference chirp is the TX chirp at baseband (zero delay)
|
||||
phase = math.pi * chirp_rate * t * t
|
||||
re_val = int(round(32767 * 0.9 * math.cos(phase)))
|
||||
im_val = int(round(32767 * 0.9 * math.sin(phase)))
|
||||
re_val = round(32767 * 0.9 * math.cos(phase))
|
||||
im_val = round(32767 * 0.9 * math.sin(phase))
|
||||
ref_re[n] = max(-32768, min(32767, re_val))
|
||||
ref_im[n] = max(-32768, min(32767, im_val))
|
||||
|
||||
@@ -284,7 +284,7 @@ def generate_adc_samples(targets, n_samples, noise_stddev=3.0,
|
||||
# Quantize to 8-bit unsigned (0-255), centered at 128
|
||||
adc_samples = []
|
||||
for val in adc_float:
|
||||
quantized = int(round(val + 128))
|
||||
quantized = round(val + 128)
|
||||
quantized = max(0, min(255, quantized))
|
||||
adc_samples.append(quantized)
|
||||
|
||||
@@ -346,8 +346,8 @@ def generate_baseband_samples(targets, n_samples_baseband, noise_stddev=0.5,
|
||||
bb_i = []
|
||||
bb_q = []
|
||||
for n in range(n_samples_baseband):
|
||||
i_val = int(round(bb_i_float[n] + noise_stddev * rand_gaussian()))
|
||||
q_val = int(round(bb_q_float[n] + noise_stddev * rand_gaussian()))
|
||||
i_val = round(bb_i_float[n] + noise_stddev * rand_gaussian())
|
||||
q_val = round(bb_q_float[n] + noise_stddev * rand_gaussian())
|
||||
bb_i.append(max(-32768, min(32767, i_val)))
|
||||
bb_q.append(max(-32768, min(32767, q_val)))
|
||||
|
||||
@@ -398,15 +398,13 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
|
||||
for target in targets:
|
||||
# Which range bin does this target fall in?
|
||||
# After matched filter + range decimation:
|
||||
# range_bin = target_delay_in_baseband_samples / decimation_factor
|
||||
delay_baseband_samples = target.delay_s * FS_SYS
|
||||
range_bin_float = delay_baseband_samples * n_range_bins / FFT_SIZE
|
||||
range_bin = int(round(range_bin_float))
|
||||
range_bin = round(range_bin_float)
|
||||
|
||||
if range_bin < 0 or range_bin >= n_range_bins:
|
||||
continue
|
||||
|
||||
# Amplitude (simplified)
|
||||
amp = target.amplitude / 4.0
|
||||
|
||||
# Doppler phase for this chirp.
|
||||
@@ -426,10 +424,7 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
|
||||
rb = range_bin + delta
|
||||
if 0 <= rb < n_range_bins:
|
||||
# sinc-like weighting
|
||||
if delta == 0:
|
||||
weight = 1.0
|
||||
else:
|
||||
weight = 0.2 / abs(delta)
|
||||
weight = 1.0 if delta == 0 else 0.2 / abs(delta)
|
||||
chirp_i[rb] += amp * weight * math.cos(total_phase)
|
||||
chirp_q[rb] += amp * weight * math.sin(total_phase)
|
||||
|
||||
@@ -437,8 +432,8 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
|
||||
row_i = []
|
||||
row_q = []
|
||||
for rb in range(n_range_bins):
|
||||
i_val = int(round(chirp_i[rb] + noise_stddev * rand_gaussian()))
|
||||
q_val = int(round(chirp_q[rb] + noise_stddev * rand_gaussian()))
|
||||
i_val = round(chirp_i[rb] + noise_stddev * rand_gaussian())
|
||||
q_val = round(chirp_q[rb] + noise_stddev * rand_gaussian())
|
||||
row_i.append(max(-32768, min(32767, i_val)))
|
||||
row_q.append(max(-32768, min(32767, q_val)))
|
||||
|
||||
@@ -466,7 +461,7 @@ def write_hex_file(filepath, samples, bits=8):
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(f"// {len(samples)} samples, {bits}-bit, hex format for $readmemh\n")
|
||||
for i, s in enumerate(samples):
|
||||
for _i, s in enumerate(samples):
|
||||
if bits <= 8:
|
||||
val = s & 0xFF
|
||||
elif bits <= 16:
|
||||
@@ -477,7 +472,6 @@ def write_hex_file(filepath, samples, bits=8):
|
||||
val = s & ((1 << bits) - 1)
|
||||
f.write(fmt.format(val) + "\n")
|
||||
|
||||
print(f" Wrote {len(samples)} samples to {filepath}")
|
||||
|
||||
|
||||
def write_csv_file(filepath, columns, headers=None):
|
||||
@@ -497,7 +491,6 @@ def write_csv_file(filepath, columns, headers=None):
|
||||
row = [str(col[i]) for col in columns]
|
||||
f.write(",".join(row) + "\n")
|
||||
|
||||
print(f" Wrote {n_rows} rows to {filepath}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -510,10 +503,6 @@ def scenario_single_target(range_m=500, velocity=0, rcs=0, n_adc_samples=16384):
|
||||
Good for validating matched filter range response.
|
||||
"""
|
||||
target = Target(range_m=range_m, velocity_mps=velocity, rcs_dbsm=rcs)
|
||||
print(f"Scenario: Single target at {range_m}m")
|
||||
print(f" {target}")
|
||||
print(f" Beat freq: {CHIRP_BW / T_LONG_CHIRP * target.delay_s:.0f} Hz")
|
||||
print(f" Delay: {target.delay_samples:.1f} ADC samples")
|
||||
|
||||
adc = generate_adc_samples([target], n_adc_samples, noise_stddev=2.0)
|
||||
return adc, [target]
|
||||
@@ -528,9 +517,8 @@ def scenario_two_targets(n_adc_samples=16384):
|
||||
Target(range_m=300, velocity_mps=0, rcs_dbsm=10, phase_deg=0),
|
||||
Target(range_m=315, velocity_mps=0, rcs_dbsm=10, phase_deg=45),
|
||||
]
|
||||
print("Scenario: Two targets (range resolution test)")
|
||||
for t in targets:
|
||||
print(f" {t}")
|
||||
for _t in targets:
|
||||
pass
|
||||
|
||||
adc = generate_adc_samples(targets, n_adc_samples, noise_stddev=2.0)
|
||||
return adc, targets
|
||||
@@ -547,9 +535,8 @@ def scenario_multi_target(n_adc_samples=16384):
|
||||
Target(range_m=2000, velocity_mps=50, rcs_dbsm=0, phase_deg=45),
|
||||
Target(range_m=5000, velocity_mps=-5, rcs_dbsm=-5, phase_deg=270),
|
||||
]
|
||||
print("Scenario: Multi-target (5 targets)")
|
||||
for t in targets:
|
||||
print(f" {t}")
|
||||
for _t in targets:
|
||||
pass
|
||||
|
||||
adc = generate_adc_samples(targets, n_adc_samples, noise_stddev=3.0)
|
||||
return adc, targets
|
||||
@@ -559,7 +546,6 @@ def scenario_noise_only(n_adc_samples=16384, noise_stddev=5.0):
|
||||
"""
|
||||
Noise-only scene — baseline for false alarm characterization.
|
||||
"""
|
||||
print(f"Scenario: Noise only (stddev={noise_stddev})")
|
||||
adc = generate_adc_samples([], n_adc_samples, noise_stddev=noise_stddev)
|
||||
return adc, []
|
||||
|
||||
@@ -568,7 +554,6 @@ def scenario_dc_tone(n_adc_samples=16384, adc_value=128):
|
||||
"""
|
||||
DC input — validates CIC decimation and DC response.
|
||||
"""
|
||||
print(f"Scenario: DC tone (ADC value={adc_value})")
|
||||
return [adc_value] * n_adc_samples, []
|
||||
|
||||
|
||||
@@ -576,11 +561,10 @@ def scenario_sine_wave(n_adc_samples=16384, freq_hz=1e6, amplitude=50):
|
||||
"""
|
||||
Pure sine wave at ADC input — validates NCO/mixer frequency response.
|
||||
"""
|
||||
print(f"Scenario: Sine wave at {freq_hz/1e6:.1f} MHz, amplitude={amplitude}")
|
||||
adc = []
|
||||
for n in range(n_adc_samples):
|
||||
t = n / FS_ADC
|
||||
val = int(round(128 + amplitude * math.sin(2 * math.pi * freq_hz * t)))
|
||||
val = round(128 + amplitude * math.sin(2 * math.pi * freq_hz * t))
|
||||
adc.append(max(0, min(255, val)))
|
||||
return adc, []
|
||||
|
||||
@@ -606,46 +590,35 @@ def generate_all_test_vectors(output_dir=None):
|
||||
if output_dir is None:
|
||||
output_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
print("=" * 60)
|
||||
print("Generating AERIS-10 Test Vectors")
|
||||
print(f"Output directory: {output_dir}")
|
||||
print("=" * 60)
|
||||
|
||||
n_adc = 16384 # ~41 us of ADC data
|
||||
|
||||
# --- Scenario 1: Single target ---
|
||||
print("\n--- Scenario 1: Single Target ---")
|
||||
adc1, targets1 = scenario_single_target(range_m=500, n_adc_samples=n_adc)
|
||||
write_hex_file(os.path.join(output_dir, "adc_single_target.hex"), adc1, bits=8)
|
||||
|
||||
# --- Scenario 2: Multi-target ---
|
||||
print("\n--- Scenario 2: Multi-Target ---")
|
||||
adc2, targets2 = scenario_multi_target(n_adc_samples=n_adc)
|
||||
write_hex_file(os.path.join(output_dir, "adc_multi_target.hex"), adc2, bits=8)
|
||||
|
||||
# --- Scenario 3: Noise only ---
|
||||
print("\n--- Scenario 3: Noise Only ---")
|
||||
adc3, _ = scenario_noise_only(n_adc_samples=n_adc)
|
||||
write_hex_file(os.path.join(output_dir, "adc_noise_only.hex"), adc3, bits=8)
|
||||
|
||||
# --- Scenario 4: DC ---
|
||||
print("\n--- Scenario 4: DC Input ---")
|
||||
adc4, _ = scenario_dc_tone(n_adc_samples=n_adc)
|
||||
write_hex_file(os.path.join(output_dir, "adc_dc.hex"), adc4, bits=8)
|
||||
|
||||
# --- Scenario 5: Sine wave ---
|
||||
print("\n--- Scenario 5: 1 MHz Sine ---")
|
||||
adc5, _ = scenario_sine_wave(n_adc_samples=n_adc, freq_hz=1e6, amplitude=50)
|
||||
write_hex_file(os.path.join(output_dir, "adc_sine_1mhz.hex"), adc5, bits=8)
|
||||
|
||||
# --- Reference chirp for matched filter ---
|
||||
print("\n--- Reference Chirp ---")
|
||||
ref_re, ref_im = generate_reference_chirp_q15()
|
||||
write_hex_file(os.path.join(output_dir, "ref_chirp_i.hex"), ref_re, bits=16)
|
||||
write_hex_file(os.path.join(output_dir, "ref_chirp_q.hex"), ref_im, bits=16)
|
||||
|
||||
# --- Baseband samples for matched filter test (bypass DDC) ---
|
||||
print("\n--- Baseband Samples (bypass DDC) ---")
|
||||
bb_targets = [
|
||||
Target(range_m=500, velocity_mps=0, rcs_dbsm=10),
|
||||
Target(range_m=1500, velocity_mps=20, rcs_dbsm=5),
|
||||
@@ -655,7 +628,6 @@ def generate_all_test_vectors(output_dir=None):
|
||||
write_hex_file(os.path.join(output_dir, "bb_mf_test_q.hex"), bb_q, bits=16)
|
||||
|
||||
# --- Scenario info CSV ---
|
||||
print("\n--- Scenario Info ---")
|
||||
with open(os.path.join(output_dir, "scenario_info.txt"), 'w') as f:
|
||||
f.write("AERIS-10 Test Vector Scenarios\n")
|
||||
f.write("=" * 60 + "\n\n")
|
||||
@@ -685,11 +657,7 @@ def generate_all_test_vectors(output_dir=None):
|
||||
for t in bb_targets:
|
||||
f.write(f" {t}\n")
|
||||
|
||||
print(f"\n Wrote scenario info to {os.path.join(output_dir, 'scenario_info.txt')}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("ALL TEST VECTORS GENERATED")
|
||||
print("=" * 60)
|
||||
|
||||
return {
|
||||
'adc_single': adc1,
|
||||
|
||||
@@ -69,7 +69,6 @@ FIR_COEFFS_HEX = [
|
||||
# DDC output interface
|
||||
DDC_OUT_BITS = 16 # 18 → 16 bit with rounding + saturation
|
||||
|
||||
# FFT (Range)
|
||||
FFT_SIZE = 1024
|
||||
FFT_DATA_W = 16
|
||||
FFT_INTERNAL_W = 32
|
||||
@@ -148,21 +147,15 @@ def load_and_quantize_adi_data(data_path, config_path, frame_idx=0):
|
||||
4. Upconvert to 120 MHz IF (add I*cos - Q*sin) to create real signal
|
||||
5. Quantize to 8-bit unsigned (matching AD9484)
|
||||
"""
|
||||
print(f"[LOAD] Loading ADI dataset from {data_path}")
|
||||
data = np.load(data_path, allow_pickle=True)
|
||||
config = np.load(config_path, allow_pickle=True)
|
||||
|
||||
print(f" Shape: {data.shape}, dtype: {data.dtype}")
|
||||
print(f" Config: sample_rate={config[0]:.0f}, IF={config[1]:.0f}, "
|
||||
f"RF={config[2]:.0f}, chirps={config[3]:.0f}, BW={config[4]:.0f}, "
|
||||
f"ramp={config[5]:.6f}s")
|
||||
|
||||
# Extract one frame
|
||||
frame = data[frame_idx] # (256, 1079) complex
|
||||
|
||||
# Use first 32 chirps, first 1024 samples
|
||||
iq_block = frame[:DOPPLER_CHIRPS, :FFT_SIZE] # (32, 1024) complex
|
||||
print(f" Using frame {frame_idx}: {DOPPLER_CHIRPS} chirps x {FFT_SIZE} samples")
|
||||
|
||||
# The ADI data is baseband complex IQ at 4 MSPS.
|
||||
# AERIS-10 sees a real signal at 400 MSPS with 120 MHz IF.
|
||||
@@ -197,9 +190,6 @@ def load_and_quantize_adi_data(data_path, config_path, frame_idx=0):
|
||||
iq_i = np.clip(iq_i, -32768, 32767)
|
||||
iq_q = np.clip(iq_q, -32768, 32767)
|
||||
|
||||
print(f" Scaled to 16-bit (peak target {INPUT_PEAK_TARGET}): "
|
||||
f"I range [{iq_i.min()}, {iq_i.max()}], "
|
||||
f"Q range [{iq_q.min()}, {iq_q.max()}]")
|
||||
|
||||
# Also create 8-bit ADC stimulus for DDC validation
|
||||
# Use just one chirp of real-valued data (I channel only, shifted to unsigned)
|
||||
@@ -243,10 +233,7 @@ def nco_lookup(phase_accum, sin_lut):
|
||||
quadrant = (lut_address >> 6) & 0x3
|
||||
|
||||
# Mirror index for odd quadrants
|
||||
if (quadrant & 1) ^ ((quadrant >> 1) & 1):
|
||||
lut_idx = (~lut_address) & 0x3F
|
||||
else:
|
||||
lut_idx = lut_address & 0x3F
|
||||
lut_idx = ~lut_address & 63 if quadrant & 1 ^ quadrant >> 1 & 1 else lut_address & 63
|
||||
|
||||
sin_abs = int(sin_lut[lut_idx])
|
||||
cos_abs = int(sin_lut[63 - lut_idx])
|
||||
@@ -294,7 +281,6 @@ def run_ddc(adc_samples):
|
||||
# Build FIR coefficients as signed integers
|
||||
fir_coeffs = np.array([hex_to_signed(c, 18) for c in FIR_COEFFS_HEX], dtype=np.int64)
|
||||
|
||||
print(f"[DDC] Processing {n_samples} ADC samples at 400 MHz")
|
||||
|
||||
# --- NCO + Mixer ---
|
||||
phase_accum = np.int64(0)
|
||||
@@ -327,7 +313,6 @@ def run_ddc(adc_samples):
|
||||
# Phase accumulator update (ignore dithering for bit-accuracy)
|
||||
phase_accum = (phase_accum + NCO_PHASE_INC) & 0xFFFFFFFF
|
||||
|
||||
print(f" Mixer output: I range [{mixed_i.min()}, {mixed_i.max()}]")
|
||||
|
||||
# --- CIC Decimator (5-stage, decimate-by-4) ---
|
||||
# Integrator section (at 400 MHz rate)
|
||||
@@ -371,7 +356,6 @@ def run_ddc(adc_samples):
|
||||
scaled = comb[CIC_STAGES - 1][k] >> CIC_GAIN_SHIFT
|
||||
cic_output[k] = saturate(scaled, CIC_OUT_BITS)
|
||||
|
||||
print(f" CIC output: {n_decimated} samples, range [{cic_output.min()}, {cic_output.max()}]")
|
||||
|
||||
# --- FIR Filter (32-tap) ---
|
||||
delay_line = np.zeros(FIR_TAPS, dtype=np.int64)
|
||||
@@ -393,7 +377,6 @@ def run_ddc(adc_samples):
|
||||
if fir_output[k] >= (1 << 17):
|
||||
fir_output[k] -= (1 << 18)
|
||||
|
||||
print(f" FIR output: range [{fir_output.min()}, {fir_output.max()}]")
|
||||
|
||||
# --- DDC Interface (18 → 16 bit) ---
|
||||
ddc_output = np.zeros(n_decimated, dtype=np.int64)
|
||||
@@ -410,7 +393,6 @@ def run_ddc(adc_samples):
|
||||
else:
|
||||
ddc_output[k] = saturate(trunc + round_bit, 16)
|
||||
|
||||
print(f" DDC output (16-bit): range [{ddc_output.min()}, {ddc_output.max()}]")
|
||||
|
||||
return ddc_output
|
||||
|
||||
@@ -421,7 +403,7 @@ def run_ddc(adc_samples):
|
||||
def load_twiddle_rom(twiddle_file):
|
||||
"""Load the quarter-wave cosine ROM from .mem file."""
|
||||
rom = []
|
||||
with open(twiddle_file, 'r') as f:
|
||||
with open(twiddle_file) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('//'):
|
||||
@@ -483,7 +465,6 @@ def run_range_fft(iq_i, iq_q, twiddle_file=None):
|
||||
# Generate twiddle factors if file not available
|
||||
cos_rom = np.round(32767 * np.cos(2 * np.pi * np.arange(N // 4) / N)).astype(np.int64)
|
||||
|
||||
print(f"[FFT] Running {N}-point range FFT (bit-accurate)")
|
||||
|
||||
# Bit-reverse and sign-extend to 32-bit internal width
|
||||
def bit_reverse(val, bits):
|
||||
@@ -521,9 +502,6 @@ def run_range_fft(iq_i, iq_q, twiddle_file=None):
|
||||
b_re = mem_re[addr_odd]
|
||||
b_im = mem_im[addr_odd]
|
||||
|
||||
# Twiddle multiply: forward FFT
|
||||
# prod_re = b_re * tw_cos + b_im * tw_sin
|
||||
# prod_im = b_im * tw_cos - b_re * tw_sin
|
||||
prod_re = b_re * tw_cos + b_im * tw_sin
|
||||
prod_im = b_im * tw_cos - b_re * tw_sin
|
||||
|
||||
@@ -546,8 +524,6 @@ def run_range_fft(iq_i, iq_q, twiddle_file=None):
|
||||
out_re[n] = saturate(mem_re[n], FFT_DATA_W)
|
||||
out_im[n] = saturate(mem_im[n], FFT_DATA_W)
|
||||
|
||||
print(f" FFT output: re range [{out_re.min()}, {out_re.max()}], "
|
||||
f"im range [{out_im.min()}, {out_im.max()}]")
|
||||
|
||||
return out_re, out_im
|
||||
|
||||
@@ -582,11 +558,6 @@ def run_range_bin_decimator(range_fft_i, range_fft_q,
|
||||
decimated_i = np.zeros((n_chirps, output_bins), dtype=np.int64)
|
||||
decimated_q = np.zeros((n_chirps, output_bins), dtype=np.int64)
|
||||
|
||||
mode_str = 'peak' if mode == 1 else 'avg' if mode == 2 else 'simple'
|
||||
print(
|
||||
f"[DECIM] Decimating {n_in}→{output_bins} bins, mode={mode_str}, "
|
||||
f"start_bin={start_bin}, {n_chirps} chirps"
|
||||
)
|
||||
|
||||
for c in range(n_chirps):
|
||||
# Index into input, skip start_bin
|
||||
@@ -635,7 +606,7 @@ def run_range_bin_decimator(range_fft_i, range_fft_q,
|
||||
# Averaging: sum group, then >> 4 (divide by 16)
|
||||
sum_i = np.int64(0)
|
||||
sum_q = np.int64(0)
|
||||
for s in range(decimation_factor):
|
||||
for _ in range(decimation_factor):
|
||||
if in_idx >= input_bins:
|
||||
break
|
||||
sum_i += int(range_fft_i[c, in_idx])
|
||||
@@ -645,9 +616,6 @@ def run_range_bin_decimator(range_fft_i, range_fft_q,
|
||||
decimated_i[c, obin] = int(sum_i) >> 4
|
||||
decimated_q[c, obin] = int(sum_q) >> 4
|
||||
|
||||
print(f" Decimated output: shape ({n_chirps}, {output_bins}), "
|
||||
f"I range [{decimated_i.min()}, {decimated_i.max()}], "
|
||||
f"Q range [{decimated_q.min()}, {decimated_q.max()}]")
|
||||
|
||||
return decimated_i, decimated_q
|
||||
|
||||
@@ -673,7 +641,6 @@ def run_doppler_fft(range_data_i, range_data_q, twiddle_file_16=None):
|
||||
n_total = DOPPLER_TOTAL_BINS
|
||||
n_sf = CHIRPS_PER_SUBFRAME
|
||||
|
||||
print(f"[DOPPLER] Processing {n_range} range bins x {n_chirps} chirps → dual {n_fft}-point FFT")
|
||||
|
||||
# Build 16-point Hamming window as signed 16-bit
|
||||
hamming = np.array([int(v) for v in HAMMING_Q15], dtype=np.int64)
|
||||
@@ -757,8 +724,6 @@ def run_doppler_fft(range_data_i, range_data_q, twiddle_file_16=None):
|
||||
doppler_map_i[rbin, bin_offset + n] = saturate(mem_re[n], 16)
|
||||
doppler_map_q[rbin, bin_offset + n] = saturate(mem_im[n], 16)
|
||||
|
||||
print(f" Doppler map: shape ({n_range}, {n_total}), "
|
||||
f"I range [{doppler_map_i.min()}, {doppler_map_i.max()}]")
|
||||
|
||||
return doppler_map_i, doppler_map_q
|
||||
|
||||
@@ -788,12 +753,10 @@ def run_mti_canceller(decim_i, decim_q, enable=True):
|
||||
mti_i = np.zeros_like(decim_i)
|
||||
mti_q = np.zeros_like(decim_q)
|
||||
|
||||
print(f"[MTI] 2-pulse canceller, enable={enable}, {n_chirps} chirps x {n_bins} bins")
|
||||
|
||||
if not enable:
|
||||
mti_i[:] = decim_i
|
||||
mti_q[:] = decim_q
|
||||
print(" Pass-through mode (MTI disabled)")
|
||||
return mti_i, mti_q
|
||||
|
||||
for c in range(n_chirps):
|
||||
@@ -809,9 +772,6 @@ def run_mti_canceller(decim_i, decim_q, enable=True):
|
||||
mti_i[c, r] = saturate(diff_i, 16)
|
||||
mti_q[c, r] = saturate(diff_q, 16)
|
||||
|
||||
print(" Chirp 0: muted (zeros)")
|
||||
print(f" Chirps 1-{n_chirps-1}: I range [{mti_i[1:].min()}, {mti_i[1:].max()}], "
|
||||
f"Q range [{mti_q[1:].min()}, {mti_q[1:].max()}]")
|
||||
return mti_i, mti_q
|
||||
|
||||
|
||||
@@ -838,17 +798,12 @@ def run_dc_notch(doppler_i, doppler_q, width=2):
|
||||
dc_notch_active = (width != 0) &&
|
||||
(bin_within_sf < width || bin_within_sf > (15 - width + 1))
|
||||
"""
|
||||
n_range, n_doppler = doppler_i.shape
|
||||
_n_range, n_doppler = doppler_i.shape
|
||||
notched_i = doppler_i.copy()
|
||||
notched_q = doppler_q.copy()
|
||||
|
||||
print(
|
||||
f"[DC NOTCH] width={width}, {n_range} range bins x "
|
||||
f"{n_doppler} Doppler bins (dual sub-frame)"
|
||||
)
|
||||
|
||||
if width == 0:
|
||||
print(" Pass-through (width=0)")
|
||||
return notched_i, notched_q
|
||||
|
||||
zeroed_count = 0
|
||||
@@ -860,7 +815,6 @@ def run_dc_notch(doppler_i, doppler_q, width=2):
|
||||
notched_q[:, dbin] = 0
|
||||
zeroed_count += 1
|
||||
|
||||
print(f" Zeroed {zeroed_count} Doppler bin columns")
|
||||
return notched_i, notched_q
|
||||
|
||||
|
||||
@@ -868,7 +822,7 @@ def run_dc_notch(doppler_i, doppler_q, width=2):
|
||||
# Stage 3e: CA-CFAR Detector (bit-accurate)
|
||||
# ===========================================================================
|
||||
def run_cfar_ca(doppler_i, doppler_q, guard=2, train=8,
|
||||
alpha_q44=0x30, mode='CA', simple_threshold=500):
|
||||
alpha_q44=0x30, mode='CA', _simple_threshold=500):
|
||||
"""
|
||||
Bit-accurate model of cfar_ca.v — Cell-Averaging CFAR detector.
|
||||
|
||||
@@ -906,9 +860,6 @@ def run_cfar_ca(doppler_i, doppler_q, guard=2, train=8,
|
||||
if train == 0:
|
||||
train = 1
|
||||
|
||||
print(f"[CFAR] mode={mode}, guard={guard}, train={train}, "
|
||||
f"alpha=0x{alpha_q44:02X} (Q4.4={alpha_q44/16:.2f}), "
|
||||
f"{n_range} range x {n_doppler} Doppler")
|
||||
|
||||
# Compute magnitudes: |I| + |Q| (17-bit unsigned, matching RTL L1 norm)
|
||||
# RTL: abs_i = I[15] ? (~I + 1) : I; abs_q = Q[15] ? (~Q + 1) : Q
|
||||
@@ -976,29 +927,19 @@ def run_cfar_ca(doppler_i, doppler_q, guard=2, train=8,
|
||||
else:
|
||||
noise_sum = leading_sum + lagging_sum # Default to CA
|
||||
|
||||
# Threshold = (alpha * noise_sum) >> ALPHA_FRAC_BITS
|
||||
# RTL: noise_product = r_alpha * noise_sum_reg (31-bit)
|
||||
# threshold = noise_product[ALPHA_FRAC_BITS +: MAG_WIDTH]
|
||||
# saturate if overflow
|
||||
noise_product = alpha_q44 * noise_sum
|
||||
threshold_raw = noise_product >> ALPHA_FRAC_BITS
|
||||
|
||||
# Saturate to MAG_WIDTH=17 bits
|
||||
MAX_MAG = (1 << 17) - 1 # 131071
|
||||
if threshold_raw > MAX_MAG:
|
||||
threshold_val = MAX_MAG
|
||||
else:
|
||||
threshold_val = int(threshold_raw)
|
||||
threshold_val = MAX_MAG if threshold_raw > MAX_MAG else int(threshold_raw)
|
||||
|
||||
# Detection: magnitude > threshold
|
||||
if int(col[cut_idx]) > threshold_val:
|
||||
detect_flags[cut_idx, dbin] = True
|
||||
total_detections += 1
|
||||
|
||||
thresholds[cut_idx, dbin] = threshold_val
|
||||
|
||||
print(f" Total detections: {total_detections}")
|
||||
print(f" Magnitude range: [{magnitudes.min()}, {magnitudes.max()}]")
|
||||
|
||||
return detect_flags, magnitudes, thresholds
|
||||
|
||||
@@ -1012,19 +953,16 @@ def run_detection(doppler_i, doppler_q, threshold=10000):
|
||||
cfar_mag = |I| + |Q| (17-bit)
|
||||
detection if cfar_mag > threshold
|
||||
"""
|
||||
print(f"[DETECT] Running magnitude threshold detection (threshold={threshold})")
|
||||
|
||||
mag = np.abs(doppler_i) + np.abs(doppler_q) # L1 norm (|I| + |Q|)
|
||||
detections = np.argwhere(mag > threshold)
|
||||
|
||||
print(f" {len(detections)} detections found")
|
||||
for d in detections[:20]: # Print first 20
|
||||
rbin, dbin = d
|
||||
m = mag[rbin, dbin]
|
||||
print(f" Range bin {rbin}, Doppler bin {dbin}: magnitude {m}")
|
||||
mag[rbin, dbin]
|
||||
|
||||
if len(detections) > 20:
|
||||
print(f" ... and {len(detections) - 20} more")
|
||||
pass
|
||||
|
||||
return mag, detections
|
||||
|
||||
@@ -1038,7 +976,6 @@ def run_float_reference(iq_i, iq_q):
|
||||
Uses the exact same RTL Hamming window coefficients (Q15) to isolate
|
||||
only the FFT fixed-point quantization error.
|
||||
"""
|
||||
print("\n[FLOAT REF] Running floating-point reference pipeline")
|
||||
|
||||
n_chirps, n_samples = iq_i.shape[0], iq_i.shape[1] if iq_i.ndim == 2 else len(iq_i)
|
||||
|
||||
@@ -1086,8 +1023,6 @@ def write_hex_files(output_dir, iq_i, iq_q, prefix="stim"):
|
||||
fi.write(signed_to_hex(int(iq_i[n]), 16) + '\n')
|
||||
fq.write(signed_to_hex(int(iq_q[n]), 16) + '\n')
|
||||
|
||||
print(f" Wrote {fn_i} ({n_samples} samples)")
|
||||
print(f" Wrote {fn_q} ({n_samples} samples)")
|
||||
|
||||
elif iq_i.ndim == 2:
|
||||
n_rows, n_cols = iq_i.shape
|
||||
@@ -1101,8 +1036,6 @@ def write_hex_files(output_dir, iq_i, iq_q, prefix="stim"):
|
||||
fi.write(signed_to_hex(int(iq_i[r, c]), 16) + '\n')
|
||||
fq.write(signed_to_hex(int(iq_q[r, c]), 16) + '\n')
|
||||
|
||||
print(f" Wrote {fn_i} ({n_rows}x{n_cols} = {n_rows * n_cols} samples)")
|
||||
print(f" Wrote {fn_q} ({n_rows}x{n_cols} = {n_rows * n_cols} samples)")
|
||||
|
||||
|
||||
def write_adc_hex(output_dir, adc_data, prefix="adc_stim"):
|
||||
@@ -1114,13 +1047,12 @@ def write_adc_hex(output_dir, adc_data, prefix="adc_stim"):
|
||||
for n in range(len(adc_data)):
|
||||
f.write(format(int(adc_data[n]) & 0xFF, '02X') + '\n')
|
||||
|
||||
print(f" Wrote {fn} ({len(adc_data)} samples)")
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Comparison metrics
|
||||
# ===========================================================================
|
||||
def compare_outputs(name, fixed_i, fixed_q, float_i, float_q):
|
||||
def compare_outputs(_name, fixed_i, fixed_q, float_i, float_q):
|
||||
"""Compare fixed-point outputs against floating-point reference.
|
||||
|
||||
Reports two metrics:
|
||||
@@ -1136,7 +1068,7 @@ def compare_outputs(name, fixed_i, fixed_q, float_i, float_q):
|
||||
|
||||
# Count saturated bins
|
||||
sat_mask = (np.abs(fi) >= 32767) | (np.abs(fq) >= 32767)
|
||||
n_saturated = np.sum(sat_mask)
|
||||
np.sum(sat_mask)
|
||||
|
||||
# Complex error — overall
|
||||
fixed_complex = fi + 1j * fq
|
||||
@@ -1145,8 +1077,8 @@ def compare_outputs(name, fixed_i, fixed_q, float_i, float_q):
|
||||
|
||||
signal_power = np.mean(np.abs(ref_complex) ** 2) + 1e-30
|
||||
noise_power = np.mean(np.abs(error) ** 2) + 1e-30
|
||||
snr_db = 10 * np.log10(signal_power / noise_power)
|
||||
max_error = np.max(np.abs(error))
|
||||
10 * np.log10(signal_power / noise_power)
|
||||
np.max(np.abs(error))
|
||||
|
||||
# Non-saturated comparison
|
||||
non_sat = ~sat_mask
|
||||
@@ -1155,17 +1087,10 @@ def compare_outputs(name, fixed_i, fixed_q, float_i, float_q):
|
||||
sig_ns = np.mean(np.abs(ref_complex[non_sat]) ** 2) + 1e-30
|
||||
noise_ns = np.mean(np.abs(error_ns) ** 2) + 1e-30
|
||||
snr_ns = 10 * np.log10(sig_ns / noise_ns)
|
||||
max_err_ns = np.max(np.abs(error_ns))
|
||||
np.max(np.abs(error_ns))
|
||||
else:
|
||||
snr_ns = 0.0
|
||||
max_err_ns = 0.0
|
||||
|
||||
print(f"\n [{name}] Comparison ({n} points):")
|
||||
print(f" Saturated: {n_saturated}/{n} ({100.0*n_saturated/n:.2f}%)")
|
||||
print(f" Overall SNR: {snr_db:.1f} dB")
|
||||
print(f" Overall max error: {max_error:.1f}")
|
||||
print(f" Non-sat SNR: {snr_ns:.1f} dB")
|
||||
print(f" Non-sat max error: {max_err_ns:.1f}")
|
||||
|
||||
return snr_ns # Return the meaningful metric
|
||||
|
||||
@@ -1198,29 +1123,19 @@ def main():
|
||||
twiddle_1024 = os.path.join(fpga_dir, "fft_twiddle_1024.mem")
|
||||
output_dir = os.path.join(script_dir, "hex")
|
||||
|
||||
print("=" * 72)
|
||||
print("AERIS-10 FPGA Golden Reference Model")
|
||||
print("Using ADI CN0566 Phaser Radar Data (10.525 GHz X-band FMCW)")
|
||||
print("=" * 72)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Load and quantize ADI data
|
||||
# -----------------------------------------------------------------------
|
||||
iq_i, iq_q, adc_8bit, config = load_and_quantize_adi_data(
|
||||
iq_i, iq_q, adc_8bit, _config = load_and_quantize_adi_data(
|
||||
amp_data, amp_config, frame_idx=args.frame
|
||||
)
|
||||
|
||||
# iq_i, iq_q: (32, 1024) int64, 16-bit range — post-DDC equivalent
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Stage 0: Data loaded and quantized to 16-bit signed")
|
||||
print(f" IQ block shape: ({iq_i.shape[0]}, {iq_i.shape[1]})")
|
||||
print(f" ADC stimulus: {len(adc_8bit)} samples (8-bit unsigned)")
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Write stimulus files
|
||||
# -----------------------------------------------------------------------
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Writing hex stimulus files for RTL testbenches")
|
||||
|
||||
# Post-DDC IQ for each chirp (for FFT + Doppler validation)
|
||||
write_hex_files(output_dir, iq_i, iq_q, "post_ddc")
|
||||
@@ -1234,8 +1149,6 @@ def main():
|
||||
# -----------------------------------------------------------------------
|
||||
# Run range FFT on first chirp (bit-accurate)
|
||||
# -----------------------------------------------------------------------
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Stage 2: Range FFT (1024-point, bit-accurate)")
|
||||
range_fft_i, range_fft_q = run_range_fft(iq_i[0], iq_q[0], twiddle_1024)
|
||||
write_hex_files(output_dir, range_fft_i, range_fft_q, "range_fft_chirp0")
|
||||
|
||||
@@ -1243,20 +1156,16 @@ def main():
|
||||
all_range_i = np.zeros((DOPPLER_CHIRPS, FFT_SIZE), dtype=np.int64)
|
||||
all_range_q = np.zeros((DOPPLER_CHIRPS, FFT_SIZE), dtype=np.int64)
|
||||
|
||||
print(f"\n Running range FFT for all {DOPPLER_CHIRPS} chirps...")
|
||||
for c in range(DOPPLER_CHIRPS):
|
||||
ri, rq = run_range_fft(iq_i[c], iq_q[c], twiddle_1024)
|
||||
all_range_i[c] = ri
|
||||
all_range_q[c] = rq
|
||||
if (c + 1) % 8 == 0:
|
||||
print(f" Chirp {c + 1}/{DOPPLER_CHIRPS} done")
|
||||
pass
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Run Doppler FFT (bit-accurate) — "direct" path (first 64 bins)
|
||||
# -----------------------------------------------------------------------
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Stage 3: Doppler FFT (dual 16-point with Hamming window)")
|
||||
print(" [direct path: first 64 range bins, no decimation]")
|
||||
twiddle_16 = os.path.join(fpga_dir, "fft_twiddle_16.mem")
|
||||
doppler_i, doppler_q = run_doppler_fft(all_range_i, all_range_q, twiddle_file_16=twiddle_16)
|
||||
write_hex_files(output_dir, doppler_i, doppler_q, "doppler_map")
|
||||
@@ -1266,8 +1175,6 @@ def main():
|
||||
# This models the actual RTL data flow:
|
||||
# range FFT → range_bin_decimator (peak detection) → Doppler
|
||||
# -----------------------------------------------------------------------
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Stage 2b: Range Bin Decimator (1024 → 64, peak detection)")
|
||||
|
||||
decim_i, decim_q = run_range_bin_decimator(
|
||||
all_range_i, all_range_q,
|
||||
@@ -1287,14 +1194,11 @@ def main():
|
||||
q_val = int(all_range_q[c, b]) & 0xFFFF
|
||||
packed = (q_val << 16) | i_val
|
||||
f.write(f"{packed:08X}\n")
|
||||
print(f" Wrote {fc_input_file} ({DOPPLER_CHIRPS * FFT_SIZE} packed IQ words)")
|
||||
|
||||
# Write decimated output reference for standalone decimator test
|
||||
write_hex_files(output_dir, decim_i, decim_q, "decimated_range")
|
||||
|
||||
# Now run Doppler on the decimated data — this is the full-chain reference
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Stage 3b: Doppler FFT on decimated data (full-chain path)")
|
||||
fc_doppler_i, fc_doppler_q = run_doppler_fft(
|
||||
decim_i, decim_q, twiddle_file_16=twiddle_16
|
||||
)
|
||||
@@ -1309,10 +1213,6 @@ def main():
|
||||
q_val = int(fc_doppler_q[rbin, dbin]) & 0xFFFF
|
||||
packed = (q_val << 16) | i_val
|
||||
f.write(f"{packed:08X}\n")
|
||||
print(
|
||||
f" Wrote {fc_doppler_packed_file} ("
|
||||
f"{DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)"
|
||||
)
|
||||
|
||||
# Save numpy arrays for the full-chain path
|
||||
np.save(os.path.join(output_dir, "decimated_range_i.npy"), decim_i)
|
||||
@@ -1325,16 +1225,12 @@ def main():
|
||||
# This models the complete RTL data flow:
|
||||
# range FFT → decimator → MTI canceller → Doppler → DC notch → CFAR
|
||||
# -----------------------------------------------------------------------
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Stage 3c: MTI Canceller (2-pulse, on decimated data)")
|
||||
mti_i, mti_q = run_mti_canceller(decim_i, decim_q, enable=True)
|
||||
write_hex_files(output_dir, mti_i, mti_q, "fullchain_mti_ref")
|
||||
np.save(os.path.join(output_dir, "fullchain_mti_i.npy"), mti_i)
|
||||
np.save(os.path.join(output_dir, "fullchain_mti_q.npy"), mti_q)
|
||||
|
||||
# Doppler on MTI-filtered data
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Stage 3b+c: Doppler FFT on MTI-filtered decimated data")
|
||||
mti_doppler_i, mti_doppler_q = run_doppler_fft(
|
||||
mti_i, mti_q, twiddle_file_16=twiddle_16
|
||||
)
|
||||
@@ -1344,8 +1240,6 @@ def main():
|
||||
|
||||
# DC notch on MTI-Doppler data
|
||||
DC_NOTCH_WIDTH = 2 # Default test value: zero bins {0, 1, 31}
|
||||
print(f"\n{'=' * 72}")
|
||||
print(f"Stage 3d: DC Notch Filter (width={DC_NOTCH_WIDTH})")
|
||||
notched_i, notched_q = run_dc_notch(mti_doppler_i, mti_doppler_q, width=DC_NOTCH_WIDTH)
|
||||
write_hex_files(output_dir, notched_i, notched_q, "fullchain_notched_ref")
|
||||
|
||||
@@ -1358,18 +1252,12 @@ def main():
|
||||
q_val = int(notched_q[rbin, dbin]) & 0xFFFF
|
||||
packed = (q_val << 16) | i_val
|
||||
f.write(f"{packed:08X}\n")
|
||||
print(
|
||||
f" Wrote {fc_notched_packed_file} ("
|
||||
f"{DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)"
|
||||
)
|
||||
|
||||
# CFAR on DC-notched data
|
||||
CFAR_GUARD = 2
|
||||
CFAR_TRAIN = 8
|
||||
CFAR_ALPHA = 0x30 # Q4.4 = 3.0
|
||||
CFAR_MODE = 'CA'
|
||||
print(f"\n{'=' * 72}")
|
||||
print(f"Stage 3e: CA-CFAR (guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})")
|
||||
cfar_flags, cfar_mag, cfar_thr = run_cfar_ca(
|
||||
notched_i, notched_q,
|
||||
guard=CFAR_GUARD, train=CFAR_TRAIN,
|
||||
@@ -1384,7 +1272,6 @@ def main():
|
||||
for dbin in range(DOPPLER_TOTAL_BINS):
|
||||
m = int(cfar_mag[rbin, dbin]) & 0x1FFFF
|
||||
f.write(f"{m:05X}\n")
|
||||
print(f" Wrote {cfar_mag_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} mag values)")
|
||||
|
||||
# 2. Threshold map (17-bit unsigned)
|
||||
cfar_thr_file = os.path.join(output_dir, "fullchain_cfar_thr.hex")
|
||||
@@ -1393,7 +1280,6 @@ def main():
|
||||
for dbin in range(DOPPLER_TOTAL_BINS):
|
||||
t = int(cfar_thr[rbin, dbin]) & 0x1FFFF
|
||||
f.write(f"{t:05X}\n")
|
||||
print(f" Wrote {cfar_thr_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} threshold values)")
|
||||
|
||||
# 3. Detection flags (1-bit per cell)
|
||||
cfar_det_file = os.path.join(output_dir, "fullchain_cfar_det.hex")
|
||||
@@ -1402,7 +1288,6 @@ def main():
|
||||
for dbin in range(DOPPLER_TOTAL_BINS):
|
||||
d = 1 if cfar_flags[rbin, dbin] else 0
|
||||
f.write(f"{d:01X}\n")
|
||||
print(f" Wrote {cfar_det_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} detection flags)")
|
||||
|
||||
# 4. Detection list (text)
|
||||
cfar_detections = np.argwhere(cfar_flags)
|
||||
@@ -1418,7 +1303,6 @@ def main():
|
||||
for det in cfar_detections:
|
||||
r, d = det
|
||||
f.write(f"{r} {d} {cfar_mag[r, d]} {cfar_thr[r, d]}\n")
|
||||
print(f" Wrote {cfar_det_list_file} ({len(cfar_detections)} detections)")
|
||||
|
||||
# Save numpy arrays
|
||||
np.save(os.path.join(output_dir, "fullchain_cfar_mag.npy"), cfar_mag)
|
||||
@@ -1426,8 +1310,6 @@ def main():
|
||||
np.save(os.path.join(output_dir, "fullchain_cfar_flags.npy"), cfar_flags)
|
||||
|
||||
# Run detection on full-chain Doppler map
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Stage 4: Detection on full-chain Doppler map")
|
||||
fc_mag, fc_detections = run_detection(fc_doppler_i, fc_doppler_q, threshold=args.threshold)
|
||||
|
||||
# Save full-chain detection reference
|
||||
@@ -1439,7 +1321,6 @@ def main():
|
||||
for d in fc_detections:
|
||||
rbin, dbin = d
|
||||
f.write(f"{rbin} {dbin} {fc_mag[rbin, dbin]}\n")
|
||||
print(f" Wrote {fc_det_file} ({len(fc_detections)} detections)")
|
||||
|
||||
# Also write detection reference as hex for RTL comparison
|
||||
fc_det_mag_file = os.path.join(output_dir, "fullchain_detection_mag.hex")
|
||||
@@ -1448,13 +1329,10 @@ def main():
|
||||
for dbin in range(DOPPLER_TOTAL_BINS):
|
||||
m = int(fc_mag[rbin, dbin]) & 0x1FFFF # 17-bit unsigned
|
||||
f.write(f"{m:05X}\n")
|
||||
print(f" Wrote {fc_det_mag_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} magnitude values)")
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Run detection on direct-path Doppler map (for backward compatibility)
|
||||
# -----------------------------------------------------------------------
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Stage 4b: Detection on direct-path Doppler map")
|
||||
mag, detections = run_detection(doppler_i, doppler_q, threshold=args.threshold)
|
||||
|
||||
# Save detection list
|
||||
@@ -1466,26 +1344,23 @@ def main():
|
||||
for d in detections:
|
||||
rbin, dbin = d
|
||||
f.write(f"{rbin} {dbin} {mag[rbin, dbin]}\n")
|
||||
print(f" Wrote {det_file} ({len(detections)} detections)")
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Float reference and comparison
|
||||
# -----------------------------------------------------------------------
|
||||
print(f"\n{'=' * 72}")
|
||||
print("Comparison: Fixed-point vs Float reference")
|
||||
|
||||
range_fft_float, doppler_float = run_float_reference(iq_i, iq_q)
|
||||
|
||||
# Compare range FFT (chirp 0)
|
||||
float_range_i = np.real(range_fft_float[0, :]).astype(np.float64)
|
||||
float_range_q = np.imag(range_fft_float[0, :]).astype(np.float64)
|
||||
snr_range = compare_outputs("Range FFT", range_fft_i, range_fft_q,
|
||||
compare_outputs("Range FFT", range_fft_i, range_fft_q,
|
||||
float_range_i, float_range_q)
|
||||
|
||||
# Compare Doppler map
|
||||
float_doppler_i = np.real(doppler_float).flatten().astype(np.float64)
|
||||
float_doppler_q = np.imag(doppler_float).flatten().astype(np.float64)
|
||||
snr_doppler = compare_outputs("Doppler FFT",
|
||||
compare_outputs("Doppler FFT",
|
||||
doppler_i.flatten(), doppler_q.flatten(),
|
||||
float_doppler_i, float_doppler_q)
|
||||
|
||||
@@ -1497,32 +1372,10 @@ def main():
|
||||
np.save(os.path.join(output_dir, "doppler_map_i.npy"), doppler_i)
|
||||
np.save(os.path.join(output_dir, "doppler_map_q.npy"), doppler_q)
|
||||
np.save(os.path.join(output_dir, "detection_mag.npy"), mag)
|
||||
print(f"\n Saved numpy reference files to {output_dir}/")
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Summary
|
||||
# -----------------------------------------------------------------------
|
||||
print(f"\n{'=' * 72}")
|
||||
print("SUMMARY")
|
||||
print(f"{'=' * 72}")
|
||||
print(f" ADI dataset: frame {args.frame} of amp_radar (CN0566, 10.525 GHz)")
|
||||
print(f" Chirps processed: {DOPPLER_CHIRPS}")
|
||||
print(f" Samples/chirp: {FFT_SIZE}")
|
||||
print(f" Range FFT: {FFT_SIZE}-point → {snr_range:.1f} dB vs float")
|
||||
print(
|
||||
f" Doppler FFT (direct): {DOPPLER_FFT_SIZE}-point Hamming "
|
||||
f"→ {snr_doppler:.1f} dB vs float"
|
||||
)
|
||||
print(f" Detections (direct): {len(detections)} (threshold={args.threshold})")
|
||||
print(" Full-chain decimator: 1024→64 peak detection")
|
||||
print(f" Full-chain detections: {len(fc_detections)} (threshold={args.threshold})")
|
||||
print(f" MTI+CFAR chain: decim → MTI → Doppler → DC notch(w={DC_NOTCH_WIDTH}) → CA-CFAR")
|
||||
print(
|
||||
f" CFAR detections: {len(cfar_detections)} "
|
||||
f"(guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})"
|
||||
)
|
||||
print(f" Hex stimulus files: {output_dir}/")
|
||||
print(" Ready for RTL co-simulation with Icarus Verilog")
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Optional plots
|
||||
@@ -1531,7 +1384,7 @@ def main():
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
|
||||
_fig, axes = plt.subplots(2, 2, figsize=(14, 10))
|
||||
|
||||
# Range FFT magnitude (chirp 0)
|
||||
range_mag = np.sqrt(range_fft_i.astype(float)**2 + range_fft_q.astype(float)**2)
|
||||
@@ -1573,11 +1426,10 @@ def main():
|
||||
plt.tight_layout()
|
||||
plot_file = os.path.join(output_dir, "golden_reference_plots.png")
|
||||
plt.savefig(plot_file, dpi=150)
|
||||
print(f"\n Saved plots to {plot_file}")
|
||||
plt.show()
|
||||
|
||||
except ImportError:
|
||||
print("\n [WARN] matplotlib not available, skipping plots")
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,25 +44,22 @@ pass_count = 0
|
||||
fail_count = 0
|
||||
warn_count = 0
|
||||
|
||||
def check(condition, label):
|
||||
def check(condition, _label):
|
||||
global pass_count, fail_count
|
||||
if condition:
|
||||
print(f" [PASS] {label}")
|
||||
pass_count += 1
|
||||
else:
|
||||
print(f" [FAIL] {label}")
|
||||
fail_count += 1
|
||||
|
||||
def warn(label):
|
||||
def warn(_label):
|
||||
global warn_count
|
||||
print(f" [WARN] {label}")
|
||||
warn_count += 1
|
||||
|
||||
def read_mem_hex(filename):
|
||||
"""Read a .mem file, return list of integer values (16-bit signed)."""
|
||||
path = os.path.join(MEM_DIR, filename)
|
||||
values = []
|
||||
with open(path, 'r') as f:
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('//'):
|
||||
@@ -79,7 +76,6 @@ def read_mem_hex(filename):
|
||||
# TEST 1: Structural validation of all .mem files
|
||||
# ============================================================================
|
||||
def test_structural():
|
||||
print("\n=== TEST 1: Structural Validation ===")
|
||||
|
||||
expected = {
|
||||
# FFT twiddle files (quarter-wave cosine ROMs)
|
||||
@@ -119,16 +115,13 @@ def test_structural():
|
||||
# TEST 2: FFT Twiddle Factor Validation
|
||||
# ============================================================================
|
||||
def test_twiddle_1024():
|
||||
print("\n=== TEST 2a: FFT Twiddle 1024 Validation ===")
|
||||
vals = read_mem_hex('fft_twiddle_1024.mem')
|
||||
|
||||
# Expected: cos(2*pi*k/1024) for k=0..255, in Q15 format
|
||||
# Q15: value = round(cos(angle) * 32767)
|
||||
max_err = 0
|
||||
err_details = []
|
||||
for k in range(min(256, len(vals))):
|
||||
angle = 2.0 * math.pi * k / 1024.0
|
||||
expected = int(round(math.cos(angle) * 32767.0))
|
||||
expected = round(math.cos(angle) * 32767.0)
|
||||
expected = max(-32768, min(32767, expected))
|
||||
actual = vals[k]
|
||||
err = abs(actual - expected)
|
||||
@@ -140,19 +133,17 @@ def test_twiddle_1024():
|
||||
check(max_err <= 1,
|
||||
f"fft_twiddle_1024.mem: max twiddle error = {max_err} LSB (tolerance: 1)")
|
||||
if err_details:
|
||||
for k, act, exp, e in err_details[:5]:
|
||||
print(f" k={k}: got {act} (0x{act & 0xFFFF:04x}), expected {exp}, err={e}")
|
||||
print(f" Max twiddle error: {max_err} LSB across {len(vals)} entries")
|
||||
for _, _act, _exp, _e in err_details[:5]:
|
||||
pass
|
||||
|
||||
|
||||
def test_twiddle_16():
|
||||
print("\n=== TEST 2b: FFT Twiddle 16 Validation ===")
|
||||
vals = read_mem_hex('fft_twiddle_16.mem')
|
||||
|
||||
max_err = 0
|
||||
for k in range(min(4, len(vals))):
|
||||
angle = 2.0 * math.pi * k / 16.0
|
||||
expected = int(round(math.cos(angle) * 32767.0))
|
||||
expected = round(math.cos(angle) * 32767.0)
|
||||
expected = max(-32768, min(32767, expected))
|
||||
actual = vals[k]
|
||||
err = abs(actual - expected)
|
||||
@@ -161,23 +152,17 @@ def test_twiddle_16():
|
||||
|
||||
check(max_err <= 1,
|
||||
f"fft_twiddle_16.mem: max twiddle error = {max_err} LSB (tolerance: 1)")
|
||||
print(f" Max twiddle error: {max_err} LSB across {len(vals)} entries")
|
||||
|
||||
# Print all 4 entries for reference
|
||||
print(" Twiddle 16 entries:")
|
||||
for k in range(min(4, len(vals))):
|
||||
angle = 2.0 * math.pi * k / 16.0
|
||||
expected = int(round(math.cos(angle) * 32767.0))
|
||||
print(f" k={k}: file=0x{vals[k] & 0xFFFF:04x} ({vals[k]:6d}), "
|
||||
f"expected=0x{expected & 0xFFFF:04x} ({expected:6d}), "
|
||||
f"err={abs(vals[k] - expected)}")
|
||||
expected = round(math.cos(angle) * 32767.0)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 3: Long Chirp .mem File Analysis
|
||||
# ============================================================================
|
||||
def test_long_chirp():
|
||||
print("\n=== TEST 3: Long Chirp .mem File Analysis ===")
|
||||
|
||||
# Load all 4 segments
|
||||
all_i = []
|
||||
@@ -193,36 +178,29 @@ def test_long_chirp():
|
||||
f"Total long chirp samples: {total_samples} (expected 4096 = 4 segs x 1024)")
|
||||
|
||||
# Compute magnitude envelope
|
||||
magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(all_i, all_q)]
|
||||
magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(all_i, all_q, strict=False)]
|
||||
max_mag = max(magnitudes)
|
||||
min_mag = min(magnitudes)
|
||||
avg_mag = sum(magnitudes) / len(magnitudes)
|
||||
min(magnitudes)
|
||||
sum(magnitudes) / len(magnitudes)
|
||||
|
||||
print(f" Magnitude: min={min_mag:.1f}, max={max_mag:.1f}, avg={avg_mag:.1f}")
|
||||
print(
|
||||
f" Max magnitude as fraction of Q15 range: "
|
||||
f"{max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)"
|
||||
)
|
||||
|
||||
# Check if this looks like it came from generate_reference_chirp_q15
|
||||
# That function uses 32767 * 0.9 scaling => max magnitude ~29490
|
||||
expected_max_from_model = 32767 * 0.9
|
||||
uses_model_scaling = max_mag > expected_max_from_model * 0.8
|
||||
if uses_model_scaling:
|
||||
print(" Scaling: CONSISTENT with radar_scene.py model (0.9 * Q15)")
|
||||
pass
|
||||
else:
|
||||
warn(f"Magnitude ({max_mag:.0f}) is much lower than expected from Python model "
|
||||
f"({expected_max_from_model:.0f}). .mem files may have unknown provenance.")
|
||||
|
||||
# Check non-zero content: how many samples are non-zero?
|
||||
nonzero_i = sum(1 for v in all_i if v != 0)
|
||||
nonzero_q = sum(1 for v in all_q if v != 0)
|
||||
print(f" Non-zero samples: I={nonzero_i}/{total_samples}, Q={nonzero_q}/{total_samples}")
|
||||
sum(1 for v in all_i if v != 0)
|
||||
sum(1 for v in all_q if v != 0)
|
||||
|
||||
# Analyze instantaneous frequency via phase differences
|
||||
# Phase = atan2(Q, I)
|
||||
phases = []
|
||||
for i_val, q_val in zip(all_i, all_q):
|
||||
for i_val, q_val in zip(all_i, all_q, strict=False):
|
||||
if abs(i_val) > 5 or abs(q_val) > 5: # Skip near-zero samples
|
||||
phases.append(math.atan2(q_val, i_val))
|
||||
else:
|
||||
@@ -243,19 +221,12 @@ def test_long_chirp():
|
||||
freq_estimates.append(f_inst)
|
||||
|
||||
if freq_estimates:
|
||||
f_start = sum(freq_estimates[:50]) / 50 if len(freq_estimates) > 50 else freq_estimates[0]
|
||||
f_end = sum(freq_estimates[-50:]) / 50 if len(freq_estimates) > 50 else freq_estimates[-1]
|
||||
sum(freq_estimates[:50]) / 50 if len(freq_estimates) > 50 else freq_estimates[0]
|
||||
sum(freq_estimates[-50:]) / 50 if len(freq_estimates) > 50 else freq_estimates[-1]
|
||||
f_min = min(freq_estimates)
|
||||
f_max = max(freq_estimates)
|
||||
f_range = f_max - f_min
|
||||
|
||||
print("\n Instantaneous frequency analysis (post-DDC baseband):")
|
||||
print(f" Start freq: {f_start/1e6:.3f} MHz")
|
||||
print(f" End freq: {f_end/1e6:.3f} MHz")
|
||||
print(f" Min freq: {f_min/1e6:.3f} MHz")
|
||||
print(f" Max freq: {f_max/1e6:.3f} MHz")
|
||||
print(f" Freq range: {f_range/1e6:.3f} MHz")
|
||||
print(f" Expected BW: {CHIRP_BW/1e6:.3f} MHz")
|
||||
|
||||
# A chirp should show frequency sweep
|
||||
is_chirp = f_range > 0.5e6 # At least 0.5 MHz sweep
|
||||
@@ -265,23 +236,19 @@ def test_long_chirp():
|
||||
# Check if bandwidth roughly matches expected
|
||||
bw_match = abs(f_range - CHIRP_BW) / CHIRP_BW < 0.5 # within 50%
|
||||
if bw_match:
|
||||
print(
|
||||
f" Bandwidth {f_range/1e6:.2f} MHz roughly matches expected "
|
||||
f"{CHIRP_BW/1e6:.2f} MHz"
|
||||
)
|
||||
pass
|
||||
else:
|
||||
warn(f"Bandwidth {f_range/1e6:.2f} MHz does NOT match expected {CHIRP_BW/1e6:.2f} MHz")
|
||||
|
||||
# Compare segment boundaries for overlap-save consistency
|
||||
# In proper overlap-save, the chirp data should be segmented at 896-sample boundaries
|
||||
# with segments being 1024-sample FFT blocks
|
||||
print("\n Segment boundary analysis:")
|
||||
for seg in range(4):
|
||||
seg_i = read_mem_hex(f'long_chirp_seg{seg}_i.mem')
|
||||
seg_q = read_mem_hex(f'long_chirp_seg{seg}_q.mem')
|
||||
seg_mags = [math.sqrt(i*i + q*q) for i, q in zip(seg_i, seg_q)]
|
||||
seg_avg = sum(seg_mags) / len(seg_mags)
|
||||
seg_max = max(seg_mags)
|
||||
seg_mags = [math.sqrt(i*i + q*q) for i, q in zip(seg_i, seg_q, strict=False)]
|
||||
sum(seg_mags) / len(seg_mags)
|
||||
max(seg_mags)
|
||||
|
||||
# Check segment 3 zero-padding (chirp is 3000 samples, seg3 starts at 3072)
|
||||
# Samples 3000-4095 should be zero (or near-zero) if chirp is exactly 3000 samples
|
||||
@@ -293,21 +260,18 @@ def test_long_chirp():
|
||||
# Wait, but the .mem files have 1024 lines with non-trivial data...
|
||||
# Let's check if seg3 has significant data
|
||||
zero_count = sum(1 for m in seg_mags if m < 2)
|
||||
print(f" Seg {seg}: avg_mag={seg_avg:.1f}, max_mag={seg_max:.1f}, "
|
||||
f"near-zero={zero_count}/{len(seg_mags)}")
|
||||
if zero_count > 500:
|
||||
print(" -> Seg 3 mostly zeros (chirp shorter than 4096 samples)")
|
||||
pass
|
||||
else:
|
||||
print(" -> Seg 3 has significant data throughout")
|
||||
pass
|
||||
else:
|
||||
print(f" Seg {seg}: avg_mag={seg_avg:.1f}, max_mag={seg_max:.1f}")
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 4: Short Chirp .mem File Analysis
|
||||
# ============================================================================
|
||||
def test_short_chirp():
|
||||
print("\n=== TEST 4: Short Chirp .mem File Analysis ===")
|
||||
|
||||
short_i = read_mem_hex('short_chirp_i.mem')
|
||||
short_q = read_mem_hex('short_chirp_q.mem')
|
||||
@@ -320,19 +284,17 @@ def test_short_chirp():
|
||||
check(len(short_i) == expected_samples,
|
||||
f"Short chirp length matches T_SHORT_CHIRP * FS_SYS = {expected_samples}")
|
||||
|
||||
magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(short_i, short_q)]
|
||||
max_mag = max(magnitudes)
|
||||
avg_mag = sum(magnitudes) / len(magnitudes)
|
||||
magnitudes = [math.sqrt(i*i + q*q) for i, q in zip(short_i, short_q, strict=False)]
|
||||
max(magnitudes)
|
||||
sum(magnitudes) / len(magnitudes)
|
||||
|
||||
print(f" Magnitude: max={max_mag:.1f}, avg={avg_mag:.1f}")
|
||||
print(f" Max as fraction of Q15: {max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)")
|
||||
|
||||
# Check non-zero
|
||||
nonzero = sum(1 for m in magnitudes if m > 1)
|
||||
check(nonzero == len(short_i), f"All {nonzero}/{len(short_i)} samples non-zero")
|
||||
|
||||
# Check it looks like a chirp (phase should be quadratic)
|
||||
phases = [math.atan2(q, i) for i, q in zip(short_i, short_q)]
|
||||
phases = [math.atan2(q, i) for i, q in zip(short_i, short_q, strict=False)]
|
||||
freq_est = []
|
||||
for n in range(1, len(phases)):
|
||||
dp = phases[n] - phases[n-1]
|
||||
@@ -343,17 +305,14 @@ def test_short_chirp():
|
||||
freq_est.append(dp * FS_SYS / (2 * math.pi))
|
||||
|
||||
if freq_est:
|
||||
f_start = freq_est[0]
|
||||
f_end = freq_est[-1]
|
||||
print(f" Freq start: {f_start/1e6:.3f} MHz, end: {f_end/1e6:.3f} MHz")
|
||||
print(f" Freq range: {abs(f_end - f_start)/1e6:.3f} MHz")
|
||||
freq_est[0]
|
||||
freq_est[-1]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 5: Generate Expected Chirp .mem and Compare
|
||||
# ============================================================================
|
||||
def test_chirp_vs_model():
|
||||
print("\n=== TEST 5: Compare .mem Files vs Python Model ===")
|
||||
|
||||
# Generate reference using the same method as radar_scene.py
|
||||
chirp_rate = CHIRP_BW / T_LONG_CHIRP # Hz/s
|
||||
@@ -365,8 +324,8 @@ def test_chirp_vs_model():
|
||||
for n in range(n_chirp):
|
||||
t = n / FS_SYS
|
||||
phase = math.pi * chirp_rate * t * t
|
||||
re_val = int(round(32767 * 0.9 * math.cos(phase)))
|
||||
im_val = int(round(32767 * 0.9 * math.sin(phase)))
|
||||
re_val = round(32767 * 0.9 * math.cos(phase))
|
||||
im_val = round(32767 * 0.9 * math.sin(phase))
|
||||
model_i.append(max(-32768, min(32767, re_val)))
|
||||
model_q.append(max(-32768, min(32767, im_val)))
|
||||
|
||||
@@ -375,37 +334,31 @@ def test_chirp_vs_model():
|
||||
mem_q = read_mem_hex('long_chirp_seg0_q.mem')
|
||||
|
||||
# Compare magnitudes
|
||||
model_mags = [math.sqrt(i*i + q*q) for i, q in zip(model_i, model_q)]
|
||||
mem_mags = [math.sqrt(i*i + q*q) for i, q in zip(mem_i, mem_q)]
|
||||
model_mags = [math.sqrt(i*i + q*q) for i, q in zip(model_i, model_q, strict=False)]
|
||||
mem_mags = [math.sqrt(i*i + q*q) for i, q in zip(mem_i, mem_q, strict=False)]
|
||||
|
||||
model_max = max(model_mags)
|
||||
mem_max = max(mem_mags)
|
||||
|
||||
print(f" Python model seg0: max_mag={model_max:.1f} (Q15 * 0.9)")
|
||||
print(f" .mem file seg0: max_mag={mem_max:.1f}")
|
||||
print(f" Ratio (mem/model): {mem_max/model_max:.4f}")
|
||||
|
||||
# Check if they match (they almost certainly won't based on magnitude analysis)
|
||||
matches = sum(1 for a, b in zip(model_i, mem_i) if a == b)
|
||||
print(f" Exact I matches: {matches}/{len(model_i)}")
|
||||
matches = sum(1 for a, b in zip(model_i, mem_i, strict=False) if a == b)
|
||||
|
||||
if matches > len(model_i) * 0.9:
|
||||
print(" -> .mem files MATCH Python model")
|
||||
pass
|
||||
else:
|
||||
warn(".mem files do NOT match Python model. They likely have different provenance.")
|
||||
# Try to detect scaling
|
||||
if mem_max > 0:
|
||||
ratio = model_max / mem_max
|
||||
print(f" Scale factor (model/mem): {ratio:.2f}")
|
||||
print(f" This suggests the .mem files used ~{1.0/ratio:.4f} scaling instead of 0.9")
|
||||
model_max / mem_max
|
||||
|
||||
# Check phase correlation (shape match regardless of scaling)
|
||||
model_phases = [math.atan2(q, i) for i, q in zip(model_i, model_q)]
|
||||
mem_phases = [math.atan2(q, i) for i, q in zip(mem_i, mem_q)]
|
||||
model_phases = [math.atan2(q, i) for i, q in zip(model_i, model_q, strict=False)]
|
||||
mem_phases = [math.atan2(q, i) for i, q in zip(mem_i, mem_q, strict=False)]
|
||||
|
||||
# Compute phase differences
|
||||
phase_diffs = []
|
||||
for mp, fp in zip(model_phases, mem_phases):
|
||||
for mp, fp in zip(model_phases, mem_phases, strict=False):
|
||||
d = mp - fp
|
||||
while d > math.pi:
|
||||
d -= 2 * math.pi
|
||||
@@ -413,12 +366,9 @@ def test_chirp_vs_model():
|
||||
d += 2 * math.pi
|
||||
phase_diffs.append(d)
|
||||
|
||||
avg_phase_diff = sum(phase_diffs) / len(phase_diffs)
|
||||
sum(phase_diffs) / len(phase_diffs)
|
||||
max_phase_diff = max(abs(d) for d in phase_diffs)
|
||||
|
||||
print("\n Phase comparison (shape regardless of amplitude):")
|
||||
print(f" Avg phase diff: {avg_phase_diff:.4f} rad ({math.degrees(avg_phase_diff):.2f} deg)")
|
||||
print(f" Max phase diff: {max_phase_diff:.4f} rad ({math.degrees(max_phase_diff):.2f} deg)")
|
||||
|
||||
phase_match = max_phase_diff < 0.5 # within 0.5 rad
|
||||
check(
|
||||
@@ -432,7 +382,6 @@ def test_chirp_vs_model():
|
||||
# TEST 6: Latency Buffer LATENCY=3187 Validation
|
||||
# ============================================================================
|
||||
def test_latency_buffer():
|
||||
print("\n=== TEST 6: Latency Buffer LATENCY=3187 Validation ===")
|
||||
|
||||
# The latency buffer delays the reference chirp data to align with
|
||||
# the matched filter processing chain output.
|
||||
@@ -491,16 +440,10 @@ def test_latency_buffer():
|
||||
f"LATENCY={LATENCY} in reasonable range [1000, 4095]")
|
||||
|
||||
# Check that the module name vs parameter is consistent
|
||||
print(f" LATENCY parameter: {LATENCY}")
|
||||
print(f" Module name: latency_buffer (parameterized, LATENCY={LATENCY})")
|
||||
# Module name was renamed from latency_buffer_2159 to latency_buffer
|
||||
# to match the actual parameterized LATENCY value. No warning needed.
|
||||
|
||||
# Validate address arithmetic won't overflow
|
||||
# read_ptr = (write_ptr - LATENCY) mod 4096
|
||||
# With 12-bit address, max write_ptr = 4095
|
||||
# When write_ptr < LATENCY: read_ptr = 4096 + write_ptr - LATENCY
|
||||
# Minimum: 4096 + 0 - 3187 = 909 (valid)
|
||||
min_read_ptr = 4096 + 0 - LATENCY
|
||||
check(min_read_ptr >= 0 and min_read_ptr < 4096,
|
||||
f"Min read_ptr after wrap = {min_read_ptr} (valid: 0..4095)")
|
||||
@@ -508,14 +451,12 @@ def test_latency_buffer():
|
||||
# The latency buffer uses valid_in gated reads, so it only counts
|
||||
# valid samples. The number of valid_in pulses between first write
|
||||
# and first read is LATENCY.
|
||||
print(f" Buffer primes after {LATENCY} valid_in pulses, then outputs continuously")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 7: Cross-check chirp memory loader addressing
|
||||
# ============================================================================
|
||||
def test_memory_addressing():
|
||||
print("\n=== TEST 7: Chirp Memory Loader Addressing ===")
|
||||
|
||||
# chirp_memory_loader_param uses: long_addr = {segment_select[1:0], sample_addr[9:0]}
|
||||
# This creates a 12-bit address: seg[1:0] ++ addr[9:0]
|
||||
@@ -541,15 +482,12 @@ def test_memory_addressing():
|
||||
# Memory is declared as: reg [15:0] long_chirp_i [0:4095]
|
||||
# $readmemh loads seg0 to [0:1023], seg1 to [1024:2047], etc.
|
||||
# Addressing via {segment_select, sample_addr} maps correctly.
|
||||
print(" Addressing scheme: {segment_select[1:0], sample_addr[9:0]} -> 12-bit address")
|
||||
print(" Memory size: [0:4095] (4096 entries) — matches 4 segments x 1024 samples")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TEST 8: Seg3 zero-padding analysis
|
||||
# ============================================================================
|
||||
def test_seg3_padding():
|
||||
print("\n=== TEST 8: Segment 3 Data Analysis ===")
|
||||
|
||||
# The long chirp has 3000 samples (30 us at 100 MHz).
|
||||
# With 4 segments of 1024 samples = 4096 total memory slots.
|
||||
@@ -578,7 +516,7 @@ def test_seg3_padding():
|
||||
seg3_i = read_mem_hex('long_chirp_seg3_i.mem')
|
||||
seg3_q = read_mem_hex('long_chirp_seg3_q.mem')
|
||||
|
||||
mags = [math.sqrt(i*i + q*q) for i, q in zip(seg3_i, seg3_q)]
|
||||
mags = [math.sqrt(i*i + q*q) for i, q in zip(seg3_i, seg3_q, strict=False)]
|
||||
|
||||
# Count trailing zeros (samples after chirp ends)
|
||||
trailing_zeros = 0
|
||||
@@ -590,14 +528,8 @@ def test_seg3_padding():
|
||||
|
||||
nonzero = sum(1 for m in mags if m > 2)
|
||||
|
||||
print(f" Seg3 non-zero samples: {nonzero}/{len(seg3_i)}")
|
||||
print(f" Seg3 trailing near-zeros: {trailing_zeros}")
|
||||
print(f" Seg3 max magnitude: {max(mags):.1f}")
|
||||
print(f" Seg3 first 5 magnitudes: {[f'{m:.1f}' for m in mags[:5]]}")
|
||||
print(f" Seg3 last 5 magnitudes: {[f'{m:.1f}' for m in mags[-5:]]}")
|
||||
|
||||
if nonzero == 1024:
|
||||
print(" -> Seg3 has data throughout (chirp extends beyond 3072 samples or is padded)")
|
||||
# This means the .mem files encode 4096 chirp samples, not 3000
|
||||
# The chirp duration used for .mem generation was different from T_LONG_CHIRP
|
||||
actual_chirp_samples = 4 * 1024 # = 4096
|
||||
@@ -607,17 +539,13 @@ def test_seg3_padding():
|
||||
f"({T_LONG_CHIRP*1e6:.1f} us)")
|
||||
elif trailing_zeros > 100:
|
||||
# Some padding at end
|
||||
actual_valid = 3072 + (1024 - trailing_zeros)
|
||||
print(f" -> Estimated valid chirp samples in .mem: ~{actual_valid}")
|
||||
3072 + (1024 - trailing_zeros)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# MAIN
|
||||
# ============================================================================
|
||||
def main():
|
||||
print("=" * 70)
|
||||
print("AERIS-10 .mem File Validation")
|
||||
print("=" * 70)
|
||||
|
||||
test_structural()
|
||||
test_twiddle_1024()
|
||||
@@ -629,13 +557,10 @@ def main():
|
||||
test_memory_addressing()
|
||||
test_seg3_padding()
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(f"SUMMARY: {pass_count} PASS, {fail_count} FAIL, {warn_count} WARN")
|
||||
if fail_count == 0:
|
||||
print("ALL CHECKS PASSED")
|
||||
pass
|
||||
else:
|
||||
print("SOME CHECKS FAILED")
|
||||
print("=" * 70)
|
||||
pass
|
||||
|
||||
return 0 if fail_count == 0 else 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user