Production Board USB
+FT2232H (USB 2.0)
+50T production board uses FT2232H. FT601 USB 3.0 is available on 200T premium dev board only.
+FT2232H (USB 2.0)
+50T production board uses FT2232H. FT601 USB 3.0 is available on 200T premium dev board only.
+WNS +0.058 ns
From a03dd1329a197adc610c4645640b842a975958e1 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:48:43 +0545 Subject: [PATCH 4/7] fix(tests): update cross-layer tests for frame_start bit and stream-gated mux - TB byte 9 check: expect 0x81 (frame_start=1 after reset + cfar=1) - contract_parser: handle ternary expressions in data_pkt_byte mux (stream_doppler_en ? doppler_real_cap : 8'd0 pattern) - contract_parser: handle intermediate variable pattern for detection field (det_byte = raw[9]; detection = det_byte & 0x01) --- .../tests/cross_layer/contract_parser.py | 51 +++++++++++++++---- .../cross_layer/tb_cross_layer_ft2232h.v | 6 ++- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/9_Firmware/tests/cross_layer/contract_parser.py b/9_Firmware/tests/cross_layer/contract_parser.py index a0220ff..33e0a19 100644 --- a/9_Firmware/tests/cross_layer/contract_parser.py +++ b/9_Firmware/tests/cross_layer/contract_parser.py @@ -188,7 +188,7 @@ def parse_python_data_packet_fields(filepath: Path | None = None) -> list[DataPa width_bits=size * 8 )) - # Match detection = raw[9] & 0x01 + # Match detection = raw[9] & 0x01 (direct access) for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]\s*&\s*(0x[0-9a-fA-F]+|\d+)', body): name = m.group(1) offset = int(m.group(2)) @@ -196,6 +196,24 @@ def parse_python_data_packet_fields(filepath: Path | None = None) -> list[DataPa name=name, byte_start=offset, byte_end=offset, width_bits=1 )) + # Match intermediate variable pattern: var = raw[N], then field = var & MASK + for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]', body): + var_name = m.group(1) + offset = int(m.group(2)) + # Find fields derived from this intermediate variable + for m2 in re.finditer( + rf'(\w+)\s*=\s*(?:\({var_name}\s*>>\s*\d+\)\s*&|{var_name}\s*&)\s*' + r'(0x[0-9a-fA-F]+|\d+)', + body, + ): + name = m2.group(1) + # Skip if already captured by direct raw[] access pattern + if not any(f.name == name for f in fields): + fields.append(DataPacketField( + name=name, byte_start=offset, byte_end=offset, + width_bits=1 + )) + fields.sort(key=lambda f: f.byte_start) return fields @@ -583,12 +601,28 @@ def parse_verilog_data_mux( for m in re.finditer( r"5'd(\d+)\s*:\s*data_pkt_byte\s*=\s*(.+?);", - mux_body + mux_body, re.DOTALL ): idx = int(m.group(1)) expr = m.group(2).strip() entries.append((idx, expr)) + # Helper: extract the dominant signal name from a mux expression. + # Handles direct refs like ``range_profile_cap[31:24]``, ternaries + # like ``stream_doppler_en ? doppler_real_cap[15:8] : 8'd0``, and + # concat-ternaries like ``stream_cfar_en ? {…, cfar_detection_cap} : …``. + def _extract_signal(expr: str) -> str | None: + # If it's a ternary, use the true-branch to find the data signal + tern = re.match(r'\w+\s*\?\s*(.+?)\s*:\s*.+', expr, re.DOTALL) + target = tern.group(1) if tern else expr + # Look for a known data signal (xxx_cap pattern or cfar_detection_cap) + cap_match = re.search(r'(\w+_cap)\b', target) + if cap_match: + return cap_match.group(1) + # Fall back to first identifier before a bit-select + sig_match = re.match(r'(\w+?)(?:\[|$)', target) + return sig_match.group(1) if sig_match else None + # Group consecutive bytes by signal root name fields: list[DataPacketField] = [] i = 0 @@ -598,22 +632,21 @@ def parse_verilog_data_mux( i += 1 continue - # Extract signal name (e.g., range_profile_cap from range_profile_cap[31:24]) - sig_match = re.match(r'(\w+?)(?:\[|$)', expr) - if not sig_match: + signal = _extract_signal(expr) + if not signal: i += 1 continue - signal = sig_match.group(1) start_byte = idx end_byte = idx # Find consecutive bytes of the same signal j = i + 1 while j < len(entries): - next_idx, next_expr = entries[j] - if next_expr.startswith(signal): - end_byte = next_idx + _next_idx, next_expr = entries[j] + next_sig = _extract_signal(next_expr) + if next_sig == signal: + end_byte = _next_idx j += 1 else: break diff --git a/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v b/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v index 94d0a82..107d36e 100644 --- a/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v +++ b/9_Firmware/tests/cross_layer/tb_cross_layer_ft2232h.v @@ -620,8 +620,10 @@ module tb_cross_layer_ft2232h; "Data pkt: byte 7 = 0x56 (doppler_imag MSB)"); check(captured_bytes[8] === 8'h78, "Data pkt: byte 8 = 0x78 (doppler_imag LSB)"); - check(captured_bytes[9] === 8'h01, - "Data pkt: byte 9 = 0x01 (cfar_detection=1)"); + // Byte 9 = {frame_start, 6'b0, cfar_detection} + // After reset sample_counter==0, so frame_start=1 → 0x81 + check(captured_bytes[9] === 8'h81, + "Data pkt: byte 9 = 0x81 (frame_start=1, cfar_detection=1)"); check(captured_bytes[10] === 8'h55, "Data pkt: byte 10 = 0x55 (footer)"); From 7a35f42e6106512c718711f86d44fe6bfa40eeb5 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:07:01 +0545 Subject: [PATCH 5/7] refactor(fpga): deduplicate RTL file lists in run_regression.sh Extract RECEIVER_RTL and SYSTEM_RTL shared arrays to replace 6 near-identical file lists. New modules now only need adding once. --- 9_Firmware/9_2_FPGA/run_regression.sh | 89 ++++++++++----------------- 1 file changed, 33 insertions(+), 56 deletions(-) diff --git a/9_Firmware/9_2_FPGA/run_regression.sh b/9_Firmware/9_2_FPGA/run_regression.sh index 9d45878..7ae9822 100755 --- a/9_Firmware/9_2_FPGA/run_regression.sh +++ b/9_Firmware/9_2_FPGA/run_regression.sh @@ -87,6 +87,33 @@ EXTRA_RTL=( frequency_matched_filter.v ) +# --------------------------------------------------------------------------- +# Shared RTL file lists for integration / system tests +# Centralised here so a new module only needs adding once. +# --------------------------------------------------------------------------- + +# Receiver chain (used by golden generate/compare tests) +RECEIVER_RTL=( + radar_receiver_final.v + radar_mode_controller.v + tb/ad9484_interface_400m_stub.v + ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v + cdc_modules.v fir_lowpass.v ddc_input_interface.v + chirp_memory_loader_param.v latency_buffer.v + matched_filter_multi_segment.v matched_filter_processing_chain.v + range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v + rx_gain_control.v mti_canceller.v +) + +# Full system top (receiver chain + TX + USB + detection + self-test) +SYSTEM_RTL=( + radar_system_top.v + radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v + "${RECEIVER_RTL[@]}" + usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v + cfar_ca.v fpga_self_test.v +) + # ---- Layer A: iverilog -Wall compilation ---- run_lint_iverilog() { local label="$1" @@ -404,83 +431,33 @@ if [[ "$QUICK" -eq 0 ]]; then run_test "Receiver (golden generate)" \ tb/tb_rx_golden_reg.vvp \ -DGOLDEN_GENERATE \ - tb/tb_radar_receiver_final.v radar_receiver_final.v \ - radar_mode_controller.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - rx_gain_control.v mti_canceller.v + tb/tb_radar_receiver_final.v "${RECEIVER_RTL[@]}" # Golden compare run_test "Receiver (golden compare)" \ tb/tb_rx_compare_reg.vvp \ - tb/tb_radar_receiver_final.v radar_receiver_final.v \ - radar_mode_controller.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - rx_gain_control.v mti_canceller.v + tb/tb_radar_receiver_final.v "${RECEIVER_RTL[@]}" # Full system top (monitoring-only, legacy) run_test "System Top (radar_system_tb)" \ tb/tb_system_reg.vvp \ - tb/radar_system_tb.v radar_system_top.v \ - radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \ - radar_receiver_final.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ - rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v + tb/radar_system_tb.v "${SYSTEM_RTL[@]}" # E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset) run_test "System E2E (tb_system_e2e)" \ tb/tb_system_e2e_reg.vvp \ - tb/tb_system_e2e.v radar_system_top.v \ - radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \ - radar_receiver_final.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ - rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v + tb/tb_system_e2e.v "${SYSTEM_RTL[@]}" # USB_MODE=1 (FT2232H production) variants of system tests run_test "System Top USB_MODE=1 (FT2232H)" \ tb/tb_system_ft2232h_reg.vvp \ -DUSB_MODE_1 \ - tb/radar_system_tb.v radar_system_top.v \ - radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \ - radar_receiver_final.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ - rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v + tb/radar_system_tb.v "${SYSTEM_RTL[@]}" run_test "System E2E USB_MODE=1 (FT2232H)" \ tb/tb_system_e2e_ft2232h_reg.vvp \ -DUSB_MODE_1 \ - tb/tb_system_e2e.v radar_system_top.v \ - radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \ - radar_receiver_final.v tb/ad9484_interface_400m_stub.v \ - ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \ - cdc_modules.v fir_lowpass.v ddc_input_interface.v \ - chirp_memory_loader_param.v latency_buffer.v \ - matched_filter_multi_segment.v matched_filter_processing_chain.v \ - range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \ - usb_data_interface.v usb_data_interface_ft2232h.v edge_detector.v radar_mode_controller.v \ - rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v + tb/tb_system_e2e.v "${SYSTEM_RTL[@]}" else echo " (skipped receiver golden + system top + E2E — use without --quick)" SKIP=$((SKIP + 6)) From 161e9a66e417ffc3f8b7d3af281aaedb47ac19f5 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:51:09 +0545 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20clarify=20comments=20=E2=80=94=20AGC?= =?UTF-8?q?=20width,=20dual-USB=20docstring,=20BE=20datasheet=20ref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 9_Firmware/9_2_FPGA/usb_data_interface.v | 4 ++-- 9_Firmware/9_3_GUI/v7/hardware.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/9_Firmware/9_2_FPGA/usb_data_interface.v b/9_Firmware/9_2_FPGA/usb_data_interface.v index 2545591..bcb0272 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface.v @@ -29,7 +29,7 @@ module usb_data_interface ( // FT601 Interface (Slave FIFO mode) // Data bus inout wire [31:0] ft601_data, // 32-bit bidirectional data bus - output reg [3:0] ft601_be, // Byte enable (active-HIGH per FT601 datasheet / AN_378) + output reg [3:0] ft601_be, // Byte enable (active-HIGH per DS_FT600Q-FT601Q Table 3.2) // Control signals // VESTIGIAL OUTPUTS — kept for 200T board port compatibility. @@ -374,7 +374,7 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin // Word 4: AGC metrics + range_mode status_words[4] <= {status_agc_current_gain, // [31:28] status_agc_peak_magnitude, // [27:20] - status_agc_saturation_count, // [19:12] + status_agc_saturation_count, // [19:12] 8-bit saturation count status_agc_enable, // [11] 9'd0, // [10:2] reserved status_range_mode}; // [1:0] diff --git a/9_Firmware/9_3_GUI/v7/hardware.py b/9_Firmware/9_3_GUI/v7/hardware.py index 2d85dc7..d36aa1a 100644 --- a/9_Firmware/9_3_GUI/v7/hardware.py +++ b/9_Firmware/9_3_GUI/v7/hardware.py @@ -47,8 +47,9 @@ class STM32USBInterface: Used ONLY for receiving GPS data from the MCU. - FPGA register commands are sent via FT2232H (see FT2232HConnection - from radar_protocol.py). The old send_start_flag() / send_settings() + FPGA register commands are sent via the USB data interface — either + FT2232HConnection (production) or FT601Connection (premium), both + from radar_protocol.py. The old send_start_flag() / send_settings() methods have been removed — they used an incompatible magic-packet protocol that the FPGA does not understand. """ From 76cfc71b194654111ba35f335c9c4106e56ee50c Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:35:01 +0545 Subject: [PATCH 7/7] fix(gui): align radar parameters to FPGA truth (radar_scene.py) - Bandwidth 500 MHz -> 20 MHz, sample rate 4 MHz -> 100 MHz (DDC output) - Range formula: deramped FMCW -> matched-filter c/(2*Fs)*decimation - Velocity formula: use PRI (167 us) and chirps_per_subframe (16) - Carrier frequency: 10.525 GHz -> 10.5 GHz per radar_scene.py - Range per bin: 4.8 m -> 24 m, max range: 307 m -> 1536 m - Fix simulator target spawn range to match new coverage (50-1400 m) - Remove dead BANDWIDTH constant, add SAMPLE_RATE to V65 Tk - All 174 tests pass, ruff clean --- 9_Firmware/9_3_GUI/GUI_V65_Tk.py | 20 +++++------ 9_Firmware/9_3_GUI/test_v7.py | 30 +++++++++-------- 9_Firmware/9_3_GUI/v7/map_widget.py | 2 +- 9_Firmware/9_3_GUI/v7/models.py | 51 ++++++++++++++++------------- 9_Firmware/9_3_GUI/v7/workers.py | 4 +-- 5 files changed, 58 insertions(+), 49 deletions(-) diff --git a/9_Firmware/9_3_GUI/GUI_V65_Tk.py b/9_Firmware/9_3_GUI/GUI_V65_Tk.py index 0ecae7b..659e280 100644 --- a/9_Firmware/9_3_GUI/GUI_V65_Tk.py +++ b/9_Firmware/9_3_GUI/GUI_V65_Tk.py @@ -98,9 +98,10 @@ class DemoTarget: __slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity") - # Physical range grid: 64 bins x ~4.8 m/bin = ~307 m max - _RANGE_PER_BIN: float = (3e8 / (2 * 500e6)) * 16 # ~4.8 m - _MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~307 m + # 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 def __init__(self, tid: int): self.id = tid @@ -187,10 +188,10 @@ class DemoSimulator: mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64) det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8) - # Range/Doppler scaling (approximate) - range_per_bin = (3e8 / (2 * 500e6)) * 16 # ~4.8 m/bin + # Range/Doppler scaling: bin spacing = c/(2*Fs)*decimation + range_per_bin = (3e8 / (2 * 100e6)) * 16 # ~24 m/bin max_range = range_per_bin * NUM_RANGE_BINS - vel_per_bin = 1.484 # m/s per Doppler bin (from WaveformConfig) + vel_per_bin = 5.34 # m/s per Doppler bin (radar_scene.py: lam/(2*16*PRI)) for t in targets: if t.range_m > max_range or t.range_m < 0: @@ -385,7 +386,7 @@ class RadarDashboard: UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh # Radar parameters used for range-axis scaling. - BANDWIDTH = 500e6 # Hz — chirp bandwidth + SAMPLE_RATE = 100e6 # Hz — DDC output I/Q rate (matched filter input) C = 3e8 # m/s — speed of light def __init__(self, root: tk.Tk, mock: bool, @@ -526,9 +527,8 @@ class RadarDashboard: def _build_display_tab(self, parent): # Compute physical axis limits - range_res = self.C / (2.0 * self.BANDWIDTH) # ~0.3 m per FFT bin - # After decimation 1024→64, each range bin = 16 FFT bins - range_per_bin = range_res * 16 + # Bin spacing = c / (2 * Fs_ddc) for matched-filter processing. + range_per_bin = self.C / (2.0 * self.SAMPLE_RATE) * 16 # ~24 m max_range = range_per_bin * NUM_RANGE_BINS doppler_bin_lo = 0 diff --git a/9_Firmware/9_3_GUI/test_v7.py b/9_Firmware/9_3_GUI/test_v7.py index 4f70ecd..636c5d4 100644 --- a/9_Firmware/9_3_GUI/test_v7.py +++ b/9_Firmware/9_3_GUI/test_v7.py @@ -65,9 +65,9 @@ class TestRadarSettings(unittest.TestCase): def test_defaults(self): s = _models().RadarSettings() - self.assertEqual(s.system_frequency, 10e9) - self.assertEqual(s.coverage_radius, 50000) - self.assertEqual(s.max_distance, 50000) + self.assertEqual(s.system_frequency, 10.5e9) + self.assertEqual(s.coverage_radius, 1536) + self.assertEqual(s.max_distance, 1536) class TestGPSData(unittest.TestCase): @@ -425,26 +425,28 @@ class TestWaveformConfig(unittest.TestCase): def test_defaults(self): from v7.models import WaveformConfig wc = WaveformConfig() - self.assertEqual(wc.sample_rate_hz, 4e6) - self.assertEqual(wc.bandwidth_hz, 500e6) - self.assertEqual(wc.chirp_duration_s, 300e-6) - self.assertEqual(wc.center_freq_hz, 10.525e9) + self.assertEqual(wc.sample_rate_hz, 100e6) + self.assertEqual(wc.bandwidth_hz, 20e6) + 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_doppler_bins, 32) + self.assertEqual(wc.chirps_per_subframe, 16) self.assertEqual(wc.fft_size, 1024) self.assertEqual(wc.decimation_factor, 16) def test_range_resolution(self): - """range_resolution_m should be ~5.62 m/bin with ADI defaults.""" + """range_resolution_m should be ~23.98 m/bin (matched filter, 100 MSPS).""" from v7.models import WaveformConfig wc = WaveformConfig() - self.assertAlmostEqual(wc.range_resolution_m, 5.621, places=1) + self.assertAlmostEqual(wc.range_resolution_m, 23.983, places=1) def test_velocity_resolution(self): - """velocity_resolution_mps should be ~1.484 m/s/bin.""" + """velocity_resolution_mps should be ~5.34 m/s/bin (PRI=167us, 16 chirps).""" from v7.models import WaveformConfig wc = WaveformConfig() - self.assertAlmostEqual(wc.velocity_resolution_mps, 1.484, places=2) + self.assertAlmostEqual(wc.velocity_resolution_mps, 5.343, places=1) def test_max_range(self): """max_range_m = range_resolution * n_range_bins.""" @@ -466,7 +468,7 @@ class TestWaveformConfig(unittest.TestCase): """Non-default parameters correctly change derived values.""" from v7.models import WaveformConfig wc1 = WaveformConfig() - wc2 = WaveformConfig(bandwidth_hz=1e9) # double BW → halve range res + wc2 = WaveformConfig(sample_rate_hz=200e6) # double Fs → halve range bin self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2) def test_zero_center_freq_velocity(self): @@ -925,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=5.621) + targets = extract_targets_from_frame(frame, range_resolution=23.983) self.assertEqual(len(targets), 1) - self.assertAlmostEqual(targets[0].range, 10 * 5.621, places=2) + self.assertAlmostEqual(targets[0].range, 10 * 23.983, places=1) self.assertAlmostEqual(targets[0].velocity, 0.0, places=2) def test_velocity_sign(self): diff --git a/9_Firmware/9_3_GUI/v7/map_widget.py b/9_Firmware/9_3_GUI/v7/map_widget.py index fa0fcb1..951418a 100644 --- a/9_Firmware/9_3_GUI/v7/map_widget.py +++ b/9_Firmware/9_3_GUI/v7/map_widget.py @@ -98,7 +98,7 @@ class RadarMapWidget(QWidget): ) self._targets: list[RadarTarget] = [] self._pending_targets: list[RadarTarget] | None = None - self._coverage_radius = 50_000 # metres + self._coverage_radius = 1_536 # metres (64 bins x ~24 m/bin) self._tile_server = TileServer.OPENSTREETMAP self._show_coverage = True self._show_trails = False diff --git a/9_Firmware/9_3_GUI/v7/models.py b/9_Firmware/9_3_GUI/v7/models.py index c4b277c..07952d4 100644 --- a/9_Firmware/9_3_GUI/v7/models.py +++ b/9_Firmware/9_3_GUI/v7/models.py @@ -108,12 +108,12 @@ class RadarSettings: range_resolution and velocity_resolution should be calibrated to the actual waveform parameters. """ - system_frequency: float = 10e9 # Hz (carrier, used for velocity calc) - range_resolution: float = 781.25 # Meters per range bin (default: 50km/64) - velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform) - max_distance: float = 50000 # Max detection range (m) - map_size: float = 50000 # Map display size (m) - coverage_radius: float = 50000 # Map coverage radius (m) + system_frequency: float = 10.5e9 # Hz (carrier, used for velocity calc) + range_resolution: float = 24.0 # Meters per range bin (c/(2*Fs)*decim) + 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) @dataclass @@ -199,39 +199,46 @@ class WaveformConfig: Encapsulates the radar waveform so that range/velocity resolution can be derived automatically instead of hardcoded in RadarSettings. - Defaults match the ADI CN0566 Phaser capture parameters used in - the golden_reference cosim (4 MSPS, 500 MHz BW, 300 us chirp). + Defaults match the AERIS-10 production system parameters from + radar_scene.py / plfm_chirp_controller.v: + 100 MSPS DDC output, 20 MHz chirp BW, 30 us long chirp, + 167 us long-chirp PRI, X-band 10.5 GHz carrier. """ - sample_rate_hz: float = 4e6 # ADC sample rate - bandwidth_hz: float = 500e6 # Chirp bandwidth - chirp_duration_s: float = 300e-6 # Chirp ramp time - center_freq_hz: float = 10.525e9 # Carrier frequency + sample_rate_hz: float = 100e6 # DDC output I/Q rate (matched filter input) + bandwidth_hz: float = 20e6 # Chirp bandwidth (not used in range calc; + # retained for time-bandwidth product / display) + 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_doppler_bins: int = 32 # After Doppler FFT + 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 @property def range_resolution_m(self) -> float: - """Meters per decimated range bin (FMCW deramped baseband). + """Meters per decimated range bin (matched-filter pulse compression). - For deramped FMCW: bin spacing = c * Fs * T / (2 * N_FFT * BW). - After decimation the bin spacing grows by *decimation_factor*. + For FFT-based matched filtering, each IFFT output bin spans + c / (2 * Fs) in range, where Fs is the I/Q sample rate at the + matched-filter input (DDC output). After decimation the bin + spacing grows by *decimation_factor*. """ c = 299_792_458.0 - raw_bin = ( - c * self.sample_rate_hz * self.chirp_duration_s - / (2.0 * self.fft_size * self.bandwidth_hz) - ) + raw_bin = c / (2.0 * self.sample_rate_hz) return raw_bin * self.decimation_factor @property def velocity_resolution_mps(self) -> float: - """m/s per Doppler bin. lambda / (2 * n_doppler * chirp_duration).""" + """m/s per Doppler bin. + + lambda / (2 * chirps_per_subframe * PRI), matching radar_scene.py. + """ c = 299_792_458.0 wavelength = c / self.center_freq_hz - return wavelength / (2.0 * self.n_doppler_bins * self.chirp_duration_s) + return wavelength / (2.0 * self.chirps_per_subframe * self.pri_s) @property def max_range_m(self) -> float: diff --git a/9_Firmware/9_3_GUI/v7/workers.py b/9_Firmware/9_3_GUI/v7/workers.py index c29f3bd..6bf115f 100644 --- a/9_Firmware/9_3_GUI/v7/workers.py +++ b/9_Firmware/9_3_GUI/v7/workers.py @@ -334,7 +334,7 @@ class TargetSimulator(QObject): self._add_random_target() def _add_random_target(self): - range_m = random.uniform(5000, 40000) + range_m = random.uniform(50, 1400) azimuth = random.uniform(0, 360) velocity = random.uniform(-100, 100) elevation = random.uniform(-5, 45) @@ -368,7 +368,7 @@ class TargetSimulator(QObject): for t in self._targets: new_range = t.range - t.velocity * 0.5 - if new_range < 500 or new_range > 50000: + if new_range < 10 or new_range > 1536: continue # target exits coverage — drop it new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2)))