From 2db32af1d08254ee4fe93345522ab9d911577b49 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:02:45 +0200 Subject: [PATCH] Add .mem file validator: verify FFT twiddle + chirp .mem files against radar parameters (55/56 PASS) --- .../9_2_FPGA/tb/cosim/validate_mem_files.py | 627 ++++++++++++++++++ 1 file changed, 627 insertions(+) create mode 100644 9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py diff --git a/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py b/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py new file mode 100644 index 0000000..6fa98e7 --- /dev/null +++ b/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py @@ -0,0 +1,627 @@ +#!/usr/bin/env python3 +""" +validate_mem_files.py — Validate all .mem files against AERIS-10 radar parameters. + +Checks: + 1. Structural: line counts, hex format, value ranges for all 12 .mem files + 2. FFT twiddle files: bit-exact match against cos(2*pi*k/N) in Q15 + 3. Long chirp .mem files: reverse-engineer parameters, check for chirp structure + 4. Short chirp .mem files: check length, value range, spectral content + 5. latency_buffer_2159 LATENCY=3187 parameter validation + +Usage: + python3 validate_mem_files.py +""" + +import math +import os +import sys + +# ============================================================================ +# AERIS-10 System Parameters (from radar_scene.py) +# ============================================================================ +F_CARRIER = 10.5e9 # 10.5 GHz carrier +C_LIGHT = 3.0e8 +F_IF = 120e6 # IF frequency +CHIRP_BW = 20e6 # 20 MHz sweep +FS_ADC = 400e6 # ADC sample rate +FS_SYS = 100e6 # System clock (100 MHz, after CIC 4x) +T_LONG_CHIRP = 30e-6 # 30 us long chirp +T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp +CIC_DECIMATION = 4 +FFT_SIZE = 1024 +DOPPLER_FFT_SIZE = 32 +LONG_CHIRP_SAMPLES = int(T_LONG_CHIRP * FS_SYS) # 3000 at 100 MHz + +# Overlap-save parameters +OVERLAP_SAMPLES = 128 +SEGMENT_ADVANCE = FFT_SIZE - OVERLAP_SAMPLES # 896 +LONG_SEGMENTS = 4 + +MEM_DIR = os.path.join(os.path.dirname(__file__), '..', '..') + +pass_count = 0 +fail_count = 0 +warn_count = 0 + +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): + 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: + for line in f: + line = line.strip() + if not line or line.startswith('//'): + continue + val = int(line, 16) + # Interpret as 16-bit signed + if val >= 0x8000: + val -= 0x10000 + values.append(val) + return values + + +# ============================================================================ +# 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) + 'fft_twiddle_1024.mem': {'lines': 256, 'desc': '1024-pt FFT quarter-wave cos ROM'}, + 'fft_twiddle_32.mem': {'lines': 8, 'desc': '32-pt FFT quarter-wave cos ROM'}, + # Long chirp segments (4 segments x 1024 samples each) + 'long_chirp_seg0_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 0 I'}, + 'long_chirp_seg0_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 0 Q'}, + 'long_chirp_seg1_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 1 I'}, + 'long_chirp_seg1_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 1 Q'}, + 'long_chirp_seg2_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 2 I'}, + 'long_chirp_seg2_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 2 Q'}, + 'long_chirp_seg3_i.mem': {'lines': 1024, 'desc': 'Long chirp seg 3 I'}, + 'long_chirp_seg3_q.mem': {'lines': 1024, 'desc': 'Long chirp seg 3 Q'}, + # Short chirp (50 samples) + 'short_chirp_i.mem': {'lines': 50, 'desc': 'Short chirp I'}, + 'short_chirp_q.mem': {'lines': 50, 'desc': 'Short chirp Q'}, + } + + for fname, info in expected.items(): + path = os.path.join(MEM_DIR, fname) + exists = os.path.isfile(path) + check(exists, f"{fname} exists") + if not exists: + continue + + vals = read_mem_hex(fname) + check(len(vals) == info['lines'], + f"{fname}: {len(vals)} data lines (expected {info['lines']})") + + # Check all values are in 16-bit signed range + in_range = all(-32768 <= v <= 32767 for v in vals) + check(in_range, f"{fname}: all values in [-32768, 32767]") + + +# ============================================================================ +# 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 = max(-32768, min(32767, expected)) + actual = vals[k] + err = abs(actual - expected) + if err > max_err: + max_err = err + if err > 1: + err_details.append((k, actual, expected, err)) + + 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") + + +def test_twiddle_32(): + print("\n=== TEST 2b: FFT Twiddle 32 Validation ===") + vals = read_mem_hex('fft_twiddle_32.mem') + + max_err = 0 + for k in range(min(8, len(vals))): + angle = 2.0 * math.pi * k / 32.0 + expected = int(round(math.cos(angle) * 32767.0)) + expected = max(-32768, min(32767, expected)) + actual = vals[k] + err = abs(actual - expected) + if err > max_err: + max_err = err + + check(max_err <= 1, + f"fft_twiddle_32.mem: max twiddle error = {max_err} LSB (tolerance: 1)") + print(f" Max twiddle error: {max_err} LSB across {len(vals)} entries") + + # Print all 8 entries for reference + print(" Twiddle 32 entries:") + for k in range(min(8, len(vals))): + angle = 2.0 * math.pi * k / 32.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)}") + + +# ============================================================================ +# 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 = [] + all_q = [] + 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') + all_i.extend(seg_i) + all_q.extend(seg_q) + + total_samples = len(all_i) + check(total_samples == 4096, + 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)] + max_mag = max(magnitudes) + min_mag = min(magnitudes) + avg_mag = 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: {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(f" Scaling: CONSISTENT with radar_scene.py model (0.9 * Q15)") + 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}") + + # Analyze instantaneous frequency via phase differences + # Phase = atan2(Q, I) + phases = [] + for i_val, q_val in zip(all_i, all_q): + if abs(i_val) > 5 or abs(q_val) > 5: # Skip near-zero samples + phases.append(math.atan2(q_val, i_val)) + else: + phases.append(None) + + # Compute phase differences (instantaneous frequency) + freq_estimates = [] + for n in range(1, len(phases)): + if phases[n] is not None and phases[n-1] is not None: + dp = phases[n] - phases[n-1] + # Unwrap + while dp > math.pi: + dp -= 2 * math.pi + while dp < -math.pi: + dp += 2 * math.pi + # Frequency in Hz (at 100 MHz sample rate, since these are post-DDC) + f_inst = dp * FS_SYS / (2 * math.pi) + 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] + f_min = min(freq_estimates) + f_max = max(freq_estimates) + f_range = f_max - f_min + + print(f"\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 + check(is_chirp, + f"Long chirp shows frequency sweep ({f_range/1e6:.2f} MHz > 0.5 MHz)") + + # 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 {CHIRP_BW/1e6:.2f} MHz") + 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(f"\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) + + # 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 + if seg == 3: + # Seg3 covers chirp samples 3072..4095 + # If chirp is only 3000 samples, then only samples 0..(3000-3072) = NONE are valid + # Actually chirp has 3000 samples total. Seg3 starts at index 3*1024=3072. + # So seg3 should only have 3000-3072 = -72 -> no valid chirp data! + # 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(f" -> Seg 3 mostly zeros (chirp shorter than 4096 samples)") + else: + print(f" -> Seg 3 has significant data throughout") + else: + print(f" Seg {seg}: avg_mag={seg_avg:.1f}, max_mag={seg_max:.1f}") + + +# ============================================================================ +# 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') + + check(len(short_i) == 50, f"Short chirp I: {len(short_i)} samples (expected 50)") + check(len(short_q) == 50, f"Short chirp Q: {len(short_q)} samples (expected 50)") + + # Expected: 0.5 us chirp at 100 MHz = 50 samples + expected_samples = int(T_SHORT_CHIRP * FS_SYS) + 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) + + 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)] + freq_est = [] + for n in range(1, len(phases)): + dp = phases[n] - phases[n-1] + while dp > math.pi: dp -= 2 * math.pi + while dp < -math.pi: dp += 2 * math.pi + 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") + + +# ============================================================================ +# 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 + + model_i = [] + model_q = [] + n_chirp = min(FFT_SIZE, LONG_CHIRP_SAMPLES) # 1024 + + 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))) + model_i.append(max(-32768, min(32767, re_val))) + model_q.append(max(-32768, min(32767, im_val))) + + # Read seg0 from .mem + mem_i = read_mem_hex('long_chirp_seg0_i.mem') + 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_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)}") + + if matches > len(model_i) * 0.9: + print(f" -> .mem files MATCH Python model") + else: + warn(f".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") + + # 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)] + + # Compute phase differences + phase_diffs = [] + for mp, fp in zip(model_phases, mem_phases): + d = mp - fp + while d > math.pi: d -= 2 * math.pi + while d < -math.pi: d += 2 * math.pi + phase_diffs.append(d) + + avg_phase_diff = sum(phase_diffs) / len(phase_diffs) + max_phase_diff = max(abs(d) for d in phase_diffs) + + print(f"\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(phase_match, + f"Phase shape match: max diff = {math.degrees(max_phase_diff):.1f} deg (tolerance: 28.6 deg)") + + +# ============================================================================ +# 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. + # + # The total latency through the processing chain depends on the branch: + # + # SYNTHESIS branch (fft_engine.v): + # - Load: 1024 cycles (input) + # - Forward FFT: LOG2N=10 stages x N/2=512 butterflies x 5-cycle pipeline = variable + # - Reference FFT: same + # - Conjugate multiply: 1024 cycles (4-stage pipeline in frequency_matched_filter) + # - Inverse FFT: same as forward + # - Output: 1024 cycles + # Total: roughly 3000-4000 cycles depending on pipeline fill + # + # The LATENCY=3187 value was likely determined empirically to align + # the reference chirp arriving at the processing chain with the + # correct time-domain position. + # + # Key constraint: LATENCY must be < 4096 (BRAM buffer size) + LATENCY = 3187 + BRAM_SIZE = 4096 + + check(LATENCY < BRAM_SIZE, + f"LATENCY ({LATENCY}) < BRAM size ({BRAM_SIZE})") + + # The fft_engine processes in stages: + # - LOAD: 1024 clocks (accepts input) + # - Per butterfly stage: 512 butterflies x 5 pipeline stages = ~2560 clocks + overhead + # Actually: 512 butterflies, each takes 5 cycles = 2560 per stage, 10 stages + # Total compute: 10 * 2560 = 25600 clocks + # But this is just for ONE FFT. The chain does 3 FFTs + multiply. + # + # For the SIMULATION branch, it's 1 clock per operation (behavioral). + # LATENCY=3187 doesn't apply to simulation branch behavior — + # it's the physical hardware pipeline latency. + # + # For synthesis: the latency_buffer feeds ref data to the chain via + # chirp_memory_loader_param → latency_buffer → chain. + # But wait — looking at radar_receiver_final.v: + # - mem_request drives valid_in on the latency buffer + # - The buffer delays {ref_i, ref_q} by LATENCY valid_in cycles + # - The delayed output feeds long_chirp_real/imag → chain + # + # The purpose: the chain in the SYNTHESIS branch reads reference data + # via the long_chirp_real/imag ports DURING ST_FWD_FFT (while collecting + # input samples). The reference data needs to arrive LATENCY cycles + # after the first mem_request, where LATENCY accounts for: + # - The fft_engine pipeline latency from input to output + # - Specifically, the chain processes: load 1024 → FFT → FFT → multiply → IFFT → output + # The reference is consumed during the second FFT (ST_REF_BITREV/BUTTERFLY) + # which starts after the first FFT completes. + + # For now, validate that LATENCY is reasonable (between 1000 and 4095) + check(1000 < LATENCY < 4095, + 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_2159 (historical, actual LATENCY={LATENCY})") + warn("Module name 'latency_buffer_2159' is inconsistent with LATENCY=3187 parameter") + + # 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)") + + # 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] + # Segment 0: addresses 0x000..0x3FF (0..1023) + # Segment 1: addresses 0x400..0x7FF (1024..2047) + # Segment 2: addresses 0x800..0xBFF (2048..3071) + # Segment 3: addresses 0xC00..0xFFF (3072..4095) + + for seg in range(4): + base = seg * 1024 + end = base + 1023 + addr_from_concat = (seg << 10) | 0 # {seg[1:0], 10'b0} + addr_end = (seg << 10) | 1023 + + check(addr_from_concat == base, + f"Seg {seg} base address: {{{seg}[1:0], 10'b0}} = {addr_from_concat} (expected {base})") + check(addr_end == end, + f"Seg {seg} end address: {{{seg}[1:0], 10'h3FF}} = {addr_end} (expected {end})") + + # 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. + # Segments are loaded contiguously into memory: + # Seg0: chirp samples 0..1023 + # Seg1: chirp samples 1024..2047 + # Seg2: chirp samples 2048..3071 + # Seg3: chirp samples 3072..4095 + # + # But the chirp only has 3000 samples! So seg3 should have: + # Valid chirp data at indices 0..(3000-3072-1) = NEGATIVE + # Wait — 3072 > 3000, so seg3 has NO valid chirp samples if chirp is exactly 3000. + # + # However, the overlap-save algorithm in matched_filter_multi_segment.v + # collects data differently: + # Seg0: collect 896 DDC samples, buffer[0:895], zero-pad [896:1023] + # Seg1: overlap from seg0[768:895] → buffer[0:127], collect 896 → buffer[128:1023] + # ... + # The chirp reference is indexed by segment_select + sample_addr, + # so it reads ALL 1024 values for each segment regardless. + # + # If the chirp is 3000 samples but only 4*1024=4096 slots exist, + # the question is: do the .mem files contain 3000 samples of real chirp + # data spread across 4096 slots, or something else? + + 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)] + + # Count trailing zeros (samples after chirp ends) + trailing_zeros = 0 + for m in reversed(mags): + if m < 2: + trailing_zeros += 1 + else: + break + + 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 + actual_duration = actual_chirp_samples / FS_SYS + warn(f"Chirp in .mem files appears to be {actual_chirp_samples} samples " + f"({actual_duration*1e6:.1f} us), not {LONG_CHIRP_SAMPLES} samples " + 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}") + + +# ============================================================================ +# MAIN +# ============================================================================ +def main(): + print("=" * 70) + print("AERIS-10 .mem File Validation") + print("=" * 70) + + test_structural() + test_twiddle_1024() + test_twiddle_32() + test_long_chirp() + test_short_chirp() + test_chirp_vs_model() + test_latency_buffer() + 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") + else: + print("SOME CHECKS FAILED") + print("=" * 70) + + return 0 if fail_count == 0 else 1 + + +if __name__ == '__main__': + sys.exit(main())