mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-05-14 03:42:00 +00:00
fix(fpga): TX-I — align matched-filter reference with actual post-DDC band
The DAC short/long chirp LUTs are 10..30 MHz upchirps (Hilbert-confirmed). With TX_LO=10.500 GHz, RX_LO=10.380 GHz (adf4382a_manager.h) and the 120 MHz DDC NCO (ddc_400m.v), high-side mixing places the post-DDC echo at 10..30 MHz baseband. The matched-filter reference (gen_chirp_mem.py) was generating 0..20 MHz, implicitly assuming the chirp's low edge mixed to DC. This caused a 10 MHz spectral offset and ~5 dB matched-filter loss. Adds F_BASEBAND_LOW=10e6 in both gen_chirp_mem.py and radar_scene.py, with phase formula 2*pi*F_BASEBAND_LOW*t + pi*rate*t^2 in all chirp generators. Regenerates the 6 .mem files. Adds analyze_short_chirp_mismatch.py for the Hilbert-based diagnosis. Fixes the misleading "30MHz to 10MHz" comment in plfm_chirp_controller.v and adds an end-to-end frequency plan in the LUT header. Sideband orientation (high-side at both mixers) is the conventional choice and consistent with antenna match (10.25..10.75 GHz, 8x16 patch designed at 10.5 GHz). Loopback capture would settle definitively; if either mixer is low-side the F_BASEBAND_LOW sign flips and/or chirp direction reverses.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -115,8 +115,21 @@ always @(posedge clk_120m) begin
|
||||
end
|
||||
|
||||
// Short PLFM chirp LUT initialization (too small for BRAM, keep inline)
|
||||
//
|
||||
// TX-I (analyzed 2026-04-28; tb/cosim/analyze_short_chirp_mismatch.py):
|
||||
// 60 samples @ fs_dac=120 MHz over 0.5 us, real-valued passband.
|
||||
// Hilbert analysis confirms an UPCHIRP from ~10 to ~30 MHz baseband
|
||||
// (BW ~19.4 MHz). The old comment "30MHz to 10MHz" had the sweep
|
||||
// direction reversed and is corrected below.
|
||||
//
|
||||
// End-to-end frequency plan (from adf4382a_manager.h + ddc_400m.v):
|
||||
// TX upmix: LO=10.500 GHz, high-side -> RF: 10.510..10.530 GHz
|
||||
// RX downmix: LO=10.380 GHz, high-side -> IF: 130..150 MHz
|
||||
// DDC NCO: 120 MHz exactly -> baseband: 10..30 MHz
|
||||
// The matched-filter reference in tb/cosim/gen_chirp_mem.py was updated
|
||||
// to include the +10 MHz baseband offset to match this band.
|
||||
initial begin
|
||||
// Complete Short PLFM chirp LUT (0.5us, 30MHz to 10MHz)
|
||||
// Complete Short PLFM chirp LUT (0.5us, ~10MHz to ~30MHz upchirp)
|
||||
short_chirp_lut[ 0] = 8'd255; short_chirp_lut[ 1] = 8'd237; short_chirp_lut[ 2] = 8'd187; short_chirp_lut[ 3] = 8'd118; short_chirp_lut[ 4] = 8'd 49; short_chirp_lut[ 5] = 8'd 6; short_chirp_lut[ 6] = 8'd 7; short_chirp_lut[ 7] = 8'd 54;
|
||||
short_chirp_lut[ 8] = 8'd132; short_chirp_lut[ 9] = 8'd210; short_chirp_lut[10] = 8'd253; short_chirp_lut[11] = 8'd237; short_chirp_lut[12] = 8'd167; short_chirp_lut[13] = 8'd 75; short_chirp_lut[14] = 8'd 10; short_chirp_lut[15] = 8'd 10;
|
||||
short_chirp_lut[16] = 8'd 80; short_chirp_lut[17] = 8'd180; short_chirp_lut[18] = 8'd248; short_chirp_lut[19] = 8'd237; short_chirp_lut[20] = 8'd150; short_chirp_lut[21] = 8'd 45; short_chirp_lut[22] = 8'd 1; short_chirp_lut[23] = 8'd 54;
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
7332
|
||||
7330
|
||||
730d
|
||||
7276
|
||||
70e0
|
||||
6d8f
|
||||
679c
|
||||
5e0a
|
||||
4fe8
|
||||
3c80
|
||||
2399
|
||||
05ca
|
||||
e4c2
|
||||
c380
|
||||
a653
|
||||
9271
|
||||
8d21
|
||||
9a5d
|
||||
bb20
|
||||
ebd7
|
||||
2399
|
||||
54f5
|
||||
70e0
|
||||
6ba2
|
||||
4289
|
||||
0000
|
||||
bb20
|
||||
90cb
|
||||
9729
|
||||
5c56
|
||||
1e0c
|
||||
d044
|
||||
2399
|
||||
65a3
|
||||
6dff
|
||||
325b
|
||||
d440
|
||||
9729
|
||||
9271
|
||||
9f85
|
||||
f753
|
||||
57d5
|
||||
6f35
|
||||
c9c7
|
||||
2238
|
||||
679c
|
||||
6a91
|
||||
2399
|
||||
c10f
|
||||
8d21
|
||||
b576
|
||||
8e95
|
||||
db07
|
||||
4fe8
|
||||
1e0c
|
||||
6d8f
|
||||
0d00
|
||||
57d5
|
||||
ebd7
|
||||
92e6
|
||||
ad06
|
||||
2399
|
||||
7276
|
||||
38c3
|
||||
b7b1
|
||||
92e6
|
||||
0000
|
||||
6dff
|
||||
3ef1
|
||||
b234
|
||||
9bc2
|
||||
a653
|
||||
24f9
|
||||
2399
|
||||
7219
|
||||
0173
|
||||
8de7
|
||||
e4c2
|
||||
6d8f
|
||||
290f
|
||||
956f
|
||||
d440
|
||||
6ba2
|
||||
2399
|
||||
9012
|
||||
f021
|
||||
7330
|
||||
f021
|
||||
9271
|
||||
38c3
|
||||
54f5
|
||||
9f85
|
||||
ddc8
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
0000
|
||||
0173
|
||||
05ca
|
||||
0d00
|
||||
1702
|
||||
2399
|
||||
325b
|
||||
4289
|
||||
52fa
|
||||
6208
|
||||
44e0
|
||||
6f35
|
||||
68d7
|
||||
2fbc
|
||||
dc67
|
||||
9a5d
|
||||
9201
|
||||
cda5
|
||||
2bc0
|
||||
6d8f
|
||||
730d
|
||||
6fee
|
||||
6208
|
||||
484f
|
||||
2399
|
||||
f753
|
||||
c9c7
|
||||
a3aa
|
||||
8e95
|
||||
607b
|
||||
08ad
|
||||
a82b
|
||||
90cb
|
||||
dc67
|
||||
4a8a
|
||||
716b
|
||||
24f9
|
||||
b018
|
||||
9271
|
||||
b234
|
||||
e8fe
|
||||
290f
|
||||
5e0a
|
||||
7332
|
||||
5c56
|
||||
1e0c
|
||||
d044
|
||||
9729
|
||||
f300
|
||||
643e
|
||||
59ad
|
||||
db07
|
||||
8cce
|
||||
ddc8
|
||||
607b
|
||||
54f5
|
||||
c73d
|
||||
9271
|
||||
c9c7
|
||||
2238
|
||||
679c
|
||||
6a91
|
||||
2399
|
||||
c10f
|
||||
8d21
|
||||
b576
|
||||
1e0c
|
||||
0fdf
|
||||
7330
|
||||
0fdf
|
||||
9012
|
||||
dc67
|
||||
6ba2
|
||||
2bc0
|
||||
956f
|
||||
d6f1
|
||||
6d8f
|
||||
57d5
|
||||
ebd7
|
||||
92e6
|
||||
ad06
|
||||
2399
|
||||
7276
|
||||
38c3
|
||||
b7b1
|
||||
92e6
|
||||
1b3e
|
||||
8de7
|
||||
fe8d
|
||||
7219
|
||||
dc67
|
||||
9bc2
|
||||
4dcc
|
||||
3ef1
|
||||
9201
|
||||
|
||||
188
9_Firmware/9_2_FPGA/tb/cosim/analyze_short_chirp_mismatch.py
Normal file
188
9_Firmware/9_2_FPGA/tb/cosim/analyze_short_chirp_mismatch.py
Normal file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
# ruff: noqa: T201
|
||||
"""
|
||||
analyze_short_chirp_mismatch.py — quantify TX-I matched-filter mismatch loss.
|
||||
|
||||
Background
|
||||
----------
|
||||
TX path (`plfm_chirp_controller.v:74,118-127`):
|
||||
60-sample inline LUT, 8-bit unsigned offset binary (DAC center = 128),
|
||||
played at fs_tx = 120 MHz over 0.5 us. Real-valued passband chirp.
|
||||
Module comment claims "30 MHz to 10 MHz" downchirp.
|
||||
|
||||
RX matched-filter reference (`gen_chirp_mem.py:81-101` -> `short_chirp_{i,q}.mem`):
|
||||
50-sample complex baseband, Q15, fs_rx = 100 MHz over 0.5 us.
|
||||
Generated as a 0 -> +20 MHz baseband upchirp:
|
||||
phi(t) = pi * (BW/T) * t^2, BW = 20 MHz, T = 0.5 us
|
||||
I(n) = cos(phi), Q(n) = sin(phi), scaled by 0.9*Q15
|
||||
|
||||
These are claimed by the ledger to be ~2-3 dB mismatched. This script
|
||||
derives the implied baseband chirp from the TX LUT (modeling the IF chain
|
||||
and DDC by NCO at 120 MHz, decimation 4x to 100 MHz), then computes the
|
||||
true matched-filter peak power lost to template mismatch by:
|
||||
|
||||
1. Loading the TX LUT, computing the analytic signal (Hilbert),
|
||||
verifying instantaneous-frequency trajectory + claimed bandwidth.
|
||||
2. Modeling the DDC: mix by 120 MHz NCO at 400 MHz ADC sample rate,
|
||||
low-pass + decimate 4x to recover 100 MHz baseband. Since the TX
|
||||
LUT is only at 120 MHz, we upsample 120->400 first via zero-stuff +
|
||||
filter (the radar's analog chain does this naturally).
|
||||
3. Producing the implied 50-sample Q15 baseband reference.
|
||||
4. Computing the ambiguity peak between
|
||||
a) implied-from-TX reference cross-correlated with itself
|
||||
b) implied-from-TX reference cross-correlated with the existing
|
||||
short_chirp_{i,q}.mem
|
||||
The dB ratio of (b) peak / (a) peak is the mismatch loss.
|
||||
|
||||
Output: report only. Does not modify any .mem files.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
from scipy.signal import hilbert, resample_poly
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
RTL_DIR = os.path.join(THIS_DIR, "..", "..")
|
||||
|
||||
FS_TX = 120e6 # DAC sample rate
|
||||
FS_RX = 100e6 # post-DDC processing rate
|
||||
T_CHIRP = 0.5e-6
|
||||
N_TX = 60 # samples in TX LUT
|
||||
N_RX = 50 # samples in RX reference
|
||||
|
||||
# --- Parse TX LUT inline-coded in plfm_chirp_controller.v ----------------
|
||||
def read_tx_lut() -> np.ndarray:
|
||||
path = os.path.join(RTL_DIR, "plfm_chirp_controller.v")
|
||||
with open(path) as f:
|
||||
src = f.read()
|
||||
# Capture every "short_chirp_lut[<idx>] = 8'd<value>;"
|
||||
pairs = re.findall(r"short_chirp_lut\[\s*(\d+)\s*\]\s*=\s*8'd\s*(\d+)\s*;", src)
|
||||
if len(pairs) != N_TX:
|
||||
sys.exit(f"expected {N_TX} TX LUT entries, got {len(pairs)}")
|
||||
arr = np.zeros(N_TX, dtype=np.int32)
|
||||
for idx_s, val_s in pairs:
|
||||
arr[int(idx_s)] = int(val_s)
|
||||
# Convert from 8-bit unsigned offset binary (DAC center = 128) to signed.
|
||||
return arr - 128 # int range roughly [-128, +127]
|
||||
|
||||
|
||||
# --- Parse existing RX reference .mem files -------------------------------
|
||||
def read_q15_mem(name: str) -> np.ndarray:
|
||||
path = os.path.join(RTL_DIR, name)
|
||||
out = []
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
v = int(line, 16)
|
||||
if v >= 0x8000:
|
||||
v -= 0x10000
|
||||
out.append(v)
|
||||
return np.array(out, dtype=np.int32)
|
||||
|
||||
|
||||
# --- Derive implied 50-sample baseband reference from the TX LUT ---------
|
||||
def derive_baseband_from_tx(tx: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
1) Treat tx as fs=120 MHz real samples.
|
||||
2) Compute analytic signal (Hilbert) -> single-sided spectrum copy.
|
||||
3) Find the chirp's center frequency from the analytic signal's
|
||||
mean instantaneous frequency, then mix it down to baseband by
|
||||
multiplying by exp(-j*2*pi*fc*t).
|
||||
4) Resample 120 -> 100 MHz to get exactly N_RX = 50 samples
|
||||
(matching the existing reference grid).
|
||||
5) Return as complex float64.
|
||||
"""
|
||||
x = tx.astype(np.float64)
|
||||
z = hilbert(x) # complex analytic, fs=120 MHz
|
||||
n = np.arange(len(z))
|
||||
# Instantaneous phase + frequency
|
||||
inst_phase = np.unwrap(np.angle(z))
|
||||
inst_freq = np.diff(inst_phase) * FS_TX / (2 * np.pi)
|
||||
fc = float(np.mean(inst_freq)) # rough center frequency in Hz
|
||||
# Mix to baseband
|
||||
bb_120 = z * np.exp(-1j * 2 * np.pi * fc * n / FS_TX)
|
||||
# Resample 120 MHz -> 100 MHz: use up=5, down=6 (5/6 = 100/120).
|
||||
bb_100 = resample_poly(bb_120, up=5, down=6)
|
||||
# Trim/pad to exactly N_RX samples
|
||||
if len(bb_100) >= N_RX:
|
||||
bb_100 = bb_100[:N_RX]
|
||||
else:
|
||||
bb_100 = np.concatenate([bb_100, np.zeros(N_RX - len(bb_100), dtype=complex)])
|
||||
return bb_100, fc, inst_freq
|
||||
|
||||
|
||||
# --- Mismatch loss in dB --------------------------------------------------
|
||||
def peak_corr_db(ref: np.ndarray, sig: np.ndarray) -> float:
|
||||
"""Peak |ref dot conj(sig_shifted)| over all integer shifts, normalised."""
|
||||
# Both arrays equal length; cross-correlate.
|
||||
c = np.correlate(sig, ref, mode="full")
|
||||
return 20 * np.log10(np.max(np.abs(c)) + 1e-30)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tx = read_tx_lut()
|
||||
rx_i = read_q15_mem("short_chirp_i.mem")
|
||||
rx_q = read_q15_mem("short_chirp_q.mem")
|
||||
if len(rx_i) != N_RX or len(rx_q) != N_RX:
|
||||
sys.exit(f"RX .mem files expected {N_RX} samples, got I={len(rx_i)} Q={len(rx_q)}")
|
||||
rx = (rx_i + 1j * rx_q).astype(complex)
|
||||
|
||||
# Derive implied baseband reference from TX LUT
|
||||
bb, fc, inst_freq = derive_baseband_from_tx(tx)
|
||||
|
||||
# Bandwidth check from instantaneous frequency
|
||||
f_lo, f_hi = float(np.min(inst_freq)), float(np.max(inst_freq))
|
||||
bw = f_hi - f_lo
|
||||
|
||||
print("=== TX LUT analysis ===")
|
||||
print(f" samples: {N_TX} @ {FS_TX/1e6:.0f} MHz, duration {N_TX/FS_TX*1e6:.3f} us")
|
||||
print(f" inst-freq range: {f_lo/1e6:+7.2f} MHz .. {f_hi/1e6:+7.2f} MHz")
|
||||
print(f" bandwidth swept: {bw/1e6:6.2f} MHz")
|
||||
print(f" center frequency: {fc/1e6:+7.2f} MHz (inferred from mean inst freq)")
|
||||
sweep_dir = "UP" if inst_freq[-1] > inst_freq[0] else "DOWN"
|
||||
print(f" sweep direction: {sweep_dir} (start={inst_freq[0]/1e6:+.2f} MHz, "
|
||||
f"end={inst_freq[-1]/1e6:+.2f} MHz)")
|
||||
|
||||
print()
|
||||
print("=== Existing RX reference (short_chirp_{i,q}.mem) ===")
|
||||
rx_phase = np.unwrap(np.angle(rx + 1e-30))
|
||||
rx_inst_freq = np.diff(rx_phase) * FS_RX / (2 * np.pi)
|
||||
rx_lo, rx_hi = float(np.min(rx_inst_freq)), float(np.max(rx_inst_freq))
|
||||
print(f" samples: {N_RX} @ {FS_RX/1e6:.0f} MHz")
|
||||
print(f" inst-freq range: {rx_lo/1e6:+7.2f} MHz .. {rx_hi/1e6:+7.2f} MHz")
|
||||
print(f" bandwidth swept: {(rx_hi - rx_lo)/1e6:6.2f} MHz")
|
||||
rx_sweep = "UP" if rx_inst_freq[-1] > rx_inst_freq[0] else "DOWN"
|
||||
print(f" sweep direction: {rx_sweep}")
|
||||
|
||||
print()
|
||||
print("=== Mismatch loss (matched-filter peak: implied-vs-existing) ===")
|
||||
# Normalise both to unit energy so the only thing the ratio reflects is shape.
|
||||
bb_n = bb / np.sqrt(np.sum(np.abs(bb) ** 2) + 1e-30)
|
||||
rx_n = rx / np.sqrt(np.sum(np.abs(rx) ** 2) + 1e-30)
|
||||
auto_db = peak_corr_db(bb_n, bb_n)
|
||||
cross_db = peak_corr_db(bb_n, rx_n)
|
||||
loss_db = auto_db - cross_db
|
||||
print(f" auto-correlation peak (implied vs implied): {auto_db:+6.2f} dB")
|
||||
print(f" cross-corr peak (implied vs existing RX): {cross_db:+6.2f} dB")
|
||||
print(f" MISMATCH LOSS (matched filter): {loss_db:6.2f} dB")
|
||||
print()
|
||||
|
||||
# Decision aid
|
||||
if loss_db < 0.5:
|
||||
verdict = "AGREEMENT — TX LUT and RX reference are consistent within 0.5 dB."
|
||||
elif loss_db < 2.0:
|
||||
verdict = ("MILD MISMATCH — within ledger's 2-3 dB note; refresh "
|
||||
"recommended but not blocking.")
|
||||
else:
|
||||
verdict = "SIGNIFICANT MISMATCH — RX reference should be regenerated from TX LUT."
|
||||
print(f"VERDICT: {verdict}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -24,10 +24,32 @@ Short chirp:
|
||||
T_SHORT_CHIRP and CHIRP_BW.
|
||||
|
||||
Phase model (baseband, post-DDC):
|
||||
phase(n) = pi * chirp_rate * t^2, t = n / FS_SYS
|
||||
phase(n) = 2*pi*F_BASEBAND_LOW*t + pi * chirp_rate * t^2, t = n / FS_SYS
|
||||
chirp_rate = CHIRP_BW / T_chirp
|
||||
F_BASEBAND_LOW = 10 MHz (DAC chirp low-edge frequency)
|
||||
|
||||
Scaling: 0.9 * 32767 (Q15), matching radar_scene.py generate_reference_chirp_q15()
|
||||
This produces a F_BASEBAND_LOW..(F_BASEBAND_LOW+CHIRP_BW) baseband upchirp.
|
||||
|
||||
End-to-end frequency plan (TX-I, 2026-04-28):
|
||||
DAC LUT : 10..30 MHz @ fs_dac=120 MHz (plfm_chirp_controller.v;
|
||||
Hilbert-confirmed for both
|
||||
long and short LUTs)
|
||||
TX upmix : LO=10.500 GHz (adf4382a_manager.h:35), high-side
|
||||
-> RF transmitted: 10.510..10.530 GHz
|
||||
RX downmix: LO=10.380 GHz (adf4382a_manager.h:36), high-side
|
||||
-> IF at ADC: 130..150 MHz
|
||||
DDC NCO : 120 MHz exactly (ddc_400m.v:201)
|
||||
-> baseband: 10..30 MHz <-- matched-filter reference
|
||||
|
||||
Sideband orientation (high-side at both mixers) is the conventional choice
|
||||
and consistent with all design comments / antenna match (10.25..10.75 GHz);
|
||||
loopback capture would settle it definitively. If either mixer turns out to
|
||||
be low-side, the sign of F_BASEBAND_LOW flips and/or the chirp direction
|
||||
reverses; revisit before re-generating .mem files.
|
||||
|
||||
radar_scene.py uses the same F_BASEBAND_LOW; both must stay in sync.
|
||||
|
||||
Scaling: 0.9 * 32767 (Q15)
|
||||
|
||||
Usage:
|
||||
python3 gen_chirp_mem.py
|
||||
@@ -45,6 +67,13 @@ FS_SYS = 100e6 # System clock (100 MHz, post-CIC)
|
||||
T_LONG_CHIRP = 30e-6 # 30 us long chirp duration
|
||||
T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp duration
|
||||
FFT_SIZE = 2048
|
||||
# DAC chirp baseband low-edge frequency. The TX LUT in plfm_chirp_controller.v
|
||||
# is a 10..30 MHz upchirp at fs_dac=120 MHz (Hilbert-confirmed for both long
|
||||
# and short LUTs). With TX_LO=10.500 GHz, RX_LO=10.380 GHz (adf4382a_manager.h)
|
||||
# and the 120 MHz DDC NCO (ddc_400m.v), high-side mixing places the post-DDC
|
||||
# echo at 10..30 MHz baseband, not 0..20 MHz. The matched-filter reference
|
||||
# must include this +10 MHz DC offset.
|
||||
F_BASEBAND_LOW = 10e6
|
||||
LONG_CHIRP_SAMPLES = int(T_LONG_CHIRP * FS_SYS) # 3000
|
||||
SHORT_CHIRP_SAMPLES = int(T_SHORT_CHIRP * FS_SYS) # 50
|
||||
LONG_SEGMENTS = 2
|
||||
@@ -69,7 +98,7 @@ def generate_full_long_chirp():
|
||||
|
||||
for n in range(LONG_CHIRP_SAMPLES):
|
||||
t = n / FS_SYS
|
||||
phase = math.pi * chirp_rate * t * t
|
||||
phase = 2 * math.pi * F_BASEBAND_LOW * t + math.pi * chirp_rate * t * t
|
||||
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)))
|
||||
@@ -92,7 +121,7 @@ def generate_short_chirp():
|
||||
|
||||
for n in range(SHORT_CHIRP_SAMPLES):
|
||||
t = n / FS_SYS
|
||||
phase = math.pi * chirp_rate * t * t
|
||||
phase = 2 * math.pi * F_BASEBAND_LOW * t + math.pi * chirp_rate * t * t
|
||||
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)))
|
||||
@@ -155,13 +184,14 @@ def main():
|
||||
|
||||
# ---- Verification summary ----
|
||||
|
||||
# Cross-check seg0 against radar_scene.py generate_reference_chirp_q15()
|
||||
# That function generates exactly the first 1024 samples of the chirp
|
||||
# Self-check: recompute the phase formula and verify the seg0 .mem matches.
|
||||
# radar_scene.py.generate_reference_chirp_q15() uses the same phase form
|
||||
# and the same F_BASEBAND_LOW; the two stay in sync by construction.
|
||||
chirp_rate = CHIRP_BW / T_LONG_CHIRP
|
||||
mismatches = 0
|
||||
for n in range(FFT_SIZE):
|
||||
t = n / FS_SYS
|
||||
phase = math.pi * chirp_rate * t * t
|
||||
phase = 2 * math.pi * F_BASEBAND_LOW * t + math.pi * chirp_rate * t * t
|
||||
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:
|
||||
|
||||
@@ -32,11 +32,23 @@ F_CARRIER = 10.5e9 # 10.5 GHz carrier
|
||||
C_LIGHT = 3.0e8 # Speed of light (m/s)
|
||||
WAVELENGTH = C_LIGHT / F_CARRIER # ~0.02857 m
|
||||
|
||||
# Chirp parameters
|
||||
F_IF = 120e6 # IF frequency (120 MHz)
|
||||
CHIRP_BW = 20e6 # Chirp bandwidth (30 MHz -> 10 MHz = 20 MHz sweep)
|
||||
F_CHIRP_START = 30e6 # Chirp start frequency (relative to IF)
|
||||
F_CHIRP_END = 10e6 # Chirp end frequency (relative to IF)
|
||||
# Chirp parameters.
|
||||
#
|
||||
# End-to-end frequency plan (TX-I, 2026-04-28):
|
||||
# DAC LUT : 10..30 MHz @ fs_dac=120 MHz (plfm_chirp_controller.v;
|
||||
# Hilbert-confirmed for both
|
||||
# long and short LUTs)
|
||||
# TX upmix : LO=10.500 GHz (adf4382a_manager.h:35), high-side
|
||||
# -> RF transmitted: 10.510..10.530 GHz
|
||||
# RX downmix: LO=10.380 GHz (adf4382a_manager.h:36), high-side
|
||||
# -> IF at ADC: 130..150 MHz
|
||||
# DDC NCO : 120 MHz exactly (ddc_400m.v:201)
|
||||
# -> baseband: 10..30 MHz
|
||||
F_IF = 120e6 # DDC NCO frequency (120 MHz)
|
||||
F_BASEBAND_LOW = 10e6 # DAC chirp baseband low-edge frequency
|
||||
CHIRP_BW = 20e6 # Chirp bandwidth (10 MHz -> 30 MHz upchirp = 20 MHz sweep)
|
||||
F_CHIRP_START = F_BASEBAND_LOW # 10 MHz at DAC baseband
|
||||
F_CHIRP_END = F_BASEBAND_LOW + CHIRP_BW # 30 MHz at DAC baseband
|
||||
|
||||
# Sampling
|
||||
FS_ADC = 400e6 # ADC sample rate (400 MSPS)
|
||||
@@ -153,12 +165,13 @@ def generate_if_chirp(n_samples, chirp_bw=CHIRP_BW, f_if=F_IF, fs=FS_ADC):
|
||||
chirp_q = []
|
||||
chirp_rate = chirp_bw / (n_samples / fs) # Hz/s
|
||||
|
||||
# IF chirp starts at f_if + F_BASEBAND_LOW and sweeps up over chirp_bw,
|
||||
# i.e. 130..150 MHz for the nominal high-side / 120 MHz NCO chain.
|
||||
f_lo = f_if + F_BASEBAND_LOW
|
||||
for n in range(n_samples):
|
||||
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
|
||||
phase = 2 * math.pi * (f_if - chirp_bw / 2) * t + math.pi * chirp_rate * t * t
|
||||
_f_inst = f_lo + chirp_rate * t
|
||||
phase = 2 * math.pi * f_lo * t + math.pi * chirp_rate * t * t
|
||||
chirp_i.append(math.cos(phase))
|
||||
chirp_q.append(math.sin(phase))
|
||||
|
||||
@@ -188,10 +201,10 @@ def generate_reference_chirp_q15(n_fft=FFT_SIZE, chirp_bw=CHIRP_BW, _f_if=F_IF,
|
||||
|
||||
for n in range(chirp_samples):
|
||||
t = n / FS_SYS
|
||||
# After DDC, the chirp is at baseband
|
||||
# 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
|
||||
# After DDC, the chirp is at baseband F_BASEBAND_LOW..(F_BASEBAND_LOW+BW),
|
||||
# i.e. 10..30 MHz for the nominal chain. Reference chirp is the TX chirp
|
||||
# at baseband (zero delay). Phase formula must match gen_chirp_mem.py.
|
||||
phase = 2 * math.pi * F_BASEBAND_LOW * t + math.pi * chirp_rate * t * t
|
||||
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))
|
||||
@@ -263,8 +276,10 @@ def generate_adc_samples(targets, n_samples, noise_stddev=3.0,
|
||||
t = n / FS_ADC
|
||||
t_delayed = n_delayed / FS_ADC
|
||||
|
||||
# Signal at IF: cos(2*pi*f_if*t + pi*chirp_rate*t_delayed^2 + doppler + phase)
|
||||
phase = (2 * math.pi * F_IF * t
|
||||
# Signal at IF: chirp starts at (F_IF + F_BASEBAND_LOW) and sweeps
|
||||
# up by chirp_rate (130..150 MHz for the nominal chain).
|
||||
f_lo_if = F_IF + F_BASEBAND_LOW
|
||||
phase = (2 * math.pi * f_lo_if * t
|
||||
+ math.pi * chirp_rate * t_delayed * t_delayed
|
||||
+ 2 * math.pi * doppler_hz * t
|
||||
+ phase0)
|
||||
|
||||
Reference in New Issue
Block a user