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:
Jason
2026-04-29 11:41:19 +05:45
parent b7ac2de1a4
commit 5ff5671fe2
10 changed files with 5242 additions and 4996 deletions

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

View File

@@ -115,8 +115,21 @@ always @(posedge clk_120m) begin
end end
// Short PLFM chirp LUT initialization (too small for BRAM, keep inline) // 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 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[ 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[ 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; 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;

View File

@@ -1,50 +1,50 @@
7332 7332
7330 5c56
730d 1e0c
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
d044 d044
2399 9729
65a3
6dff
325b
d440
9271 9271
9f85 c9c7
f753 2238
57d5 679c
6f35 6a91
2399 2399
c10f
8d21
b576 b576
8e95 1e0c
db07
4fe8
6d8f 6d8f
0d00 57d5
ebd7
92e6
ad06
2399
7276
38c3
b7b1
92e6
0000
6dff
3ef1
b234
9bc2 9bc2
a653 2399
24f9 7219
0173
8de7
e4c2
6d8f
290f
956f
d440
6ba2
2399
9012
f021
7330
f021
9271
38c3
54f5
9f85
ddc8

View File

@@ -1,50 +1,50 @@
0000 0000
0173 44e0
05ca 6f35
0d00 68d7
1702 2fbc
2399 dc67
325b 9a5d
4289 9201
52fa cda5
6208 2bc0
6d8f 6d8f
730d 607b
6fee 08ad
6208 a82b
484f 90cb
2399 dc67
f753 4a8a
c9c7 716b
a3aa 24f9
8e95 b018
9271 9271
b234 f300
e8fe 643e
290f 59ad
5e0a db07
7332 8cce
5c56 ddc8
1e0c 607b
d044 54f5
9729 c73d
9271 9271
c9c7 0fdf
2238 7330
679c 0fdf
6a91 9012
2399 dc67
c10f 6ba2
8d21 2bc0
b576 956f
1e0c d6f1
6d8f 6d8f
57d5 1b3e
ebd7 8de7
92e6 fe8d
ad06 7219
2399 dc67
7276 9bc2
38c3 4dcc
b7b1 3ef1
92e6 9201

View 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())

View File

@@ -24,10 +24,32 @@ Short chirp:
T_SHORT_CHIRP and CHIRP_BW. T_SHORT_CHIRP and CHIRP_BW.
Phase model (baseband, post-DDC): 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 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: Usage:
python3 gen_chirp_mem.py 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_LONG_CHIRP = 30e-6 # 30 us long chirp duration
T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp duration T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp duration
FFT_SIZE = 2048 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 LONG_CHIRP_SAMPLES = int(T_LONG_CHIRP * FS_SYS) # 3000
SHORT_CHIRP_SAMPLES = int(T_SHORT_CHIRP * FS_SYS) # 50 SHORT_CHIRP_SAMPLES = int(T_SHORT_CHIRP * FS_SYS) # 50
LONG_SEGMENTS = 2 LONG_SEGMENTS = 2
@@ -69,7 +98,7 @@ def generate_full_long_chirp():
for n in range(LONG_CHIRP_SAMPLES): for n in range(LONG_CHIRP_SAMPLES):
t = n / FS_SYS 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)) re_val = round(Q15_MAX * SCALE * math.cos(phase))
im_val = round(Q15_MAX * SCALE * math.sin(phase)) im_val = round(Q15_MAX * SCALE * math.sin(phase))
chirp_i.append(max(-32768, min(32767, re_val))) chirp_i.append(max(-32768, min(32767, re_val)))
@@ -92,7 +121,7 @@ def generate_short_chirp():
for n in range(SHORT_CHIRP_SAMPLES): for n in range(SHORT_CHIRP_SAMPLES):
t = n / FS_SYS 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)) re_val = round(Q15_MAX * SCALE * math.cos(phase))
im_val = round(Q15_MAX * SCALE * math.sin(phase)) im_val = round(Q15_MAX * SCALE * math.sin(phase))
chirp_i.append(max(-32768, min(32767, re_val))) chirp_i.append(max(-32768, min(32767, re_val)))
@@ -155,13 +184,14 @@ def main():
# ---- Verification summary ---- # ---- Verification summary ----
# Cross-check seg0 against radar_scene.py generate_reference_chirp_q15() # Self-check: recompute the phase formula and verify the seg0 .mem matches.
# That function generates exactly the first 1024 samples of the chirp # 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 chirp_rate = CHIRP_BW / T_LONG_CHIRP
mismatches = 0 mismatches = 0
for n in range(FFT_SIZE): for n in range(FFT_SIZE):
t = n / FS_SYS 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_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)))) 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: if long_i[n] != expected_i or long_q[n] != expected_q:

View File

@@ -32,11 +32,23 @@ F_CARRIER = 10.5e9 # 10.5 GHz carrier
C_LIGHT = 3.0e8 # Speed of light (m/s) C_LIGHT = 3.0e8 # Speed of light (m/s)
WAVELENGTH = C_LIGHT / F_CARRIER # ~0.02857 m WAVELENGTH = C_LIGHT / F_CARRIER # ~0.02857 m
# Chirp parameters # Chirp parameters.
F_IF = 120e6 # IF frequency (120 MHz) #
CHIRP_BW = 20e6 # Chirp bandwidth (30 MHz -> 10 MHz = 20 MHz sweep) # End-to-end frequency plan (TX-I, 2026-04-28):
F_CHIRP_START = 30e6 # Chirp start frequency (relative to IF) # DAC LUT : 10..30 MHz @ fs_dac=120 MHz (plfm_chirp_controller.v;
F_CHIRP_END = 10e6 # Chirp end frequency (relative to IF) # 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 # Sampling
FS_ADC = 400e6 # ADC sample rate (400 MSPS) 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_q = []
chirp_rate = chirp_bw / (n_samples / fs) # Hz/s 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): for n in range(n_samples):
t = n / fs t = n / fs
# Instantaneous frequency: f_if - chirp_bw/2 + chirp_rate * t _f_inst = f_lo + chirp_rate * t
# Phase: integral of 2*pi*f(t)*dt phase = 2 * math.pi * f_lo * t + math.pi * chirp_rate * t * t
_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_i.append(math.cos(phase))
chirp_q.append(math.sin(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): for n in range(chirp_samples):
t = n / FS_SYS t = n / FS_SYS
# After DDC, the chirp is at baseband # After DDC, the chirp is at baseband F_BASEBAND_LOW..(F_BASEBAND_LOW+BW),
# The beat frequency from a target at delay tau is: f_beat = chirp_rate * tau # i.e. 10..30 MHz for the nominal chain. Reference chirp is the TX chirp
# Reference chirp is the TX chirp at baseband (zero delay) # at baseband (zero delay). Phase formula must match gen_chirp_mem.py.
phase = math.pi * chirp_rate * t * t phase = 2 * math.pi * F_BASEBAND_LOW * t + math.pi * chirp_rate * t * t
re_val = round(32767 * 0.9 * math.cos(phase)) re_val = round(32767 * 0.9 * math.cos(phase))
im_val = round(32767 * 0.9 * math.sin(phase)) im_val = round(32767 * 0.9 * math.sin(phase))
ref_re[n] = max(-32768, min(32767, re_val)) 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 = n / FS_ADC
t_delayed = n_delayed / 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) # Signal at IF: chirp starts at (F_IF + F_BASEBAND_LOW) and sweeps
phase = (2 * math.pi * F_IF * t # 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 + math.pi * chirp_rate * t_delayed * t_delayed
+ 2 * math.pi * doppler_hz * t + 2 * math.pi * doppler_hz * t
+ phase0) + phase0)