mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-13 08:51:19 +00:00
fix(radar): RX chain corrections, GUI bin alignment, MCU boot ordering
FPGA — RX chain
matched_filter_multi_segment.v: drop the gratuitous /4 scaling on
DDC sign-extended input (was ddc_i[17:2] + ddc_i[1]); use
ddc_i[15:0] directly. fft_engine has INTERNAL_W=32 with
saturating 16-bit output, so full 16-bit input is safe. Restores
~12 dB of MF input dynamic range.
radar_receiver_final.v: remove latency_buffer (count-N-pulses-then-
prime FIFO that left frame 1 with all-zero ref). Replaced with
a single-FF alignment register on ref_i/ref_q that matches the
1-FF stage multi_segment ST_PROCESSING uses on adc_data.
Verified by tb/tb_rxb_fullchain_latency.v — autocorrelation peak
at bin 0 with peak/mean ~88x.
doppler_processor.v / mti_canceller.v / cfar_ca.v /
range_bin_decimator.v / radar_receiver_final.v / radar_system_top.v
/ usb_data_interface_ft2232h.v: switch port and parameter widths
from RP_NUM_RANGE_BINS / RP_RANGE_BIN_BITS (always 512 / 9-bit)
to RP_MAX_OUTPUT_BINS / RP_RANGE_BIN_WIDTH_MAX (auto-scales:
50T 512 / 9-bit, 200T 4096 / 12-bit). Unblocks 200T 20 km mode
at the RX module boundary; USB wire-protocol extension still
pending.
radar_receiver_final.v: doppler_frame_done_prev reset value 0 -> 1
to prevent false done pulse on cycle 1 when level signal is
HIGH at reset.
matched_filter_processing_chain.v: delete the broken `ifdef
SIMULATION inline behavioural FFT (482 lines removed). It
produced wrong-bin peaks and 100-1000x weak magnitudes. Chain
now uses production fft_engine.v + frequency_matched_filter.v
in both iverilog and Vivado. Iverilog tests are ~38x slower per
chain pass but produce correct results. Misleading "OK with
Xilinx IP" comments at three test sites updated since the FFT
is in-house, not an IP placeholder.
FPGA — testbenches
tb/tb_rxb_latency_measure.v (new): measures chain internal pipeline
depth (~2057 cycles, chirp-agnostic).
tb/tb_rxb_fullchain_latency.v (new): full-chain autocorrelation
verification — drives ddc with the same chirp samples the loader
serves as ref, finds peak position and peak/mean.
tb/tb_matched_filter_processing_chain.v: wait timeouts bumped
50000 -> 500000 cycles to accommodate production FFT pipeline.
MCU
main.cpp checkSystemHealthStatus: latch system_emergency_state on
the error_count > 10 path so the SAFE-MODE blink loop in main()
actually engages (was bypassed because predicate was false).
main.cpp: move FPGA reset BEFORE the if(PowerAmplifier) block so
adar_tr_x is driven LOW (RX commanded externally) before PA Vdd
reaches 22 V. Old reset block at the original location removed.
main.cpp MX_GPIO_Init: add GPIO_PIN_12 (FPGA reset) to the
explicit WritePin(LOW) list so the safe initial state is no
longer implicit.
main.cpp checkSystemHealth: rate-limit ADAR1000
verifyDeviceCommunication (HAL_Delay 1ms x 4 devices = 4 ms
blocking SPI burst per main-loop iteration) from every-loop to
every 2 s. readTemperature stays per-loop so over-temp
detection latency is unchanged.
USBHandler.cpp processSettingsData: dispatch threshold bumped
74 -> 82 (matches parser minimum); buffer drained after parse
attempt (slide remaining bytes left) so a false END find no
longer sticks the buffer until 256-byte overflow.
GUI
radar_protocol.py: NUM_RANGE_BINS 64 -> 512 (matches FPGA
RP_NUM_RANGE_BINS); NUM_CELLS 2048 -> 16384.
radar_protocol.py _ingest_sample: honor FPGA frame_start bit for
resync after a USB drop; capture range_profile[rbin] once per
range bin at dbin == 0 (FPGA emits the same range_i/range_q for
all 32 Doppler cells of a given range bin; previous accumulator
inflated the profile 32x).
v7/models.py RadarSettings: range_resolution 24 -> 6 m (matches
c/(2*100MHz)*4); max_distance and coverage_radius 1536 -> 3072 m;
map_size 2000 -> 4000.
v7/models.py WaveformConfig: n_range_bins 64 -> 512, fft_size
1024 -> 2048, decimation_factor 16 -> 4.
GUI_V65_Tk.py: _RANGE_PER_BIN math and stale "~24 m / ~1536 m"
comments updated.
test_v7.py: assertion values updated to match new defaults.
Tests
test_ddc_cosim_fuzz.py: remove unused os/tempfile imports, wrap
three long lines for ruff E501 compliance.
This commit is contained in:
@@ -98,10 +98,10 @@ class DemoTarget:
|
||||
|
||||
__slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity")
|
||||
|
||||
# Physical range grid: 64 bins x ~24 m/bin = ~1536 m max
|
||||
# Bin spacing = c / (2 * Fs) * decimation, where Fs = 100 MHz DDC output.
|
||||
_RANGE_PER_BIN: float = (3e8 / (2 * 100e6)) * 16 # ~24 m
|
||||
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~1536 m
|
||||
# Physical range grid: 512 bins x ~6 m/bin = ~3072 m max (3 km mode)
|
||||
# Bin spacing = c / (2 * Fs) * decimation, where Fs = 100 MHz DDC output, decim = 4.
|
||||
_RANGE_PER_BIN: float = (3e8 / (2 * 100e6)) * 4 # ~6 m
|
||||
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~3072 m
|
||||
|
||||
def __init__(self, tid: int):
|
||||
self.id = tid
|
||||
|
||||
@@ -43,9 +43,9 @@ STATUS_HEADER_BYTE = 0xBB
|
||||
DATA_PACKET_SIZE = 11 # 1 + 4 + 2 + 2 + 1 + 1
|
||||
STATUS_PACKET_SIZE = 26 # 1 + 24 + 1
|
||||
|
||||
NUM_RANGE_BINS = 64
|
||||
NUM_RANGE_BINS = 512
|
||||
NUM_DOPPLER_BINS = 32
|
||||
NUM_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS # 2048
|
||||
NUM_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS # 16384
|
||||
|
||||
WATERFALL_DEPTH = 64
|
||||
|
||||
@@ -777,6 +777,13 @@ class RadarAcquisition(threading.Thread):
|
||||
|
||||
def _ingest_sample(self, sample: dict):
|
||||
"""Place sample into current frame and emit when complete."""
|
||||
# [GUI-C2 FIX] Use FPGA frame_start bit as the authoritative sync token.
|
||||
# If FPGA flags frame_start mid-stream (after a USB drop or any glitch),
|
||||
# finalize whatever we have and re-align to bin (0, 0). Without this the
|
||||
# count-only sync stays permanently misaligned after a single dropped byte.
|
||||
if sample.get("frame_start", 0) and self._sample_idx > 0:
|
||||
self._finalize_frame() # resets _sample_idx to 0 and starts a new frame
|
||||
|
||||
rbin = self._sample_idx // NUM_DOPPLER_BINS
|
||||
dbin = self._sample_idx % NUM_DOPPLER_BINS
|
||||
|
||||
@@ -788,12 +795,15 @@ class RadarAcquisition(threading.Thread):
|
||||
if sample.get("detection", 0):
|
||||
self._frame.detections[rbin, dbin] = 1
|
||||
self._frame.detection_count += 1
|
||||
# Accumulate FPGA range profile data (matched-filter output)
|
||||
# Each sample carries the range_i/range_q for this range bin.
|
||||
# Accumulate magnitude across Doppler bins for the range profile.
|
||||
ri = int(sample.get("range_i", 0))
|
||||
rq = int(sample.get("range_q", 0))
|
||||
self._frame.range_profile[rbin] += abs(ri) + abs(rq)
|
||||
# [GUI-C4 FIX] FPGA emits the same range_i/range_q for all 32 Doppler
|
||||
# bins of a given range bin (it's the matched-filter range output,
|
||||
# repeated per Doppler cell). Accumulating across all 32 inflates
|
||||
# the profile 32x. Capture once per range bin at the first Doppler
|
||||
# cell instead.
|
||||
if dbin == 0:
|
||||
ri = int(sample.get("range_i", 0))
|
||||
rq = int(sample.get("range_q", 0))
|
||||
self._frame.range_profile[rbin] = abs(ri) + abs(rq)
|
||||
|
||||
self._sample_idx += 1
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ class TestRadarSettings(unittest.TestCase):
|
||||
def test_defaults(self):
|
||||
s = _models().RadarSettings()
|
||||
self.assertEqual(s.system_frequency, 10.5e9)
|
||||
self.assertEqual(s.coverage_radius, 1536)
|
||||
self.assertEqual(s.max_distance, 1536)
|
||||
self.assertEqual(s.coverage_radius, 3072)
|
||||
self.assertEqual(s.max_distance, 3072)
|
||||
|
||||
|
||||
class TestGPSData(unittest.TestCase):
|
||||
@@ -430,17 +430,17 @@ class TestWaveformConfig(unittest.TestCase):
|
||||
self.assertEqual(wc.chirp_duration_s, 30e-6)
|
||||
self.assertEqual(wc.pri_s, 167e-6)
|
||||
self.assertEqual(wc.center_freq_hz, 10.5e9)
|
||||
self.assertEqual(wc.n_range_bins, 64)
|
||||
self.assertEqual(wc.n_range_bins, 512)
|
||||
self.assertEqual(wc.n_doppler_bins, 32)
|
||||
self.assertEqual(wc.chirps_per_subframe, 16)
|
||||
self.assertEqual(wc.fft_size, 1024)
|
||||
self.assertEqual(wc.decimation_factor, 16)
|
||||
self.assertEqual(wc.fft_size, 2048)
|
||||
self.assertEqual(wc.decimation_factor, 4)
|
||||
|
||||
def test_range_resolution(self):
|
||||
"""range_resolution_m should be ~23.98 m/bin (matched filter, 100 MSPS)."""
|
||||
"""range_resolution_m should be ~6.0 m/bin (matched filter, 100 MSPS, decim 4)."""
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
self.assertAlmostEqual(wc.range_resolution_m, 23.983, places=1)
|
||||
self.assertAlmostEqual(wc.range_resolution_m, 5.996, places=2)
|
||||
|
||||
def test_velocity_resolution(self):
|
||||
"""velocity_resolution_mps should be ~5.34 m/s/bin (PRI=167us, 16 chirps)."""
|
||||
@@ -452,7 +452,7 @@ class TestWaveformConfig(unittest.TestCase):
|
||||
"""max_range_m = range_resolution * n_range_bins."""
|
||||
from v7.models import WaveformConfig
|
||||
wc = WaveformConfig()
|
||||
self.assertAlmostEqual(wc.max_range_m, wc.range_resolution_m * 64, places=1)
|
||||
self.assertAlmostEqual(wc.max_range_m, wc.range_resolution_m * 512, places=1)
|
||||
|
||||
def test_max_velocity(self):
|
||||
"""max_velocity_mps = velocity_resolution * n_doppler_bins / 2."""
|
||||
@@ -927,9 +927,9 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
|
||||
"""Detection at range bin 10 → range = 10 * range_resolution."""
|
||||
from v7.processing import extract_targets_from_frame
|
||||
frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0
|
||||
targets = extract_targets_from_frame(frame, range_resolution=23.983)
|
||||
targets = extract_targets_from_frame(frame, range_resolution=5.996)
|
||||
self.assertEqual(len(targets), 1)
|
||||
self.assertAlmostEqual(targets[0].range, 10 * 23.983, places=1)
|
||||
self.assertAlmostEqual(targets[0].range, 10 * 5.996, places=1)
|
||||
self.assertAlmostEqual(targets[0].velocity, 0.0, places=2)
|
||||
|
||||
def test_velocity_sign(self):
|
||||
|
||||
@@ -109,11 +109,11 @@ class RadarSettings:
|
||||
the actual waveform parameters.
|
||||
"""
|
||||
system_frequency: float = 10.5e9 # Hz (carrier, used for velocity calc)
|
||||
range_resolution: float = 24.0 # Meters per range bin (c/(2*Fs)*decim)
|
||||
range_resolution: float = 6.0 # Meters per range bin (c/(2*Fs)*decim = 1.5*4)
|
||||
velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform)
|
||||
max_distance: float = 1536 # Max detection range (m)
|
||||
map_size: float = 2000 # Map display size (m)
|
||||
coverage_radius: float = 1536 # Map coverage radius (m)
|
||||
max_distance: float = 3072 # Max detection range (m), 3 km mode
|
||||
map_size: float = 4000 # Map display size (m)
|
||||
coverage_radius: float = 3072 # Map coverage radius (m), 3 km mode
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -211,11 +211,11 @@ class WaveformConfig:
|
||||
chirp_duration_s: float = 30e-6 # Long chirp ramp time
|
||||
pri_s: float = 167e-6 # Pulse repetition interval (chirp + listen)
|
||||
center_freq_hz: float = 10.5e9 # Carrier frequency (radar_scene.py: F_CARRIER)
|
||||
n_range_bins: int = 64 # After decimation
|
||||
n_range_bins: int = 512 # After decimation (3 km mode; 4096 in 20 km)
|
||||
n_doppler_bins: int = 32 # Total Doppler bins (2 sub-frames x 16)
|
||||
chirps_per_subframe: int = 16 # Chirps in one Doppler sub-frame
|
||||
fft_size: int = 1024 # Pre-decimation FFT length
|
||||
decimation_factor: int = 16 # 1024 → 64
|
||||
fft_size: int = 2048 # Pre-decimation FFT length
|
||||
decimation_factor: int = 4 # 2048 → 512
|
||||
|
||||
@property
|
||||
def range_resolution_m(self) -> float:
|
||||
|
||||
Reference in New Issue
Block a user