Fix staggered-PRF Doppler processing with dual 16-point FFTs

This commit is contained in:
Jason
2026-03-27 23:05:28 +02:00
parent 2a89713c21
commit a577b7628b
18 changed files with 12801 additions and 12657 deletions
@@ -36,6 +36,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
DOPPLER_FFT = 32
RANGE_BINS = 64
TOTAL_OUTPUTS = RANGE_BINS * DOPPLER_FFT # 2048
SUBFRAME_SIZE = 16
SCENARIOS = {
'stationary': {
@@ -125,6 +126,19 @@ def find_peak_bin(i_arr, q_arr):
return max(range(len(mags)), key=lambda k: mags[k])
def peak_bins_match(py_peak, rtl_peak):
"""Return True if peaks match within +/-1 bin inside the same sub-frame."""
py_sf = py_peak // SUBFRAME_SIZE
rtl_sf = rtl_peak // SUBFRAME_SIZE
if py_sf != rtl_sf:
return False
py_bin = py_peak % SUBFRAME_SIZE
rtl_bin = rtl_peak % SUBFRAME_SIZE
diff = abs(py_bin - rtl_bin)
return diff <= 1 or diff >= SUBFRAME_SIZE - 1
def total_energy(data_dict):
"""Sum of I^2 + Q^2 across all range bins and Doppler bins."""
total = 0
@@ -207,8 +221,8 @@ def compare_scenario(name, config, base_dir):
py_peak = find_peak_bin(py_i, py_q)
rtl_peak = find_peak_bin(rtl_i, rtl_q)
# Peak agreement (allow +/- 1 bin tolerance)
if abs(py_peak - rtl_peak) <= 1 or abs(py_peak - rtl_peak) >= DOPPLER_FFT - 1:
# Peak agreement (allow +/-1 bin tolerance, but only within a sub-frame)
if peak_bins_match(py_peak, rtl_peak):
peak_agreements += 1
py_mag = magnitude_l1(py_i, py_q)
@@ -242,7 +256,7 @@ def compare_scenario(name, config, base_dir):
avg_corr_q = sum(q_correlations) / len(q_correlations)
print(f"\n Per-range-bin metrics:")
print(f" Peak Doppler bin agreement (+/-1): {peak_agreements}/{RANGE_BINS} "
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}")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1106,8 +1106,8 @@ FFFF0000
00000000
00000000
00000000
FFFF0001
FFFF0000
00000001
00000000
FFFF0005
00000001
00000001
@@ -1172,7 +1172,7 @@ FFFF0000
00010000
00010000
00010000
00060003
00060002
00010001
00000001
00000000
@@ -1236,7 +1236,7 @@ FFFF0000
00000000
0001FFFF
0002FFFF
0006FFFD
0005FFFC
00010000
0001FFFF
00000001
@@ -1300,7 +1300,7 @@ FFFF0000
00000000
00000000
FFFFFFFF
FFFFFFFA
FFFEFFFA
0000FFFF
0000FFFF
00010001
@@ -1364,9 +1364,9 @@ FFFF0000
00000000
00000000
FFFF0000
FFFAFFFD
FFFAFFFF
FFFFFFFF
00000000
00000001
00000001
FFFF0000
00000000
@@ -1427,74 +1427,74 @@ FFFF0000
FFFF0000
00000000
FFFF0000
00000001
FFFB0005
FFFE0001
00000000
00010000
00000000
00000000
00000001
00000000
0000FFFF
00010001
00000000
00000000
00000000
00000000
00000000
00000001
00000001
00000000
00010001
00000000
00000000
00000000
00000000
00000000
00000000
00000000
FFFFFFFF
FFFFFFFF
0000FFFF
00000000
00000000
00000001
00000000
00000000
FFFF0000
FFFF0000
00000001
00010000
00000000
FFFF0000
00010000
00000001
FFFF0000
FFFF0000
00010001
FFFF0000
FFFFFFFF
00000000
00010000
FFFF0000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00010001
00000000
00000000
FFFF0000
00000000
00010001
00000001
00010006
00000002
FFFD0006
FFFE0001
00000001
00010000
00000000
00000000
00000001
00000000
0000FFFF
00010001
00000000
00000000
00000000
00000000
00000000
00000001
00000001
00000000
00010001
00000000
00000000
00000000
00000000
00000000
00000000
00000000
FFFFFFFF
FFFFFFFF
0000FFFF
00000000
00000000
00000001
00000000
00000000
FFFF0000
FFFF0000
00000001
00010000
00000000
FFFF0000
00010000
00000001
FFFF0000
FFFF0000
00010001
FFFF0000
FFFFFFFF
00000000
00010000
FFFF0000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00010001
00000000
00000000
FFFF0000
00000000
00010000
00010001
00030005
00010001
00010001
00000000
00000000
FFFF0000
@@ -1556,8 +1556,8 @@ FFFFFFFF
00000000
00010000
00020000
00060001
00010000
0006FFFE
0001FFFF
00010000
FFFF0000
00000001
@@ -1619,9 +1619,9 @@ FFFFFFFE
00000001
0000FFFF
00010000
0001FFFF
0004FFFB
0002FFFF
0001FFFE
0001FFFA
0002FFFE
00010000
FFFF0000
FFFF0000
@@ -1682,9 +1682,9 @@ FFFF0000
00000000
00000001
00000001
00000000
FFFF0000
FFFEFFFA
FFFF0000
FFFBFFFC
FFFFFFFF
FFFF0000
0000FFFF
@@ -1747,9 +1747,9 @@ FFFFFFFF
00000000
0000FFFF
FFFF0001
FFFF0000
FFFA0000
FFFE0000
FFFF0001
FFFA0003
FFFF0001
FFFF0000
00000000
00000001
@@ -1811,74 +1811,74 @@ FFFF0001
00010000
0000FFFF
00000000
FFFF0002
FFFD0005
FFFF0001
00000001
0000FFFF
FFFF0001
00000000
00000000
00000000
FFFFFFFF
00010001
FFFFFFFF
00000001
00000000
00000000
00000000
00010000
00000000
00000000
FFFF0000
00000000
00000000
00010000
00000000
00000000
00000000
00000000
00000000
0000FFFF
00000000
0000FFFF
00000000
00000000
00000001
00000001
00000000
00000000
00000000
00000000
00000001
FFFF0000
00010000
FFFF0000
FFFF0000
00000000
00000000
00000000
00000001
00000000
FFFF0000
00000001
FFFF0000
00000000
0000FFFF
FFFF0000
0000FFFF
00010000
FFFF0000
0001FFFF
0000FFFF
0001FFFF
00000000
0000FFFF
00000001
00010002
00030005
00000002
00000006
FFFF0002
00010001
0000FFFF
FFFF0001
00000000
00000000
00000000
FFFFFFFF
00010001
FFFFFFFF
00000001
00000000
00000000
00000000
00010000
00000000
00000000
FFFF0000
00000000
00000000
00010000
00000000
00000000
00000000
00000000
00000000
0000FFFF
00000000
0000FFFF
00000000
00000000
00000001
00000001
00000000
00000000
00000000
00000000
00000001
FFFF0000
00010000
FFFF0000
FFFF0000
00000000
00000000
00000000
00000001
00000000
FFFF0000
00000001
FFFF0000
00000000
0000FFFF
FFFF0000
0000FFFF
00010000
FFFF0000
0001FFFF
0000FFFF
0001FFFF
00000000
0000FFFF
00010000
00020001
00060002
00000001
00010000
0001FFFF
00000000
00000000
@@ -1939,9 +1939,9 @@ FFFF0000
00000000
0000FFFF
0001FFFF
0001FFFF
00070000
00000000
0000FFFE
0005FFFC
0000FFFF
00010001
FFFF0000
0000FFFF
@@ -2003,9 +2003,9 @@ FFFF0000
00000001
00000000
0000FFFF
0001FFFF
0002FFF9
0000FFFF
FFFDFFF9
FFFFFFFF
FFFFFFFF
00000000
00000000
@@ -1099,7 +1099,7 @@ FFFF0000
00000000
00000002
FFFF0003
FFFE0012
FFFF0012
00000003
FFFF0002
00010001
@@ -1163,7 +1163,7 @@ FFFF0000
00010001
00010002
00020003
000C000D
000D000C
00030003
00000001
00000001
@@ -1226,9 +1226,9 @@ FFFF0000
00000000
FFFF0000
00020000
00030000
00110004
00030000
0003FFFF
00120002
0003FFFF
00020000
00000000
FFFF0000
@@ -1291,8 +1291,8 @@ FFFF0000
00010000
0002FFFF
0003FFFE
000FFFF6
0004FFFF
000EFFF4
0003FFFE
0002FFFF
00000000
FFFF0000
@@ -1312,8 +1312,8 @@ FFFF0000
00010000
00000001
0000FFFF
00000000
00010000
00010001
FFFF0000
00000001
0000FFFF
@@ -1353,10 +1353,10 @@ FFFF0000
00010001
0001FFFF
00010000
0001FFFE
0001FFFD
0006FFF0
0001FFFD
0000FFFE
0000FFFD
0003FFEF
0000FFFD
0000FFFE
00000000
00010000
@@ -1376,7 +1376,7 @@ FFFF0000
0000FFFF
00010000
00000001
00010001
00010002
00000000
00000001
00000000
@@ -1418,10 +1418,10 @@ FFFF0000
0000FFFF
FFFF0000
FFFFFFFE
FFFEFFFD
FFF9FFF1
FFFEFFFD
FFFFFFFF
FFFDFFFD
FFF5FFF2
FFFEFFFE
FFFE0000
FFFF0000
00000001
FFFF0000
@@ -1439,8 +1439,8 @@ FFFF0000
0000FFFF
00010001
FFFF0000
FFFF0001
FFFF0001
FFFF0000
FFFF0000
00000000
00000000
00000001
@@ -1482,10 +1482,10 @@ FFFF0000
00000000
00000000
FFFF0000
FFFCFFFF
FFEFFFF9
FFFCFFFF
FFFF0000
FFFC0000
FFEEFFFE
FFFC0000
FFFF0001
00000000
00000000
FFFF0000
@@ -1504,7 +1504,7 @@ FFFF0000
00000000
00000000
00000000
FFFFFFFF
0000FFFF
FFFF0001
00000000
00010000
@@ -1546,10 +1546,10 @@ FFFFFFFF
00000000
FFFFFFFF
FFFE0001
FFFD0001
FFEF0006
FFFD0001
FFFF0000
FFFD0002
FFF1000B
FFFD0002
FFFF0001
00000000
FFFFFFFF
00010000
@@ -1609,77 +1609,77 @@ FFFF0001
00000000
00000001
00000000
FFFF0002
FFFE0003
FFF7000E
FFFF0005
FFFF0001
0001FFFF
00000000
00000001
0000FFFF
00000000
00000000
FFFF0000
00010000
00010000
FFFF0000
FFFF0000
0000FFFF
00000000
00000000
00010000
00000000
00000000
00010000
00020001
00000000
00000000
00000000
FFFF0000
00000000
00000000
00010000
00000001
00000001
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000001
0000FFFF
00000000
0000FFFF
00010000
FFFF0000
0001FFFF
00010001
00000000
FFFF0001
00010000
0000FFFF
00000001
FFFF0000
00000000
0000FFFF
FFFF0000
00000001
00000000
FFFF0000
FFFF0000
00000000
0000FFFF
00000001
00000002
00000003
00050012
00010003
FFFF0004
FFFC0010
00000005
00000001
0001FFFF
00000000
00000001
0000FFFF
00000000
00000000
FFFF0000
00010000
00010000
FFFF0000
FFFF0000
0000FFFF
00000000
00000000
00010000
00000000
00000000
00010000
00010002
00000000
00000000
00000000
FFFF0000
00000000
00000000
00010000
00000001
00000001
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000001
0000FFFF
00000000
0000FFFF
00010000
FFFF0000
0001FFFF
00010001
00000000
FFFF0001
00010000
0000FFFF
00000001
FFFF0000
00000000
0000FFFF
FFFF0000
00000001
00000000
FFFF0000
FFFF0000
00000000
0000FFFF
00000001
00000002
00010003
000B000F
00020003
00020002
00000000
00000000
00000001
00000001
00000001
@@ -1696,9 +1696,9 @@ FFFFFFFF
00000000
0000FFFF
00000000
00000002
00010000
00000000
FFFF0001
0000FFFF
FFFF0000
00000000
00000000
00000000
@@ -1737,160 +1737,160 @@ FFFFFFFF
00000000
00000000
00000001
00020001
00030000
00110004
00040000
00020000
00000000
00000000
00000000
0000FFFF
00000001
00000000
00000001
00000000
00000000
00000000
00000001
FFFFFFFF
0000FFFF
FFFF0000
00000000
FFFF0000
00000001
00000000
0000FFFF
FFFFFFFF
00000000
00000000
FFFF0000
FFFF0000
0000FFFF
00010000
00000001
00010000
00010001
00000000
0000FFFF
00000001
00000000
FFFF0001
00010001
00000000
00000000
00000000
00000000
FFFFFFFF
FFFF0000
00000000
00010001
00010000
FFFFFFFF
00000000
00000001
00000000
00000000
00000000
00000000
00000000
00010000
00000000
FFFF0000
0000FFFF
0000FFFF
00000000
00000000
0001FFFF
0004FFFE
000FFFF7
0004FFFE
00010000
FFFF0001
0000FFFF
00010000
0000FFFF
00000000
FFFF0001
00000000
FFFF0000
00010000
0000FFFF
FFFF0001
00000000
00000000
00000000
FFFFFFFF
00010001
FFFFFFFF
00000000
00010000
00000000
00000000
00010000
00000000
00000000
FFFF0000
00000000
00000000
00010000
00000000
00000000
00000000
00000000
00000000
0000FFFF
00000000
0000FFFF
00000000
00000000
00000001
00000001
00000000
00000000
00000000
00000000
00000001
FFFF0000
00010000
FFFF0000
FFFF0000
00000000
00000000
00000000
00000001
00000000
FFFF0000
00000001
FFFF0000
00000000
0000FFFF
0000FFFE
0001FFFB
0005FFEF
0000FFFC
0001FFFE
0000FFFF
0001FFFF
00000000
0000FFFF
00000000
00010001
00000000
FFFF0001
00000000
0001FFFF
00000000
00000000
00010000
FFFF0000
00000000
0001FFFF
00000000
00000001
00020002
00030001
000E000A
00040001
00020001
00000000
00000000
00000000
0000FFFF
00000001
00000000
00000001
00000000
00000000
00000000
00000001
FFFFFFFF
0000FFFF
FFFF0000
00000000
FFFF0000
00000001
00000000
FFFFFFFF
FFFFFFFF
00000000
00000000
FFFF0000
FFFF0000
0000FFFF
00010000
00000001
00010000
00010001
00000000
0000FFFF
00000001
00000000
FFFF0001
00010001
00000000
00000000
00000000
00000000
FFFFFFFF
FFFF0000
00000000
00010001
00010000
FFFFFFFF
00000000
00000001
00000000
00000000
00000000
00000000
00000000
00010000
00000000
FFFF0000
0000FFFF
0000FFFF
00000000
00000000
00020000
00050000
0012FFFE
00040000
00020000
FFFF0001
0000FFFF
00010000
0000FFFF
00000000
FFFF0001
00000000
FFFF0000
00010000
0000FFFF
FFFF0001
00000000
00000000
00000000
FFFFFFFF
00010001
FFFFFFFF
00000000
0000FFFF
00000000
00000000
00010000
00000000
00000000
FFFF0000
00000000
00000000
00010000
00000000
00000000
00000000
00000000
00000000
0000FFFF
00000000
0000FFFF
00000000
00000000
00000001
00000001
00000000
00000000
00000000
00000000
00000001
FFFF0000
00010000
FFFF0000
FFFF0000
00000000
00000000
00000000
00000001
00000000
FFFF0000
00000001
FFFF0000
00000000
0000FFFF
0000FFFE
0003FFFC
000CFFF3
0001FFFD
0002FFFE
0000FFFF
0001FFFF
00000000
0000FFFF
00000000
00010001
00000000
FFFF0001
00000000
0001FFFF
00000000
00000000
00010000
FFFF0000
00000000
0001FFFF
00010000
00000000
00030001
00000000
0001FFFF
00000000
00000000
0000FFFF
@@ -1929,78 +1929,78 @@ FFFF0000
FFFF0000
00000000
00000000
0000FFFE
FFFFFFFD
FFFFFFEE
FFFFFFFC
FFFFFFFE
00000000
FFFF0000
00000000
0000FFFF
0000FFFF
FFFFFFFF
00000000
FFFF0000
00000001
FFFF0000
0000FFFF
00000000
00000000
00000000
00010000
FFFF0000
00000000
00000000
00010001
00000000
00000000
0000FFFF
00000000
00000000
00000000
00000000
00000001
0000FFFF
00000000
00000000
00000000
00000000
00010000
00000000
00000001
00000000
FFFF0000
00000000
00000001
00010000
00000000
00000001
00010000
00000000
FFFF0000
00000001
00000000
00000000
00000000
00000000
00000000
00000001
00010000
00000000
00000000
0001FFFF
0000FFFF
00010000
FFFF0000
FFFFFFFF
FFFEFFFE
FFF3FFF3
FFFEFFFD
FFF7FFF1
FFFEFFFD
FFFEFFFE
00000000
FFFF0000
00000000
0000FFFF
0000FFFF
FFFFFFFF
00000000
FFFF0000
00000001
FFFF0000
0000FFFF
00000000
00000000
00000000
00010000
FFFF0000
00000000
00000000
FFFF0000
00000000
00000000
0000FFFF
00000000
00000000
00000000
00000000
00000001
0000FFFF
00000000
00000000
00000000
00000000
00010000
00000000
00000001
00000000
FFFF0000
00000000
00000001
00010000
00000000
00000001
00010000
00000000
FFFF0000
00000001
00000000
00000000
00000000
00000000
00000000
00000001
00010000
00000000
00000000
0001FFFF
0000FFFF
00010000
FFFF0000
FFFF0000
FFFEFFFF
FFEEFFFB
FFFDFFFE
FFFEFFFF
00000000
FFFF0000
00000001
00000000
00000000
00000001
@@ -2016,7 +2016,7 @@ FFFF0001
00010000
00000000
0001FFFF
FFFE0000
FFFFFFFF
00000001
00000000
00010000
+66 -50
View File
@@ -1075,44 +1075,43 @@ class RangeBinDecimator:
# =============================================================================
# Doppler Processor (Hamming window + 32-point FFT)
# Doppler Processor (Hamming window + dual 16-point FFT)
# =============================================================================
# Hamming window LUT (32 entries, 16-bit unsigned Q15)
# Hamming window LUT (16 entries, 16-bit unsigned Q15)
# Matches doppler_processor.v window_coeff[0:15]
# w[n] = 0.54 - 0.46 * cos(2*pi*n/15), n=0..15, symmetric
HAMMING_WINDOW = [
0x0800, 0x0862, 0x09CB, 0x0C3B, 0x0FB2, 0x142F, 0x19B2, 0x2039,
0x27C4, 0x3050, 0x39DB, 0x4462, 0x4FE3, 0x5C5A, 0x69C4, 0x781D,
0x7FFF, 0x781D, 0x69C4, 0x5C5A, 0x4FE3, 0x4462, 0x39DB, 0x3050,
0x27C4, 0x2039, 0x19B2, 0x142F, 0x0FB2, 0x0C3B, 0x09CB, 0x0862,
0x0A3D, 0x0E5C, 0x1B6D, 0x3088, 0x4B33, 0x6573, 0x7642, 0x7F62,
0x7F62, 0x7642, 0x6573, 0x4B33, 0x3088, 0x1B6D, 0x0E5C, 0x0A3D,
]
class DopplerProcessor:
"""
Bit-accurate model of doppler_processor_optimized.v
Bit-accurate model of doppler_processor_optimized.v (dual 16-pt FFT architecture).
For each range bin (0-63):
1. Read 32 chirps of data from accumulation buffer
2. Apply Hamming window (Q15 multiply, round, >>>15)
3. 32-point FFT
The staggered-PRF frame has 32 chirps total:
- Sub-frame 0 (long PRI): chirps 0-15 -> 16-pt Hamming -> 16-pt FFT -> bins 0-15
- Sub-frame 1 (short PRI): chirps 16-31 -> 16-pt Hamming -> 16-pt FFT -> bins 16-31
The 32-point FFT uses xfft_32.v (Xilinx IP wrapper around fft_engine).
For the Python model, we use FFTEngine with N=32.
Output: doppler_bin[4:0] = {sub_frame_id, bin_in_subframe[3:0]}
Total output per range bin: 32 bins (16 + 16), same interface as before.
"""
DOPPLER_FFT_SIZE = 32
DOPPLER_FFT_SIZE = 16 # Per sub-frame
RANGE_BINS = 64
CHIRPS_PER_FRAME = 32
CHIRPS_PER_SUBFRAME = 16
def __init__(self, twiddle_file_32=None):
def __init__(self, twiddle_file_16=None):
"""
For 32-point FFT, we need the 32-point twiddle file.
For 16-point FFT, we need the 16-point twiddle file.
If not provided, we generate twiddle factors mathematically
(since the 32-pt twiddle ROM is cos(2*pi*k/32) for k=0..7).
(cos(2*pi*k/16) for k=0..3, quarter-wave ROM with 4 entries).
"""
self.fft32 = None
self._twiddle_file_32 = twiddle_file_32
# We'll use a simple 32-pt FFT with computed twiddles
self.fft16 = None
self._twiddle_file_16 = twiddle_file_16
@staticmethod
def window_multiply(data_16, window_16):
@@ -1134,7 +1133,7 @@ class DopplerProcessor:
def process_frame(self, chirp_data_i, chirp_data_q):
"""
Process one complete Doppler frame.
Process one complete Doppler frame using dual 16-pt FFTs.
Args:
chirp_data_i: 2D array [32 chirps][64 range bins] of signed 16-bit I
@@ -1143,46 +1142,63 @@ class DopplerProcessor:
Returns:
(doppler_map_i, doppler_map_q): 2D arrays [64 range bins][32 doppler bins]
of signed 16-bit
Bins 0-15 = sub-frame 0 (long PRI)
Bins 16-31 = sub-frame 1 (short PRI)
"""
doppler_map_i = []
doppler_map_q = []
# Generate 32-pt twiddle factors (quarter-wave cos, 8 entries)
# cos(2*pi*k/32) for k=0..7
# Generate 16-pt twiddle factors (quarter-wave cos, 4 entries)
# cos(2*pi*k/16) for k=0..3
# Matches fft_twiddle_16.mem: 7FFF, 7641, 5A82, 30FB
import math
cos_rom_32 = []
for k in range(8):
val = round(32767.0 * math.cos(2.0 * math.pi * k / 32.0))
cos_rom_32.append(sign_extend(val & 0xFFFF, 16))
cos_rom_16 = []
for k in range(4):
val = round(32767.0 * math.cos(2.0 * math.pi * k / 16.0))
cos_rom_16.append(sign_extend(val & 0xFFFF, 16))
fft32 = FFTEngine.__new__(FFTEngine)
fft32.N = 32
fft32.LOG2N = 5
fft32.cos_rom = cos_rom_32
fft32.mem_re = [0] * 32
fft32.mem_im = [0] * 32
fft16 = FFTEngine.__new__(FFTEngine)
fft16.N = 16
fft16.LOG2N = 4
fft16.cos_rom = cos_rom_16
fft16.mem_re = [0] * 16
fft16.mem_im = [0] * 16
for rbin in range(self.RANGE_BINS):
# Gather 32 chirps for this range bin
fft_in_re = []
fft_in_im = []
# Output bins for this range bin: 32 total (16 from each sub-frame)
out_re = [0] * 32
out_im = [0] * 32
for chirp in range(self.CHIRPS_PER_FRAME):
re_val = sign_extend(chirp_data_i[chirp][rbin] & 0xFFFF, 16)
im_val = sign_extend(chirp_data_q[chirp][rbin] & 0xFFFF, 16)
# Process each sub-frame independently
for sf in range(2):
chirp_start = sf * self.CHIRPS_PER_SUBFRAME
bin_offset = sf * self.DOPPLER_FFT_SIZE
# Apply Hamming window
win_re = self.window_multiply(re_val, HAMMING_WINDOW[chirp])
win_im = self.window_multiply(im_val, HAMMING_WINDOW[chirp])
fft_in_re = []
fft_in_im = []
fft_in_re.append(win_re)
fft_in_im.append(win_im)
for c in range(self.CHIRPS_PER_SUBFRAME):
chirp = chirp_start + c
re_val = sign_extend(chirp_data_i[chirp][rbin] & 0xFFFF, 16)
im_val = sign_extend(chirp_data_q[chirp][rbin] & 0xFFFF, 16)
# 32-point forward FFT
fft_out_re, fft_out_im = fft32.compute(fft_in_re, fft_in_im, inverse=False)
# Apply 16-pt Hamming window (index = c within sub-frame)
win_re = self.window_multiply(re_val, HAMMING_WINDOW[c])
win_im = self.window_multiply(im_val, HAMMING_WINDOW[c])
doppler_map_i.append(fft_out_re)
doppler_map_q.append(fft_out_im)
fft_in_re.append(win_re)
fft_in_im.append(win_im)
# 16-point forward FFT
fft_out_re, fft_out_im = fft16.compute(fft_in_re, fft_in_im, inverse=False)
# Pack into output: sub-frame 0 -> bins 0-15, sub-frame 1 -> bins 16-31
for b in range(self.DOPPLER_FFT_SIZE):
out_re[bin_offset + b] = fft_out_re[b]
out_im[bin_offset + b] = fft_out_im[b]
doppler_map_i.append(out_re)
doppler_map_q.append(out_im)
return doppler_map_i, doppler_map_q
@@ -1207,7 +1223,7 @@ class SignalChain:
IF_FREQ = 120_000_000 # IF frequency
FTW_120MHZ = 0x4CCCCCCD # Phase increment for 120 MHz at 400 MSPS
def __init__(self, twiddle_file_1024=None, twiddle_file_32=None):
def __init__(self, twiddle_file_1024=None, twiddle_file_16=None):
self.nco = NCO()
self.mixer = Mixer()
self.cic_i = CICDecimator()
@@ -1217,7 +1233,7 @@ class SignalChain:
self.ddc_interface = DDCInputInterface()
self.matched_filter = MatchedFilterChain(fft_size=1024, twiddle_file=twiddle_file_1024)
self.range_decimator = RangeBinDecimator()
self.doppler = DopplerProcessor(twiddle_file_32=twiddle_file_32)
self.doppler = DopplerProcessor(twiddle_file_16=twiddle_file_16)
def ddc_step(self, adc_data_8bit, ftw=None):
"""
@@ -3,23 +3,17 @@
Generate Doppler processor co-simulation golden reference data.
Uses the bit-accurate Python model (fpga_model.py) to compute the expected
Doppler FFT output. Also generates the input hex files consumed by the
Verilog testbench (tb_doppler_cosim.v).
Doppler FFT output for the dual 16-pt FFT architecture. Also generates the
input hex files consumed by the Verilog testbench (tb_doppler_cosim.v).
Two output modes:
1. "clean" — straight Python model (correct windowing alignment)
2. "buggy" — replicates the RTL's windowing pipeline misalignment:
* Sample 0: fft_input = 0 (from reset mult value)
* Sample 1: fft_input = window_multiply(data[wrong_rbin_or_0], window[0])
* Sample k (k>=2): fft_input = window_multiply(data[k-2], window[k-1])
Default mode is "clean". The comparison script uses correlation-based
metrics that are tolerant of the pipeline shift.
Architecture:
Sub-frame 0 (long PRI): chirps 0-15 -> 16-pt Hamming -> 16-pt FFT -> bins 0-15
Sub-frame 1 (short PRI): chirps 16-31 -> 16-pt Hamming -> 16-pt FFT -> bins 16-31
Usage:
cd ~/PLFM_RADAR/9_Firmware/9_2_FPGA/tb/cosim
python3 gen_doppler_golden.py # clean model
python3 gen_doppler_golden.py --buggy # replicate RTL pipeline bug
python3 gen_doppler_golden.py
python3 gen_doppler_golden.py stationary # single scenario
Author: Phase 0.5 Doppler co-simulation suite for PLFM_RADAR
"""
@@ -31,7 +25,7 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fpga_model import (
DopplerProcessor, FFTEngine, sign_extend, HAMMING_WINDOW
DopplerProcessor, sign_extend, HAMMING_WINDOW
)
from radar_scene import Target, generate_doppler_frame
@@ -40,7 +34,8 @@ from radar_scene import Target, generate_doppler_frame
# Constants
# =============================================================================
DOPPLER_FFT_SIZE = 32
DOPPLER_FFT_SIZE = 16 # Per sub-frame
DOPPLER_TOTAL_BINS = 32 # Total output (2 sub-frames x 16)
RANGE_BINS = 64
CHIRPS_PER_FRAME = 32
TOTAL_SAMPLES = CHIRPS_PER_FRAME * RANGE_BINS # 2048
@@ -82,154 +77,6 @@ def write_hex_16bit(filepath, data):
# Buggy-model helpers (match RTL pipeline misalignment)
# =============================================================================
def window_multiply(data_16, window_16):
"""Hamming window multiply matching RTL."""
d = sign_extend(data_16 & 0xFFFF, 16)
w = sign_extend(window_16 & 0xFFFF, 16)
product = d * w
rounded = product + (1 << 14)
result = rounded >> 15
return sign_extend(result & 0xFFFF, 16)
def buggy_process_frame(chirp_data_i, chirp_data_q):
"""
Replicate the RTL's exact windowing pipeline for all 64 range bins.
For each range bin we model the three-stage pipeline:
Stage A (BRAM registered read):
mem_rdata captures doppler_i_mem[mem_read_addr] one cycle AFTER
mem_read_addr is presented.
Stage B (multiply):
mult_i <= mem_rdata_i * window_coeff[read_doppler_index]
-- read_doppler_index is the CURRENT cycle's value, but mem_rdata_i
-- is from the PREVIOUS cycle's address.
Stage C (round+shift):
fft_input_i <= (mult_i + (1<<14)) >>> 15
-- uses the PREVIOUS cycle's mult_i.
Additionally, at the S_ACCUMULATE->S_LOAD_FFT transition (rbin=0) or
S_OUTPUT->S_LOAD_FFT transition (rbin>0), the BRAM address during the
transition cycle depends on the stale read_doppler_index and read_range_bin
values.
This function models every detail to produce bit-exact FFT inputs.
"""
# Build the 32-pt FFT engine (matching fpga_model.py)
import math as _math
cos_rom_32 = []
for k in range(8):
val = round(32767.0 * _math.cos(2.0 * _math.pi * k / 32.0))
cos_rom_32.append(sign_extend(val & 0xFFFF, 16))
fft32 = FFTEngine.__new__(FFTEngine)
fft32.N = 32
fft32.LOG2N = 5
fft32.cos_rom = cos_rom_32
fft32.mem_re = [0] * 32
fft32.mem_im = [0] * 32
# Build flat BRAM contents: addr = chirp_index * 64 + range_bin
bram_i = [0] * TOTAL_SAMPLES
bram_q = [0] * TOTAL_SAMPLES
for chirp in range(CHIRPS_PER_FRAME):
for rb in range(RANGE_BINS):
addr = chirp * RANGE_BINS + rb
bram_i[addr] = sign_extend(chirp_data_i[chirp][rb] & 0xFFFF, 16)
bram_q[addr] = sign_extend(chirp_data_q[chirp][rb] & 0xFFFF, 16)
doppler_map_i = []
doppler_map_q = []
# State carried across range bins (simulates the RTL registers)
# After reset: read_doppler_index=0, read_range_bin=0, mult_i=0, mult_q=0,
# fft_input_i=0, fft_input_q=0
# The BRAM read is always active: mem_rdata <= doppler_i_mem[mem_read_addr]
# mem_read_addr = read_doppler_index * 64 + read_range_bin
# We need to track what read_doppler_index and read_range_bin are at each
# transition, since the BRAM captures data one cycle before S_LOAD_FFT runs.
# Before processing starts (just entered S_LOAD_FFT from S_ACCUMULATE):
# At the S_ACCUMULATE clock that transitions:
# read_doppler_index <= 0 (NBA)
# read_range_bin <= 0 (NBA)
# These take effect NEXT cycle. At the transition clock itself,
# read_doppler_index and read_range_bin still had their old values.
# From reset, both were 0. So BRAM captures addr=0*64+0=0.
#
# For rbin>0 transitions from S_OUTPUT:
# At S_OUTPUT clock:
# read_doppler_index <= 0 (was 0, since it wrapped from 32->0 in 5 bits)
# read_range_bin <= prev_rbin + 1 (NBA, takes effect next cycle)
# At S_OUTPUT clock, the current read_range_bin = prev_rbin,
# read_doppler_index = 0 (wrapped). So BRAM captures addr=0*64+prev_rbin.
for rbin in range(RANGE_BINS):
# Determine what BRAM data was captured during the transition clock
# (one cycle before S_LOAD_FFT's first execution cycle).
if rbin == 0:
# From S_ACCUMULATE: both indices were 0 (from reset or previous NBA)
# BRAM captures addr = 0*64+0 = 0 -> data[chirp=0][rbin=0]
transition_bram_addr = 0 * RANGE_BINS + 0
else:
# From S_OUTPUT: read_doppler_index=0 (wrapped), read_range_bin=rbin-1
# BRAM captures addr = 0*64+(rbin-1) -> data[chirp=0][rbin-1]
transition_bram_addr = 0 * RANGE_BINS + (rbin - 1)
transition_data_i = bram_i[transition_bram_addr]
transition_data_q = bram_q[transition_bram_addr]
# Now simulate the 32 cycles of S_LOAD_FFT for this range bin.
# Register pipeline state at entry:
mult_i_reg = 0 # From reset (rbin=0) or from end of previous S_FFT_WAIT
mult_q_reg = 0
fft_in_i_list = []
fft_in_q_list = []
for k in range(DOPPLER_FFT_SIZE):
# read_doppler_index = k at this cycle's start
# mem_read_addr = k * 64 + rbin
# What mem_rdata holds THIS cycle:
if k == 0:
# BRAM captured transition_bram_addr last cycle
rd_i = transition_data_i
rd_q = transition_data_q
else:
# BRAM captured addr from PREVIOUS cycle: (k-1)*64 + rbin
prev_addr = (k - 1) * RANGE_BINS + rbin
rd_i = bram_i[prev_addr]
rd_q = bram_q[prev_addr]
# Stage B: multiply (uses current read_doppler_index = k)
new_mult_i = sign_extend(rd_i & 0xFFFF, 16) * \
sign_extend(HAMMING_WINDOW[k] & 0xFFFF, 16)
new_mult_q = sign_extend(rd_q & 0xFFFF, 16) * \
sign_extend(HAMMING_WINDOW[k] & 0xFFFF, 16)
# Stage C: round+shift (uses PREVIOUS cycle's mult)
fft_i = (mult_i_reg + (1 << 14)) >> 15
fft_q = (mult_q_reg + (1 << 14)) >> 15
fft_in_i_list.append(sign_extend(fft_i & 0xFFFF, 16))
fft_in_q_list.append(sign_extend(fft_q & 0xFFFF, 16))
# Update pipeline registers for next cycle
mult_i_reg = new_mult_i
mult_q_reg = new_mult_q
# 32-point FFT
fft_out_re, fft_out_im = fft32.compute(
fft_in_i_list, fft_in_q_list, inverse=False
)
doppler_map_i.append(fft_out_re)
doppler_map_q.append(fft_out_im)
return doppler_map_i, doppler_map_q
# =============================================================================
# Test scenario definitions
@@ -244,9 +91,10 @@ def make_scenario_stationary():
def make_scenario_moving():
"""Single target with moderate Doppler shift."""
# v = 15 m/s → fd = 2*v*fc/c ≈ 1050 Hz
# PRI = 167 us → Doppler bin = fd * N_chirps * PRI = 1050 * 32 * 167e-6 ≈ 5.6
# Long PRI = 167 us → sub-frame 0 bin = fd * 16 * 167e-6 ≈ 2.8 → bin ~3
# Short PRI = 175 us → sub-frame 1 bin = fd * 16 * 175e-6 ≈ 2.9 → bin 16+3 = 19
targets = [Target(range_m=500, velocity_mps=15.0, rcs_dbsm=20.0)]
return targets, "Single moving target v=15m/s (~1050Hz Doppler, bin~5-6)"
return targets, "Single moving target v=15m/s (~1050Hz Doppler, sf0 bin~3, sf1 bin~19)"
def make_scenario_two_targets():
@@ -269,12 +117,11 @@ SCENARIOS = {
# Main generator
# =============================================================================
def generate_scenario(name, targets, description, base_dir, use_buggy_model=False):
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}")
model_label = "BUGGY (RTL pipeline)" if use_buggy_model else "CLEAN"
print(f"Model: {model_label}")
print(f"Model: CLEAN (dual 16-pt FFT)")
print(f"{'='*60}")
# Generate Doppler frame (32 chirps x 64 range bins)
@@ -292,26 +139,24 @@ def generate_scenario(name, targets, description, base_dir, use_buggy_model=Fals
input_hex = os.path.join(base_dir, f"doppler_input_{name}.hex")
write_hex_32bit(input_hex, packed_samples)
# ---- Run through Python model ----
if use_buggy_model:
doppler_i, doppler_q = buggy_process_frame(frame_i, frame_q)
else:
dp = DopplerProcessor()
doppler_i, doppler_q = dp.process_frame(frame_i, frame_q)
# ---- Run through Python model (dual 16-pt FFT) ----
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")
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
# Ordered same as RTL output: all doppler bins for rbin 0, then rbin 1, ...
# Bins 0-15 = sub-frame 0 (long PRI), bins 16-31 = sub-frame 1 (short PRI)
flat_rbin = []
flat_dbin = []
flat_i = []
flat_q = []
for rbin in range(RANGE_BINS):
for dbin in range(DOPPLER_FFT_SIZE):
for dbin in range(DOPPLER_TOTAL_BINS):
flat_rbin.append(rbin)
flat_dbin.append(dbin)
flat_i.append(doppler_i[rbin][dbin])
@@ -331,8 +176,8 @@ def generate_scenario(name, targets, description, base_dir, use_buggy_model=Fals
peak_info = []
for rbin in range(RANGE_BINS):
mags = [abs(doppler_i[rbin][d]) + abs(doppler_q[rbin][d])
for d in range(DOPPLER_FFT_SIZE)]
peak_dbin = max(range(DOPPLER_FFT_SIZE), key=lambda d: mags[d])
for d in range(DOPPLER_TOTAL_BINS)]
peak_dbin = max(range(DOPPLER_TOTAL_BINS), key=lambda d: mags[d])
peak_mag = mags[peak_dbin]
peak_info.append((rbin, peak_dbin, peak_mag))
@@ -341,33 +186,14 @@ def generate_scenario(name, targets, description, base_dir, use_buggy_model=Fals
for rbin, dbin, mag in peak_info[:5]:
i_val = doppler_i[rbin][dbin]
q_val = doppler_q[rbin][dbin]
print(f" rbin={rbin:2d}, dbin={dbin:2d}, mag={mag:6d}, "
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}")
# ---- Write frame data for debugging ----
# Also write per-range-bin FFT input (for debugging pipeline alignment)
if use_buggy_model:
# Write the buggy FFT inputs for debugging
debug_csv = os.path.join(base_dir, f"doppler_fft_inputs_{name}.csv")
# Regenerate to capture FFT inputs
dp_debug = DopplerProcessor()
clean_i, clean_q = dp_debug.process_frame(frame_i, frame_q)
# Show the difference between clean and buggy
print(f"\n Comparing clean vs buggy model outputs:")
mismatches = 0
for rbin in range(RANGE_BINS):
for dbin in range(DOPPLER_FFT_SIZE):
if (doppler_i[rbin][dbin] != clean_i[rbin][dbin] or
doppler_q[rbin][dbin] != clean_q[rbin][dbin]):
mismatches += 1
total = RANGE_BINS * DOPPLER_FFT_SIZE
print(f" {mismatches}/{total} output samples differ "
f"({100*mismatches/total:.1f}%)")
return {
'name': name,
'description': description,
'model': 'buggy' if use_buggy_model else 'clean',
'peak_info': peak_info[:5],
}
@@ -375,11 +201,9 @@ def generate_scenario(name, targets, description, base_dir, use_buggy_model=Fals
def main():
base_dir = os.path.dirname(os.path.abspath(__file__))
use_buggy = '--buggy' in sys.argv
print("=" * 60)
print("Doppler Processor Co-Sim Golden Reference Generator")
print(f"Model: {'BUGGY (RTL pipeline replication)' if use_buggy else 'CLEAN'}")
print(f"Architecture: dual {DOPPLER_FFT_SIZE}-pt FFT ({DOPPLER_TOTAL_BINS} total bins)")
print("=" * 60)
scenarios_to_run = list(SCENARIOS.keys())
@@ -395,15 +219,14 @@ def main():
results = []
for name in scenarios_to_run:
targets, description = SCENARIOS[name]()
r = generate_scenario(name, targets, description, base_dir,
use_buggy_model=use_buggy)
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} [{r['model']}] top peak: "
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]}")
+17 -7
View File
@@ -48,19 +48,24 @@ ADC_BITS = 8 # ADC resolution
T_LONG_CHIRP = 30e-6 # 30 us long chirp duration
T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp
T_LISTEN_LONG = 137e-6 # 137 us listening window
T_PRI_LONG = 167e-6 # 30 us chirp + 137 us listen
T_PRI_SHORT = 175e-6 # staggered short-PRI sub-frame
N_SAMPLES_LISTEN = int(T_LISTEN_LONG * FS_ADC) # 54800 samples
# Processing chain
CIC_DECIMATION = 4
FFT_SIZE = 1024
RANGE_BINS = 64
DOPPLER_FFT_SIZE = 32
DOPPLER_FFT_SIZE = 16 # Per sub-frame
DOPPLER_TOTAL_BINS = 32 # Total output bins (2 sub-frames x 16)
CHIRPS_PER_SUBFRAME = 16
CHIRPS_PER_FRAME = 32
# Derived
RANGE_RESOLUTION = C_LIGHT / (2 * CHIRP_BW) # 7.5 m
MAX_UNAMBIGUOUS_RANGE = C_LIGHT * T_LISTEN_LONG / 2 # ~20.55 km
VELOCITY_RESOLUTION = WAVELENGTH / (2 * CHIRPS_PER_FRAME * T_LONG_CHIRP)
VELOCITY_RESOLUTION_LONG = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_LONG)
VELOCITY_RESOLUTION_SHORT = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_SHORT)
# Short chirp LUT (60 entries, 8-bit unsigned)
SHORT_CHIRP_LUT = [
@@ -384,9 +389,6 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
break
return math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2)
# Chirp repetition interval (PRI)
t_pri = T_LONG_CHIRP + T_LISTEN_LONG # ~167 us
frame_i = []
frame_q = []
@@ -408,8 +410,16 @@ def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
# Amplitude (simplified)
amp = target.amplitude / 4.0
# Doppler phase for this chirp
doppler_phase = 2 * math.pi * target.doppler_hz * chirp_idx * t_pri
# Doppler phase for this chirp.
# The frame uses staggered PRF: chirps 0-15 use the long PRI,
# chirps 16-31 use the short PRI.
if chirp_idx < CHIRPS_PER_SUBFRAME:
slow_time_s = chirp_idx * T_PRI_LONG
else:
slow_time_s = (CHIRPS_PER_SUBFRAME * T_PRI_LONG) + \
((chirp_idx - CHIRPS_PER_SUBFRAME) * T_PRI_SHORT)
doppler_phase = 2 * math.pi * target.doppler_hz * slow_time_s
total_phase = doppler_phase + target.phase_deg * math.pi / 180.0
# Spread across a few bins (sinc-like response from matched filter)
@@ -91,6 +91,7 @@ doppler_processor_optimized dut (
.doppler_valid(doppler_valid),
.doppler_bin(doppler_bin),
.range_bin(range_bin),
.sub_frame(), // Not used in this testbench
.processing_active(processing_active),
.frame_complete(frame_complete),
.status(dut_status)
@@ -75,6 +75,7 @@ doppler_processor_optimized dut (
.doppler_valid(doppler_valid),
.doppler_bin(doppler_bin),
.range_bin(range_bin),
.sub_frame(), // Not used in this testbench
.processing_active(processing_active),
.frame_complete(frame_complete),
.status(dut_status)