Compare commits

..

4 Commits

Author SHA1 Message Date
Jason 7cb7688814 chore: add .gitattributes to enforce LF line endings for new files 2026-04-15 13:03:54 +05:45
Jason 86b493a780 feat: CI test suite phases A+B, WaveformConfig separation, dead golden code cleanup
- Phase A: Remove self-blessing golden test from FPGA regression, wire
  MF co-sim (4 scenarios) into run_regression.sh, add opcode count guards
  to cross-layer tests (+3 tests)
- Phase B: Add radar_params.vh parser and architectural param consistency
  tests (+7 tests), add banned stale-value pattern scanner (+1 test)
- Separate WaveformConfig.range_resolution_m (physical, bandwidth-dependent)
  from bin_spacing_m (sample-rate dependent); rename all callers
- Remove 151 lines of dead golden generate/compare code from
  tb_radar_receiver_final.v; testbench now runs structural + bounds only
- Untrack generated MF co-sim CSV files, gitignore tb/golden/ directory

CI: 256 tests total (168 python + 40 cross-layer + 27 FPGA + 21 MCU), all green
2026-04-15 12:45:41 +05:45
Jason c023337949 chore: gitignore sim artifacts (doppler CSV/mem) and MCU test binaries
- Untrack rx_final_doppler_out.csv and golden_doppler.mem (regenerated by tests)
- Add 6 missing MCU test binaries to tests/.gitignore
2026-04-15 10:39:05 +05:45
Jason d259e5c106 fix: align all range/carrier/velocity values to PLFM hardware + FPGA bug fixes
- Correct carrier from 10.525/10 GHz to 10.5 GHz (verified ADF4382 config)
- Correct range-per-bin from 4.8/5.6/781.25 m to 24.0 m (matched-filter)
- Correct velocity resolution from 1.484 to 2.67 m/s/bin (PRI-based)
- Correct processing rate from 4 MSPS to 100 MSPS (post-DDC)
- Correct max range from 307/5000/50000 m to 1536 m (64 bins x 24 m)
- Add WaveformConfig.pri_s field (167 us PRI for velocity calculation)
- Fix short chirp chirp_complete deadlock (Bug A)
- Remove dead short_chirp ports, rename long_chirp to ref_chirp (Bug B)
- Fix stale latency comment 2159 -> 3187 cycles (Bug C)
- Create radar_params.vh as single source of truth for FPGA parameters
- Lower RadarSettings.cpp map_size validation bound from 1000 to 100
- Add PLFM hardware constants to golden_reference.py
- Update all GUI versions, tests, and cross-layer contracts

All 244 tests passing (167 Python + 21 MCU + 29 cross-layer + 27 FPGA)
2026-04-15 10:38:59 +05:45
66 changed files with 1419 additions and 14654 deletions
+21
View File
@@ -0,0 +1,21 @@
# Enforce LF line endings for all text files going forward.
# Existing CRLF files are left as-is to avoid polluting git blame.
* text=auto eol=lf
# Binary files — ensure git doesn't mangle these
*.npy binary
*.h5 binary
*.hdf5 binary
*.png binary
*.jpg binary
*.pdf binary
*.zip binary
*.bin binary
*.mem binary
*.hex binary
*.vvp binary
*.s2p binary
*.s3p binary
*.step binary
*.FCStd binary
*.FCBak binary
+6
View File
@@ -32,6 +32,12 @@
9_Firmware/9_2_FPGA/tb/cosim/rtl_doppler_*.csv 9_Firmware/9_2_FPGA/tb/cosim/rtl_doppler_*.csv
9_Firmware/9_2_FPGA/tb/cosim/compare_doppler_*.csv 9_Firmware/9_2_FPGA/tb/cosim/compare_doppler_*.csv
9_Firmware/9_2_FPGA/tb/cosim/rtl_multiseg_*.csv 9_Firmware/9_2_FPGA/tb/cosim/rtl_multiseg_*.csv
9_Firmware/9_2_FPGA/tb/cosim/rx_final_doppler_out.csv
9_Firmware/9_2_FPGA/tb/cosim/rtl_mf_*.csv
9_Firmware/9_2_FPGA/tb/cosim/compare_mf_*.csv
# Golden reference outputs (regenerated by testbenches)
9_Firmware/9_2_FPGA/tb/golden/
# macOS # macOS
.DS_Store .DS_Store
+4 -1
View File
@@ -1,7 +1,10 @@
import numpy as np import numpy as np
# Define parameters # Define parameters
fs = 120e6 # Sampling frequency # NOTE: This is a standalone LUT generation utility. The production chirp LUT
# is generated by 9_Firmware/9_2_FPGA/tb/cosim/gen_chirp_mem.py with
# CHIRP_BW=20e6 (target: 30e6 Phase 1) and DAC_CLK=120e6.
fs = 120e6 # Sampling frequency (DAC clock from AD9523 OUT10)
Ts = 1 / fs # Sampling time Ts = 1 / fs # Sampling time
Tb = 1e-6 # Burst time Tb = 1e-6 # Burst time
Tau = 30e-6 # Pulse repetition time Tau = 30e-6 # Pulse repetition time
@@ -6,16 +6,16 @@ RadarSettings::RadarSettings() {
} }
void RadarSettings::resetToDefaults() { void RadarSettings::resetToDefaults() {
system_frequency = 10.0e9; // 10 GHz system_frequency = 10.5e9; // 10.5 GHz (PLFM TX LO, ADF4382 config)
chirp_duration_1 = 30.0e-6; // 30 s chirp_duration_1 = 30.0e-6; // 30 µs
chirp_duration_2 = 0.5e-6; // 0.5 s chirp_duration_2 = 0.5e-6; // 0.5 µs
chirps_per_position = 32; chirps_per_position = 32;
freq_min = 10.0e6; // 10 MHz freq_min = 10.0e6; // 10 MHz
freq_max = 30.0e6; // 30 MHz freq_max = 30.0e6; // 30 MHz
prf1 = 1000.0; // 1 kHz prf1 = 1000.0; // 1 kHz
prf2 = 2000.0; // 2 kHz prf2 = 2000.0; // 2 kHz
max_distance = 50000.0; // 50 km max_distance = 1536.0; // 1536 m (64 bins × 24 m, 3 km mode)
map_size = 50000.0; // 50 km map_size = 1536.0; // 1536 m
settings_valid = true; settings_valid = true;
} }
@@ -88,7 +88,7 @@ bool RadarSettings::validateSettings() {
if (prf1 < 100 || prf1 > 10000) return false; if (prf1 < 100 || prf1 > 10000) return false;
if (prf2 < 100 || prf2 > 10000) return false; if (prf2 < 100 || prf2 > 10000) return false;
if (max_distance < 100 || max_distance > 100000) return false; if (max_distance < 100 || max_distance > 100000) return false;
if (map_size < 1000 || map_size > 200000) return false; if (map_size < 100 || map_size > 200000) return false;
return true; return true;
} }
@@ -112,7 +112,7 @@ extern "C" {
* "BF" -- ADAR1000 beamformer * "BF" -- ADAR1000 beamformer
* "PA" -- Power amplifier bias/monitoring * "PA" -- Power amplifier bias/monitoring
* "FPGA" -- FPGA communication and handshake * "FPGA" -- FPGA communication and handshake
* "USB" -- USB data path (FT2232H production / FT601 premium) * "USB" -- FT601 USB data path
* "PWR" -- Power sequencing and rail monitoring * "PWR" -- Power sequencing and rail monitoring
* "IMU" -- IMU/GPS/barometer sensors * "IMU" -- IMU/GPS/barometer sensors
* "MOT" -- Stepper motor/scan mechanics * "MOT" -- Stepper motor/scan mechanics
@@ -620,8 +620,7 @@ typedef enum {
ERROR_POWER_SUPPLY, ERROR_POWER_SUPPLY,
ERROR_TEMPERATURE_HIGH, ERROR_TEMPERATURE_HIGH,
ERROR_MEMORY_ALLOC, ERROR_MEMORY_ALLOC,
ERROR_WATCHDOG_TIMEOUT, ERROR_WATCHDOG_TIMEOUT
ERROR_COUNT // must be last — used for bounds checking error_strings[]
} SystemError_t; } SystemError_t;
static SystemError_t last_error = ERROR_NONE; static SystemError_t last_error = ERROR_NONE;
@@ -632,27 +631,6 @@ static bool system_emergency_state = false;
SystemError_t checkSystemHealth(void) { SystemError_t checkSystemHealth(void) {
SystemError_t current_error = ERROR_NONE; SystemError_t current_error = ERROR_NONE;
// 0. Watchdog: detect main-loop stall (checkSystemHealth not called for >60 s).
// Timestamp is captured at function ENTRY and updated unconditionally, so
// any early return from a sub-check below cannot leave a stale value that
// would later trip a spurious ERROR_WATCHDOG_TIMEOUT. A dedicated cold-start
// branch ensures the first call after boot never trips (last_health_check==0
// would otherwise make `HAL_GetTick() - 0 > 60000` true forever after the
// 60-s mark of the init sequence).
static uint32_t last_health_check = 0;
uint32_t now_tick = HAL_GetTick();
if (last_health_check == 0) {
last_health_check = now_tick; // cold start: seed only
} else {
uint32_t elapsed = now_tick - last_health_check;
last_health_check = now_tick; // update BEFORE any early return
if (elapsed > 60000) {
current_error = ERROR_WATCHDOG_TIMEOUT;
DIAG_ERR("SYS", "Health check: Watchdog timeout (>60s since last check)");
return current_error;
}
}
// 1. Check AD9523 Clock Generator // 1. Check AD9523 Clock Generator
static uint32_t last_clock_check = 0; static uint32_t last_clock_check = 0;
if (HAL_GetTick() - last_clock_check > 5000) { if (HAL_GetTick() - last_clock_check > 5000) {
@@ -756,7 +734,14 @@ SystemError_t checkSystemHealth(void) {
return current_error; return current_error;
} }
// 9. Watchdog check is performed at function entry (see step 0). // 9. Simple watchdog check
static uint32_t last_health_check = 0;
if (HAL_GetTick() - last_health_check > 60000) {
current_error = ERROR_WATCHDOG_TIMEOUT;
DIAG_ERR("SYS", "Health check: Watchdog timeout (>60s since last check)");
return current_error;
}
last_health_check = HAL_GetTick();
if (current_error != ERROR_NONE) { if (current_error != ERROR_NONE) {
DIAG_ERR("SYS", "checkSystemHealth returning error code %d", current_error); DIAG_ERR("SYS", "checkSystemHealth returning error code %d", current_error);
@@ -868,7 +853,7 @@ void handleSystemError(SystemError_t error) {
DIAG_ERR("SYS", "handleSystemError: error=%d error_count=%lu", error, error_count); DIAG_ERR("SYS", "handleSystemError: error=%d error_count=%lu", error, error_count);
char error_msg[100]; char error_msg[100];
static const char* const error_strings[] = { const char* error_strings[] = {
"No error", "No error",
"AD9523 Clock failure", "AD9523 Clock failure",
"ADF4382 TX LO unlocked", "ADF4382 TX LO unlocked",
@@ -888,16 +873,9 @@ void handleSystemError(SystemError_t error) {
"Watchdog timeout" "Watchdog timeout"
}; };
static_assert(sizeof(error_strings) / sizeof(error_strings[0]) == ERROR_COUNT,
"error_strings[] and SystemError_t enum are out of sync");
const char* err_name = (error >= 0 && error < (int)(sizeof(error_strings) / sizeof(error_strings[0])))
? error_strings[error]
: "Unknown error";
snprintf(error_msg, sizeof(error_msg), snprintf(error_msg, sizeof(error_msg),
"ERROR #%d: %s (Count: %lu)\r\n", "ERROR #%d: %s (Count: %lu)\r\n",
error, err_name, error_count); error, error_strings[error], error_count);
HAL_UART_Transmit(&huart3, (uint8_t*)error_msg, strlen(error_msg), 1000); HAL_UART_Transmit(&huart3, (uint8_t*)error_msg, strlen(error_msg), 1000);
// Blink LED pattern based on error code // Blink LED pattern based on error code
@@ -907,23 +885,9 @@ void handleSystemError(SystemError_t error) {
HAL_Delay(200); HAL_Delay(200);
} }
// Critical errors trigger emergency shutdown. // Critical errors trigger emergency shutdown
// if (error >= ERROR_RF_PA_OVERCURRENT && error <= ERROR_POWER_SUPPLY) {
// Safety-critical range: any fault that can damage the PAs or leave the DIAG_ERR("SYS", "CRITICAL ERROR (code %d: %s) -- initiating Emergency_Stop()", error, error_strings[error]);
// system in an undefined state must cut the RF rails via Emergency_Stop().
// This covers:
// ERROR_RF_PA_OVERCURRENT .. ERROR_POWER_SUPPLY (9..13) -- PA/supply faults
// ERROR_TEMPERATURE_HIGH (14) -- >75 C on the PA thermal sensors;
// without cutting bias + 5V/5V5/RFPA rails
// the GaN QPA2962 stage can thermal-runaway.
// ERROR_WATCHDOG_TIMEOUT (16) -- health-check loop has stalled (>60 s);
// transmitter state is unknown, safest to
// latch Emergency_Stop rather than rely on
// IWDG reset (which re-energises the rails).
if ((error >= ERROR_RF_PA_OVERCURRENT && error <= ERROR_POWER_SUPPLY) ||
error == ERROR_TEMPERATURE_HIGH ||
error == ERROR_WATCHDOG_TIMEOUT) {
DIAG_ERR("SYS", "CRITICAL ERROR (code %d: %s) -- initiating Emergency_Stop()", error, err_name);
snprintf(error_msg, sizeof(error_msg), snprintf(error_msg, sizeof(error_msg),
"CRITICAL ERROR! Initiating emergency shutdown.\r\n"); "CRITICAL ERROR! Initiating emergency shutdown.\r\n");
HAL_UART_Transmit(&huart3, (uint8_t*)error_msg, strlen(error_msg), 1000); HAL_UART_Transmit(&huart3, (uint8_t*)error_msg, strlen(error_msg), 1000);
+10 -24
View File
@@ -3,38 +3,24 @@
*.dSYM/ *.dSYM/
# Test binaries (built by Makefile) # Test binaries (built by Makefile)
# TESTS_WITH_REAL
test_bug1_timed_sync_init_ordering test_bug1_timed_sync_init_ordering
test_bug2_ad9523_double_setup
test_bug3_timed_sync_noop test_bug3_timed_sync_noop
test_bug4_phase_shift_before_check test_bug4_phase_shift_before_check
test_bug5_fine_phase_gpio_only test_bug5_fine_phase_gpio_only
test_bug9_platform_ops_null
test_bug10_spi_cs_not_toggled
test_bug15_htim3_dangling_extern
# TESTS_MOCK_ONLY
test_bug2_ad9523_double_setup
test_bug6_timer_variable_collision test_bug6_timer_variable_collision
test_bug7_gpio_pin_conflict test_bug7_gpio_pin_conflict
test_bug8_uart_commented_out test_bug8_uart_commented_out
test_bug14_diag_section_args test_bug9_platform_ops_null
test_gap3_emergency_stop_rails test_bug10_spi_cs_not_toggled
test_bug11_platform_spi_transmit_only
# TESTS_STANDALONE
test_bug12_pa_cal_loop_inverted test_bug12_pa_cal_loop_inverted
test_bug13_dac2_adc_buffer_mismatch test_bug13_dac2_adc_buffer_mismatch
test_bug14_diag_section_args
test_bug15_htim3_dangling_extern
test_agc_outer_loop
test_gap3_emergency_state_ordering
test_gap3_emergency_stop_rails
test_gap3_idq_periodic_reread
test_gap3_iwdg_config test_gap3_iwdg_config
test_gap3_temperature_max test_gap3_temperature_max
test_gap3_idq_periodic_reread
test_gap3_emergency_state_ordering
test_gap3_overtemp_emergency_stop
test_gap3_health_watchdog_cold_start
# TESTS_WITH_PLATFORM
test_bug11_platform_spi_transmit_only
# TESTS_WITH_CXX
test_agc_outer_loop
# Manual / one-off test builds
test_um982_gps
+2 -17
View File
@@ -64,9 +64,7 @@ TESTS_STANDALONE := test_bug12_pa_cal_loop_inverted \
test_gap3_iwdg_config \ test_gap3_iwdg_config \
test_gap3_temperature_max \ test_gap3_temperature_max \
test_gap3_idq_periodic_reread \ test_gap3_idq_periodic_reread \
test_gap3_emergency_state_ordering \ test_gap3_emergency_state_ordering
test_gap3_overtemp_emergency_stop \
test_gap3_health_watchdog_cold_start
# Tests that need platform_noos_stm32.o + mocks # Tests that need platform_noos_stm32.o + mocks
TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only TESTS_WITH_PLATFORM := test_bug11_platform_spi_transmit_only
@@ -78,8 +76,7 @@ ALL_TESTS := $(TESTS_WITH_REAL) $(TESTS_MOCK_ONLY) $(TESTS_STANDALONE) $(TESTS_W
.PHONY: all build test clean \ .PHONY: all build test clean \
$(addprefix test_,bug1 bug2 bug3 bug4 bug5 bug6 bug7 bug8 bug9 bug10 bug11 bug12 bug13 bug14 bug15) \ $(addprefix test_,bug1 bug2 bug3 bug4 bug5 bug6 bug7 bug8 bug9 bug10 bug11 bug12 bug13 bug14 bug15) \
test_gap3_estop test_gap3_iwdg test_gap3_temp test_gap3_idq test_gap3_order \ test_gap3_estop test_gap3_iwdg test_gap3_temp test_gap3_idq test_gap3_order
test_gap3_overtemp test_gap3_wdog
all: build test all: build test
@@ -165,12 +162,6 @@ test_gap3_idq_periodic_reread: test_gap3_idq_periodic_reread.c
test_gap3_emergency_state_ordering: test_gap3_emergency_state_ordering.c test_gap3_emergency_state_ordering: test_gap3_emergency_state_ordering.c
$(CC) $(CFLAGS) $< -o $@ $(CC) $(CFLAGS) $< -o $@
test_gap3_overtemp_emergency_stop: test_gap3_overtemp_emergency_stop.c
$(CC) $(CFLAGS) $< -o $@
test_gap3_health_watchdog_cold_start: test_gap3_health_watchdog_cold_start.c
$(CC) $(CFLAGS) $< -o $@
# Tests that need platform_noos_stm32.o + mocks # Tests that need platform_noos_stm32.o + mocks
$(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ) $(TESTS_WITH_PLATFORM): %: %.c $(MOCK_OBJS) $(PLATFORM_OBJ)
$(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@ $(CC) $(CFLAGS) $(INCLUDES) $< $(MOCK_OBJS) $(PLATFORM_OBJ) -o $@
@@ -255,12 +246,6 @@ test_gap3_idq: test_gap3_idq_periodic_reread
test_gap3_order: test_gap3_emergency_state_ordering test_gap3_order: test_gap3_emergency_state_ordering
./test_gap3_emergency_state_ordering ./test_gap3_emergency_state_ordering
test_gap3_overtemp: test_gap3_overtemp_emergency_stop
./test_gap3_overtemp_emergency_stop
test_gap3_wdog: test_gap3_health_watchdog_cold_start
./test_gap3_health_watchdog_cold_start
# --- Clean --- # --- Clean ---
clean: clean:
@@ -34,25 +34,22 @@ static void Mock_Emergency_Stop(void)
state_was_true_when_estop_called = system_emergency_state; state_was_true_when_estop_called = system_emergency_state;
} }
/* Error codes (subset matching main.cpp SystemError_t) */ /* Error codes (subset matching main.cpp) */
typedef enum { typedef enum {
ERROR_NONE = 0, ERROR_NONE = 0,
ERROR_RF_PA_OVERCURRENT = 9, ERROR_RF_PA_OVERCURRENT = 9,
ERROR_RF_PA_BIAS = 10, ERROR_RF_PA_BIAS = 10,
ERROR_STEPPER_MOTOR = 11, ERROR_STEPPER_FAULT = 11,
ERROR_FPGA_COMM = 12, ERROR_FPGA_COMM = 12,
ERROR_POWER_SUPPLY = 13, ERROR_POWER_SUPPLY = 13,
ERROR_TEMPERATURE_HIGH = 14, ERROR_TEMPERATURE_HIGH = 14,
ERROR_MEMORY_ALLOC = 15,
ERROR_WATCHDOG_TIMEOUT = 16,
} SystemError_t; } SystemError_t;
/* Extracted critical-error handling logic (matches post-fix main.cpp predicate) */ /* Extracted critical-error handling logic (post-fix ordering) */
static void simulate_handleSystemError_critical(SystemError_t error) static void simulate_handleSystemError_critical(SystemError_t error)
{ {
if ((error >= ERROR_RF_PA_OVERCURRENT && error <= ERROR_POWER_SUPPLY) || /* Only critical errors (PA overcurrent through power supply) trigger e-stop */
error == ERROR_TEMPERATURE_HIGH || if (error >= ERROR_RF_PA_OVERCURRENT && error <= ERROR_POWER_SUPPLY) {
error == ERROR_WATCHDOG_TIMEOUT) {
/* FIX 5: set flag BEFORE calling Emergency_Stop */ /* FIX 5: set flag BEFORE calling Emergency_Stop */
system_emergency_state = true; system_emergency_state = true;
Mock_Emergency_Stop(); Mock_Emergency_Stop();
@@ -96,39 +93,17 @@ int main(void)
assert(state_was_true_when_estop_called == true); assert(state_was_true_when_estop_called == true);
printf("PASS\n"); printf("PASS\n");
/* Test 4: Overtemp → MUST trigger e-stop (was incorrectly non-critical before fix) */ /* Test 4: Non-critical error → no e-stop, flag stays false */
printf(" Test 4: Overtemp triggers e-stop... "); printf(" Test 4: Non-critical error (no e-stop)... ");
system_emergency_state = false; system_emergency_state = false;
emergency_stop_called = false; emergency_stop_called = false;
state_was_true_when_estop_called = false;
simulate_handleSystemError_critical(ERROR_TEMPERATURE_HIGH); simulate_handleSystemError_critical(ERROR_TEMPERATURE_HIGH);
assert(emergency_stop_called == true);
assert(system_emergency_state == true);
assert(state_was_true_when_estop_called == true);
printf("PASS\n");
/* Test 5: Watchdog timeout → MUST trigger e-stop */
printf(" Test 5: Watchdog timeout triggers e-stop... ");
system_emergency_state = false;
emergency_stop_called = false;
state_was_true_when_estop_called = false;
simulate_handleSystemError_critical(ERROR_WATCHDOG_TIMEOUT);
assert(emergency_stop_called == true);
assert(system_emergency_state == true);
assert(state_was_true_when_estop_called == true);
printf("PASS\n");
/* Test 6: Non-critical error (memory alloc) → no e-stop */
printf(" Test 6: Non-critical error (no e-stop)... ");
system_emergency_state = false;
emergency_stop_called = false;
simulate_handleSystemError_critical(ERROR_MEMORY_ALLOC);
assert(emergency_stop_called == false); assert(emergency_stop_called == false);
assert(system_emergency_state == false); assert(system_emergency_state == false);
printf("PASS\n"); printf("PASS\n");
/* Test 7: ERROR_NONE → no e-stop */ /* Test 5: ERROR_NONE → no e-stop */
printf(" Test 7: ERROR_NONE (no action)... "); printf(" Test 5: ERROR_NONE (no action)... ");
system_emergency_state = false; system_emergency_state = false;
emergency_stop_called = false; emergency_stop_called = false;
simulate_handleSystemError_critical(ERROR_NONE); simulate_handleSystemError_critical(ERROR_NONE);
@@ -136,6 +111,6 @@ int main(void)
assert(system_emergency_state == false); assert(system_emergency_state == false);
printf("PASS\n"); printf("PASS\n");
printf("\n=== Gap-3 Fix 5: ALL 7 TESTS PASSED ===\n\n"); printf("\n=== Gap-3 Fix 5: ALL TESTS PASSED ===\n\n");
return 0; return 0;
} }
@@ -1,132 +0,0 @@
/*******************************************************************************
* test_gap3_health_watchdog_cold_start.c
*
* Safety bug: checkSystemHealth()'s internal watchdog (step 9, pre-fix) had two
* linked defects that, once ERROR_WATCHDOG_TIMEOUT was escalated to
* Emergency_Stop() by the overtemp/watchdog PR, would false-latch the radar:
*
* (1) Cold-start false trip:
* static uint32_t last_health_check = 0;
* if (HAL_GetTick() - last_health_check > 60000) { ... }
* On the very first call, last_health_check == 0, so once the MCU has
* been up >60 s (which is typical after the ADAR1000 / AD9523 / ADF4382
* init sequence) the subtraction `now - 0` exceeds 60 000 ms and the
* watchdog trips spuriously.
*
* (2) Stale-timestamp after early returns:
* last_health_check = HAL_GetTick(); // at END of function
* Every earlier sub-check (IMU, BMP180, GPS, PA Idq, temperature) has an
* `if (fault) return current_error;` path that skips the update. After a
* cumulative 60 s of transient faults, the next clean call compares
* `now` against the long-stale `last_health_check` and trips.
*
* After fix: Watchdog logic moved to function ENTRY. A dedicated cold-start
* branch seeds the timestamp on the first call without checking.
* On every subsequent call, the elapsed delta is captured FIRST
* and last_health_check is updated BEFORE any sub-check runs, so
* early returns no longer leave a stale value.
*
* Test strategy:
* Extract the post-fix watchdog predicate into a standalone function that
* takes a simulated HAL_GetTick() value and returns whether the watchdog
* should trip. Walk through boot + fault sequences that would have tripped
* the pre-fix code and assert the post-fix code does NOT trip.
******************************************************************************/
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
/* --- Post-fix watchdog state + predicate, extracted verbatim --- */
static uint32_t last_health_check = 0;
/* Returns 1 iff this call should raise ERROR_WATCHDOG_TIMEOUT.
Updates last_health_check BEFORE returning (matches post-fix behaviour). */
static int health_watchdog_step(uint32_t now_tick)
{
if (last_health_check == 0) {
last_health_check = now_tick; /* cold start: seed only, never trip */
return 0;
}
uint32_t elapsed = now_tick - last_health_check;
last_health_check = now_tick; /* update BEFORE any early return */
return (elapsed > 60000) ? 1 : 0;
}
/* Test helper: reset the static state between scenarios. */
static void reset_state(void) { last_health_check = 0; }
int main(void)
{
printf("=== Safety fix: checkSystemHealth() watchdog cold-start + stale-ts ===\n");
/* ---------- Scenario 1: cold-start after 60 s of init must NOT trip ---- */
printf(" Test 1: first call at t=75000 ms (post-init) does not trip... ");
reset_state();
assert(health_watchdog_step(75000) == 0);
printf("PASS\n");
/* ---------- Scenario 2: first call far beyond 60 s (PRE-FIX BUG) ------- */
printf(" Test 2: first call at t=600000 ms still does not trip... ");
reset_state();
assert(health_watchdog_step(600000) == 0);
printf("PASS\n");
/* ---------- Scenario 3: healthy main-loop pacing (10 ms period) -------- */
printf(" Test 3: 1000 calls at 10 ms intervals never trip... ");
reset_state();
(void)health_watchdog_step(1000); /* seed */
for (int i = 1; i <= 1000; i++) {
assert(health_watchdog_step(1000 + i * 10) == 0);
}
printf("PASS\n");
/* ---------- Scenario 4: stale-timestamp after a burst of early returns -
Pre-fix bug: many early returns skipped the timestamp update, so a
later clean call would compare `now` against a 60+ s old value. Post-fix,
every call (including ones that would have early-returned in the real
function) updates the timestamp at the top, so this scenario is modelled
by calling health_watchdog_step() on every iteration of the main loop. */
printf(" Test 4: 70 s of 100 ms-spaced calls after seed do not trip... ");
reset_state();
(void)health_watchdog_step(50000); /* seed mid-run */
for (int i = 1; i <= 700; i++) { /* 70 s @ 100 ms */
int tripped = health_watchdog_step(50000 + i * 100);
assert(tripped == 0);
}
printf("PASS\n");
/* ---------- Scenario 5: genuine stall MUST trip ------------------------ */
printf(" Test 5: real 60+ s gap between calls does trip... ");
reset_state();
(void)health_watchdog_step(10000); /* seed */
assert(health_watchdog_step(10000 + 60001) == 1);
printf("PASS\n");
/* ---------- Scenario 6: exactly 60 s gap is the boundary -- do NOT trip
Post-fix predicate uses strict >60000, matching the pre-fix comparator. */
printf(" Test 6: exactly 60000 ms gap does not trip (boundary)... ");
reset_state();
(void)health_watchdog_step(10000);
assert(health_watchdog_step(10000 + 60000) == 0);
printf("PASS\n");
/* ---------- Scenario 7: trip, then recover on next paced call ---------- */
printf(" Test 7: after a genuine stall+trip, next paced call does not re-trip... ");
reset_state();
(void)health_watchdog_step(5000); /* seed */
assert(health_watchdog_step(5000 + 70000) == 1); /* stall -> trip */
assert(health_watchdog_step(5000 + 70000 + 10) == 0); /* resume paced */
printf("PASS\n");
/* ---------- Scenario 8: HAL_GetTick() 32-bit wrap (~49.7 days) ---------
Because we subtract unsigned 32-bit values, wrap is handled correctly as
long as the true elapsed time is < 2^32 ms. */
printf(" Test 8: tick wrap from 0xFFFFFF00 -> 0x00000064 (200 ms span) does not trip... ");
reset_state();
(void)health_watchdog_step(0xFFFFFF00u);
assert(health_watchdog_step(0x00000064u) == 0); /* elapsed = 0x164 = 356 ms */
printf("PASS\n");
printf("\n=== Safety fix: ALL TESTS PASSED ===\n\n");
return 0;
}
@@ -1,119 +0,0 @@
/*******************************************************************************
* test_gap3_overtemp_emergency_stop.c
*
* Safety bug: handleSystemError() did not escalate ERROR_TEMPERATURE_HIGH
* (or ERROR_WATCHDOG_TIMEOUT) to Emergency_Stop().
*
* Before fix: The critical-error gate was
* if (error >= ERROR_RF_PA_OVERCURRENT &&
* error <= ERROR_POWER_SUPPLY) { Emergency_Stop(); }
* So overtemp (code 14) and watchdog timeout (code 16) fell
* through to attemptErrorRecovery()'s default branch (log and
* continue), leaving the 10 W GaN PAs biased at >75 °C.
*
* After fix: The gate also matches ERROR_TEMPERATURE_HIGH and
* ERROR_WATCHDOG_TIMEOUT, so thermal and watchdog faults
* latch Emergency_Stop() exactly like PA overcurrent.
*
* Test strategy:
* Replicate the critical-error predicate and assert that every error
* enum value which threatens RF/power safety is accepted, and that the
* non-critical ones (comm, sensor, memory) are not.
******************************************************************************/
#include <assert.h>
#include <stdio.h>
/* Mirror of SystemError_t from main.cpp (keep in lockstep). */
typedef enum {
ERROR_NONE = 0,
ERROR_AD9523_CLOCK,
ERROR_ADF4382_TX_UNLOCK,
ERROR_ADF4382_RX_UNLOCK,
ERROR_ADAR1000_COMM,
ERROR_ADAR1000_TEMP,
ERROR_IMU_COMM,
ERROR_BMP180_COMM,
ERROR_GPS_COMM,
ERROR_RF_PA_OVERCURRENT,
ERROR_RF_PA_BIAS,
ERROR_STEPPER_MOTOR,
ERROR_FPGA_COMM,
ERROR_POWER_SUPPLY,
ERROR_TEMPERATURE_HIGH,
ERROR_MEMORY_ALLOC,
ERROR_WATCHDOG_TIMEOUT
} SystemError_t;
/* Extracted post-fix predicate: returns 1 when Emergency_Stop() must fire. */
static int triggers_emergency_stop(SystemError_t e)
{
return ((e >= ERROR_RF_PA_OVERCURRENT && e <= ERROR_POWER_SUPPLY) ||
e == ERROR_TEMPERATURE_HIGH ||
e == ERROR_WATCHDOG_TIMEOUT);
}
int main(void)
{
printf("=== Safety fix: overtemp / watchdog -> Emergency_Stop() ===\n");
/* --- Errors that MUST latch Emergency_Stop --- */
printf(" Test 1: ERROR_RF_PA_OVERCURRENT triggers... ");
assert(triggers_emergency_stop(ERROR_RF_PA_OVERCURRENT));
printf("PASS\n");
printf(" Test 2: ERROR_RF_PA_BIAS triggers... ");
assert(triggers_emergency_stop(ERROR_RF_PA_BIAS));
printf("PASS\n");
printf(" Test 3: ERROR_STEPPER_MOTOR triggers... ");
assert(triggers_emergency_stop(ERROR_STEPPER_MOTOR));
printf("PASS\n");
printf(" Test 4: ERROR_FPGA_COMM triggers... ");
assert(triggers_emergency_stop(ERROR_FPGA_COMM));
printf("PASS\n");
printf(" Test 5: ERROR_POWER_SUPPLY triggers... ");
assert(triggers_emergency_stop(ERROR_POWER_SUPPLY));
printf("PASS\n");
printf(" Test 6: ERROR_TEMPERATURE_HIGH triggers (regression)... ");
assert(triggers_emergency_stop(ERROR_TEMPERATURE_HIGH));
printf("PASS\n");
printf(" Test 7: ERROR_WATCHDOG_TIMEOUT triggers (regression)... ");
assert(triggers_emergency_stop(ERROR_WATCHDOG_TIMEOUT));
printf("PASS\n");
/* --- Errors that MUST NOT escalate (recoverable / informational) --- */
printf(" Test 8: ERROR_NONE does not trigger... ");
assert(!triggers_emergency_stop(ERROR_NONE));
printf("PASS\n");
printf(" Test 9: ERROR_AD9523_CLOCK does not trigger... ");
assert(!triggers_emergency_stop(ERROR_AD9523_CLOCK));
printf("PASS\n");
printf(" Test 10: ERROR_ADF4382_TX_UNLOCK does not trigger (recoverable)... ");
assert(!triggers_emergency_stop(ERROR_ADF4382_TX_UNLOCK));
printf("PASS\n");
printf(" Test 11: ERROR_ADAR1000_COMM does not trigger... ");
assert(!triggers_emergency_stop(ERROR_ADAR1000_COMM));
printf("PASS\n");
printf(" Test 12: ERROR_IMU_COMM does not trigger... ");
assert(!triggers_emergency_stop(ERROR_IMU_COMM));
printf("PASS\n");
printf(" Test 13: ERROR_GPS_COMM does not trigger... ");
assert(!triggers_emergency_stop(ERROR_GPS_COMM));
printf("PASS\n");
printf(" Test 14: ERROR_MEMORY_ALLOC does not trigger... ");
assert(!triggers_emergency_stop(ERROR_MEMORY_ALLOC));
printf("PASS\n");
printf("\n=== Safety fix: ALL TESTS PASSED ===\n\n");
return 0;
}
+7 -8
View File
@@ -32,8 +32,8 @@ the `USB_MODE` parameter in `radar_system_top.v`:
| USB_MODE | Interface | Bus Width | Speed | Board Target | | USB_MODE | Interface | Bus Width | Speed | Board Target |
|----------|-----------|-----------|-------|--------------| |----------|-----------|-----------|-------|--------------|
| 0 | FT601 (USB 3.0) | 32-bit | 100 MHz | 200T premium dev board | | 0 (default) | FT601 (USB 3.0) | 32-bit | 100 MHz | 200T premium dev board |
| 1 (default) | FT2232H (USB 2.0) | 8-bit | 60 MHz | 50T production board | | 1 | FT2232H (USB 2.0) | 8-bit | 60 MHz | 50T production board |
### How USB_MODE Works ### How USB_MODE Works
@@ -72,8 +72,7 @@ The parameter is set via a **wrapper module** that overrides the default:
``` ```
- **200T dev board**: `radar_system_top` is used directly as the top module. - **200T dev board**: `radar_system_top` is used directly as the top module.
`USB_MODE` defaults to `1` (FT2232H) since production is the primary target. `USB_MODE` defaults to `0` (FT601). No wrapper needed.
Override with `.USB_MODE(0)` for FT601 builds.
### RTL Files by USB Interface ### RTL Files by USB Interface
@@ -159,7 +158,7 @@ The build scripts automatically select the correct top module and constraints:
You do NOT need to set `USB_MODE` manually. The top module selection handles it: You do NOT need to set `USB_MODE` manually. The top module selection handles it:
- `radar_system_top_50t` forces `USB_MODE=1` internally - `radar_system_top_50t` forces `USB_MODE=1` internally
- `radar_system_top` defaults to `USB_MODE=1` (FT2232H, production default) - `radar_system_top` defaults to `USB_MODE=0`
## How to Select Constraints in Vivado ## How to Select Constraints in Vivado
@@ -191,9 +190,9 @@ read_xdc constraints/te0713_te0701_minimal.xdc
| Target | Top module | USB_MODE | USB Interface | Notes | | Target | Top module | USB_MODE | USB Interface | Notes |
|--------|------------|----------|---------------|-------| |--------|------------|----------|---------------|-------|
| 50T Production (FTG256) | `radar_system_top_50t` | 1 | FT2232H (8-bit) | Wrapper sets USB_MODE=1, ties off FT601 | | 50T Production (FTG256) | `radar_system_top_50t` | 1 | FT2232H (8-bit) | Wrapper sets USB_MODE=1, ties off FT601 |
| 200T Dev (FBG484) | `radar_system_top` | 0 (override) | FT601 (32-bit) | Build script overrides default USB_MODE=1 | | 200T Dev (FBG484) | `radar_system_top` | 0 (default) | FT601 (32-bit) | No wrapper needed |
| Trenz TE0712/TE0701 | `radar_system_top_te0712_dev` | 0 (override) | FT601 (32-bit) | Minimal bring-up wrapper | | Trenz TE0712/TE0701 | `radar_system_top_te0712_dev` | 0 (default) | FT601 (32-bit) | Minimal bring-up wrapper |
| Trenz TE0713/TE0701 | `radar_system_top_te0713_dev` | 0 (override) | FT601 (32-bit) | Alternate SoM wrapper | | Trenz TE0713/TE0701 | `radar_system_top_te0713_dev` | 0 (default) | FT601 (32-bit) | Alternate SoM wrapper |
## Trenz Split Status ## Trenz Split Status
@@ -70,10 +70,9 @@ set_input_jitter [get_clocks clk_100m] 0.1
# NOTE: The physical DAC (U3, AD9708) receives its clock directly from the # NOTE: The physical DAC (U3, AD9708) receives its clock directly from the
# AD9523 via a separate net (DAC_CLOCK), NOT from the FPGA. The FPGA # AD9523 via a separate net (DAC_CLOCK), NOT from the FPGA. The FPGA
# uses this clock input for internal DAC data timing only. The RTL port # uses this clock input for internal DAC data timing only. The RTL port
# `dac_clk` is an RTL output that assigns clk_120m directly. It has no # `dac_clk` is an output that assigns clk_120m directly — it has no
# physical pin on the 50T board and is left unconnected here. The port # separate physical pin on this board and should be removed from the
# CANNOT be removed from the RTL because the 200T board uses it with # RTL or left unconnected.
# ODDR clock forwarding (pin H17, see xc7a200t_fbg484.xdc).
# FIX: Moved from C13 (IO_L12N = N-type) to D13 (IO_L12P = P-type MRCC). # FIX: Moved from C13 (IO_L12N = N-type) to D13 (IO_L12P = P-type MRCC).
# Clock inputs must use the P-type pin of an MRCC pair (PLIO-9 DRC). # Clock inputs must use the P-type pin of an MRCC pair (PLIO-9 DRC).
set_property PACKAGE_PIN D13 [get_ports {clk_120m_dac}] set_property PACKAGE_PIN D13 [get_ports {clk_120m_dac}]
@@ -333,44 +332,6 @@ set_property DRIVE 8 [get_ports {ft_data[*]}]
# ft_clkout constrained above in CLOCK CONSTRAINTS section (C4, 60 MHz) # ft_clkout constrained above in CLOCK CONSTRAINTS section (C4, 60 MHz)
# --------------------------------------------------------------------------
# FT2232H Source-Synchronous Timing Constraints
# --------------------------------------------------------------------------
# FT2232H 245 Synchronous FIFO mode timing (60 MHz, period = 16.667 ns):
#
# FPGA Read Path (FT2232H drives data, FPGA samples):
# - Data valid before CLKOUT rising edge: t_vr(max) = 7.0 ns
# - Data hold after CLKOUT rising edge: t_hr(min) = 0.0 ns
# - Input delay max = period - t_vr = 16.667 - 7.0 = 9.667 ns
# - Input delay min = t_hr = 0.0 ns
#
# FPGA Write Path (FPGA drives data, FT2232H samples):
# - Data setup before next CLKOUT rising: t_su = 5.0 ns
# - Data hold after CLKOUT rising: t_hd = 0.0 ns
# - Output delay max = period - t_su = 16.667 - 5.0 = 11.667 ns
# - Output delay min = t_hd = 0.0 ns
# --------------------------------------------------------------------------
# Input delays: FT2232H → FPGA (data bus and status signals)
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_data[*]}]
set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_data[*]}]
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_rxf_n}]
set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_rxf_n}]
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_txe_n}]
set_input_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_txe_n}]
# Output delays: FPGA → FT2232H (control strobes and data bus when writing)
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_data[*]}]
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_data[*]}]
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_rd_n}]
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_rd_n}]
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_wr_n}]
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_wr_n}]
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_oe_n}]
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_oe_n}]
set_output_delay -clock [get_clocks ft_clkout] -max 11.667 [get_ports {ft_siwu}]
set_output_delay -clock [get_clocks ft_clkout] -min 0.0 [get_ports {ft_siwu}]
# ============================================================================ # ============================================================================
# STATUS / DEBUG OUTPUTS — NO PHYSICAL CONNECTIONS # STATUS / DEBUG OUTPUTS — NO PHYSICAL CONNECTIONS
# ============================================================================ # ============================================================================
@@ -457,10 +418,10 @@ set_property BITSTREAM.CONFIG.UNUSEDPIN Pullup [current_design]
# 4. JTAG: FPGA_TCK (L7), FPGA_TDI (N7), FPGA_TDO (N8), FPGA_TMS (M7). # 4. JTAG: FPGA_TCK (L7), FPGA_TDI (N7), FPGA_TDO (N8), FPGA_TMS (M7).
# Dedicated pins — no XDC constraints needed. # Dedicated pins — no XDC constraints needed.
# #
# 5. dac_clk port: Not connected on the 50T board (DAC clocked directly from # 5. dac_clk port: The RTL top module declares `dac_clk` as an output, but
# AD9523). The RTL port exists for 200T board compatibility, where the FPGA # the physical board wires the DAC clock (AD9708 CLOCK pin) directly from
# forwards the DAC clock via ODDR to pin H17 with generated clock and # the AD9523, not from the FPGA. This port should be removed from the RTL
# timing constraints (see xc7a200t_fbg484.xdc). Do NOT remove from RTL. # or left unconnected. It currently just assigns clk_120m_dac passthrough.
# #
# ============================================================================ # ============================================================================
# END OF CONSTRAINTS # END OF CONSTRAINTS
@@ -18,10 +18,9 @@ module matched_filter_multi_segment (
input wire mc_new_elevation, // Toggle for new elevation (32) input wire mc_new_elevation, // Toggle for new elevation (32)
input wire mc_new_azimuth, // Toggle for new azimuth (50) input wire mc_new_azimuth, // Toggle for new azimuth (50)
input wire [15:0] long_chirp_real, // Reference chirp (upstream memory loader selects long/short via use_long_chirp)
input wire [15:0] long_chirp_imag, input wire [15:0] ref_chirp_real,
input wire [15:0] short_chirp_real, input wire [15:0] ref_chirp_imag,
input wire [15:0] short_chirp_imag,
// Memory system interface // Memory system interface
output reg [1:0] segment_request, output reg [1:0] segment_request,
@@ -244,6 +243,7 @@ always @(posedge clk or negedge reset_n) begin
if (!use_long_chirp) begin if (!use_long_chirp) begin
if (chirp_samples_collected >= SHORT_CHIRP_SAMPLES - 1) begin if (chirp_samples_collected >= SHORT_CHIRP_SAMPLES - 1) begin
state <= ST_ZERO_PAD; state <= ST_ZERO_PAD;
chirp_complete <= 1; // Bug A fix: mark chirp done so ST_OUTPUT exits to IDLE
`ifdef SIMULATION `ifdef SIMULATION
$display("[MULTI_SEG_FIXED] Short chirp: collected %d samples, starting zero-pad", $display("[MULTI_SEG_FIXED] Short chirp: collected %d samples, starting zero-pad",
chirp_samples_collected + 1); chirp_samples_collected + 1);
@@ -500,11 +500,9 @@ matched_filter_processing_chain m_f_p_c(
// Chirp Selection // Chirp Selection
.chirp_counter(chirp_counter), .chirp_counter(chirp_counter),
// Reference Chirp Memory Interfaces // Reference Chirp Memory Interface (single pair upstream selects long/short)
.long_chirp_real(long_chirp_real), .ref_chirp_real(ref_chirp_real),
.long_chirp_imag(long_chirp_imag), .ref_chirp_imag(ref_chirp_imag),
.short_chirp_real(short_chirp_real),
.short_chirp_imag(short_chirp_imag),
// Output // Output
.range_profile_i(fft_pc_i), .range_profile_i(fft_pc_i),
@@ -15,7 +15,7 @@
* .clk, .reset_n * .clk, .reset_n
* .adc_data_i, .adc_data_q, .adc_valid <- from input buffer * .adc_data_i, .adc_data_q, .adc_valid <- from input buffer
* .chirp_counter <- 6-bit frame counter * .chirp_counter <- 6-bit frame counter
* .long_chirp_real/imag, .short_chirp_real/imag <- reference (time-domain) * .ref_chirp_real/imag <- reference (time-domain)
* .range_profile_i, .range_profile_q, .range_profile_valid -> output * .range_profile_i, .range_profile_q, .range_profile_valid -> output
* .chain_state -> 4-bit status * .chain_state -> 4-bit status
* *
@@ -48,10 +48,10 @@ module matched_filter_processing_chain (
input wire [5:0] chirp_counter, input wire [5:0] chirp_counter,
// Reference chirp (time-domain, latency-aligned by upstream buffer) // Reference chirp (time-domain, latency-aligned by upstream buffer)
input wire [15:0] long_chirp_real, // Upstream chirp_memory_loader_param selects long/short reference
input wire [15:0] long_chirp_imag, // via use_long_chirp this single pair carries whichever is active.
input wire [15:0] short_chirp_real, input wire [15:0] ref_chirp_real,
input wire [15:0] short_chirp_imag, input wire [15:0] ref_chirp_imag,
// Output: range profile (pulse-compressed) // Output: range profile (pulse-compressed)
output wire signed [15:0] range_profile_i, output wire signed [15:0] range_profile_i,
@@ -189,8 +189,8 @@ always @(posedge clk or negedge reset_n) begin
// Store first sample (signal + reference) // Store first sample (signal + reference)
fwd_buf_i[0] <= $signed(adc_data_i); fwd_buf_i[0] <= $signed(adc_data_i);
fwd_buf_q[0] <= $signed(adc_data_q); fwd_buf_q[0] <= $signed(adc_data_q);
ref_buf_i[0] <= $signed(long_chirp_real); ref_buf_i[0] <= $signed(ref_chirp_real);
ref_buf_q[0] <= $signed(long_chirp_imag); ref_buf_q[0] <= $signed(ref_chirp_imag);
fwd_in_count <= 1; fwd_in_count <= 1;
state <= ST_FWD_FFT; state <= ST_FWD_FFT;
end end
@@ -205,8 +205,8 @@ always @(posedge clk or negedge reset_n) begin
if (adc_valid && fwd_in_count < FFT_SIZE) begin if (adc_valid && fwd_in_count < FFT_SIZE) begin
fwd_buf_i[fwd_in_count] <= $signed(adc_data_i); fwd_buf_i[fwd_in_count] <= $signed(adc_data_i);
fwd_buf_q[fwd_in_count] <= $signed(adc_data_q); fwd_buf_q[fwd_in_count] <= $signed(adc_data_q);
ref_buf_i[fwd_in_count] <= $signed(long_chirp_real); ref_buf_i[fwd_in_count] <= $signed(ref_chirp_real);
ref_buf_q[fwd_in_count] <= $signed(long_chirp_imag); ref_buf_q[fwd_in_count] <= $signed(ref_chirp_imag);
fwd_in_count <= fwd_in_count + 1; fwd_in_count <= fwd_in_count + 1;
end end
@@ -775,16 +775,16 @@ always @(posedge clk) begin : ref_bram_port
if (adc_valid) begin if (adc_valid) begin
we = 1'b1; we = 1'b1;
addr = 0; addr = 0;
wdata_i = $signed(long_chirp_real); wdata_i = $signed(ref_chirp_real);
wdata_q = $signed(long_chirp_imag); wdata_q = $signed(ref_chirp_imag);
end end
end end
ST_COLLECT: begin ST_COLLECT: begin
if (adc_valid && collect_count < FFT_SIZE) begin if (adc_valid && collect_count < FFT_SIZE) begin
we = 1'b1; we = 1'b1;
addr = collect_count[ADDR_BITS-1:0]; addr = collect_count[ADDR_BITS-1:0];
wdata_i = $signed(long_chirp_real); wdata_i = $signed(ref_chirp_real);
wdata_q = $signed(long_chirp_imag); wdata_q = $signed(ref_chirp_imag);
end end
end end
ST_REF_FFT: begin ST_REF_FFT: begin
+200
View File
@@ -0,0 +1,200 @@
// ============================================================================
// radar_params.vh — Single Source of Truth for AERIS-10 FPGA Parameters
// ============================================================================
//
// ALL modules in the FPGA processing chain MUST `include this file instead of
// hardcoding range bins, segment counts, chirp samples, or timing values.
//
// This file uses `define macros (not localparam) so it can be included at any
// scope. Each consuming module should include this file inside its body and
// optionally alias macros to localparams for readability.
//
// BOARD VARIANTS:
// SUPPORT_LONG_RANGE = 0 (50T, USB_MODE=1) — 3 km mode only, 64 range bins
// SUPPORT_LONG_RANGE = 1 (200T, USB_MODE=0) — 3 km + 20 km modes, up to 1024 bins
//
// RANGE MODES (runtime, via host_range_mode register, opcode 0x20):
// 2'b00 = 3 km (default on both boards)
// 2'b01 = 20 km (200T only; clamped to 3 km on 50T)
// 2'b10 = Reserved
// 2'b11 = Reserved
//
// USAGE:
// `include "radar_params.vh"
// Then reference `RP_FFT_SIZE, `RP_MAX_OUTPUT_BINS, etc.
//
// PHYSICAL CONSTANTS (derived from hardware):
// ADC clock: 400 MSPS
// CIC decimation: 4x
// Processing rate: 100 MSPS (post-DDC)
// Range per sample: c / (2 * 100e6) = 1.5 m
// Decimation factor: 16 (1024 FFT bins -> 64 output bins per segment)
// Range per dec. bin: 1.5 m * 16 = 24.0 m
// Carrier frequency: 10.5 GHz
//
// CHIRP BANDWIDTH (Phase 1 target — currently 20 MHz, planned 30 MHz):
// Range resolution: c / (2 * BW)
// 20 MHz -> 7.5 m
// 30 MHz -> 5.0 m
// NOTE: Range resolution is independent of range-per-bin. Resolution
// determines the minimum separation between two targets; range-per-bin
// determines the spatial sampling grid.
// ============================================================================
`ifndef RADAR_PARAMS_VH
`define RADAR_PARAMS_VH
// ============================================================================
// BOARD VARIANT — set at synthesis time, NOT runtime
// ============================================================================
// Default to 50T (conservative). Override in top-level or synthesis script:
// +define+SUPPORT_LONG_RANGE
// or via Vivado: set_property verilog_define {SUPPORT_LONG_RANGE} [current_fileset]
// Note: SUPPORT_LONG_RANGE is a flag define (ifdef/ifndef), not a value.
// `ifndef SUPPORT_LONG_RANGE means 50T (no long range).
// `ifdef SUPPORT_LONG_RANGE means 200T (long range supported).
// ============================================================================
// FFT AND PROCESSING CONSTANTS (fixed, both modes)
// ============================================================================
`define RP_FFT_SIZE 1024 // Range FFT points per segment
`define RP_OVERLAP_SAMPLES 128 // Overlap between adjacent segments
`define RP_SEGMENT_ADVANCE 896 // FFT_SIZE - OVERLAP = 1024 - 128
`define RP_DECIMATION_FACTOR 16 // Range bin decimation (1024 -> 64)
`define RP_BINS_PER_SEGMENT 64 // FFT_SIZE / DECIMATION_FACTOR
`define RP_DOPPLER_FFT_SIZE 16 // Per sub-frame Doppler FFT
`define RP_CHIRPS_PER_FRAME 32 // Total chirps (16 long + 16 short)
`define RP_CHIRPS_PER_SUBFRAME 16 // Chirps per Doppler sub-frame
`define RP_NUM_DOPPLER_BINS 32 // 2 sub-frames * 16 = 32
`define RP_DATA_WIDTH 16 // ADC/processing data width
// ============================================================================
// 3 KM MODE PARAMETERS (both 50T and 200T)
// ============================================================================
`define RP_LONG_CHIRP_SAMPLES_3KM 3000 // 30 us at 100 MSPS
`define RP_LONG_SEGMENTS_3KM 4 // ceil((3000-1024)/896) + 1 = 4
`define RP_OUTPUT_RANGE_BINS_3KM 64 // Downstream pipeline expects 64 range bins (NOTE: will become 128 after 2048-pt FFT upgrade)
`define RP_SHORT_CHIRP_SAMPLES 50 // 0.5 us at 100 MSPS (same both modes)
`define RP_SHORT_SEGMENTS 1 // Single segment for short chirp
// Derived 3 km limits
`define RP_MAX_RANGE_3KM 1536 // 64 bins * 24 m = 1536 m
// ============================================================================
// 20 KM MODE PARAMETERS (200T only)
// ============================================================================
`define RP_LONG_CHIRP_SAMPLES_20KM 13700 // 137 us at 100 MSPS (= listen window)
`define RP_LONG_SEGMENTS_20KM 16 // ceil((13700-1024)/896) + 1 = 16
`define RP_OUTPUT_RANGE_BINS_20KM 1024 // 16 segments * 64 dec. bins each
// Derived 20 km limits
`define RP_MAX_RANGE_20KM 24576 // 1024 bins * 24 m = 24576 m
// ============================================================================
// MAX VALUES (for sizing buffers — compile-time, based on board variant)
// ============================================================================
`ifdef SUPPORT_LONG_RANGE
`define RP_MAX_SEGMENTS 16
`define RP_MAX_OUTPUT_BINS 1024
`define RP_MAX_CHIRP_SAMPLES 13700
`else
`define RP_MAX_SEGMENTS 4
`define RP_MAX_OUTPUT_BINS 64
`define RP_MAX_CHIRP_SAMPLES 3000
`endif
// ============================================================================
// BIT WIDTHS (derived from MAX values)
// ============================================================================
// Segment index: ceil(log2(MAX_SEGMENTS))
// 50T: log2(4) = 2 bits
// 200T: log2(16) = 4 bits
`ifdef SUPPORT_LONG_RANGE
`define RP_SEGMENT_IDX_WIDTH 4
`define RP_RANGE_BIN_WIDTH 10
`define RP_CHIRP_MEM_ADDR_W 14 // log2(16*1024) = 14
`define RP_DOPPLER_MEM_ADDR_W 15 // log2(1024*32) = 15
`define RP_CFAR_MAG_ADDR_W 15 // log2(1024*32) = 15
`else
`define RP_SEGMENT_IDX_WIDTH 2
`define RP_RANGE_BIN_WIDTH 6
`define RP_CHIRP_MEM_ADDR_W 12 // log2(4*1024) = 12
`define RP_DOPPLER_MEM_ADDR_W 11 // log2(64*32) = 11
`define RP_CFAR_MAG_ADDR_W 11 // log2(64*32) = 11
`endif
// Derived depths (for memory declarations)
// Usage: reg [15:0] mem [0:`RP_CHIRP_MEM_DEPTH-1];
`define RP_CHIRP_MEM_DEPTH (`RP_MAX_SEGMENTS * `RP_FFT_SIZE)
`define RP_DOPPLER_MEM_DEPTH (`RP_MAX_OUTPUT_BINS * `RP_CHIRPS_PER_FRAME)
`define RP_CFAR_MAG_DEPTH (`RP_MAX_OUTPUT_BINS * `RP_NUM_DOPPLER_BINS)
// ============================================================================
// CHIRP TIMING DEFAULTS (100 MHz clock cycles)
// ============================================================================
// Reset defaults for host-configurable timing registers.
// Match radar_mode_controller.v parameters and main.cpp STM32 defaults.
`define RP_DEF_LONG_CHIRP_CYCLES 3000 // 30 us
`define RP_DEF_LONG_LISTEN_CYCLES 13700 // 137 us
`define RP_DEF_GUARD_CYCLES 17540 // 175.4 us
`define RP_DEF_SHORT_CHIRP_CYCLES 50 // 0.5 us
`define RP_DEF_SHORT_LISTEN_CYCLES 17450 // 174.5 us
`define RP_DEF_CHIRPS_PER_ELEV 32
// ============================================================================
// BLIND ZONE CONSTANTS (informational, for comments and GUI)
// ============================================================================
// Long chirp blind zone: c * 30 us / 2 = 4500 m
// Short chirp blind zone: c * 0.5 us / 2 = 75 m
`define RP_LONG_BLIND_ZONE_M 4500
`define RP_SHORT_BLIND_ZONE_M 75
// ============================================================================
// PHYSICAL CONSTANTS (integer-scaled for Verilog — use in comments/assertions)
// ============================================================================
// Range per ADC sample: 1.5 m (stored as 15 in units of 0.1 m)
// Range per decimated bin: 24.0 m (stored as 240 in units of 0.1 m)
// Processing rate: 100 MSPS
`define RP_RANGE_PER_SAMPLE_DM 15 // 1.5 m in decimeters
`define RP_RANGE_PER_BIN_DM 240 // 24.0 m in decimeters
`define RP_PROCESSING_RATE_MHZ 100
// ============================================================================
// AGC DEFAULTS
// ============================================================================
`define RP_DEF_AGC_TARGET 200
`define RP_DEF_AGC_ATTACK 1
`define RP_DEF_AGC_DECAY 1
`define RP_DEF_AGC_HOLDOFF 4
// ============================================================================
// CFAR DEFAULTS
// ============================================================================
`define RP_DEF_CFAR_GUARD 2
`define RP_DEF_CFAR_TRAIN 8
`define RP_DEF_CFAR_ALPHA 8'h30 // 3.0 in Q4.4
`define RP_DEF_CFAR_MODE 2'b00 // CA-CFAR
// ============================================================================
// DETECTION DEFAULTS
// ============================================================================
`define RP_DEF_DETECT_THRESHOLD 10000
// ============================================================================
// RANGE MODE ENCODING
// ============================================================================
`define RP_RANGE_MODE_3KM 2'b00
`define RP_RANGE_MODE_20KM 2'b01
`define RP_RANGE_MODE_RSVD2 2'b10
`define RP_RANGE_MODE_RSVD3 2'b11
`endif // RADAR_PARAMS_VH
+11 -13
View File
@@ -102,9 +102,9 @@ wire [7:0] gc_saturation_count; // Diagnostic: per-frame clipped sample counter
wire [7:0] gc_peak_magnitude; // Diagnostic: per-frame peak magnitude wire [7:0] gc_peak_magnitude; // Diagnostic: per-frame peak magnitude
wire [3:0] gc_current_gain; // Diagnostic: effective gain_shift wire [3:0] gc_current_gain; // Diagnostic: effective gain_shift
// Reference signals for the processing chain // Reference signal for the processing chain (carries long OR short ref
wire [15:0] long_chirp_real, long_chirp_imag; // depending on use_long_chirp selected by chirp_memory_loader_param)
wire [15:0] short_chirp_real, short_chirp_imag; wire [15:0] ref_chirp_real, ref_chirp_imag;
// ========== DOPPLER PROCESSING SIGNALS ========== // ========== DOPPLER PROCESSING SIGNALS ==========
wire [31:0] range_data_32bit; wire [31:0] range_data_32bit;
@@ -292,7 +292,8 @@ end
// sample_addr_wire removed was unused implicit wire (synthesis warning) // sample_addr_wire removed was unused implicit wire (synthesis warning)
// 4. CRITICAL: Reference Chirp Latency Buffer // 4. CRITICAL: Reference Chirp Latency Buffer
// This aligns reference data with FFT output (2159 cycle delay) // This aligns reference data with FFT output (3187 cycle delay)
// TODO: verify empirically during hardware bring-up with correlation test
wire [15:0] delayed_ref_i, delayed_ref_q; wire [15:0] delayed_ref_i, delayed_ref_q;
wire mem_ready_delayed; wire mem_ready_delayed;
@@ -308,11 +309,10 @@ latency_buffer #(
.valid_out(mem_ready_delayed) .valid_out(mem_ready_delayed)
); );
// Assign delayed reference signals // Assign delayed reference signals (single pair chirp_memory_loader_param
assign long_chirp_real = delayed_ref_i; // selects long/short reference upstream via use_long_chirp)
assign long_chirp_imag = delayed_ref_q; assign ref_chirp_real = delayed_ref_i;
assign short_chirp_real = delayed_ref_i; assign ref_chirp_imag = delayed_ref_q;
assign short_chirp_imag = delayed_ref_q;
// 5. Dual Chirp Matched Filter // 5. Dual Chirp Matched Filter
@@ -336,10 +336,8 @@ matched_filter_multi_segment mf_dual (
.mc_new_chirp(mc_new_chirp), .mc_new_chirp(mc_new_chirp),
.mc_new_elevation(mc_new_elevation), .mc_new_elevation(mc_new_elevation),
.mc_new_azimuth(mc_new_azimuth), .mc_new_azimuth(mc_new_azimuth),
.long_chirp_real(delayed_ref_i), // From latency buffer .ref_chirp_real(delayed_ref_i), // From latency buffer (long or short ref)
.long_chirp_imag(delayed_ref_q), .ref_chirp_imag(delayed_ref_q),
.short_chirp_real(delayed_ref_i), // Same for short chirp
.short_chirp_imag(delayed_ref_q),
.segment_request(segment_request), .segment_request(segment_request),
.mem_request(mem_request), .mem_request(mem_request),
.sample_addr_out(sample_addr_from_chain), .sample_addr_out(sample_addr_from_chain),
+1 -1
View File
@@ -142,7 +142,7 @@ module radar_system_top (
parameter USE_LONG_CHIRP = 1'b1; // Default to long chirp parameter USE_LONG_CHIRP = 1'b1; // Default to long chirp
parameter DOPPLER_ENABLE = 1'b1; // Enable Doppler processing parameter DOPPLER_ENABLE = 1'b1; // Enable Doppler processing
parameter USB_ENABLE = 1'b1; // Enable USB data transfer parameter USB_ENABLE = 1'b1; // Enable USB data transfer
parameter USB_MODE = 1; // 0=FT601 (32-bit, 200T), 1=FT2232H (8-bit, 50T production default) parameter USB_MODE = 0; // 0=FT601 (32-bit, 200T), 1=FT2232H (8-bit, 50T)
// ============================================================================ // ============================================================================
// INTERNAL SIGNALS // INTERNAL SIGNALS
@@ -138,12 +138,7 @@ usb_data_interface usb_inst (
.status_range_mode(2'b01), .status_range_mode(2'b01),
.status_self_test_flags(5'b11111), .status_self_test_flags(5'b11111),
.status_self_test_detail(8'hA5), .status_self_test_detail(8'hA5),
.status_self_test_busy(1'b0), .status_self_test_busy(1'b0)
// AGC status: tie off with benign defaults (no AGC on dev board)
.status_agc_current_gain(4'd0),
.status_agc_peak_magnitude(8'd0),
.status_agc_saturation_count(8'd0),
.status_agc_enable(1'b0)
); );
endmodule endmodule
+128 -56
View File
@@ -70,7 +70,6 @@ PROD_RTL=(
xfft_16.v xfft_16.v
fft_engine.v fft_engine.v
usb_data_interface.v usb_data_interface.v
usb_data_interface_ft2232h.v
edge_detector.v edge_detector.v
radar_mode_controller.v radar_mode_controller.v
rx_gain_control.v rx_gain_control.v
@@ -87,33 +86,6 @@ EXTRA_RTL=(
frequency_matched_filter.v 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 ---- # ---- Layer A: iverilog -Wall compilation ----
run_lint_iverilog() { run_lint_iverilog() {
local label="$1" local label="$1"
@@ -247,9 +219,26 @@ run_lint_static() {
fi fi
done done
# CHECK 5 ($readmemh in synth code) and CHECK 6 (unused includes) # --- Single-line regex checks across all production RTL ---
# require multi-line ifdef tracking / cross-file analysis. Not feasible for f in "$@"; do
# with line-by-line regex. Omitted — use Vivado lint instead. [[ -f "$f" ]] || continue
case "$f" in tb/*) continue ;; esac
local linenum=0
while IFS= read -r line; do
linenum=$((linenum + 1))
# CHECK 5: $readmemh / $readmemb in synthesizable code
# (Only valid in simulation blocks — flag if outside `ifdef SIMULATION)
# This is hard to check line-by-line without tracking ifdefs.
# Skip for v1.
# CHECK 6: Unused `include files (informational only)
# Skip for v1.
: # placeholder — prevents empty loop body
done < "$f"
done
if [[ "$err_count" -gt 0 ]]; then if [[ "$err_count" -gt 0 ]]; then
echo -e "${RED}FAIL${NC} ($err_count errors, $warn_count warnings)" echo -e "${RED}FAIL${NC} ($err_count errors, $warn_count warnings)"
@@ -264,6 +253,68 @@ run_lint_static() {
fi fi
} }
# ---------------------------------------------------------------------------
# Helper: compile, run, and compare a matched-filter co-sim scenario
# run_mf_cosim <scenario_name> <define_flag>
# ---------------------------------------------------------------------------
run_mf_cosim() {
local name="$1"
local define="$2"
local vvp="tb/tb_mf_cosim_${name}.vvp"
local scenario_lower="$name"
printf " %-45s " "MF Co-Sim ($name)"
# Compile — build command as string to handle optional define
local cmd="iverilog -g2001 -DSIMULATION"
if [[ -n "$define" ]]; then
cmd="$cmd $define"
fi
cmd="$cmd -o $vvp tb/tb_mf_cosim.v matched_filter_processing_chain.v fft_engine.v chirp_memory_loader_param.v"
if ! eval "$cmd" 2>/tmp/iverilog_err_$$; then
echo -e "${RED}COMPILE FAIL${NC}"
ERRORS="$ERRORS\n MF Co-Sim ($name): compile error ($(head -1 /tmp/iverilog_err_$$))"
FAIL=$((FAIL + 1))
return
fi
# Run TB
local output
output=$(timeout 120 vvp "$vvp" 2>&1) || true
rm -f "$vvp"
# Check TB internal pass/fail
local tb_fail
tb_fail=$(echo "$output" | grep -Ec '^\[FAIL' || true)
if [[ "$tb_fail" -gt 0 ]]; then
echo -e "${RED}FAIL${NC} (TB internal failure)"
ERRORS="$ERRORS\n MF Co-Sim ($name): TB internal failure"
FAIL=$((FAIL + 1))
return
fi
# Run Python compare
if command -v python3 >/dev/null 2>&1; then
local compare_out
local compare_rc=0
compare_out=$(python3 tb/cosim/compare_mf.py "$scenario_lower" 2>&1) || compare_rc=$?
if [[ "$compare_rc" -ne 0 ]]; then
echo -e "${RED}FAIL${NC} (compare_mf.py mismatch)"
ERRORS="$ERRORS\n MF Co-Sim ($name): Python compare failed"
FAIL=$((FAIL + 1))
return
fi
else
echo -e "${YELLOW}SKIP${NC} (RTL passed, python3 not found — compare skipped)"
SKIP=$((SKIP + 1))
return
fi
echo -e "${GREEN}PASS${NC} (RTL + Python compare)"
PASS=$((PASS + 1))
}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Helper: compile and run a single testbench # Helper: compile and run a single testbench
# run_test <name> <vvp_path> <iverilog_args...> # run_test <name> <vvp_path> <iverilog_args...>
@@ -427,44 +478,65 @@ run_test "Full-Chain Real-Data (decim→Doppler, exact match)" \
doppler_processor.v xfft_16.v fft_engine.v doppler_processor.v xfft_16.v fft_engine.v
if [[ "$QUICK" -eq 0 ]]; then if [[ "$QUICK" -eq 0 ]]; then
# Golden generate # NOTE: The "Receiver golden generate/compare" pair was REMOVED because
run_test "Receiver (golden generate)" \ # it was self-blessing: both passes ran the same RTL with the same
tb/tb_rx_golden_reg.vvp \ # deterministic stimulus, so the test always passed regardless of bugs.
-DGOLDEN_GENERATE \ # Real co-sim coverage is provided by:
tb/tb_radar_receiver_final.v "${RECEIVER_RTL[@]}" # - tb_doppler_realdata.v (committed Python golden hex, exact match)
# - tb_fullchain_realdata.v (committed Python golden hex, exact match)
# Golden compare # A proper full-pipeline co-sim (DDC→MF→Decim→Doppler vs Python) is
run_test "Receiver (golden compare)" \ # planned as a replacement (Phase C of CI test plan).
tb/tb_rx_compare_reg.vvp \
tb/tb_radar_receiver_final.v "${RECEIVER_RTL[@]}"
# Full system top (monitoring-only, legacy) # Full system top (monitoring-only, legacy)
run_test "System Top (radar_system_tb)" \ run_test "System Top (radar_system_tb)" \
tb/tb_system_reg.vvp \ tb/tb_system_reg.vvp \
tb/radar_system_tb.v "${SYSTEM_RTL[@]}" 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 edge_detector.v radar_mode_controller.v \
rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
# E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset) # E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset)
run_test "System E2E (tb_system_e2e)" \ run_test "System E2E (tb_system_e2e)" \
tb/tb_system_e2e_reg.vvp \ tb/tb_system_e2e_reg.vvp \
tb/tb_system_e2e.v "${SYSTEM_RTL[@]}" tb/tb_system_e2e.v radar_system_top.v \
radar_transmitter.v dac_interface_single.v plfm_chirp_controller.v \
# USB_MODE=1 (FT2232H production) variants of system tests radar_receiver_final.v tb/ad9484_interface_400m_stub.v \
run_test "System Top USB_MODE=1 (FT2232H)" \ ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v \
tb/tb_system_ft2232h_reg.vvp \ cdc_modules.v fir_lowpass.v ddc_input_interface.v \
-DUSB_MODE_1 \ chirp_memory_loader_param.v latency_buffer.v \
tb/radar_system_tb.v "${SYSTEM_RTL[@]}" matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
run_test "System E2E USB_MODE=1 (FT2232H)" \ usb_data_interface.v edge_detector.v radar_mode_controller.v \
tb/tb_system_e2e_ft2232h_reg.vvp \ rx_gain_control.v cfar_ca.v mti_canceller.v fpga_self_test.v
-DUSB_MODE_1 \
tb/tb_system_e2e.v "${SYSTEM_RTL[@]}"
else else
echo " (skipped receiver golden + system top + E2E — use without --quick)" echo " (skipped system top + E2E — use without --quick)"
SKIP=$((SKIP + 6)) SKIP=$((SKIP + 2))
fi fi
echo "" echo ""
# ===========================================================================
# PHASE 2b: MATCHED FILTER CO-SIMULATION (RTL vs Python golden reference)
# Runs tb_mf_cosim.v for 4 scenarios, then compare_mf.py validates output
# against committed Python golden CSV files. In SIMULATION mode, thresholds
# are generous (behavioral vs fixed-point twiddles differ) — validates
# state machine mechanics, output count, and energy sanity.
# ===========================================================================
echo "--- PHASE 2b: Matched Filter Co-Sim ---"
run_mf_cosim "chirp" ""
run_mf_cosim "dc" "-DSCENARIO_DC"
run_mf_cosim "impulse" "-DSCENARIO_IMPULSE"
run_mf_cosim "tone5" "-DSCENARIO_TONE5"
echo ""
# =========================================================================== # ===========================================================================
# PHASE 3: UNIT TESTS — Signal Processing # PHASE 3: UNIT TESTS — Signal Processing
# =========================================================================== # ===========================================================================
@@ -108,9 +108,6 @@ add_files -fileset constrs_1 -norecurse [file join $project_root "constraints" "
set_property top $top_module [current_fileset] set_property top $top_module [current_fileset]
set_property verilog_define {FFT_XPM_BRAM} [current_fileset] set_property verilog_define {FFT_XPM_BRAM} [current_fileset]
# Override USB_MODE to 0 (FT601) for 200T premium board.
# The RTL default is USB_MODE=1 (FT2232H, production 50T).
set_property generic {USB_MODE=0} [current_fileset]
# ============================================================================== # ==============================================================================
# 2. Synthesis # 2. Synthesis
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
@@ -2,8 +2,8 @@
""" """
golden_reference.py — AERIS-10 FPGA bit-accurate golden reference model golden_reference.py — AERIS-10 FPGA bit-accurate golden reference model
Uses ADI CN0566 Phaser radar data (10.525 GHz X-band FMCW) to validate Uses ADI CN0566 Phaser radar data (10.525 GHz, used as test stimulus only) to
the FPGA signal processing pipeline stage by stage: validate the FPGA signal processing pipeline stage by stage:
ADC → DDC (NCO+mixer+CIC+FIR) → Range FFT → Doppler FFT → Detection ADC → DDC (NCO+mixer+CIC+FIR) → Range FFT → Doppler FFT → Detection
@@ -90,7 +90,8 @@ HAMMING_Q15 = [
0x3088, 0x1B6D, 0x0E5C, 0x0A3D, 0x3088, 0x1B6D, 0x0E5C, 0x0A3D,
] ]
# ADI dataset parameters # ADI dataset parameters — used ONLY for loading/requantizing ADI Phaser test data.
# These are NOT PLFM hardware parameters. See AERIS-10 constants below.
ADI_SAMPLE_RATE = 4e6 # 4 MSPS ADI_SAMPLE_RATE = 4e6 # 4 MSPS
ADI_IF_FREQ = 100e3 # 100 kHz IF ADI_IF_FREQ = 100e3 # 100 kHz IF
ADI_RF_FREQ = 9.9e9 # 9.9 GHz ADI_RF_FREQ = 9.9e9 # 9.9 GHz
@@ -99,9 +100,17 @@ ADI_RAMP_TIME = 300e-6 # 300 us
ADI_NUM_CHIRPS = 256 ADI_NUM_CHIRPS = 256
ADI_SAMPLES_PER_CHIRP = 1079 ADI_SAMPLES_PER_CHIRP = 1079
# AERIS-10 parameters # AERIS-10 hardware parameters (from ADF4382/AD9523/main.cpp configuration)
AERIS_FS = 400e6 # 400 MHz ADC clock AERIS_FS = 400e6 # 400 MHz ADC clock (AD9523 OUT4)
AERIS_IF = 120e6 # 120 MHz IF AERIS_IF = 120e6 # 120 MHz IF (TX 10.5 GHz - RX 10.38 GHz)
AERIS_FS_PROCESSING = 100e6 # Post-DDC rate (400 MSPS / 4x CIC)
AERIS_CARRIER_HZ = 10.5e9 # TX LO (ADF4382, verified)
AERIS_RX_LO_HZ = 10.38e9 # RX LO (ADF4382)
AERIS_CHIRP_BW = 20e6 # Chirp bandwidth (target: 30 MHz Phase 1)
AERIS_LONG_CHIRP_S = 30e-6 # Long chirp duration
AERIS_PRI_S = 167e-6 # Pulse repetition interval
AERIS_DECIMATION = 16 # Range bin decimation (1024 → 64)
AERIS_RANGE_PER_BIN = 24.0 # Meters per decimated bin
# =========================================================================== # ===========================================================================
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
@@ -421,13 +421,13 @@ def test_latency_buffer():
# #
# For synthesis: the latency_buffer feeds ref data to the chain via # For synthesis: the latency_buffer feeds ref data to the chain via
# chirp_memory_loader_param → latency_buffer → chain. # chirp_memory_loader_param → latency_buffer → chain.
# But wait — looking at radar_receiver_final.v: # Looking at radar_receiver_final.v:
# - mem_request drives valid_in on the latency buffer # - mem_request drives valid_in on the latency buffer
# - The buffer delays {ref_i, ref_q} by LATENCY valid_in cycles # - The buffer delays {ref_i, ref_q} by LATENCY valid_in cycles
# - The delayed output feeds long_chirp_real/imag → chain # - The delayed output feeds ref_chirp_real/imag → chain
# #
# The purpose: the chain in the SYNTHESIS branch reads reference data # The purpose: the chain in the SYNTHESIS branch reads reference data
# via the long_chirp_real/imag ports DURING ST_FWD_FFT (while collecting # via the ref_chirp_real/imag ports DURING ST_FWD_FFT (while collecting
# input samples). The reference data needs to arrive LATENCY cycles # input samples). The reference data needs to arrive LATENCY cycles
# after the first mem_request, where LATENCY accounts for: # after the first mem_request, where LATENCY accounts for:
# - The fft_engine pipeline latency from input to output # - The fft_engine pipeline latency from input to output
File diff suppressed because it is too large Load Diff
+1 -11
View File
@@ -430,13 +430,7 @@ end
// DUT INSTANTIATION // DUT INSTANTIATION
// ============================================================================ // ============================================================================
radar_system_top #( radar_system_top dut (
`ifdef USB_MODE_1
.USB_MODE(1) // FT2232H interface (production 50T board)
`else
.USB_MODE(0) // FT601 interface (200T dev board)
`endif
) dut (
// System Clocks // System Clocks
.clk_100m(clk_100m), .clk_100m(clk_100m),
.clk_120m_dac(clk_120m_dac), .clk_120m_dac(clk_120m_dac),
@@ -625,11 +619,7 @@ initial begin
// Optional: dump specific signals for debugging // Optional: dump specific signals for debugging
$dumpvars(1, dut.tx_inst); $dumpvars(1, dut.tx_inst);
$dumpvars(1, dut.rx_inst); $dumpvars(1, dut.rx_inst);
`ifdef USB_MODE_1
$dumpvars(1, dut.gen_ft2232h.usb_inst);
`else
$dumpvars(1, dut.gen_ft601.usb_inst); $dumpvars(1, dut.gen_ft601.usb_inst);
`endif
end end
endmodule endmodule
@@ -18,10 +18,8 @@ module tb_matched_filter_processing_chain;
reg [15:0] adc_data_q; reg [15:0] adc_data_q;
reg adc_valid; reg adc_valid;
reg [5:0] chirp_counter; reg [5:0] chirp_counter;
reg [15:0] long_chirp_real; reg [15:0] ref_chirp_real;
reg [15:0] long_chirp_imag; reg [15:0] ref_chirp_imag;
reg [15:0] short_chirp_real;
reg [15:0] short_chirp_imag;
wire signed [15:0] range_profile_i; wire signed [15:0] range_profile_i;
wire signed [15:0] range_profile_q; wire signed [15:0] range_profile_q;
wire range_profile_valid; wire range_profile_valid;
@@ -83,10 +81,8 @@ module tb_matched_filter_processing_chain;
.adc_data_q (adc_data_q), .adc_data_q (adc_data_q),
.adc_valid (adc_valid), .adc_valid (adc_valid),
.chirp_counter (chirp_counter), .chirp_counter (chirp_counter),
.long_chirp_real (long_chirp_real), .ref_chirp_real (ref_chirp_real),
.long_chirp_imag (long_chirp_imag), .ref_chirp_imag (ref_chirp_imag),
.short_chirp_real (short_chirp_real),
.short_chirp_imag (short_chirp_imag),
.range_profile_i (range_profile_i), .range_profile_i (range_profile_i),
.range_profile_q (range_profile_q), .range_profile_q (range_profile_q),
.range_profile_valid (range_profile_valid), .range_profile_valid (range_profile_valid),
@@ -133,10 +129,8 @@ module tb_matched_filter_processing_chain;
adc_data_i = 16'd0; adc_data_i = 16'd0;
adc_data_q = 16'd0; adc_data_q = 16'd0;
chirp_counter = 6'd0; chirp_counter = 6'd0;
long_chirp_real = 16'd0; ref_chirp_real = 16'd0;
long_chirp_imag = 16'd0; ref_chirp_imag = 16'd0;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
cap_enable = 0; cap_enable = 0;
cap_count = 0; cap_count = 0;
cap_max_abs = 0; cap_max_abs = 0;
@@ -168,10 +162,8 @@ module tb_matched_filter_processing_chain;
angle = 6.28318530718 * tone_bin * k / (1.0 * FFT_SIZE); angle = 6.28318530718 * tone_bin * k / (1.0 * FFT_SIZE);
adc_data_i = $rtoi(8000.0 * $cos(angle)); adc_data_i = $rtoi(8000.0 * $cos(angle));
adc_data_q = $rtoi(8000.0 * $sin(angle)); adc_data_q = $rtoi(8000.0 * $sin(angle));
long_chirp_real = $rtoi(8000.0 * $cos(angle)); ref_chirp_real = $rtoi(8000.0 * $cos(angle));
long_chirp_imag = $rtoi(8000.0 * $sin(angle)); ref_chirp_imag = $rtoi(8000.0 * $sin(angle));
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); @(posedge clk);
#1; #1;
@@ -187,10 +179,8 @@ module tb_matched_filter_processing_chain;
for (k = 0; k < FFT_SIZE; k = k + 1) begin for (k = 0; k < FFT_SIZE; k = k + 1) begin
adc_data_i = 16'sh1000; adc_data_i = 16'sh1000;
adc_data_q = 16'sh0000; adc_data_q = 16'sh0000;
long_chirp_real = 16'sh1000; ref_chirp_real = 16'sh1000;
long_chirp_imag = 16'sh0000; ref_chirp_imag = 16'sh0000;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); @(posedge clk);
#1; #1;
@@ -233,10 +223,8 @@ module tb_matched_filter_processing_chain;
for (k = 0; k < FFT_SIZE; k = k + 1) begin for (k = 0; k < FFT_SIZE; k = k + 1) begin
adc_data_i = gold_sig_i[k]; adc_data_i = gold_sig_i[k];
adc_data_q = gold_sig_q[k]; adc_data_q = gold_sig_q[k];
long_chirp_real = gold_ref_i[k]; ref_chirp_real = gold_ref_i[k];
long_chirp_imag = gold_ref_q[k]; ref_chirp_imag = gold_ref_q[k];
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); @(posedge clk);
#1; #1;
@@ -374,10 +362,8 @@ module tb_matched_filter_processing_chain;
for (i = 0; i < FFT_SIZE; i = i + 1) begin for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = 16'd0; adc_data_i = 16'd0;
adc_data_q = 16'd0; adc_data_q = 16'd0;
long_chirp_real = 16'd0; ref_chirp_real = 16'd0;
long_chirp_imag = 16'd0; ref_chirp_imag = 16'd0;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
end end
@@ -449,10 +435,8 @@ module tb_matched_filter_processing_chain;
for (i = 0; i < FFT_SIZE; i = i + 1) begin for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = $rtoi(8000.0 * $cos(6.28318530718 * 5 * i / 1024.0)); adc_data_i = $rtoi(8000.0 * $cos(6.28318530718 * 5 * i / 1024.0));
adc_data_q = $rtoi(8000.0 * $sin(6.28318530718 * 5 * i / 1024.0)); adc_data_q = $rtoi(8000.0 * $sin(6.28318530718 * 5 * i / 1024.0));
long_chirp_real = $rtoi(8000.0 * $cos(6.28318530718 * 10 * i / 1024.0)); ref_chirp_real = $rtoi(8000.0 * $cos(6.28318530718 * 10 * i / 1024.0));
long_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0)); ref_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0));
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
end end
@@ -568,10 +552,8 @@ module tb_matched_filter_processing_chain;
for (i = 0; i < FFT_SIZE; i = i + 1) begin for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = 16'sh7FFF; adc_data_i = 16'sh7FFF;
adc_data_q = 16'sh7FFF; adc_data_q = 16'sh7FFF;
long_chirp_real = 16'sh7FFF; ref_chirp_real = 16'sh7FFF;
long_chirp_imag = 16'sh7FFF; ref_chirp_imag = 16'sh7FFF;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
end end
@@ -589,10 +571,8 @@ module tb_matched_filter_processing_chain;
for (i = 0; i < FFT_SIZE; i = i + 1) begin for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = 16'sh8000; adc_data_i = 16'sh8000;
adc_data_q = 16'sh8000; adc_data_q = 16'sh8000;
long_chirp_real = 16'sh8000; ref_chirp_real = 16'sh8000;
long_chirp_imag = 16'sh8000; ref_chirp_imag = 16'sh8000;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
end end
@@ -611,16 +591,14 @@ module tb_matched_filter_processing_chain;
if (i % 2 == 0) begin if (i % 2 == 0) begin
adc_data_i = 16'sh7FFF; adc_data_i = 16'sh7FFF;
adc_data_q = 16'sh7FFF; adc_data_q = 16'sh7FFF;
long_chirp_real = 16'sh7FFF; ref_chirp_real = 16'sh7FFF;
long_chirp_imag = 16'sh7FFF; ref_chirp_imag = 16'sh7FFF;
end else begin end else begin
adc_data_i = 16'sh8000; adc_data_i = 16'sh8000;
adc_data_q = 16'sh8000; adc_data_q = 16'sh8000;
long_chirp_real = 16'sh8000; ref_chirp_real = 16'sh8000;
long_chirp_imag = 16'sh8000; ref_chirp_imag = 16'sh8000;
end end
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
end end
@@ -641,10 +619,8 @@ module tb_matched_filter_processing_chain;
for (i = 0; i < 512; i = i + 1) begin for (i = 0; i < 512; i = i + 1) begin
adc_data_i = 16'sh1000; adc_data_i = 16'sh1000;
adc_data_q = 16'sh0000; adc_data_q = 16'sh0000;
long_chirp_real = 16'sh1000; ref_chirp_real = 16'sh1000;
long_chirp_imag = 16'sh0000; ref_chirp_imag = 16'sh0000;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
end end
@@ -683,10 +659,8 @@ module tb_matched_filter_processing_chain;
for (i = 0; i < FFT_SIZE; i = i + 1) begin for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = 16'sh1000; adc_data_i = 16'sh1000;
adc_data_q = 16'sh0000; adc_data_q = 16'sh0000;
long_chirp_real = 16'sh1000; ref_chirp_real = 16'sh1000;
long_chirp_imag = 16'sh0000; ref_chirp_imag = 16'sh0000;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
+24 -46
View File
@@ -28,10 +28,8 @@ module tb_mf_chain_synth;
reg [15:0] adc_data_q; reg [15:0] adc_data_q;
reg adc_valid; reg adc_valid;
reg [5:0] chirp_counter; reg [5:0] chirp_counter;
reg [15:0] long_chirp_real; reg [15:0] ref_chirp_real;
reg [15:0] long_chirp_imag; reg [15:0] ref_chirp_imag;
reg [15:0] short_chirp_real;
reg [15:0] short_chirp_imag;
wire signed [15:0] range_profile_i; wire signed [15:0] range_profile_i;
wire signed [15:0] range_profile_q; wire signed [15:0] range_profile_q;
wire range_profile_valid; wire range_profile_valid;
@@ -78,10 +76,8 @@ module tb_mf_chain_synth;
.adc_data_q (adc_data_q), .adc_data_q (adc_data_q),
.adc_valid (adc_valid), .adc_valid (adc_valid),
.chirp_counter (chirp_counter), .chirp_counter (chirp_counter),
.long_chirp_real (long_chirp_real), .ref_chirp_real (ref_chirp_real),
.long_chirp_imag (long_chirp_imag), .ref_chirp_imag (ref_chirp_imag),
.short_chirp_real (short_chirp_real),
.short_chirp_imag (short_chirp_imag),
.range_profile_i (range_profile_i), .range_profile_i (range_profile_i),
.range_profile_q (range_profile_q), .range_profile_q (range_profile_q),
.range_profile_valid (range_profile_valid), .range_profile_valid (range_profile_valid),
@@ -130,10 +126,8 @@ module tb_mf_chain_synth;
adc_data_i = 16'd0; adc_data_i = 16'd0;
adc_data_q = 16'd0; adc_data_q = 16'd0;
chirp_counter = 6'd0; chirp_counter = 6'd0;
long_chirp_real = 16'd0; ref_chirp_real = 16'd0;
long_chirp_imag = 16'd0; ref_chirp_imag = 16'd0;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
cap_enable = 0; cap_enable = 0;
cap_count = 0; cap_count = 0;
cap_max_abs = 0; cap_max_abs = 0;
@@ -177,10 +171,8 @@ module tb_mf_chain_synth;
for (k = 0; k < FFT_SIZE; k = k + 1) begin for (k = 0; k < FFT_SIZE; k = k + 1) begin
adc_data_i = 16'sh1000; // +4096 adc_data_i = 16'sh1000; // +4096
adc_data_q = 16'sh0000; adc_data_q = 16'sh0000;
long_chirp_real = 16'sh1000; ref_chirp_real = 16'sh1000;
long_chirp_imag = 16'sh0000; ref_chirp_imag = 16'sh0000;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); @(posedge clk);
#1; #1;
@@ -199,10 +191,8 @@ module tb_mf_chain_synth;
angle = 6.28318530718 * tone_bin * k / (1.0 * FFT_SIZE); angle = 6.28318530718 * tone_bin * k / (1.0 * FFT_SIZE);
adc_data_i = $rtoi(8000.0 * $cos(angle)); adc_data_i = $rtoi(8000.0 * $cos(angle));
adc_data_q = $rtoi(8000.0 * $sin(angle)); adc_data_q = $rtoi(8000.0 * $sin(angle));
long_chirp_real = $rtoi(8000.0 * $cos(angle)); ref_chirp_real = $rtoi(8000.0 * $cos(angle));
long_chirp_imag = $rtoi(8000.0 * $sin(angle)); ref_chirp_imag = $rtoi(8000.0 * $sin(angle));
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); @(posedge clk);
#1; #1;
@@ -219,16 +209,14 @@ module tb_mf_chain_synth;
if (k == 0) begin if (k == 0) begin
adc_data_i = 16'sh4000; // 0.5 in Q15 adc_data_i = 16'sh4000; // 0.5 in Q15
adc_data_q = 16'sh0000; adc_data_q = 16'sh0000;
long_chirp_real = 16'sh4000; ref_chirp_real = 16'sh4000;
long_chirp_imag = 16'sh0000; ref_chirp_imag = 16'sh0000;
end else begin end else begin
adc_data_i = 16'sh0000; adc_data_i = 16'sh0000;
adc_data_q = 16'sh0000; adc_data_q = 16'sh0000;
long_chirp_real = 16'sh0000; ref_chirp_real = 16'sh0000;
long_chirp_imag = 16'sh0000; ref_chirp_imag = 16'sh0000;
end end
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); @(posedge clk);
#1; #1;
@@ -309,10 +297,8 @@ module tb_mf_chain_synth;
for (i = 0; i < FFT_SIZE; i = i + 1) begin for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = 16'd0; adc_data_i = 16'd0;
adc_data_q = 16'd0; adc_data_q = 16'd0;
long_chirp_real = 16'd0; ref_chirp_real = 16'd0;
long_chirp_imag = 16'd0; ref_chirp_imag = 16'd0;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
end end
@@ -379,10 +365,8 @@ module tb_mf_chain_synth;
for (i = 0; i < 512; i = i + 1) begin for (i = 0; i < 512; i = i + 1) begin
adc_data_i = 16'sh1000; adc_data_i = 16'sh1000;
adc_data_q = 16'sh0000; adc_data_q = 16'sh0000;
long_chirp_real = 16'sh1000; ref_chirp_real = 16'sh1000;
long_chirp_imag = 16'sh0000; ref_chirp_imag = 16'sh0000;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
end end
@@ -439,10 +423,8 @@ module tb_mf_chain_synth;
for (i = 0; i < FFT_SIZE; i = i + 1) begin for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = $rtoi(8000.0 * $cos(6.28318530718 * 5 * i / 1024.0)); adc_data_i = $rtoi(8000.0 * $cos(6.28318530718 * 5 * i / 1024.0));
adc_data_q = $rtoi(8000.0 * $sin(6.28318530718 * 5 * i / 1024.0)); adc_data_q = $rtoi(8000.0 * $sin(6.28318530718 * 5 * i / 1024.0));
long_chirp_real = $rtoi(8000.0 * $cos(6.28318530718 * 10 * i / 1024.0)); ref_chirp_real = $rtoi(8000.0 * $cos(6.28318530718 * 10 * i / 1024.0));
long_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0)); ref_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0));
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
end end
@@ -469,10 +451,8 @@ module tb_mf_chain_synth;
for (i = 0; i < FFT_SIZE; i = i + 1) begin for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = 16'sh7FFF; adc_data_i = 16'sh7FFF;
adc_data_q = 16'sh7FFF; adc_data_q = 16'sh7FFF;
long_chirp_real = 16'sh7FFF; ref_chirp_real = 16'sh7FFF;
long_chirp_imag = 16'sh7FFF; ref_chirp_imag = 16'sh7FFF;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
end end
@@ -495,10 +475,8 @@ module tb_mf_chain_synth;
for (i = 0; i < FFT_SIZE; i = i + 1) begin for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = 16'sh1000; adc_data_i = 16'sh1000;
adc_data_q = 16'sh0000; adc_data_q = 16'sh0000;
long_chirp_real = 16'sh1000; ref_chirp_real = 16'sh1000;
long_chirp_imag = 16'sh0000; ref_chirp_imag = 16'sh0000;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1; adc_valid = 1'b1;
@(posedge clk); #1; @(posedge clk); #1;
+10 -18
View File
@@ -88,10 +88,8 @@ reg [15:0] adc_data_i;
reg [15:0] adc_data_q; reg [15:0] adc_data_q;
reg adc_valid; reg adc_valid;
reg [5:0] chirp_counter; reg [5:0] chirp_counter;
reg [15:0] long_chirp_real; reg [15:0] ref_chirp_real;
reg [15:0] long_chirp_imag; reg [15:0] ref_chirp_imag;
reg [15:0] short_chirp_real;
reg [15:0] short_chirp_imag;
wire signed [15:0] range_profile_i; wire signed [15:0] range_profile_i;
wire signed [15:0] range_profile_q; wire signed [15:0] range_profile_q;
@@ -108,10 +106,8 @@ matched_filter_processing_chain dut (
.adc_data_q(adc_data_q), .adc_data_q(adc_data_q),
.adc_valid(adc_valid), .adc_valid(adc_valid),
.chirp_counter(chirp_counter), .chirp_counter(chirp_counter),
.long_chirp_real(long_chirp_real), .ref_chirp_real(ref_chirp_real),
.long_chirp_imag(long_chirp_imag), .ref_chirp_imag(ref_chirp_imag),
.short_chirp_real(short_chirp_real),
.short_chirp_imag(short_chirp_imag),
.range_profile_i(range_profile_i), .range_profile_i(range_profile_i),
.range_profile_q(range_profile_q), .range_profile_q(range_profile_q),
.range_profile_valid(range_profile_valid), .range_profile_valid(range_profile_valid),
@@ -157,10 +153,8 @@ task apply_reset;
adc_data_q <= 16'd0; adc_data_q <= 16'd0;
adc_valid <= 1'b0; adc_valid <= 1'b0;
chirp_counter <= 6'd0; chirp_counter <= 6'd0;
long_chirp_real <= 16'd0; ref_chirp_real <= 16'd0;
long_chirp_imag <= 16'd0; ref_chirp_imag <= 16'd0;
short_chirp_real <= 16'd0;
short_chirp_imag <= 16'd0;
repeat(4) @(posedge clk); repeat(4) @(posedge clk);
reset_n <= 1'b1; reset_n <= 1'b1;
@(posedge clk); @(posedge clk);
@@ -201,18 +195,16 @@ initial begin
@(posedge clk); @(posedge clk);
adc_data_i <= sig_mem_i[i]; adc_data_i <= sig_mem_i[i];
adc_data_q <= sig_mem_q[i]; adc_data_q <= sig_mem_q[i];
long_chirp_real <= ref_mem_i[i]; ref_chirp_real <= ref_mem_i[i];
long_chirp_imag <= ref_mem_q[i]; ref_chirp_imag <= ref_mem_q[i];
short_chirp_real <= 16'd0;
short_chirp_imag <= 16'd0;
adc_valid <= 1'b1; adc_valid <= 1'b1;
end end
@(posedge clk); @(posedge clk);
adc_valid <= 1'b0; adc_valid <= 1'b0;
adc_data_i <= 16'd0; adc_data_i <= 16'd0;
adc_data_q <= 16'd0; adc_data_q <= 16'd0;
long_chirp_real <= 16'd0; ref_chirp_real <= 16'd0;
long_chirp_imag <= 16'd0; ref_chirp_imag <= 16'd0;
$display("All samples fed. Waiting for processing..."); $display("All samples fed. Waiting for processing...");
+10 -16
View File
@@ -56,10 +56,8 @@ reg [5:0] chirp_counter;
reg mc_new_chirp; reg mc_new_chirp;
reg mc_new_elevation; reg mc_new_elevation;
reg mc_new_azimuth; reg mc_new_azimuth;
reg [15:0] long_chirp_real; reg [15:0] ref_chirp_real;
reg [15:0] long_chirp_imag; reg [15:0] ref_chirp_imag;
reg [15:0] short_chirp_real;
reg [15:0] short_chirp_imag;
reg mem_ready; reg mem_ready;
wire signed [15:0] pc_i_w; wire signed [15:0] pc_i_w;
@@ -84,10 +82,8 @@ matched_filter_multi_segment dut (
.mc_new_chirp(mc_new_chirp), .mc_new_chirp(mc_new_chirp),
.mc_new_elevation(mc_new_elevation), .mc_new_elevation(mc_new_elevation),
.mc_new_azimuth(mc_new_azimuth), .mc_new_azimuth(mc_new_azimuth),
.long_chirp_real(long_chirp_real), .ref_chirp_real(ref_chirp_real),
.long_chirp_imag(long_chirp_imag), .ref_chirp_imag(ref_chirp_imag),
.short_chirp_real(short_chirp_real),
.short_chirp_imag(short_chirp_imag),
.segment_request(segment_request), .segment_request(segment_request),
.sample_addr_out(sample_addr_out), .sample_addr_out(sample_addr_out),
.mem_request(mem_request), .mem_request(mem_request),
@@ -123,11 +119,11 @@ end
always @(posedge clk) begin always @(posedge clk) begin
if (mem_request) begin if (mem_request) begin
if (use_long_chirp) begin if (use_long_chirp) begin
long_chirp_real <= ref_mem_i[{segment_request, sample_addr_out}]; ref_chirp_real <= ref_mem_i[{segment_request, sample_addr_out}];
long_chirp_imag <= ref_mem_q[{segment_request, sample_addr_out}]; ref_chirp_imag <= ref_mem_q[{segment_request, sample_addr_out}];
end else begin end else begin
short_chirp_real <= ref_mem_i[sample_addr_out]; ref_chirp_real <= ref_mem_i[sample_addr_out];
short_chirp_imag <= ref_mem_q[sample_addr_out]; ref_chirp_imag <= ref_mem_q[sample_addr_out];
end end
mem_ready <= 1'b1; mem_ready <= 1'b1;
end else begin end else begin
@@ -176,10 +172,8 @@ task apply_reset;
mc_new_chirp <= 1'b0; mc_new_chirp <= 1'b0;
mc_new_elevation <= 1'b0; mc_new_elevation <= 1'b0;
mc_new_azimuth <= 1'b0; mc_new_azimuth <= 1'b0;
long_chirp_real <= 16'd0; ref_chirp_real <= 16'd0;
long_chirp_imag <= 16'd0; ref_chirp_imag <= 16'd0;
short_chirp_real <= 16'd0;
short_chirp_imag <= 16'd0;
mem_ready <= 1'b0; mem_ready <= 1'b0;
repeat(10) @(posedge clk); repeat(10) @(posedge clk);
reset_n <= 1'b1; reset_n <= 1'b1;
@@ -7,43 +7,21 @@
// -> matched_filter_multi_segment -> range_bin_decimator // -> matched_filter_multi_segment -> range_bin_decimator
// -> doppler_processor_optimized -> doppler_output // -> doppler_processor_optimized -> doppler_output
// //
// ============================================================================
// TWO MODES (compile-time define):
//
// 1. GOLDEN_GENERATE mode (-DGOLDEN_GENERATE):
// Dumps all Doppler output samples to golden reference files.
// Run once on known-good RTL:
// iverilog -g2001 -DSIMULATION -DGOLDEN_GENERATE -o tb_golden_gen.vvp \
// <src files> tb/tb_radar_receiver_final.v
// mkdir -p tb/golden
// vvp tb_golden_gen.vvp
//
// 2. Default mode (no GOLDEN_GENERATE):
// Loads golden files, compares each Doppler output against reference,
// and runs physics-based bounds checks.
// iverilog -g2001 -DSIMULATION -o tb_radar_receiver_final.vvp \
// <src files> tb/tb_radar_receiver_final.v
// vvp tb_radar_receiver_final.vvp
//
// PREREQUISITES:
// - The directory tb/golden/ must exist before running either mode.
// Create it with: mkdir -p tb/golden
//
// TAP POINTS: // TAP POINTS:
// Tap 1 (DDC output) - bounds checking only (CDC jitter -> non-deterministic) // Tap 1 (DDC output) - bounds checking only (CDC jitter -> non-deterministic)
// Signals: dut.ddc_out_i [17:0], dut.ddc_out_q [17:0], dut.ddc_valid_i // Signals: dut.ddc_out_i [17:0], dut.ddc_out_q [17:0], dut.ddc_valid_i
// Tap 2 (Doppler output) - golden compared (deterministic after MF buffering) // Tap 2 (Doppler output) - structural + bounds checks (deterministic after MF)
// Signals: doppler_output[31:0], doppler_valid, doppler_bin[4:0], // Signals: doppler_output[31:0], doppler_valid, doppler_bin[4:0],
// range_bin_out[5:0] // range_bin_out[5:0]
// //
// Golden file: tb/golden/golden_doppler.mem
// 2048 entries of 32-bit hex, indexed by range_bin*32 + doppler_bin
//
// Strategy: // Strategy:
// - Uses behavioral stub for ad9484_interface_400m (no Xilinx primitives) // - Uses behavioral stub for ad9484_interface_400m (no Xilinx primitives)
// - Overrides radar_mode_controller timing params for fast simulation // - Overrides radar_mode_controller timing params for fast simulation
// - Feeds 120 MHz tone at ADC input (IF frequency -> DDC passband) // - Feeds 120 MHz tone at ADC input (IF frequency -> DDC passband)
// - Verifies structural correctness + golden comparison + bounds checks // - Verifies structural correctness (S1-S10) + physics bounds checks (B1-B5)
// - Bit-accurate golden comparison is done by the MF co-sim tests
// (tb_mf_cosim.v + compare_mf.py) and full-chain co-sim tests
// (tb_doppler_realdata.v, tb_fullchain_realdata.v), not here.
// //
// Convention: check task, VCD dump, CSV output, pass/fail summary // Convention: check task, VCD dump, CSV output, pass/fail summary
// ============================================================================ // ============================================================================
@@ -194,46 +172,6 @@ task check;
end end
endtask endtask
// ============================================================================
// GOLDEN MEMORY DECLARATIONS AND LOAD/STORE LOGIC
// ============================================================================
localparam GOLDEN_ENTRIES = 2048; // 64 range bins * 32 Doppler bins
localparam GOLDEN_TOLERANCE = 2; // +/- 2 LSB tolerance for comparison
reg [31:0] golden_doppler [0:2047];
// -- Golden comparison tracking --
integer golden_match_count;
integer golden_mismatch_count;
integer golden_max_err_i;
integer golden_max_err_q;
integer golden_compare_count;
`ifdef GOLDEN_GENERATE
// In generate mode, we just initialize the array to X/0
// and fill it as outputs arrive
integer gi;
initial begin
for (gi = 0; gi < GOLDEN_ENTRIES; gi = gi + 1)
golden_doppler[gi] = 32'd0;
golden_match_count = 0;
golden_mismatch_count = 0;
golden_max_err_i = 0;
golden_max_err_q = 0;
golden_compare_count = 0;
end
`else
// In comparison mode, load the golden reference
initial begin
$readmemh("tb/golden/golden_doppler.mem", golden_doppler);
golden_match_count = 0;
golden_mismatch_count = 0;
golden_max_err_i = 0;
golden_max_err_q = 0;
golden_compare_count = 0;
end
`endif
// ============================================================================ // ============================================================================
// DDC ENERGY ACCUMULATOR (Bounds Check B1) // DDC ENERGY ACCUMULATOR (Bounds Check B1)
// ============================================================================ // ============================================================================
@@ -257,7 +195,7 @@ always @(posedge clk_100m) begin
end end
// ============================================================================ // ============================================================================
// DOPPLER OUTPUT CAPTURE, GOLDEN COMPARISON, AND DUPLICATE DETECTION // DOPPLER OUTPUT CAPTURE AND DUPLICATE DETECTION
// ============================================================================ // ============================================================================
integer doppler_output_count; integer doppler_output_count;
integer doppler_frame_count; integer doppler_frame_count;
@@ -311,13 +249,6 @@ end
// Monitor doppler outputs -- only after reset released // Monitor doppler outputs -- only after reset released
always @(posedge clk_100m) begin always @(posedge clk_100m) begin
if (reset_n && doppler_valid) begin : doppler_capture_block if (reset_n && doppler_valid) begin : doppler_capture_block
// ---- Signed intermediates for golden comparison ----
reg signed [16:0] actual_i, actual_q;
reg signed [16:0] expected_i, expected_q;
reg signed [16:0] err_i_signed, err_q_signed;
integer abs_err_i, abs_err_q;
integer gidx;
reg [31:0] expected_val;
// ---- Magnitude intermediates for B2 ---- // ---- Magnitude intermediates for B2 ----
reg signed [16:0] mag_i_signed, mag_q_signed; reg signed [16:0] mag_i_signed, mag_q_signed;
integer mag_i, mag_q, mag_sum; integer mag_i, mag_q, mag_sum;
@@ -350,9 +281,6 @@ always @(posedge clk_100m) begin
if ((doppler_output_count % 256) == 0) if ((doppler_output_count % 256) == 0)
$display("[INFO] %0d doppler outputs so far (t=%0t)", doppler_output_count, $time); $display("[INFO] %0d doppler outputs so far (t=%0t)", doppler_output_count, $time);
// ---- Golden index computation ----
gidx = range_bin_out * 32 + doppler_bin;
// ---- Duplicate detection (B5) ---- // ---- Duplicate detection (B5) ----
if (range_bin_out < 64 && doppler_bin < 32) begin if (range_bin_out < 64 && doppler_bin < 32) begin
if (index_seen[range_bin_out][doppler_bin]) begin if (index_seen[range_bin_out][doppler_bin]) begin
@@ -376,44 +304,6 @@ always @(posedge clk_100m) begin
if (mag_sum > peak_dbin_mag[range_bin_out]) if (mag_sum > peak_dbin_mag[range_bin_out])
peak_dbin_mag[range_bin_out] = mag_sum; peak_dbin_mag[range_bin_out] = mag_sum;
end end
`ifdef GOLDEN_GENERATE
// ---- GOLDEN GENERATE: store output ----
if (gidx < GOLDEN_ENTRIES)
golden_doppler[gidx] = doppler_output;
`else
// ---- GOLDEN COMPARE: check against reference ----
if (gidx < GOLDEN_ENTRIES) begin
expected_val = golden_doppler[gidx];
actual_i = $signed(doppler_output[15:0]);
actual_q = $signed(doppler_output[31:16]);
expected_i = $signed(expected_val[15:0]);
expected_q = $signed(expected_val[31:16]);
err_i_signed = actual_i - expected_i;
err_q_signed = actual_q - expected_q;
abs_err_i = (err_i_signed < 0) ? -err_i_signed : err_i_signed;
abs_err_q = (err_q_signed < 0) ? -err_q_signed : err_q_signed;
golden_compare_count = golden_compare_count + 1;
if (abs_err_i > golden_max_err_i) golden_max_err_i = abs_err_i;
if (abs_err_q > golden_max_err_q) golden_max_err_q = abs_err_q;
if (abs_err_i <= GOLDEN_TOLERANCE && abs_err_q <= GOLDEN_TOLERANCE) begin
golden_match_count = golden_match_count + 1;
end else begin
golden_mismatch_count = golden_mismatch_count + 1;
if (golden_mismatch_count <= 20)
$display("[MISMATCH] idx=%0d rbin=%0d dbin=%0d actual=%08h expected=%08h err_i=%0d err_q=%0d",
gidx, range_bin_out, doppler_bin,
doppler_output, expected_val,
abs_err_i, abs_err_q);
end
end
`endif
end end
// Track frame completions via doppler_proc -- only after reset // Track frame completions via doppler_proc -- only after reset
@@ -556,13 +446,6 @@ initial begin
end end
end end
// ---- DUMP GOLDEN FILE (generate mode only) ----
`ifdef GOLDEN_GENERATE
$writememh("tb/golden/golden_doppler.mem", golden_doppler);
$display("[GOLDEN_GENERATE] Wrote tb/golden/golden_doppler.mem (%0d entries captured)",
doppler_output_count);
`endif
// ================================================================ // ================================================================
// RUN CHECKS // RUN CHECKS
// ================================================================ // ================================================================
@@ -649,33 +532,7 @@ initial begin
"S10: DDC produced substantial output (>100 valid samples)"); "S10: DDC produced substantial output (>100 valid samples)");
// ================================================================ // ================================================================
// GOLDEN COMPARISON REPORT // BOUNDS CHECKS
// ================================================================
`ifdef GOLDEN_GENERATE
$display("");
$display("Golden comparison: SKIPPED (GOLDEN_GENERATE mode)");
$display(" Wrote golden reference with %0d Doppler samples", doppler_output_count);
`else
$display("");
$display("------------------------------------------------------------");
$display("GOLDEN COMPARISON (tolerance=%0d LSB)", GOLDEN_TOLERANCE);
$display("------------------------------------------------------------");
$display("Golden comparison: %0d/%0d match (tolerance=%0d LSB)",
golden_match_count, golden_compare_count, GOLDEN_TOLERANCE);
$display(" Mismatches: %0d (I-ch max_err=%0d, Q-ch max_err=%0d)",
golden_mismatch_count, golden_max_err_i, golden_max_err_q);
// CHECK G1: All golden comparisons match
if (golden_compare_count > 0) begin
check(golden_mismatch_count == 0,
"G1: All Doppler outputs match golden reference within tolerance");
end else begin
check(0, "G1: All Doppler outputs match golden reference (NO COMPARISONS)");
end
`endif
// ================================================================
// BOUNDS CHECKS (active in both modes)
// ================================================================ // ================================================================
$display(""); $display("");
$display("------------------------------------------------------------"); $display("------------------------------------------------------------");
@@ -748,16 +605,8 @@ initial begin
// ================================================================ // ================================================================
$display(""); $display("");
$display("============================================================"); $display("============================================================");
$display("INTEGRATION TEST -- GOLDEN COMPARISON + BOUNDS"); $display("INTEGRATION TEST -- STRUCTURAL + BOUNDS");
$display("============================================================"); $display("============================================================");
`ifdef GOLDEN_GENERATE
$display("Mode: GOLDEN_GENERATE (reference dump, comparison skipped)");
`else
$display("Golden comparison: %0d/%0d match (tolerance=%0d LSB)",
golden_match_count, golden_compare_count, GOLDEN_TOLERANCE);
$display(" Mismatches: %0d (I-ch max_err=%0d, Q-ch max_err=%0d)",
golden_mismatch_count, golden_max_err_i, golden_max_err_q);
`endif
$display("Bounds checks:"); $display("Bounds checks:");
$display(" B1: DDC RMS energy in range [%0d, %0d]", $display(" B1: DDC RMS energy in range [%0d, %0d]",
(ddc_energy_acc > 0) ? 1 : 0, DDC_MAX_ENERGY); (ddc_energy_acc > 0) ? 1 : 0, DDC_MAX_ENERGY);
+8 -14
View File
@@ -382,13 +382,7 @@ end
// ============================================================================ // ============================================================================
// DUT INSTANTIATION // DUT INSTANTIATION
// ============================================================================ // ============================================================================
radar_system_top #( radar_system_top dut (
`ifdef USB_MODE_1
.USB_MODE(1) // FT2232H interface (production 50T board)
`else
.USB_MODE(0) // FT601 interface (200T dev board)
`endif
) dut (
.clk_100m(clk_100m), .clk_100m(clk_100m),
.clk_120m_dac(clk_120m_dac), .clk_120m_dac(clk_120m_dac),
.ft601_clk_in(ft601_clk_in), .ft601_clk_in(ft601_clk_in),
@@ -560,10 +554,10 @@ initial begin
do_reset; do_reset;
// CRITICAL: Configure stream control to range-only BEFORE any chirps // CRITICAL: Configure stream control to range-only BEFORE any chirps
// fire. The USB write FSM gates on pending flags: if doppler stream is // fire. The USB write FSM blocks on doppler_valid_ft if doppler stream
// enabled but no Doppler data arrives (needs 32 chirps/frame), the FSM // is enabled but no Doppler data arrives (needs 32 chirps/frame).
// stays in IDLE waiting for doppler_data_pending. With the write FSM // Without this, the write FSM deadlocks and the read FSM can never
// not in IDLE, the read FSM cannot activate (bus arbitration rule). // activate (it requires write FSM == IDLE).
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only
// Wait for stream_control CDC to propagate (2-stage sync in ft601_clk) // Wait for stream_control CDC to propagate (2-stage sync in ft601_clk)
// Must be long enough that stream_ctrl_sync_1 is updated before any // Must be long enough that stream_ctrl_sync_1 is updated before any
@@ -784,7 +778,7 @@ initial begin
// Restore defaults for subsequent tests // Restore defaults for subsequent tests
bfm_send_cmd(8'h01, 8'h00, 16'h0001); // mode = auto-scan bfm_send_cmd(8'h01, 8'h00, 16'h0001); // mode = auto-scan
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // keep range-only (TB lacks 32-chirp doppler data) bfm_send_cmd(8'h04, 8'h00, 16'h0001); // keep range-only (prevents write FSM deadlock)
bfm_send_cmd(8'h10, 8'h00, 16'd3000); // restore long chirp cycles bfm_send_cmd(8'h10, 8'h00, 16'd3000); // restore long chirp cycles
$display(""); $display("");
@@ -919,7 +913,7 @@ initial begin
// Need to re-send configuration since reset clears all registers // Need to re-send configuration since reset clears all registers
stm32_mixers_enable = 1; stm32_mixers_enable = 1;
ft601_txe = 0; ft601_txe = 0;
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only (TB lacks doppler data) bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = range only (prevent deadlock)
#500; // Wait for stream_control CDC #500; // Wait for stream_control CDC
bfm_send_cmd(8'h01, 8'h00, 16'h0001); // auto-scan bfm_send_cmd(8'h01, 8'h00, 16'h0001); // auto-scan
bfm_send_cmd(8'h10, 8'h00, 16'd100); // short timing bfm_send_cmd(8'h10, 8'h00, 16'd100); // short timing
@@ -953,7 +947,7 @@ initial begin
check(dut.host_stream_control == 3'b000, check(dut.host_stream_control == 3'b000,
"G10.2: All streams disabled (stream_control = 3'b000)"); "G10.2: All streams disabled (stream_control = 3'b000)");
// G10.3: Re-enable range only (TB uses range-only no doppler processing) // G10.3: Re-enable range only (keep range-only to prevent write FSM deadlock)
bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = 3'b001 bfm_send_cmd(8'h04, 8'h00, 16'h0001); // stream_control = 3'b001
check(dut.host_stream_control == 3'b001, check(dut.host_stream_control == 3'b001,
"G10.3: Range stream re-enabled (stream_control = 3'b001)"); "G10.3: Range stream re-enabled (stream_control = 3'b001)");
+207 -177
View File
@@ -6,11 +6,15 @@ module tb_usb_data_interface;
localparam CLK_PERIOD = 10.0; // 100 MHz main clock localparam CLK_PERIOD = 10.0; // 100 MHz main clock
localparam FT_CLK_PERIOD = 10.0; // 100 MHz FT601 clock (asynchronous) localparam FT_CLK_PERIOD = 10.0; // 100 MHz FT601 clock (asynchronous)
// State definitions (mirror the DUT 4-state packed-word FSM) // State definitions (mirror the DUT)
localparam [3:0] S_IDLE = 4'd0, localparam [2:0] S_IDLE = 3'd0,
S_SEND_DATA_WORD = 4'd1, S_SEND_HEADER = 3'd1,
S_SEND_STATUS = 4'd2, S_SEND_RANGE = 3'd2,
S_WAIT_ACK = 4'd3; S_SEND_DOPPLER = 3'd3,
S_SEND_DETECT = 3'd4,
S_SEND_FOOTER = 3'd5,
S_WAIT_ACK = 3'd6,
S_SEND_STATUS = 3'd7; // Gap 2: status readback
// Signals // Signals
reg clk; reg clk;
@@ -215,9 +219,9 @@ module tb_usb_data_interface;
end end
endtask endtask
// Helper: wait for DUT to reach a specific write FSM state // Helper: wait for DUT to reach a specific state
task wait_for_state; task wait_for_state;
input [3:0] target; input [2:0] target;
input integer max_cyc; input integer max_cyc;
integer cnt; integer cnt;
begin begin
@@ -276,7 +280,7 @@ module tb_usb_data_interface;
// Set data_pending flags directly via hierarchical access. // Set data_pending flags directly via hierarchical access.
// This is the standard TB technique for internal state setup // This is the standard TB technique for internal state setup
// bypasses the CDC path for immediate, reliable flag setting. // bypasses the CDC path for immediate, reliable flag setting.
// Call BEFORE assert_range_valid in tests that need doppler/cfar data. // Call BEFORE assert_range_valid in tests that need SEND_DOPPLER/DETECT.
task preload_pending_data; task preload_pending_data;
begin begin
@(posedge ft601_clk_in); @(posedge ft601_clk_in);
@@ -350,26 +354,24 @@ module tb_usb_data_interface;
end end
endtask endtask
// Drive a complete data packet through the new 3-word packed FSM. // Drive a complete packet through the FSM by sequentially providing
// Pre-loads pending flags, triggers range_valid, and waits for IDLE. // range, doppler (4x), and cfar valid pulses.
// With the new FSM, all data is pre-packed in IDLE then sent as 3 words.
task drive_full_packet; task drive_full_packet;
input [31:0] rng; input [31:0] rng;
input [15:0] dr; input [15:0] dr;
input [15:0] di; input [15:0] di;
input det; input det;
begin begin
// Set doppler/cfar captured values via CDC inputs // Pre-load pending flags so FSM enters doppler/cfar states
@(posedge clk);
doppler_real = dr;
doppler_imag = di;
cfar_detection = det;
@(posedge clk);
// Pre-load pending flags so FSM includes doppler/cfar in packet
preload_pending_data; preload_pending_data;
// Trigger the packet
assert_range_valid(rng); assert_range_valid(rng);
// Wait for complete packet cycle: IDLE SEND_DATA_WORD(×3) WAIT_ACK IDLE wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(dr, di);
pulse_doppler_once(dr, di);
pulse_doppler_once(dr, di);
pulse_doppler_once(dr, di);
wait_for_state(S_SEND_DETECT, 100);
pulse_cfar_once(det);
wait_for_state(S_IDLE, 100); wait_for_state(S_IDLE, 100);
end end
endtask endtask
@@ -412,138 +414,101 @@ module tb_usb_data_interface;
"ft601_siwu_n=1 after reset"); "ft601_siwu_n=1 after reset");
// //
// TEST GROUP 2: Data packet word packing // TEST GROUP 2: Range data packet
// //
// New FSM packs 11-byte data into 3 × 32-bit words: // Use backpressure to freeze the FSM at specific states
// Word 0: {HEADER, range[31:24], range[23:16], range[15:8]} // so we can reliably sample outputs.
// Word 1: {range[7:0], dop_re_hi, dop_re_lo, dop_im_hi}
// Word 2: {dop_im_lo, detection, FOOTER, 0x00} BE=1110
//
// The DUT uses range_data_ready (1-cycle delayed range_valid_ft)
// to trigger packing. Doppler/CFAR _cap registers must be
// pre-loaded via hierarchical access because no valid pulse is
// given in this test (we only want to verify packing, not CDC).
// //
$display("\n--- Test Group 2: Data Packet Word Packing ---"); $display("\n--- Test Group 2: Range Data Packet ---");
apply_reset; apply_reset;
ft601_txe = 1; // Stall so we can inspect packed words
// Set known doppler/cfar values on clk-domain inputs // Stall at SEND_HEADER so we can verify first range word later
@(posedge clk); ft601_txe = 1;
doppler_real = 16'hABCD;
doppler_imag = 16'hEF01;
cfar_detection = 1'b1;
@(posedge clk);
// Pre-load pending flags AND captured-data registers directly.
// No doppler/cfar valid pulses are given, so the CDC capture path
// never fires we must set the _cap registers via hierarchical
// access for the word-packing checks to be meaningful.
preload_pending_data; preload_pending_data;
@(posedge ft601_clk_in);
uut.doppler_real_cap = 16'hABCD;
uut.doppler_imag_cap = 16'hEF01;
uut.cfar_detection_cap = 1'b1;
@(posedge ft601_clk_in);
assert_range_valid(32'hDEAD_BEEF); assert_range_valid(32'hDEAD_BEEF);
wait_for_state(S_SEND_HEADER, 50);
// FSM should be in SEND_DATA_WORD, stalled on ft601_txe=1
wait_for_state(S_SEND_DATA_WORD, 50);
repeat (2) @(posedge ft601_clk_in); #1; repeat (2) @(posedge ft601_clk_in); #1;
check(uut.current_state === S_SEND_HEADER,
"Stalled in SEND_HEADER (backpressure)");
check(uut.current_state === S_SEND_DATA_WORD, // Release: FSM drives header then moves to SEND_RANGE_DATA
"Stalled in SEND_DATA_WORD (backpressure)");
// Verify pre-packed words
// range_profile = 0xDEAD_BEEF range[31:24]=0xDE, [23:16]=0xAD, [15:8]=0xBE, [7:0]=0xEF
// Word 0: {0xAA, 0xDE, 0xAD, 0xBE}
check(uut.data_pkt_word0 === {8'hAA, 8'hDE, 8'hAD, 8'hBE},
"Word 0: {HEADER=AA, range[31:8]}");
// Word 1: {0xEF, 0xAB, 0xCD, 0xEF}
check(uut.data_pkt_word1 === {8'hEF, 8'hAB, 8'hCD, 8'hEF},
"Word 1: {range[7:0], dop_re, dop_im_hi}");
// Word 2: {0x01, detection_byte, 0x55, 0x00}
// detection_byte bit 7 = frame_start (sample_counter==0 1), bit 0 = cfar=1
// so detection_byte = 8'b1000_0001 = 8'h81
check(uut.data_pkt_word2 === {8'h01, 8'h81, 8'h55, 8'h00},
"Word 2: {dop_im_lo, det=81, FOOTER=55, pad=00}");
check(uut.data_pkt_be2 === 4'b1110,
"Word 2 BE=1110 (3 valid bytes + 1 pad)");
// Release backpressure and verify word 0 appears on bus.
// On the first posedge with !ft601_txe the FSM drives word 0 and
// advances data_word_idx 01 via NBA. After #1 the NBA has
// resolved, so we see idx=1 and ft601_data_out=word0.
ft601_txe = 0; ft601_txe = 0;
@(posedge ft601_clk_in); #1; @(posedge ft601_clk_in); #1;
// Now the FSM registered the header output and will transition
// At the NEXT posedge the state becomes SEND_RANGE_DATA
@(posedge ft601_clk_in); #1;
check(uut.current_state === S_SEND_RANGE,
"Entered SEND_RANGE_DATA after header");
// The first range word should be on the data bus (byte_counter=0 just
// drove range_profile_cap, byte_counter incremented to 1)
check(uut.ft601_data_out === 32'hDEAD_BEEF || uut.byte_counter <= 8'd1,
"Range data word 0 driven (range_profile_cap)");
check(uut.ft601_data_out === {8'hAA, 8'hDE, 8'hAD, 8'hBE},
"Word 0 driven on data bus after backpressure release");
check(ft601_wr_n === 1'b0, check(ft601_wr_n === 1'b0,
"Write strobe active during SEND_DATA_WORD"); "Write strobe active during range data");
check(ft601_be === 4'b1111, check(ft601_be === 4'b1111,
"Byte enable=1111 for word 0"); "Byte enable=1111 for range data");
check(uut.ft601_data_oe === 1'b1,
"Data bus output enabled during SEND_DATA_WORD");
// Next posedge: FSM drives word 1, advances idx 12. // Wait for all 4 range words to complete
// After NBA: idx=2, ft601_data_out=word1. wait_for_state(S_SEND_DOPPLER, 50);
@(posedge ft601_clk_in); #1; #1;
check(uut.data_word_idx === 2'd2, check(uut.current_state === S_SEND_DOPPLER,
"data_word_idx advanced past word 1 (now 2)"); "Advanced to SEND_DOPPLER_DATA after 4 range words");
check(uut.ft601_data_out === {8'hEF, 8'hAB, 8'hCD, 8'hEF},
"Word 1 driven on data bus");
check(ft601_be === 4'b1111,
"Byte enable=1111 for word 1");
// Next posedge: FSM drives word 2, idx resets 20,
// and current_state transitions to WAIT_ACK.
@(posedge ft601_clk_in); #1;
check(uut.current_state === S_WAIT_ACK,
"Transitioned to WAIT_ACK after 3 data words");
check(uut.ft601_data_out === {8'h01, 8'h81, 8'h55, 8'h00},
"Word 2 driven on data bus");
check(ft601_be === 4'b1110,
"Byte enable=1110 for word 2 (last byte is pad)");
// Then back to IDLE
@(posedge ft601_clk_in); #1;
check(uut.current_state === S_IDLE,
"Returned to IDLE after WAIT_ACK");
// //
// TEST GROUP 3: Header and footer verification // TEST GROUP 3: Header verification (stall to observe)
// //
$display("\n--- Test Group 3: Header and Footer Verification ---"); $display("\n--- Test Group 3: Header Verification ---");
apply_reset; apply_reset;
ft601_txe = 1; // Stall to inspect ft601_txe = 1; // Stall at SEND_HEADER
@(posedge clk); @(posedge clk);
doppler_real = 16'h0000; range_profile = 32'hCAFE_BABE;
doppler_imag = 16'h0000; range_valid = 1;
cfar_detection = 1'b0; repeat (4) @(posedge ft601_clk_in);
@(posedge clk); @(posedge clk);
preload_pending_data; range_valid = 0;
assert_range_valid(32'hCAFE_BABE); repeat (3) @(posedge ft601_clk_in);
wait_for_state(S_SEND_DATA_WORD, 50); wait_for_state(S_SEND_HEADER, 50);
repeat (2) @(posedge ft601_clk_in); #1; repeat (2) @(posedge ft601_clk_in); #1;
// Header is in byte 3 (MSB) of word 0 check(uut.current_state === S_SEND_HEADER,
check(uut.data_pkt_word0[31:24] === 8'hAA, "Stalled in SEND_HEADER with backpressure");
"Header byte 0xAA in word 0 MSB");
// Footer is in byte 1 (bits [15:8]) of word 2 // Release backpressure - header will be latched at next posedge
check(uut.data_pkt_word2[15:8] === 8'h55, ft601_txe = 0;
"Footer byte 0x55 in word 2"); @(posedge ft601_clk_in); #1;
check(uut.ft601_data_out[7:0] === 8'hAA,
"Header byte 0xAA on data bus");
check(ft601_be === 4'b0001,
"Byte enable=0001 for header (lower byte only)");
check(ft601_wr_n === 1'b0,
"Write strobe active during header");
check(uut.ft601_data_oe === 1'b1,
"Data bus output enabled during header");
// //
// TEST GROUP 4: Doppler data capture verification // TEST GROUP 4: Doppler data verification
// //
$display("\n--- Test Group 4: Doppler Data Capture ---"); $display("\n--- Test Group 4: Doppler Data Verification ---");
apply_reset; apply_reset;
ft601_txe = 0; ft601_txe = 0;
// Preload only doppler pending (not cfar) so the FSM sends
// doppler data. After doppler, SEND_DETECT sees cfar_data_pending=0
// and skips to SEND_FOOTER, then WAIT_ACK, then IDLE.
preload_doppler_pending;
assert_range_valid(32'h0000_0001);
wait_for_state(S_SEND_DOPPLER, 100);
#1;
check(uut.current_state === S_SEND_DOPPLER,
"Reached SEND_DOPPLER_DATA");
// Provide doppler data via valid pulse (updates captured values) // Provide doppler data via valid pulse (updates captured values)
@(posedge clk); @(posedge clk);
doppler_real = 16'hAAAA; doppler_real = 16'hAAAA;
@@ -559,70 +524,110 @@ module tb_usb_data_interface;
check(uut.doppler_imag_cap === 16'h5555, check(uut.doppler_imag_cap === 16'h5555,
"doppler_imag captured correctly"); "doppler_imag captured correctly");
// Drive a packet with pending doppler + cfar (both needed for gating // The FSM has doppler_data_pending set and sends 4 bytes, then
// since all streams are enabled after reset/apply_reset). // transitions past SEND_DETECT (cfar_data_pending=0) to IDLE.
preload_pending_data;
assert_range_valid(32'h0000_0001);
wait_for_state(S_IDLE, 100); wait_for_state(S_IDLE, 100);
#1; #1;
check(uut.current_state === S_IDLE, check(uut.current_state === S_IDLE,
"Packet completed with doppler data"); "Doppler done, packet completed");
check(uut.doppler_data_pending === 1'b0,
"doppler_data_pending cleared after packet");
// //
// TEST GROUP 5: CFAR detection data // TEST GROUP 5: CFAR detection data
// //
$display("\n--- Test Group 5: CFAR Detection Data ---"); $display("\n--- Test Group 5: CFAR Detection Data ---");
// Start a new packet with both doppler and cfar pending to verify
// cfar data is properly sent in SEND_DETECTION_DATA.
apply_reset; apply_reset;
ft601_txe = 0; ft601_txe = 0;
preload_pending_data; preload_pending_data;
assert_range_valid(32'h0000_0002); assert_range_valid(32'h0000_0002);
// FSM races through: HEADER -> RANGE -> DOPPLER -> DETECT -> FOOTER -> IDLE
// All pending flags consumed proves SEND_DETECT was entered.
wait_for_state(S_IDLE, 200); wait_for_state(S_IDLE, 200);
#1; #1;
check(uut.cfar_data_pending === 1'b0, check(uut.cfar_data_pending === 1'b0,
"cfar_data_pending cleared after packet"); "Starting in SEND_DETECTION_DATA");
// Verify the full packet completed with cfar data consumed
check(uut.current_state === S_IDLE && check(uut.current_state === S_IDLE &&
uut.doppler_data_pending === 1'b0 && uut.doppler_data_pending === 1'b0 &&
uut.cfar_data_pending === 1'b0, uut.cfar_data_pending === 1'b0,
"CFAR detection sent, all pending flags cleared"); "CFAR detection sent, FSM advanced past SEND_DETECTION_DATA");
// //
// TEST GROUP 6: Footer retained after packet // TEST GROUP 6: Footer check
//
// Strategy: drive packet with ft601_txe=0 all the way through.
// The SEND_FOOTER state is only active for 1 cycle, but we can
// poll the state machine at each ft601_clk_in edge to observe
// it. We use a monitor-style approach: run the packet and
// capture what ft601_data_out contains when we see SEND_FOOTER.
// //
$display("\n--- Test Group 6: Footer Retention ---"); $display("\n--- Test Group 6: Footer Check ---");
apply_reset; apply_reset;
ft601_txe = 0; ft601_txe = 0;
@(posedge clk); // Drive packet through range data
cfar_detection = 1'b1;
@(posedge clk);
preload_pending_data; preload_pending_data;
assert_range_valid(32'hFACE_FEED); assert_range_valid(32'hFACE_FEED);
wait_for_state(S_SEND_DOPPLER, 100);
// Feed doppler data (need 4 pulses)
pulse_doppler_once(16'h1111, 16'h2222);
pulse_doppler_once(16'h1111, 16'h2222);
pulse_doppler_once(16'h1111, 16'h2222);
pulse_doppler_once(16'h1111, 16'h2222);
wait_for_state(S_SEND_DETECT, 100);
// Feed cfar data, but keep ft601_txe=0 so it flows through
pulse_cfar_once(1'b1);
// Now the FSM should pass through SEND_FOOTER quickly.
// Use wait_for_state to reach SEND_FOOTER, or it may already
// be at WAIT_ACK/IDLE. Let's catch WAIT_ACK or IDLE.
// The footer values are latched into registers, so we can
// verify them even after the state transitions.
// Key verification: the FOOTER constant (0x55) must have been
// driven. We check this by looking at the constant definition.
// Since we can't easily freeze the FSM at SEND_FOOTER without
// also stalling SEND_DETECTION_DATA (both check ft601_txe),
// we verify the footer indirectly:
// 1. The packet completed (reached IDLE/WAIT_ACK)
// 2. ft601_data_out last held 0x55 during SEND_FOOTER
wait_for_state(S_IDLE, 100); wait_for_state(S_IDLE, 100);
#1; #1;
// If we reached IDLE, the full sequence ran including footer
check(uut.current_state === S_IDLE, check(uut.current_state === S_IDLE,
"Full packet incl. footer completed, back in IDLE"); "Full packet incl. footer completed, back in IDLE");
// The last word driven was word 2 which contains footer 0x55. // The registered ft601_data_out should still hold 0x55 from
// WAIT_ACK and IDLE don't overwrite ft601_data_out, so it retains // SEND_FOOTER (WAIT_ACK and IDLE don't overwrite ft601_data_out).
// the last driven value. // Actually, looking at the DUT: WAIT_ACK only sets wr_n=1 and
check(uut.ft601_data_out[15:8] === 8'h55, // data_oe=0, it doesn't change ft601_data_out. So it retains 0x55.
"ft601_data_out retains footer 0x55 in word 2 position"); check(uut.ft601_data_out[7:0] === 8'h55,
"ft601_data_out retains footer 0x55 after packet");
// Verify WAIT_ACK IDLE transition // Verify WAIT_ACK behavior by doing another packet and catching it
apply_reset; apply_reset;
ft601_txe = 0; ft601_txe = 0;
preload_pending_data; preload_pending_data;
assert_range_valid(32'h1234_5678); assert_range_valid(32'h1234_5678);
wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(16'hABCD, 16'hEF01);
pulse_doppler_once(16'hABCD, 16'hEF01);
pulse_doppler_once(16'hABCD, 16'hEF01);
pulse_doppler_once(16'hABCD, 16'hEF01);
wait_for_state(S_SEND_DETECT, 100);
pulse_cfar_once(1'b0);
// WAIT_ACK lasts exactly 1 ft601_clk_in cycle then goes IDLE.
// Poll for IDLE (which means WAIT_ACK already happened).
wait_for_state(S_IDLE, 100); wait_for_state(S_IDLE, 100);
#1; #1;
check(uut.current_state === S_IDLE, check(uut.current_state === S_IDLE,
"Returned to IDLE after WAIT_ACK"); "Returned to IDLE after WAIT_ACK");
check(ft601_wr_n === 1'b1, check(ft601_wr_n === 1'b1,
"ft601_wr_n deasserted in IDLE"); "ft601_wr_n deasserted in IDLE (was deasserted in WAIT_ACK)");
check(uut.ft601_data_oe === 1'b0, check(uut.ft601_data_oe === 1'b0,
"Data bus released in IDLE"); "Data bus released in IDLE (was released in WAIT_ACK)");
// //
// TEST GROUP 7: Full packet sequence (end-to-end) // TEST GROUP 7: Full packet sequence (end-to-end)
@@ -641,24 +646,23 @@ module tb_usb_data_interface;
// //
$display("\n--- Test Group 8: FIFO Backpressure ---"); $display("\n--- Test Group 8: FIFO Backpressure ---");
apply_reset; apply_reset;
ft601_txe = 1; // FIFO full stall ft601_txe = 1;
preload_pending_data;
assert_range_valid(32'hBBBB_CCCC); assert_range_valid(32'hBBBB_CCCC);
wait_for_state(S_SEND_DATA_WORD, 50); wait_for_state(S_SEND_HEADER, 50);
repeat (10) @(posedge ft601_clk_in); #1; repeat (10) @(posedge ft601_clk_in); #1;
check(uut.current_state === S_SEND_DATA_WORD, check(uut.current_state === S_SEND_HEADER,
"Stalled in SEND_DATA_WORD when ft601_txe=1 (FIFO full)"); "Stalled in SEND_HEADER when ft601_txe=1 (FIFO full)");
check(ft601_wr_n === 1'b1, check(ft601_wr_n === 1'b1,
"ft601_wr_n not asserted during backpressure stall"); "ft601_wr_n not asserted during backpressure stall");
ft601_txe = 0; ft601_txe = 0;
repeat (6) @(posedge ft601_clk_in); #1; repeat (2) @(posedge ft601_clk_in); #1;
check(uut.current_state === S_IDLE || uut.current_state === S_WAIT_ACK, check(uut.current_state !== S_SEND_HEADER,
"Resumed and completed after backpressure released"); "Resumed from SEND_HEADER after backpressure released");
// //
// TEST GROUP 9: Clock divider // TEST GROUP 9: Clock divider
@@ -701,6 +705,13 @@ module tb_usb_data_interface;
ft601_txe = 0; ft601_txe = 0;
preload_pending_data; preload_pending_data;
assert_range_valid(32'h1111_2222); assert_range_valid(32'h1111_2222);
wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(16'h3333, 16'h4444);
pulse_doppler_once(16'h3333, 16'h4444);
pulse_doppler_once(16'h3333, 16'h4444);
pulse_doppler_once(16'h3333, 16'h4444);
wait_for_state(S_SEND_DETECT, 100);
pulse_cfar_once(1'b0);
wait_for_state(S_WAIT_ACK, 50); wait_for_state(S_WAIT_ACK, 50);
#1; #1;
@@ -794,7 +805,7 @@ module tb_usb_data_interface;
// Start a write packet // Start a write packet
preload_pending_data; preload_pending_data;
assert_range_valid(32'hFACE_FEED); assert_range_valid(32'hFACE_FEED);
wait_for_state(S_SEND_DATA_WORD, 50); wait_for_state(S_SEND_HEADER, 50);
@(posedge ft601_clk_in); #1; @(posedge ft601_clk_in); #1;
// While write FSM is active, assert RXF=0 (host has data) // While write FSM is active, assert RXF=0 (host has data)
@@ -807,6 +818,13 @@ module tb_usb_data_interface;
// Deassert RXF, complete the write packet // Deassert RXF, complete the write packet
ft601_rxf = 1; ft601_rxf = 1;
wait_for_state(S_SEND_DOPPLER, 100);
pulse_doppler_once(16'hAAAA, 16'hBBBB);
pulse_doppler_once(16'hAAAA, 16'hBBBB);
pulse_doppler_once(16'hAAAA, 16'hBBBB);
pulse_doppler_once(16'hAAAA, 16'hBBBB);
wait_for_state(S_SEND_DETECT, 100);
pulse_cfar_once(1'b1);
wait_for_state(S_IDLE, 100); wait_for_state(S_IDLE, 100);
@(posedge ft601_clk_in); #1; @(posedge ft601_clk_in); #1;
@@ -823,42 +841,32 @@ module tb_usb_data_interface;
// //
// TEST GROUP 15: Stream Control Gating (Gap 2) // TEST GROUP 15: Stream Control Gating (Gap 2)
// Verify that disabling individual streams causes the write // Verify that disabling individual streams causes the write
// FSM to zero those fields in the packed words. // FSM to skip those data phases.
// //
$display("\n--- Test Group 15: Stream Control Gating (Gap 2) ---"); $display("\n--- Test Group 15: Stream Control Gating (Gap 2) ---");
// 15a: Disable doppler stream (stream_control = 3'b101 = range + cfar only) // 15a: Disable doppler stream (stream_control = 3'b101 = range + cfar only)
apply_reset; apply_reset;
ft601_txe = 1; // Stall to inspect packed words ft601_txe = 0;
stream_control = 3'b101; // range + cfar, no doppler stream_control = 3'b101; // range + cfar, no doppler
// Wait for CDC propagation (2-stage sync) // Wait for CDC propagation (2-stage sync)
repeat (6) @(posedge ft601_clk_in); repeat (6) @(posedge ft601_clk_in);
@(posedge clk); // Preload cfar pending so the FSM enters the SEND_DETECT data path
doppler_real = 16'hAAAA; // (without it, SEND_DETECT skips immediately on !cfar_data_pending).
doppler_imag = 16'hBBBB;
cfar_detection = 1'b1;
@(posedge clk);
preload_cfar_pending; preload_cfar_pending;
// Drive range valid triggers write FSM
assert_range_valid(32'hAA11_BB22); assert_range_valid(32'hAA11_BB22);
// FSM: IDLE -> SEND_HEADER -> SEND_RANGE (doppler disabled) -> SEND_DETECT -> FOOTER
wait_for_state(S_SEND_DATA_WORD, 200); // The FSM races through SEND_DETECT in 1 cycle (cfar_data_pending is consumed).
repeat (2) @(posedge ft601_clk_in); #1; // Verify the packet completed correctly (doppler was skipped).
wait_for_state(S_IDLE, 200);
// With doppler disabled, doppler fields in words 1 and 2 should be zero
// Word 1: {range[7:0], 0x00, 0x00, 0x00} (doppler zeroed)
check(uut.data_pkt_word1[23:0] === 24'h000000,
"Stream gate: doppler bytes zeroed in word 1 when disabled");
// Word 2 byte 3 (dop_im_lo) should also be zero
check(uut.data_pkt_word2[31:24] === 8'h00,
"Stream gate: dop_im_lo zeroed in word 2 when disabled");
// Let it complete
ft601_txe = 0;
wait_for_state(S_IDLE, 100);
#1; #1;
// Reaching IDLE proves: HEADER -> RANGE -> (skip DOPPLER) -> DETECT -> FOOTER -> ACK -> IDLE.
// cfar_data_pending consumed confirms SEND_DETECT was entered.
check(uut.current_state === S_IDLE && uut.cfar_data_pending === 1'b0,
"Stream gate: reached SEND_DETECT (range sent, doppler skipped)");
check(uut.current_state === S_IDLE, check(uut.current_state === S_IDLE,
"Stream gate: packet completed without doppler"); "Stream gate: packet completed without doppler");
@@ -943,6 +951,28 @@ module tb_usb_data_interface;
"Status readback: returned to IDLE after 8-word response"); "Status readback: returned to IDLE after 8-word response");
// Verify the status snapshot was captured correctly. // Verify the status snapshot was captured correctly.
// status_words[0] = {0xFF, 3'b000, mode[1:0], 5'b0, stream_ctrl[2:0], cfar_threshold[15:0]}
// = {8'hFF, 3'b000, 2'b01, 5'b00000, 3'b101, 16'hABCD}
// = 0xFF_09_05_ABCD... let's compute:
// Byte 3: 0xFF = 8'hFF
// Byte 2: {3'b000, 2'b01} = 5'b00001 + 3 high bits of next field...
// Actually the packing is: {8'hFF, 3'b000, status_radar_mode[1:0], 5'b00000, status_stream_ctrl[2:0], status_cfar_threshold[15:0]}
// = {8'hFF, 3'b000, 2'b01, 5'b00000, 3'b101, 16'hABCD}
// = 8'hFF, 5'b00001, 8'b00000101, 16'hABCD
// = FF_09_05_ABCD? Let me compute carefully:
// Bits [31:24] = 8'hFF = 0xFF
// Bits [23:21] = 3'b000
// Bits [20:19] = 2'b01 (mode)
// Bits [18:14] = 5'b00000
// Bits [13:11] = 3'b101 (stream_ctrl)
// Bits [10:0] = ... wait, cfar_threshold is 16 bits [15:0]
// Total bits = 8+3+2+5+3+16 = 37 bits won't fit in 32!
// Re-reading the RTL: the packing at line 241 is:
// {8'hFF, 3'b000, status_radar_mode, 5'b00000, status_stream_ctrl, status_cfar_threshold}
// = 8 + 3 + 2 + 5 + 3 + 16 = 37 bits
// This would be truncated to 32 bits. Let me re-read the actual RTL to check.
// For now, just verify status_words[1] (word index 1 in the packet = idx 2 in FSM)
// status_words[1] = {status_long_chirp, status_long_listen} = {16'd3000, 16'd13700}
check(uut.status_words[1] === {16'd3000, 16'd13700}, check(uut.status_words[1] === {16'd3000, 16'd13700},
"Status readback: word 1 = {long_chirp, long_listen}"); "Status readback: word 1 = {long_chirp, long_listen}");
check(uut.status_words[2] === {16'd17540, 16'd50}, check(uut.status_words[2] === {16'd17540, 16'd50},
+129 -186
View File
@@ -1,17 +1,3 @@
/**
* usb_data_interface.v
*
* FT601 USB 3.0 SuperSpeed FIFO Interface (32-bit bus, 100 MHz ft601_clk).
* Used on the 200T premium dev board. Production 50T board uses
* usb_data_interface_ft2232h.v (FT2232H, 8-bit, 60 MHz) instead.
*
* USB disconnect recovery:
* A clock-activity watchdog in the clk domain detects when ft601_clk_in
* stops (USB cable unplugged). After ~0.65 ms of silence (65536 system
* clocks) it asserts ft601_clk_lost, which is OR'd into the FT-domain
* reset so FSMs and FIFOs return to a clean state. When ft601_clk_in
* resumes, a 2-stage reset synchronizer deasserts the reset cleanly.
*/
module usb_data_interface ( module usb_data_interface (
input wire clk, // Main clock (100MHz recommended) input wire clk, // Main clock (100MHz recommended)
input wire reset_n, input wire reset_n,
@@ -29,18 +15,13 @@ module usb_data_interface (
// FT601 Interface (Slave FIFO mode) // FT601 Interface (Slave FIFO mode)
// Data bus // Data bus
inout wire [31:0] ft601_data, // 32-bit bidirectional data bus inout wire [31:0] ft601_data, // 32-bit bidirectional data bus
output reg [3:0] ft601_be, // Byte enable (active-HIGH per DS_FT600Q-FT601Q Table 3.2) output reg [3:0] ft601_be, // Byte enable (4 lanes for 32-bit mode)
// Control signals // Control signals
// VESTIGIAL OUTPUTS — kept for 200T board port compatibility. output reg ft601_txe_n, // Transmit enable (active low)
// On the 200T, these are constrained to physical pins G21 (TXE) and output reg ft601_rxf_n, // Receive enable (active low)
// G22 (RXF) in xc7a200t_fbg484.xdc. Removing them from the RTL would input wire ft601_txe, // TXE: Transmit FIFO Not Full (high = space available to write)
// break the 200T build. They are reset to 1 and never driven; the input wire ft601_rxf, // RXF: Receive FIFO Not Empty (high = data available to read)
// actual FT601 flow-control inputs are ft601_txe and ft601_rxf below.
output reg ft601_txe_n, // VESTIGIAL: unused output, always 1
output reg ft601_rxf_n, // VESTIGIAL: unused output, always 1
input wire ft601_txe, // TXE: Transmit FIFO Not Full (active-low: 0 = space available)
input wire ft601_rxf, // RXF: Receive FIFO Not Empty (active-low: 0 = data available)
output reg ft601_wr_n, // Write strobe (active low) output reg ft601_wr_n, // Write strobe (active low)
output reg ft601_rd_n, // Read strobe (active low) output reg ft601_rd_n, // Read strobe (active low)
output reg ft601_oe_n, // Output enable (active low) output reg ft601_oe_n, // Output enable (active low)
@@ -116,26 +97,21 @@ localparam FT601_BURST_SIZE = 512; // Max burst size in bytes
// ============================================================================ // ============================================================================
// WRITE FSM State definitions (Verilog-2001 compatible) // WRITE FSM State definitions (Verilog-2001 compatible)
// ============================================================================ // ============================================================================
// Rewritten: data packet is now 3 x 32-bit writes (11 payload bytes + 1 pad). localparam [2:0] IDLE = 3'd0,
// Word 0: {HEADER, range[31:24], range[23:16], range[15:8]} BE=1111 SEND_HEADER = 3'd1,
// Word 1: {range[7:0], doppler_real[15:8], doppler_real[7:0], doppler_imag[15:8]} BE=1111 SEND_RANGE_DATA = 3'd2,
// Word 2: {doppler_imag[7:0], detection, FOOTER, 8'h00} BE=1110 SEND_DOPPLER_DATA = 3'd3,
localparam [3:0] IDLE = 4'd0, SEND_DETECTION_DATA = 3'd4,
SEND_DATA_WORD = 4'd1, SEND_FOOTER = 3'd5,
SEND_STATUS = 4'd2, WAIT_ACK = 3'd6,
WAIT_ACK = 4'd3; SEND_STATUS = 3'd7; // Gap 2: status readback
reg [3:0] current_state; reg [2:0] current_state;
reg [1:0] data_word_idx; // 0..2 for 3-word data packet reg [7:0] byte_counter;
reg [31:0] data_buffer;
reg [31:0] ft601_data_out; reg [31:0] ft601_data_out;
reg ft601_data_oe; // Output enable for bidirectional data bus reg ft601_data_oe; // Output enable for bidirectional data bus
// Pre-packed data words (registered snapshot of CDC'd data)
reg [31:0] data_pkt_word0;
reg [31:0] data_pkt_word1;
reg [31:0] data_pkt_word2;
reg [3:0] data_pkt_be2; // BE for last word (BE=1110 since byte 3 is pad)
// ============================================================================ // ============================================================================
// READ FSM State definitions (Gap 4: USB Read Path) // READ FSM State definitions (Gap 4: USB Read Path)
// ============================================================================ // ============================================================================
@@ -208,67 +184,6 @@ always @(posedge clk or negedge reset_n) begin
end end
end end
// ============================================================================
// CLOCK-ACTIVITY WATCHDOG (clk domain)
// ============================================================================
// Detects when ft601_clk_in stops (USB cable unplugged). A toggle register
// in the ft601_clk domain flips every edge. The clk domain synchronizes it
// and checks for transitions. If no transition is seen for 2^16 = 65536
// clk cycles (~0.65 ms at 100 MHz), ft601_clk_lost asserts.
// Toggle register: flips every ft601_clk edge (ft601_clk domain)
reg ft601_heartbeat;
always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (!ft601_reset_n)
ft601_heartbeat <= 1'b0;
else
ft601_heartbeat <= ~ft601_heartbeat;
end
// Synchronize heartbeat into clk domain (2-stage)
(* ASYNC_REG = "TRUE" *) reg [1:0] ft601_hb_sync;
reg ft601_hb_prev;
reg [15:0] ft601_clk_timeout;
reg ft601_clk_lost;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
ft601_hb_sync <= 2'b00;
ft601_hb_prev <= 1'b0;
ft601_clk_timeout <= 16'd0;
ft601_clk_lost <= 1'b0;
end else begin
ft601_hb_sync <= {ft601_hb_sync[0], ft601_heartbeat};
ft601_hb_prev <= ft601_hb_sync[1];
if (ft601_hb_sync[1] != ft601_hb_prev) begin
// ft601_clk is alive reset counter, clear lost flag
ft601_clk_timeout <= 16'd0;
ft601_clk_lost <= 1'b0;
end else if (!ft601_clk_lost) begin
if (ft601_clk_timeout == 16'hFFFF)
ft601_clk_lost <= 1'b1;
else
ft601_clk_timeout <= ft601_clk_timeout + 16'd1;
end
end
end
// Effective FT601-domain reset: asserted by global reset OR clock loss.
// Deassertion synchronized to ft601_clk via 2-stage sync to avoid
// metastability on the recovery edge.
(* ASYNC_REG = "TRUE" *) reg [1:0] ft601_reset_sync;
wire ft601_reset_raw_n = ft601_reset_n & ~ft601_clk_lost;
always @(posedge ft601_clk_in or negedge ft601_reset_raw_n) begin
if (!ft601_reset_raw_n)
ft601_reset_sync <= 2'b00;
else
ft601_reset_sync <= {ft601_reset_sync[0], 1'b1};
end
wire ft601_effective_reset_n = ft601_reset_sync[1];
// FT601-domain captured data (sampled from holding regs on sync'd edge) // FT601-domain captured data (sampled from holding regs on sync'd edge)
reg [31:0] range_profile_cap; reg [31:0] range_profile_cap;
reg [15:0] doppler_real_cap; reg [15:0] doppler_real_cap;
@@ -282,18 +197,6 @@ reg cfar_detection_cap;
reg doppler_data_pending; reg doppler_data_pending;
reg cfar_data_pending; reg cfar_data_pending;
// 1-cycle delayed range trigger. range_valid_ft fires on the same clock
// edge that range_profile_cap is captured (non-blocking). If the FSM
// reads range_profile_cap on that same edge it sees the STALE value.
// Delaying the trigger by one cycle guarantees the capture register has
// settled before the FSM packs the data words.
reg range_data_ready;
// Frame sync: sample counter (ft601_clk domain, wraps at NUM_CELLS)
// Bit 7 of detection byte is set when sample_counter == 0 (frame start).
localparam [11:0] NUM_CELLS = 12'd2048; // 64 range x 32 doppler
reg [11:0] sample_counter;
// Gap 2: CDC for stream_control (clk_100m -> ft601_clk_in) // Gap 2: CDC for stream_control (clk_100m -> ft601_clk_in)
// stream_control changes infrequently (only on host USB command), so // stream_control changes infrequently (only on host USB command), so
// per-bit 2-stage synchronizers are sufficient. No Gray coding needed // per-bit 2-stage synchronizers are sufficient. No Gray coding needed
@@ -325,8 +228,8 @@ wire range_valid_ft;
wire doppler_valid_ft; wire doppler_valid_ft;
wire cfar_valid_ft; wire cfar_valid_ft;
always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (!ft601_effective_reset_n) begin if (!ft601_reset_n) begin
range_valid_sync <= 2'b00; range_valid_sync <= 2'b00;
doppler_valid_sync <= 2'b00; doppler_valid_sync <= 2'b00;
cfar_valid_sync <= 2'b00; cfar_valid_sync <= 2'b00;
@@ -337,7 +240,6 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
doppler_real_cap <= 16'd0; doppler_real_cap <= 16'd0;
doppler_imag_cap <= 16'd0; doppler_imag_cap <= 16'd0;
cfar_detection_cap <= 1'b0; cfar_detection_cap <= 1'b0;
range_data_ready <= 1'b0;
// Fix #5: Default to range-only on reset (prevents write FSM deadlock) // Fix #5: Default to range-only on reset (prevents write FSM deadlock)
stream_ctrl_sync_0 <= 3'b001; stream_ctrl_sync_0 <= 3'b001;
stream_ctrl_sync_1 <= 3'b001; stream_ctrl_sync_1 <= 3'b001;
@@ -374,7 +276,7 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
// Word 4: AGC metrics + range_mode // Word 4: AGC metrics + range_mode
status_words[4] <= {status_agc_current_gain, // [31:28] status_words[4] <= {status_agc_current_gain, // [31:28]
status_agc_peak_magnitude, // [27:20] status_agc_peak_magnitude, // [27:20]
status_agc_saturation_count, // [19:12] 8-bit saturation count status_agc_saturation_count, // [19:12]
status_agc_enable, // [11] status_agc_enable, // [11]
9'd0, // [10:2] reserved 9'd0, // [10:2] reserved
status_range_mode}; // [1:0] status_range_mode}; // [1:0]
@@ -400,10 +302,6 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
if (cfar_valid_sync[1] && !cfar_valid_sync_d) begin if (cfar_valid_sync[1] && !cfar_valid_sync_d) begin
cfar_detection_cap <= cfar_detection_hold; cfar_detection_cap <= cfar_detection_hold;
end end
// 1-cycle delayed trigger: ensures range_profile_cap has settled
// before the FSM reads it for word packing.
range_data_ready <= range_valid_ft;
end end
end end
@@ -416,11 +314,11 @@ assign cfar_valid_ft = cfar_valid_sync[1] && !cfar_valid_sync_d;
// FT601 data bus direction control // FT601 data bus direction control
assign ft601_data = ft601_data_oe ? ft601_data_out : 32'hzzzz_zzzz; assign ft601_data = ft601_data_oe ? ft601_data_out : 32'hzzzz_zzzz;
always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (!ft601_effective_reset_n) begin if (!ft601_reset_n) begin
current_state <= IDLE; current_state <= IDLE;
read_state <= RD_IDLE; read_state <= RD_IDLE;
data_word_idx <= 2'd0; byte_counter <= 0;
ft601_data_out <= 0; ft601_data_out <= 0;
ft601_data_oe <= 0; ft601_data_oe <= 0;
ft601_be <= 4'b1111; // All bytes enabled for 32-bit mode ft601_be <= 4'b1111; // All bytes enabled for 32-bit mode
@@ -438,11 +336,6 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
cmd_value <= 16'd0; cmd_value <= 16'd0;
doppler_data_pending <= 1'b0; doppler_data_pending <= 1'b0;
cfar_data_pending <= 1'b0; cfar_data_pending <= 1'b0;
data_pkt_word0 <= 32'd0;
data_pkt_word1 <= 32'd0;
data_pkt_word2 <= 32'd0;
data_pkt_be2 <= 4'b1110;
sample_counter <= 12'd0;
// NOTE: ft601_clk_out is driven by the clk-domain always block below. // NOTE: ft601_clk_out is driven by the clk-domain always block below.
// Do NOT assign it here (ft601_clk_in domain) causes multi-driven net. // Do NOT assign it here (ft601_clk_in domain) causes multi-driven net.
end else begin end else begin
@@ -531,67 +424,125 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
current_state <= SEND_STATUS; current_state <= SEND_STATUS;
status_word_idx <= 3'd0; status_word_idx <= 3'd0;
end end
// Trigger on range_data_ready (1 cycle after range_valid_ft) // Trigger write FSM on range_valid edge (primary data source).
// so that range_profile_cap has settled from the CDC block. // Doppler/cfar data_pending flags are checked inside
// Gate on pending flags: only send when all enabled // SEND_DOPPLER_DATA and SEND_DETECTION_DATA to skip or send.
// streams have fresh data (avoids stale doppler/CFAR) // Do NOT trigger on pending flags alone — they're sticky and
else if (range_data_ready && stream_range_en // would cause repeated packet starts without new range data.
&& (!stream_doppler_en || doppler_data_pending) else if (range_valid_ft && stream_range_en) begin
&& (!stream_cfar_en || cfar_data_pending)) begin
// Don't start write if a read is about to begin // Don't start write if a read is about to begin
if (ft601_rxf) begin // rxf=1 means no host data pending if (ft601_rxf) begin // rxf=1 means no host data pending
// Pack 11-byte data packet into 3 x 32-bit words current_state <= SEND_HEADER;
// Doppler fields zeroed when stream disabled byte_counter <= 0;
// CFAR field zeroed when stream disabled
data_pkt_word0 <= {HEADER,
range_profile_cap[31:24],
range_profile_cap[23:16],
range_profile_cap[15:8]};
data_pkt_word1 <= {range_profile_cap[7:0],
stream_doppler_en ? doppler_real_cap[15:8] : 8'd0,
stream_doppler_en ? doppler_real_cap[7:0] : 8'd0,
stream_doppler_en ? doppler_imag_cap[15:8] : 8'd0};
data_pkt_word2 <= {stream_doppler_en ? doppler_imag_cap[7:0] : 8'd0,
stream_cfar_en
? {(sample_counter == 12'd0), 6'b0, cfar_detection_cap}
: {(sample_counter == 12'd0), 7'd0},
FOOTER,
8'h00}; // pad byte
data_pkt_be2 <= 4'b1110; // 3 valid bytes + 1 pad
data_word_idx <= 2'd0;
current_state <= SEND_DATA_WORD;
end end
end end
end end
SEND_DATA_WORD: begin SEND_HEADER: begin
if (!ft601_txe) begin // FT601 TX FIFO not empty
ft601_data_oe <= 1;
ft601_data_out <= {24'b0, HEADER};
ft601_be <= 4'b0001; // Only lower byte valid
ft601_wr_n <= 0; // Assert write strobe
// Gap 2: skip to first enabled stream
if (stream_range_en)
current_state <= SEND_RANGE_DATA;
else if (stream_doppler_en)
current_state <= SEND_DOPPLER_DATA;
else if (stream_cfar_en)
current_state <= SEND_DETECTION_DATA;
else
current_state <= SEND_FOOTER; // No streams — send footer only
end
end
SEND_RANGE_DATA: begin
if (!ft601_txe) begin if (!ft601_txe) begin
ft601_data_oe <= 1; ft601_data_oe <= 1;
ft601_wr_n <= 0; ft601_be <= 4'b1111; // All bytes valid for 32-bit word
case (data_word_idx)
2'd0: begin case (byte_counter)
ft601_data_out <= data_pkt_word0; 0: ft601_data_out <= range_profile_cap;
ft601_be <= 4'b1111; 1: ft601_data_out <= {range_profile_cap[23:0], 8'h00};
end 2: ft601_data_out <= {range_profile_cap[15:0], 16'h0000};
2'd1: begin 3: ft601_data_out <= {range_profile_cap[7:0], 24'h000000};
ft601_data_out <= data_pkt_word1;
ft601_be <= 4'b1111;
end
2'd2: begin
ft601_data_out <= data_pkt_word2;
ft601_be <= data_pkt_be2;
end
default: ;
endcase endcase
if (data_word_idx == 2'd2) begin
data_word_idx <= 2'd0; ft601_wr_n <= 0;
current_state <= WAIT_ACK;
if (byte_counter == 3) begin
byte_counter <= 0;
// Gap 2: skip disabled streams
if (stream_doppler_en)
current_state <= SEND_DOPPLER_DATA;
else if (stream_cfar_en)
current_state <= SEND_DETECTION_DATA;
else
current_state <= SEND_FOOTER;
end else begin end else begin
data_word_idx <= data_word_idx + 2'd1; byte_counter <= byte_counter + 1;
end end
end end
end end
SEND_DOPPLER_DATA: begin
if (!ft601_txe && doppler_data_pending) begin
ft601_data_oe <= 1;
ft601_be <= 4'b1111;
case (byte_counter)
0: ft601_data_out <= {doppler_real_cap, doppler_imag_cap};
1: ft601_data_out <= {doppler_imag_cap, doppler_real_cap[15:8], 8'h00};
2: ft601_data_out <= {doppler_real_cap[7:0], doppler_imag_cap[15:8], 16'h0000};
3: ft601_data_out <= {doppler_imag_cap[7:0], 24'h000000};
endcase
ft601_wr_n <= 0;
if (byte_counter == 3) begin
byte_counter <= 0;
doppler_data_pending <= 1'b0;
if (stream_cfar_en)
current_state <= SEND_DETECTION_DATA;
else
current_state <= SEND_FOOTER;
end else begin
byte_counter <= byte_counter + 1;
end
end else if (!doppler_data_pending) begin
// No doppler data available yet skip to next stream
byte_counter <= 0;
if (stream_cfar_en)
current_state <= SEND_DETECTION_DATA;
else
current_state <= SEND_FOOTER;
end
end
SEND_DETECTION_DATA: begin
if (!ft601_txe && cfar_data_pending) begin
ft601_data_oe <= 1;
ft601_be <= 4'b0001;
ft601_data_out <= {24'b0, 7'b0, cfar_detection_cap};
ft601_wr_n <= 0;
cfar_data_pending <= 1'b0;
current_state <= SEND_FOOTER;
end else if (!cfar_data_pending) begin
// No CFAR data available yet skip to footer
current_state <= SEND_FOOTER;
end
end
SEND_FOOTER: begin
if (!ft601_txe) begin
ft601_data_oe <= 1;
ft601_be <= 4'b0001;
ft601_data_out <= {24'b0, FOOTER};
ft601_wr_n <= 0;
current_state <= WAIT_ACK;
end
end
// Gap 2: Status readback send 6 x 32-bit status words // Gap 2: Status readback send 6 x 32-bit status words
// Format: HEADER, status_words[0..5], FOOTER // Format: HEADER, status_words[0..5], FOOTER
SEND_STATUS: begin SEND_STATUS: begin
@@ -630,14 +581,6 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin
WAIT_ACK: begin WAIT_ACK: begin
ft601_wr_n <= 1; ft601_wr_n <= 1;
ft601_data_oe <= 0; // Release data bus ft601_data_oe <= 0; // Release data bus
// Clear pending flags data consumed
doppler_data_pending <= 1'b0;
cfar_data_pending <= 1'b0;
// Advance frame sync counter
if (sample_counter == NUM_CELLS - 12'd1)
sample_counter <= 12'd0;
else
sample_counter <= sample_counter + 12'd1;
current_state <= IDLE; current_state <= IDLE;
end end
endcase endcase
@@ -670,8 +613,8 @@ ODDR #(
`else `else
// Simulation: behavioral clock forwarding // Simulation: behavioral clock forwarding
reg ft601_clk_out_sim; reg ft601_clk_out_sim;
always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin always @(posedge ft601_clk_in or negedge ft601_reset_n) begin
if (!ft601_effective_reset_n) if (!ft601_reset_n)
ft601_clk_out_sim <= 1'b0; ft601_clk_out_sim <= 1'b0;
else else
ft601_clk_out_sim <= 1'b1; ft601_clk_out_sim <= 1'b1;
+13 -125
View File
@@ -36,13 +36,6 @@
* Clock domains: * Clock domains:
* clk = 100 MHz system clock (radar data domain) * clk = 100 MHz system clock (radar data domain)
* ft_clk = 60 MHz from FT2232H CLKOUT (USB FIFO domain) * ft_clk = 60 MHz from FT2232H CLKOUT (USB FIFO domain)
*
* USB disconnect recovery:
* A clock-activity watchdog in the clk domain detects when ft_clk stops
* (USB cable unplugged). After ~0.65 ms of silence (65536 system clocks)
* it asserts ft_clk_lost, which is OR'd into the FT-domain reset so
* FSMs and FIFOs return to a clean state. When ft_clk resumes, a 2-stage
* reset synchronizer deasserts the reset cleanly in the ft_clk domain.
*/ */
module usb_data_interface_ft2232h ( module usb_data_interface_ft2232h (
@@ -66,9 +59,7 @@ module usb_data_interface_ft2232h (
output reg ft_rd_n, // Read strobe (active low) output reg ft_rd_n, // Read strobe (active low)
output reg ft_wr_n, // Write strobe (active low) output reg ft_wr_n, // Write strobe (active low)
output reg ft_oe_n, // Output enable (active low) bus direction output reg ft_oe_n, // Output enable (active low) bus direction
output reg ft_siwu, // Send Immediate / WakeUp UNUSED: held low. output reg ft_siwu, // Send Immediate / WakeUp
// SIWU could flush the TX FIFO for lower latency
// but is not needed at current data rates. Deferred.
// Clock from FT2232H (directly used no ODDR forwarding needed) // Clock from FT2232H (directly used no ODDR forwarding needed)
input wire ft_clk, // 60 MHz from FT2232H CLKOUT input wire ft_clk, // 60 MHz from FT2232H CLKOUT
@@ -143,7 +134,6 @@ localparam [2:0] RD_IDLE = 3'd0,
reg [2:0] rd_state; reg [2:0] rd_state;
reg [1:0] rd_byte_cnt; // 0..3 for 4-byte command word reg [1:0] rd_byte_cnt; // 0..3 for 4-byte command word
reg [31:0] rd_shift_reg; // Shift register to assemble 4-byte command reg [31:0] rd_shift_reg; // Shift register to assemble 4-byte command
reg rd_cmd_complete; // Set when all 4 bytes received (distinguishes from abort)
// ============================================================================ // ============================================================================
// DATA BUS DIRECTION CONTROL // DATA BUS DIRECTION CONTROL
@@ -202,70 +192,6 @@ always @(posedge clk or negedge reset_n) begin
end end
end end
// ============================================================================
// CLOCK-ACTIVITY WATCHDOG (clk domain)
// ============================================================================
// Detects when ft_clk stops (USB cable unplugged). A toggle register in the
// ft_clk domain flips every ft_clk edge. The clk domain synchronizes it and
// checks for transitions. If no transition is seen for 2^16 = 65536 clk
// cycles (~0.65 ms at 100 MHz), ft_clk_lost asserts.
//
// ft_clk_lost feeds into the effective reset for the ft_clk domain so that
// FSMs and capture registers return to a clean state automatically.
// Toggle register: flips every ft_clk edge (ft_clk domain)
reg ft_heartbeat;
always @(posedge ft_clk or negedge ft_reset_n) begin
if (!ft_reset_n)
ft_heartbeat <= 1'b0;
else
ft_heartbeat <= ~ft_heartbeat;
end
// Synchronize heartbeat into clk domain (2-stage)
(* ASYNC_REG = "TRUE" *) reg [1:0] ft_hb_sync;
reg ft_hb_prev;
reg [15:0] ft_clk_timeout;
reg ft_clk_lost;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
ft_hb_sync <= 2'b00;
ft_hb_prev <= 1'b0;
ft_clk_timeout <= 16'd0;
ft_clk_lost <= 1'b0;
end else begin
ft_hb_sync <= {ft_hb_sync[0], ft_heartbeat};
ft_hb_prev <= ft_hb_sync[1];
if (ft_hb_sync[1] != ft_hb_prev) begin
// ft_clk is alive reset counter, clear lost flag
ft_clk_timeout <= 16'd0;
ft_clk_lost <= 1'b0;
end else if (!ft_clk_lost) begin
if (ft_clk_timeout == 16'hFFFF)
ft_clk_lost <= 1'b1;
else
ft_clk_timeout <= ft_clk_timeout + 16'd1;
end
end
end
// Effective FT-domain reset: asserted by global reset OR clock loss.
// Deassertion synchronized to ft_clk via 2-stage sync to avoid
// metastability on the recovery edge.
(* ASYNC_REG = "TRUE" *) reg [1:0] ft_reset_sync;
wire ft_reset_raw_n = ft_reset_n & ~ft_clk_lost;
always @(posedge ft_clk or negedge ft_reset_raw_n) begin
if (!ft_reset_raw_n)
ft_reset_sync <= 2'b00;
else
ft_reset_sync <= {ft_reset_sync[0], 1'b1};
end
wire ft_effective_reset_n = ft_reset_sync[1];
// --- 3-stage synchronizers (ft_clk domain) --- // --- 3-stage synchronizers (ft_clk domain) ---
// 3 stages for better MTBF at 60 MHz // 3 stages for better MTBF at 60 MHz
@@ -302,25 +228,12 @@ reg cfar_detection_cap;
reg doppler_data_pending; reg doppler_data_pending;
reg cfar_data_pending; reg cfar_data_pending;
// 1-cycle delayed range trigger. range_valid_ft fires on the same clock
// edge that range_profile_cap is captured (non-blocking). If the FSM
// reads range_profile_cap on that same edge it sees the STALE value.
// Delaying the trigger by one cycle guarantees the capture register has
// settled before the byte mux reads it.
reg range_data_ready;
// Frame sync: sample counter (ft_clk domain, wraps at NUM_CELLS)
// Bit 7 of detection byte is set when sample_counter == 0 (frame start).
// This allows the Python host to resynchronize without a protocol change.
localparam [11:0] NUM_CELLS = 12'd2048; // 64 range x 32 doppler
reg [11:0] sample_counter;
// Status snapshot (ft_clk domain) // Status snapshot (ft_clk domain)
reg [31:0] status_words [0:5]; reg [31:0] status_words [0:5];
integer si; // status_words loop index integer si; // status_words loop index
always @(posedge ft_clk or negedge ft_effective_reset_n) begin always @(posedge ft_clk or negedge ft_reset_n) begin
if (!ft_effective_reset_n) begin if (!ft_reset_n) begin
range_toggle_sync <= 3'b000; range_toggle_sync <= 3'b000;
doppler_toggle_sync <= 3'b000; doppler_toggle_sync <= 3'b000;
cfar_toggle_sync <= 3'b000; cfar_toggle_sync <= 3'b000;
@@ -333,7 +246,6 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
doppler_real_cap <= 16'd0; doppler_real_cap <= 16'd0;
doppler_imag_cap <= 16'd0; doppler_imag_cap <= 16'd0;
cfar_detection_cap <= 1'b0; cfar_detection_cap <= 1'b0;
range_data_ready <= 1'b0;
// Default to range-only on reset (prevents write FSM deadlock) // Default to range-only on reset (prevents write FSM deadlock)
stream_ctrl_sync_0 <= 3'b001; stream_ctrl_sync_0 <= 3'b001;
stream_ctrl_sync_1 <= 3'b001; stream_ctrl_sync_1 <= 3'b001;
@@ -367,10 +279,6 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
if (cfar_valid_ft) if (cfar_valid_ft)
cfar_detection_cap <= cfar_detection_hold; cfar_detection_cap <= cfar_detection_hold;
// 1-cycle delayed trigger: ensures range_profile_cap has settled
// before the FSM reads it via the byte mux.
range_data_ready <= range_valid_ft;
// Status snapshot on request // Status snapshot on request
if (status_req_ft) begin if (status_req_ft) begin
// Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]} // Word 0: {0xFF[31:24], mode[23:22], stream[21:19], 3'b000[18:16], threshold[15:0]}
@@ -407,16 +315,11 @@ always @(*) begin
5'd2: data_pkt_byte = range_profile_cap[23:16]; 5'd2: data_pkt_byte = range_profile_cap[23:16];
5'd3: data_pkt_byte = range_profile_cap[15:8]; 5'd3: data_pkt_byte = range_profile_cap[15:8];
5'd4: data_pkt_byte = range_profile_cap[7:0]; // range LSB 5'd4: data_pkt_byte = range_profile_cap[7:0]; // range LSB
// Doppler fields: zero when stream_doppler_en is off 5'd5: data_pkt_byte = doppler_real_cap[15:8]; // doppler_real MSB
5'd5: data_pkt_byte = stream_doppler_en ? doppler_real_cap[15:8] : 8'd0; 5'd6: data_pkt_byte = doppler_real_cap[7:0]; // doppler_real LSB
5'd6: data_pkt_byte = stream_doppler_en ? doppler_real_cap[7:0] : 8'd0; 5'd7: data_pkt_byte = doppler_imag_cap[15:8]; // doppler_imag MSB
5'd7: data_pkt_byte = stream_doppler_en ? doppler_imag_cap[15:8] : 8'd0; 5'd8: data_pkt_byte = doppler_imag_cap[7:0]; // doppler_imag LSB
5'd8: data_pkt_byte = stream_doppler_en ? doppler_imag_cap[7:0] : 8'd0; 5'd9: data_pkt_byte = {7'b0, cfar_detection_cap}; // detection
// Detection field: zero when stream_cfar_en is off
// Bit 7 = frame_start flag (sample_counter == 0), bit 0 = cfar_detection
5'd9: data_pkt_byte = stream_cfar_en
? {(sample_counter == 12'd0), 6'b0, cfar_detection_cap}
: {(sample_counter == 12'd0), 7'd0};
5'd10: data_pkt_byte = FOOTER; 5'd10: data_pkt_byte = FOOTER;
default: data_pkt_byte = 8'h00; default: data_pkt_byte = 8'h00;
endcase endcase
@@ -473,13 +376,12 @@ end
// Write FSM and Read FSM share the bus. Write FSM operates when Read FSM // Write FSM and Read FSM share the bus. Write FSM operates when Read FSM
// is idle. Read FSM takes priority when host has data available. // is idle. Read FSM takes priority when host has data available.
always @(posedge ft_clk or negedge ft_effective_reset_n) begin always @(posedge ft_clk or negedge ft_reset_n) begin
if (!ft_effective_reset_n) begin if (!ft_reset_n) begin
wr_state <= WR_IDLE; wr_state <= WR_IDLE;
wr_byte_idx <= 5'd0; wr_byte_idx <= 5'd0;
rd_state <= RD_IDLE; rd_state <= RD_IDLE;
rd_byte_cnt <= 2'd0; rd_byte_cnt <= 2'd0;
rd_cmd_complete <= 1'b0;
rd_shift_reg <= 32'd0; rd_shift_reg <= 32'd0;
ft_data_out <= 8'd0; ft_data_out <= 8'd0;
ft_data_oe <= 1'b0; ft_data_oe <= 1'b0;
@@ -494,7 +396,6 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
cmd_value <= 16'd0; cmd_value <= 16'd0;
doppler_data_pending <= 1'b0; doppler_data_pending <= 1'b0;
cfar_data_pending <= 1'b0; cfar_data_pending <= 1'b0;
sample_counter <= 12'd0;
end else begin end else begin
// Default: clear one-shot signals // Default: clear one-shot signals
cmd_valid <= 1'b0; cmd_valid <= 1'b0;
@@ -538,7 +439,6 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
// All 4 bytes received // All 4 bytes received
ft_rd_n <= 1'b1; ft_rd_n <= 1'b1;
rd_byte_cnt <= 2'd0; rd_byte_cnt <= 2'd0;
rd_cmd_complete <= 1'b1;
rd_state <= RD_DEASSERT; rd_state <= RD_DEASSERT;
end else begin end else begin
rd_byte_cnt <= rd_byte_cnt + 2'd1; rd_byte_cnt <= rd_byte_cnt + 2'd1;
@@ -547,7 +447,6 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
// Host ran out of data mid-command abort // Host ran out of data mid-command abort
ft_rd_n <= 1'b1; ft_rd_n <= 1'b1;
rd_byte_cnt <= 2'd0; rd_byte_cnt <= 2'd0;
rd_cmd_complete <= 1'b0;
rd_state <= RD_DEASSERT; rd_state <= RD_DEASSERT;
end end
end end
@@ -557,8 +456,7 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
// Deassert OE (1 cycle after RD deasserted) // Deassert OE (1 cycle after RD deasserted)
ft_oe_n <= 1'b1; ft_oe_n <= 1'b1;
// Only process if we received a full 4-byte command // Only process if we received a full 4-byte command
if (rd_cmd_complete) begin if (rd_byte_cnt == 2'd0) begin
rd_cmd_complete <= 1'b0;
rd_state <= RD_PROCESS; rd_state <= RD_PROCESS;
end else begin end else begin
// Incomplete command discard // Incomplete command discard
@@ -593,13 +491,8 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
wr_state <= WR_STATUS_SEND; wr_state <= WR_STATUS_SEND;
wr_byte_idx <= 5'd0; wr_byte_idx <= 5'd0;
end end
// Trigger on range_data_ready (1 cycle after range_valid_ft) // Trigger on range_valid edge (primary data trigger)
// so that range_profile_cap has settled from the CDC block. else if (range_valid_ft && stream_range_en) begin
// Gate on pending flags: only send when all enabled
// streams have fresh data (avoids stale doppler/CFAR)
else if (range_data_ready && stream_range_en
&& (!stream_doppler_en || doppler_data_pending)
&& (!stream_cfar_en || cfar_data_pending)) begin
if (ft_rxf_n) begin // No host read pending if (ft_rxf_n) begin // No host read pending
wr_state <= WR_DATA_SEND; wr_state <= WR_DATA_SEND;
wr_byte_idx <= 5'd0; wr_byte_idx <= 5'd0;
@@ -645,11 +538,6 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
// Clear pending flags data consumed // Clear pending flags data consumed
doppler_data_pending <= 1'b0; doppler_data_pending <= 1'b0;
cfar_data_pending <= 1'b0; cfar_data_pending <= 1'b0;
// Advance frame sync counter
if (sample_counter == NUM_CELLS - 12'd1)
sample_counter <= 12'd0;
else
sample_counter <= sample_counter + 12'd1;
wr_state <= WR_IDLE; wr_state <= WR_IDLE;
end end
+5 -5
View File
@@ -108,7 +108,7 @@ class GPSData:
@dataclass @dataclass
class RadarSettings: class RadarSettings:
"""Radar system configuration""" """Radar system configuration"""
system_frequency: float = 10e9 # Hz system_frequency: float = 10.5e9 # Hz (PLFM TX LO)
chirp_duration_1: float = 30e-6 # Long chirp duration (s) chirp_duration_1: float = 30e-6 # Long chirp duration (s)
chirp_duration_2: float = 0.5e-6 # Short chirp duration (s) chirp_duration_2: float = 0.5e-6 # Short chirp duration (s)
chirps_per_position: int = 32 chirps_per_position: int = 32
@@ -116,8 +116,8 @@ class RadarSettings:
freq_max: float = 30e6 # Hz freq_max: float = 30e6 # Hz
prf1: float = 1000 # PRF 1 (Hz) prf1: float = 1000 # PRF 1 (Hz)
prf2: float = 2000 # PRF 2 (Hz) prf2: float = 2000 # PRF 2 (Hz)
max_distance: float = 50000 # Max detection range (m) max_distance: float = 1536 # Max detection range (m) -- 64 bins x 24 m
coverage_radius: float = 50000 # Map coverage radius (m) coverage_radius: float = 1536 # Map coverage radius (m)
class TileServer(Enum): class TileServer(Enum):
@@ -198,7 +198,7 @@ class RadarMapWidget(QWidget):
pitch=0.0 pitch=0.0
) )
self._targets: list[RadarTarget] = [] self._targets: list[RadarTarget] = []
self._coverage_radius = 50000 # meters self._coverage_radius = 1536 # meters (64 bins x 24 m, 3 km mode)
self._tile_server = TileServer.OPENSTREETMAP self._tile_server = TileServer.OPENSTREETMAP
self._show_coverage = True self._show_coverage = True
self._show_trails = False self._show_trails = False
@@ -1088,7 +1088,7 @@ class TargetSimulator(QObject):
new_range = target.range - target.velocity * 0.5 # 0.5 second update new_range = target.range - target.velocity * 0.5 # 0.5 second update
# Check if target is still in range # Check if target is still in range
if new_range < 500 or new_range > 50000: if new_range < 50 or new_range > 1536:
# Remove this target and add a new one # Remove this target and add a new one
continue continue
+5 -5
View File
@@ -81,7 +81,7 @@ class RadarTarget:
@dataclass @dataclass
class RadarSettings: class RadarSettings:
system_frequency: float = 10e9 system_frequency: float = 10.5e9
chirp_duration_1: float = 30e-6 # Long chirp duration chirp_duration_1: float = 30e-6 # Long chirp duration
chirp_duration_2: float = 0.5e-6 # Short chirp duration chirp_duration_2: float = 0.5e-6 # Short chirp duration
chirps_per_position: int = 32 chirps_per_position: int = 32
@@ -89,8 +89,8 @@ class RadarSettings:
freq_max: float = 30e6 freq_max: float = 30e6
prf1: float = 1000 prf1: float = 1000
prf2: float = 2000 prf2: float = 2000
max_distance: float = 50000 max_distance: float = 1536
map_size: float = 50000 # Map size in meters map_size: float = 1536 # Map size in meters (64 bins x 24 m)
@dataclass @dataclass
@@ -1196,8 +1196,8 @@ class RadarGUI:
("Frequency Max (Hz):", "freq_max", 30e6), ("Frequency Max (Hz):", "freq_max", 30e6),
("PRF1 (Hz):", "prf1", 1000), ("PRF1 (Hz):", "prf1", 1000),
("PRF2 (Hz):", "prf2", 2000), ("PRF2 (Hz):", "prf2", 2000),
("Max Distance (m):", "max_distance", 50000), ("Max Distance (m):", "max_distance", 1536),
("Map Size (m):", "map_size", 50000), ("Map Size (m):", "map_size", 1536),
("Google Maps API Key:", "google_maps_api_key", "YOUR_GOOGLE_MAPS_API_KEY"), ("Google Maps API Key:", "google_maps_api_key", "YOUR_GOOGLE_MAPS_API_KEY"),
] ]
+5 -5
View File
@@ -77,7 +77,7 @@ class RadarTarget:
@dataclass @dataclass
class RadarSettings: class RadarSettings:
system_frequency: float = 10e9 system_frequency: float = 10.5e9
chirp_duration_1: float = 30e-6 # Long chirp duration chirp_duration_1: float = 30e-6 # Long chirp duration
chirp_duration_2: float = 0.5e-6 # Short chirp duration chirp_duration_2: float = 0.5e-6 # Short chirp duration
chirps_per_position: int = 32 chirps_per_position: int = 32
@@ -85,8 +85,8 @@ class RadarSettings:
freq_max: float = 30e6 freq_max: float = 30e6
prf1: float = 1000 prf1: float = 1000
prf2: float = 2000 prf2: float = 2000
max_distance: float = 50000 max_distance: float = 1536
map_size: float = 50000 # Map size in meters map_size: float = 1536 # Map size in meters (64 bins x 24 m)
@dataclass @dataclass
@@ -1254,8 +1254,8 @@ class RadarGUI:
("Frequency Max (Hz):", "freq_max", 30e6), ("Frequency Max (Hz):", "freq_max", 30e6),
("PRF1 (Hz):", "prf1", 1000), ("PRF1 (Hz):", "prf1", 1000),
("PRF2 (Hz):", "prf2", 2000), ("PRF2 (Hz):", "prf2", 2000),
("Max Distance (m):", "max_distance", 50000), ("Max Distance (m):", "max_distance", 1536),
("Map Size (m):", "map_size", 50000), ("Map Size (m):", "map_size", 1536),
] ]
self.settings_vars = {} self.settings_vars = {}
+5 -11
View File
@@ -1,9 +1,3 @@
# =============================================================================
# DEPRECATED: GUI V6 is superseded by GUI_V65_Tk (tkinter) and V7 (PyQt6).
# This file is retained for reference only. Do not use for new development.
# Removal planned for next major release.
# =============================================================================
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox from tkinter import ttk, messagebox
import threading import threading
@@ -70,7 +64,7 @@ class RadarTarget:
@dataclass @dataclass
class RadarSettings: class RadarSettings:
system_frequency: float = 10e9 system_frequency: float = 10.5e9
chirp_duration_1: float = 30e-6 # Long chirp duration chirp_duration_1: float = 30e-6 # Long chirp duration
chirp_duration_2: float = 0.5e-6 # Short chirp duration chirp_duration_2: float = 0.5e-6 # Short chirp duration
chirps_per_position: int = 32 chirps_per_position: int = 32
@@ -78,8 +72,8 @@ class RadarSettings:
freq_max: float = 30e6 freq_max: float = 30e6
prf1: float = 1000 prf1: float = 1000
prf2: float = 2000 prf2: float = 2000
max_distance: float = 50000 max_distance: float = 1536
map_size: float = 50000 # Map size in meters map_size: float = 1536 # Map size in meters (64 bins x 24 m)
@dataclass @dataclass
class GPSData: class GPSData:
@@ -1659,8 +1653,8 @@ class RadarGUI:
('Frequency Max (Hz):', 'freq_max', 30e6), ('Frequency Max (Hz):', 'freq_max', 30e6),
('PRF1 (Hz):', 'prf1', 1000), ('PRF1 (Hz):', 'prf1', 1000),
('PRF2 (Hz):', 'prf2', 2000), ('PRF2 (Hz):', 'prf2', 2000),
('Max Distance (m):', 'max_distance', 50000), ('Max Distance (m):', 'max_distance', 1536),
('Map Size (m):', 'map_size', 50000), ('Map Size (m):', 'map_size', 1536),
('Google Maps API Key:', 'google_maps_api_key', 'YOUR_GOOGLE_MAPS_API_KEY') ('Google Maps API Key:', 'google_maps_api_key', 'YOUR_GOOGLE_MAPS_API_KEY')
] ]
+23 -49
View File
@@ -59,7 +59,7 @@ except (ModuleNotFoundError, ImportError):
# Import protocol layer (no GUI deps) # Import protocol layer (no GUI deps)
from radar_protocol import ( from radar_protocol import (
RadarProtocol, FT2232HConnection, FT601Connection, RadarProtocol, FT2232HConnection,
DataRecorder, RadarAcquisition, DataRecorder, RadarAcquisition,
RadarFrame, StatusResponse, RadarFrame, StatusResponse,
NUM_RANGE_BINS, NUM_DOPPLER_BINS, WATERFALL_DEPTH, NUM_RANGE_BINS, NUM_DOPPLER_BINS, WATERFALL_DEPTH,
@@ -98,10 +98,10 @@ class DemoTarget:
__slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity") __slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity")
# Physical range grid: 64 bins x ~24 m/bin = ~1536 m max # Physical range grid: matched-filter receiver, 100 MSPS post-DDC, 16:1 decimation
# Bin spacing = c / (2 * Fs) * decimation, where Fs = 100 MHz DDC output. # range_per_bin = c / (2 * 100e6) * 16 = 24.0 m
_RANGE_PER_BIN: float = (3e8 / (2 * 100e6)) * 16 # ~24 m _RANGE_PER_BIN: float = (3e8 / (2 * 100e6)) * 16 # 24.0 m
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~1536 m _MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # 1536 m
def __init__(self, tid: int): def __init__(self, tid: int):
self.id = tid self.id = tid
@@ -188,10 +188,10 @@ class DemoSimulator:
mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64) mag = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.float64)
det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8) det = np.zeros((NUM_RANGE_BINS, NUM_DOPPLER_BINS), dtype=np.uint8)
# Range/Doppler scaling: bin spacing = c/(2*Fs)*decimation # Range/Doppler scaling -- matched-filter receiver, 100 MSPS, 16:1 decimation
range_per_bin = (3e8 / (2 * 100e6)) * 16 # ~24 m/bin range_per_bin = (3e8 / (2 * 100e6)) * 16 # 24.0 m/bin
max_range = range_per_bin * NUM_RANGE_BINS max_range = range_per_bin * NUM_RANGE_BINS
vel_per_bin = 5.34 # m/s per Doppler bin (radar_scene.py: lam/(2*16*PRI)) vel_per_bin = 2.67 # m/s per Doppler bin (lam/(2*32*167us))
for t in targets: for t in targets:
if t.range_m > max_range or t.range_m < 0: if t.range_m > max_range or t.range_m < 0:
@@ -386,14 +386,15 @@ class RadarDashboard:
UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh UPDATE_INTERVAL_MS = 100 # 10 Hz display refresh
# Radar parameters used for range-axis scaling. # Radar parameters used for range-axis scaling.
SAMPLE_RATE = 100e6 # Hz — DDC output I/Q rate (matched filter input) # Matched-filter receiver: range_per_bin = c / (2 * fs_processing) * decimation
# = 3e8 / (2 * 100e6) * 16 = 24.0 m/bin
BANDWIDTH = 20e6 # Hz — chirp bandwidth (for display/info only)
C = 3e8 # m/s — speed of light C = 3e8 # m/s — speed of light
def __init__(self, root: tk.Tk, mock: bool, def __init__(self, root: tk.Tk, connection: FT2232HConnection,
recorder: DataRecorder, device_index: int = 0): recorder: DataRecorder, device_index: int = 0):
self.root = root self.root = root
self._mock = mock self.conn = connection
self.conn: FT2232HConnection | FT601Connection | None = None
self.recorder = recorder self.recorder = recorder
self.device_index = device_index self.device_index = device_index
@@ -487,16 +488,6 @@ class RadarDashboard:
style="Accent.TButton") style="Accent.TButton")
self.btn_connect.pack(side="right", padx=4) self.btn_connect.pack(side="right", padx=4)
# USB Interface selector (production FT2232H / premium FT601)
self._usb_iface_var = tk.StringVar(value="FT2232H (Production)")
self.cmb_usb_iface = ttk.Combobox(
top, textvariable=self._usb_iface_var,
values=["FT2232H (Production)", "FT601 (Premium)"],
state="readonly", width=20,
)
self.cmb_usb_iface.pack(side="right", padx=4)
ttk.Label(top, text="USB:", font=("Menlo", 10)).pack(side="right")
self.btn_record = ttk.Button(top, text="Record", command=self._on_record) self.btn_record = ttk.Button(top, text="Record", command=self._on_record)
self.btn_record.pack(side="right", padx=4) self.btn_record.pack(side="right", padx=4)
@@ -526,9 +517,9 @@ class RadarDashboard:
self._build_log_tab(tab_log) self._build_log_tab(tab_log)
def _build_display_tab(self, parent): def _build_display_tab(self, parent):
# Compute physical axis limits # Compute physical axis limits -- matched-filter receiver
# Bin spacing = c / (2 * Fs_ddc) for matched-filter processing. # Range per bin: c / (2 * fs_processing) * decimation_factor = 24.0 m
range_per_bin = self.C / (2.0 * self.SAMPLE_RATE) * 16 # ~24 m range_per_bin = self.C / (2.0 * 100e6) * 16 # 24.0 m
max_range = range_per_bin * NUM_RANGE_BINS max_range = range_per_bin * NUM_RANGE_BINS
doppler_bin_lo = 0 doppler_bin_lo = 0
@@ -1029,17 +1020,15 @@ class RadarDashboard:
# ------------------------------------------------------------ Actions # ------------------------------------------------------------ Actions
def _on_connect(self): def _on_connect(self):
if self.conn is not None and self.conn.is_open: if self.conn.is_open:
# Disconnect # Disconnect
if self._acq_thread is not None: if self._acq_thread is not None:
self._acq_thread.stop() self._acq_thread.stop()
self._acq_thread.join(timeout=2) self._acq_thread.join(timeout=2)
self._acq_thread = None self._acq_thread = None
self.conn.close() self.conn.close()
self.conn = None
self.lbl_status.config(text="DISCONNECTED", foreground=RED) self.lbl_status.config(text="DISCONNECTED", foreground=RED)
self.btn_connect.config(text="Connect") self.btn_connect.config(text="Connect")
self.cmb_usb_iface.config(state="readonly")
log.info("Disconnected") log.info("Disconnected")
return return
@@ -1049,16 +1038,6 @@ class RadarDashboard:
if self._replay_active: if self._replay_active:
self._replay_stop() self._replay_stop()
# Create connection based on USB Interface selector
iface = self._usb_iface_var.get()
if "FT601" in iface:
self.conn = FT601Connection(mock=self._mock)
else:
self.conn = FT2232HConnection(mock=self._mock)
# Disable interface selector while connecting/connected
self.cmb_usb_iface.config(state="disabled")
# Open connection in a background thread to avoid blocking the GUI # Open connection in a background thread to avoid blocking the GUI
self.lbl_status.config(text="CONNECTING...", foreground=YELLOW) self.lbl_status.config(text="CONNECTING...", foreground=YELLOW)
self.btn_connect.config(state="disabled") self.btn_connect.config(state="disabled")
@@ -1085,8 +1064,6 @@ class RadarDashboard:
else: else:
self.lbl_status.config(text="CONNECT FAILED", foreground=RED) self.lbl_status.config(text="CONNECT FAILED", foreground=RED)
self.btn_connect.config(text="Connect") self.btn_connect.config(text="Connect")
self.cmb_usb_iface.config(state="readonly")
self.conn = None
def _on_record(self): def _on_record(self):
if self.recorder.recording: if self.recorder.recording:
@@ -1135,9 +1112,6 @@ class RadarDashboard:
f"Opcode 0x{opcode:02X} is hardware-only (ignored in replay)")) f"Opcode 0x{opcode:02X} is hardware-only (ignored in replay)"))
return return
cmd = RadarProtocol.build_command(opcode, value) cmd = RadarProtocol.build_command(opcode, value)
if self.conn is None:
log.warning("No connection — command not sent")
return
ok = self.conn.write(cmd) ok = self.conn.write(cmd)
log.info(f"CMD 0x{opcode:02X} val={value} ({'OK' if ok else 'FAIL'})") log.info(f"CMD 0x{opcode:02X} val={value} ({'OK' if ok else 'FAIL'})")
@@ -1176,7 +1150,7 @@ class RadarDashboard:
if self._replay_active or self._replay_ctrl is not None: if self._replay_active or self._replay_ctrl is not None:
self._replay_stop() self._replay_stop()
if self._acq_thread is not None: if self._acq_thread is not None:
if self.conn is not None and self.conn.is_open: if self.conn.is_open:
self._on_connect() # disconnect self._on_connect() # disconnect
else: else:
# Connection dropped unexpectedly — just clean up the thread # Connection dropped unexpectedly — just clean up the thread
@@ -1575,17 +1549,17 @@ def main():
args = parser.parse_args() args = parser.parse_args()
if args.live: if args.live:
mock = False conn = FT2232HConnection(mock=False)
mode_str = "LIVE" mode_str = "LIVE"
else: else:
mock = True conn = FT2232HConnection(mock=True)
mode_str = "MOCK" mode_str = "MOCK"
recorder = DataRecorder() recorder = DataRecorder()
root = tk.Tk() root = tk.Tk()
dashboard = RadarDashboard(root, mock, recorder, device_index=args.device) dashboard = RadarDashboard(root, conn, recorder, device_index=args.device)
if args.record: if args.record:
filepath = os.path.join( filepath = os.path.join(
@@ -1610,8 +1584,8 @@ def main():
if dashboard._acq_thread is not None: if dashboard._acq_thread is not None:
dashboard._acq_thread.stop() dashboard._acq_thread.stop()
dashboard._acq_thread.join(timeout=2) dashboard._acq_thread.join(timeout=2)
if dashboard.conn is not None and dashboard.conn.is_open: if conn.is_open:
dashboard.conn.close() conn.close()
if recorder.recording: if recorder.recording:
recorder.stop() recorder.stop()
root.destroy() root.destroy()
+2 -8
View File
@@ -1,11 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# =============================================================================
# DEPRECATED: GUI V6 Demo is superseded by GUI_V65_Tk and V7.
# This file is retained for reference only. Do not use for new development.
# Removal planned for next major release.
# =============================================================================
""" """
Radar System GUI - Fully Functional Demo Version Radar System GUI - Fully Functional Demo Version
All buttons work, simulated radar data is generated in real-time All buttons work, simulated radar data is generated in real-time
@@ -51,7 +45,7 @@ class RadarSettings:
range_bins: int = 1024 range_bins: int = 1024
doppler_bins: int = 32 doppler_bins: int = 32
prf: float = 1000 prf: float = 1000
max_range: float = 5000 max_range: float = 1536
max_velocity: float = 100 max_velocity: float = 100
cfar_threshold: float = 13.0 cfar_threshold: float = 13.0
@@ -583,7 +577,7 @@ class RadarDemoGUI:
('Range Bins:', 'range_bins', 1024, 256, 2048), ('Range Bins:', 'range_bins', 1024, 256, 2048),
('Doppler Bins:', 'doppler_bins', 32, 8, 128), ('Doppler Bins:', 'doppler_bins', 32, 8, 128),
('PRF (Hz):', 'prf', 1000, 100, 10000), ('PRF (Hz):', 'prf', 1000, 100, 10000),
('Max Range (m):', 'max_range', 5000, 100, 50000), ('Max Range (m):', 'max_range', 1536, 100, 25000),
('Max Velocity (m/s):', 'max_vel', 100, 10, 500), ('Max Velocity (m/s):', 'max_vel', 100, 10, 500),
('CFAR Threshold (dB):', 'cfar', 13.0, 5.0, 30.0) ('CFAR Threshold (dB):', 'cfar', 13.0, 5.0, 30.0)
] ]
+1 -1
View File
@@ -6,7 +6,7 @@ GUI_V4 ==> Added pitch correction
GUI_V5 ==> Added Mercury Color GUI_V5 ==> Added Mercury Color
GUI_V6 ==> Added USB3 FT601 support [DEPRECATED — superseded by V65/V7] GUI_V6 ==> Added USB3 FT601 support
GUI_V65_Tk ==> Board bring-up dashboard (FT2232H reader, real-time R-D heatmap, CFAR overlay, waterfall, host commands, HDF5 recording, replay, demo mode) GUI_V65_Tk ==> Board bring-up dashboard (FT2232H reader, real-time R-D heatmap, CFAR overlay, waterfall, host commands, HDF5 recording, replay, demo mode)
radar_protocol ==> Protocol layer (packet parsing, command building, FT2232H connection, data recorder, acquisition thread) radar_protocol ==> Protocol layer (packet parsing, command building, FT2232H connection, data recorder, acquisition thread)
-338
View File
@@ -1,338 +0,0 @@
# ruff: noqa: T201
#!/usr/bin/env python3
"""
One-off AGC saturation analysis for ADI CN0566 raw IQ captures.
Bit-accurate simulation of rx_gain_control.v AGC inner loop applied
to real captured IQ data. Three scenarios per dataset:
Row 1 AGC OFF: Fixed gain_shift=0 (pass-through). Shows raw clipping.
Row 2 AGC ON: Auto-adjusts from gain_shift=0. Clipping clears.
Row 3 AGC delayed: OFF for first half, ON at midpoint.
Shows the transition: clipping AGC activates clears.
Key RTL details modelled exactly:
- gain_shift[3]=direction (0=amplify/left, 1=attenuate/right), [2:0]=amount
- Internal agc_gain is signed -7..+7
- Peak is measured PRE-gain (raw input |sample|, upper 8 of 15 bits)
- Saturation is measured POST-gain (overflow from shift)
- Attack: gain -= agc_attack when any sample clips (immediate)
- Decay: gain += agc_decay when peak < target AND holdoff expired
- Hold: when peak >= target AND no saturation, hold gain, reset holdoff
Usage:
python adi_agc_analysis.py
python adi_agc_analysis.py --data /path/to/file.npy --label "my capture"
"""
import argparse
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from v7.agc_sim import (
encoding_to_signed,
apply_gain_shift,
quantize_iq,
AGCConfig,
AGCState,
process_agc_frame,
)
# ---------------------------------------------------------------------------
# FPGA AGC parameters (rx_gain_control.v reset defaults)
# ---------------------------------------------------------------------------
AGC_TARGET = 200 # host_agc_target (8-bit, default 200)
ADC_RAIL = 4095 # 12-bit ADC max absolute value
# ---------------------------------------------------------------------------
# Per-frame AGC simulation using v7.agc_sim (bit-accurate to RTL)
# ---------------------------------------------------------------------------
def simulate_agc(frames: np.ndarray, agc_enabled: bool = True,
enable_at_frame: int = 0,
initial_gain_enc: int = 0x00) -> dict:
"""Simulate FPGA inner-loop AGC across all frames.
Parameters
----------
frames : (N, chirps, samples) complex raw ADC captures (12-bit range)
agc_enabled : if False, gain stays fixed
enable_at_frame : frame index where AGC activates
initial_gain_enc : gain_shift[3:0] encoding when AGC enables (default 0x00 = pass-through)
"""
n_frames = frames.shape[0]
# Output arrays
out_gain_enc = np.zeros(n_frames, dtype=int)
out_gain_signed = np.zeros(n_frames, dtype=int)
out_peak_mag = np.zeros(n_frames, dtype=int)
out_sat_count = np.zeros(n_frames, dtype=int)
out_sat_rate = np.zeros(n_frames, dtype=float)
out_rms_post = np.zeros(n_frames, dtype=float)
# AGC state — managed by process_agc_frame()
state = AGCState(
gain=encoding_to_signed(initial_gain_enc),
holdoff_counter=0,
was_enabled=False,
)
for i in range(n_frames):
frame_i, frame_q = quantize_iq(frames[i])
agc_active = agc_enabled and (i >= enable_at_frame)
# Build per-frame config (enable toggles at enable_at_frame)
config = AGCConfig(enabled=agc_active)
result = process_agc_frame(frame_i, frame_q, config, state)
# RMS of shifted signal
rms = float(np.sqrt(np.mean(
result.shifted_i.astype(np.float64)**2
+ result.shifted_q.astype(np.float64)**2)))
total_samples = frame_i.size + frame_q.size
sat_rate = result.overflow_raw / total_samples if total_samples > 0 else 0.0
# Record outputs
out_gain_enc[i] = result.gain_enc
out_gain_signed[i] = result.gain_signed
out_peak_mag[i] = result.peak_mag_8bit
out_sat_count[i] = result.saturation_count
out_sat_rate[i] = sat_rate
out_rms_post[i] = rms
return {
"gain_enc": out_gain_enc,
"gain_signed": out_gain_signed,
"peak_mag": out_peak_mag,
"sat_count": out_sat_count,
"sat_rate": out_sat_rate,
"rms_post": out_rms_post,
}
# ---------------------------------------------------------------------------
# Range-Doppler processing for heatmap display
# ---------------------------------------------------------------------------
def process_frame_rd(frame: np.ndarray, gain_enc: int,
n_range: int = 64,
n_doppler: int = 32) -> np.ndarray:
"""Range-Doppler magnitude for one frame with gain applied."""
frame_i, frame_q = quantize_iq(frame)
si, sq, _ = apply_gain_shift(frame_i, frame_q, gain_enc)
iq = si.astype(np.float64) + 1j * sq.astype(np.float64)
n_chirps, _ = iq.shape
range_fft = np.fft.fft(iq, axis=1)[:, :n_range]
doppler_fft = np.fft.fftshift(np.fft.fft(range_fft, axis=0), axes=0)
center = n_chirps // 2
half_d = n_doppler // 2
doppler_fft = doppler_fft[center - half_d:center + half_d, :]
rd_mag = np.abs(doppler_fft.real) + np.abs(doppler_fft.imag)
return rd_mag.T # (n_range, n_doppler)
# ---------------------------------------------------------------------------
# Plotting
# ---------------------------------------------------------------------------
def plot_scenario(axes, data: np.ndarray, agc: dict, title: str,
enable_frame: int = 0):
"""Plot one AGC scenario across 5 axes."""
n = data.shape[0]
xs = np.arange(n)
# Range-Doppler heatmap
if enable_frame > 0 and enable_frame < n:
f_before = max(0, enable_frame - 1)
f_after = min(n - 1, n - 2)
rd_before = process_frame_rd(data[f_before], int(agc["gain_enc"][f_before]))
rd_after = process_frame_rd(data[f_after], int(agc["gain_enc"][f_after]))
combined = np.hstack([rd_before, rd_after])
im = axes[0].imshow(
20 * np.log10(combined + 1), aspect="auto", origin="lower",
cmap="inferno", interpolation="nearest")
axes[0].axvline(x=rd_before.shape[1] - 0.5, color="cyan",
linewidth=2, linestyle="--")
axes[0].set_title(f"{title}\nL: f{f_before} (pre) | R: f{f_after} (post)")
else:
worst = int(np.argmax(agc["sat_count"]))
best = int(np.argmin(agc["sat_count"]))
f_show = worst if agc["sat_count"][worst] > 0 else best
rd = process_frame_rd(data[f_show], int(agc["gain_enc"][f_show]))
im = axes[0].imshow(
20 * np.log10(rd + 1), aspect="auto", origin="lower",
cmap="inferno", interpolation="nearest")
axes[0].set_title(f"{title}\nFrame {f_show}")
axes[0].set_xlabel("Doppler bin")
axes[0].set_ylabel("Range bin")
plt.colorbar(im, ax=axes[0], label="dB", shrink=0.8)
# Signed gain history (the real AGC state)
axes[1].plot(xs, agc["gain_signed"], color="#00ff88", linewidth=1.5)
axes[1].axhline(y=0, color="gray", linestyle=":", alpha=0.5,
label="Pass-through")
if enable_frame > 0:
axes[1].axvline(x=enable_frame, color="yellow", linewidth=2,
linestyle="--", label="AGC ON")
axes[1].set_ylim(-8, 8)
axes[1].set_ylabel("Gain (signed)")
axes[1].set_title("AGC Internal Gain (-7=max atten, +7=max amp)")
axes[1].legend(fontsize=7, loc="upper right")
axes[1].grid(True, alpha=0.3)
# Peak magnitude (PRE-gain, 8-bit)
axes[2].plot(xs, agc["peak_mag"], color="#ffaa00", linewidth=1.0)
axes[2].axhline(y=AGC_TARGET, color="cyan", linestyle="--",
alpha=0.7, label=f"Target ({AGC_TARGET})")
axes[2].axhspan(240, 255, color="red", alpha=0.15, label="Clip zone")
if enable_frame > 0:
axes[2].axvline(x=enable_frame, color="yellow", linewidth=2,
linestyle="--", alpha=0.8)
axes[2].set_ylim(0, 260)
axes[2].set_ylabel("Peak (8-bit)")
axes[2].set_title("Peak Magnitude (pre-gain, raw input)")
axes[2].legend(fontsize=7, loc="upper right")
axes[2].grid(True, alpha=0.3)
# Saturation count (POST-gain overflow)
axes[3].fill_between(xs, agc["sat_count"], color="red", alpha=0.4)
axes[3].plot(xs, agc["sat_count"], color="red", linewidth=0.8)
if enable_frame > 0:
axes[3].axvline(x=enable_frame, color="yellow", linewidth=2,
linestyle="--", alpha=0.8)
axes[3].set_ylabel("Overflow Count")
total = int(agc["sat_count"].sum())
axes[3].set_title(f"Post-Gain Overflow (total={total})")
axes[3].grid(True, alpha=0.3)
# RMS signal level (post-gain)
axes[4].plot(xs, agc["rms_post"], color="#44aaff", linewidth=1.0)
if enable_frame > 0:
axes[4].axvline(x=enable_frame, color="yellow", linewidth=2,
linestyle="--", alpha=0.8)
axes[4].set_ylabel("RMS")
axes[4].set_xlabel("Frame")
axes[4].set_title("Post-Gain RMS Level")
axes[4].grid(True, alpha=0.3)
def analyze_dataset(data: np.ndarray, label: str):
"""Run 3-scenario analysis for one dataset."""
n_frames = data.shape[0]
mid = n_frames // 2
print(f"\n{'='*60}")
print(f" {label} — shape {data.shape}")
print(f"{'='*60}")
# Raw ADC stats
raw_sat = np.sum((np.abs(data.real) >= ADC_RAIL) |
(np.abs(data.imag) >= ADC_RAIL))
print(f" Raw ADC saturation: {raw_sat} samples "
f"({100*raw_sat/(2*data.size):.2f}%)")
# Scenario 1: AGC OFF — pass-through (gain_shift=0x00)
print(" [1/3] AGC OFF (gain=0, pass-through) ...")
agc_off = simulate_agc(data, agc_enabled=False, initial_gain_enc=0x00)
print(f" Post-gain overflow: {agc_off['sat_count'].sum()} "
f"(should be 0 — no amplification)")
# Scenario 2: AGC ON from frame 0
print(" [2/3] AGC ON (from start) ...")
agc_on = simulate_agc(data, agc_enabled=True, enable_at_frame=0,
initial_gain_enc=0x00)
print(f" Final gain: {agc_on['gain_signed'][-1]} "
f"(enc=0x{agc_on['gain_enc'][-1]:X})")
print(f" Post-gain overflow: {agc_on['sat_count'].sum()}")
# Scenario 3: AGC delayed
print(f" [3/3] AGC delayed (ON at frame {mid}) ...")
agc_delayed = simulate_agc(data, agc_enabled=True,
enable_at_frame=mid,
initial_gain_enc=0x00)
pre_sat = int(agc_delayed["sat_count"][:mid].sum())
post_sat = int(agc_delayed["sat_count"][mid:].sum())
print(f" Pre-AGC overflow: {pre_sat} "
f"Post-AGC overflow: {post_sat}")
# Plot
fig, axes = plt.subplots(3, 5, figsize=(28, 14))
fig.suptitle(f"AERIS-10 AGC Analysis — {label}\n"
f"({n_frames} frames, {data.shape[1]} chirps, "
f"{data.shape[2]} samples/chirp, "
f"raw ADC sat={100*raw_sat/(2*data.size):.2f}%)",
fontsize=13, fontweight="bold", y=0.99)
plot_scenario(axes[0], data, agc_off, "AGC OFF (pass-through)")
plot_scenario(axes[1], data, agc_on, "AGC ON (from start)")
plot_scenario(axes[2], data, agc_delayed,
f"AGC delayed (ON at frame {mid})", enable_frame=mid)
for ax, lbl in zip(axes[:, 0],
["AGC OFF", "AGC ON", "AGC DELAYED"],
strict=True):
ax.annotate(lbl, xy=(-0.35, 0.5), xycoords="axes fraction",
fontsize=13, fontweight="bold", color="white",
ha="center", va="center", rotation=90)
plt.tight_layout(rect=[0.03, 0, 1, 0.95])
return fig
def main():
parser = argparse.ArgumentParser(
description="AGC analysis for ADI raw IQ captures "
"(bit-accurate rx_gain_control.v simulation)")
parser.add_argument("--amp", type=str,
default=str(Path.home() / "Downloads/adi_radar_data"
"/amp_radar"
"/phaser_amp_4MSPS_500M_300u_256_m3dB.npy"),
help="Path to amplified radar .npy")
parser.add_argument("--noamp", type=str,
default=str(Path.home() / "Downloads/adi_radar_data"
"/no_amp_radar"
"/phaser_NOamp_4MSPS_500M_300u_256.npy"),
help="Path to non-amplified radar .npy")
parser.add_argument("--data", type=str, default=None,
help="Single dataset mode")
parser.add_argument("--label", type=str, default="Custom Data")
args = parser.parse_args()
plt.style.use("dark_background")
if args.data:
data = np.load(args.data)
analyze_dataset(data, args.label)
plt.show()
return
figs = []
for path, label in [(args.amp, "With Amplifier (-3 dB)"),
(args.noamp, "No Amplifier")]:
if not Path(path).exists():
print(f"WARNING: {path} not found, skipping")
continue
data = np.load(path)
fig = analyze_dataset(data, label)
figs.append(fig)
if not figs:
print("No data found. Use --amp/--noamp or --data.")
sys.exit(1)
plt.show()
if __name__ == "__main__":
main()
+6 -200
View File
@@ -6,7 +6,6 @@ Pure-logic module for USB packet parsing and command building.
No GUI dependencies safe to import from tests and headless scripts. No GUI dependencies safe to import from tests and headless scripts.
USB Interface: FT2232H USB 2.0 (8-bit, 50T production board) via pyftdi USB Interface: FT2232H USB 2.0 (8-bit, 50T production board) via pyftdi
FT601 USB 3.0 (32-bit, 200T premium board) via ftd3xx
USB Packet Protocol (11-byte): USB Packet Protocol (11-byte):
TX (FPGAHost): TX (FPGAHost):
@@ -23,7 +22,7 @@ import queue
import logging import logging
import contextlib import contextlib
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, ClassVar from typing import Any
from enum import IntEnum from enum import IntEnum
@@ -201,9 +200,7 @@ class RadarProtocol:
range_i = _to_signed16(struct.unpack_from(">H", raw, 3)[0]) range_i = _to_signed16(struct.unpack_from(">H", raw, 3)[0])
doppler_i = _to_signed16(struct.unpack_from(">H", raw, 5)[0]) doppler_i = _to_signed16(struct.unpack_from(">H", raw, 5)[0])
doppler_q = _to_signed16(struct.unpack_from(">H", raw, 7)[0]) doppler_q = _to_signed16(struct.unpack_from(">H", raw, 7)[0])
det_byte = raw[9] detection = raw[9] & 0x01
detection = det_byte & 0x01
frame_start = (det_byte >> 7) & 0x01
return { return {
"range_i": range_i, "range_i": range_i,
@@ -211,7 +208,6 @@ class RadarProtocol:
"doppler_i": doppler_i, "doppler_i": doppler_i,
"doppler_q": doppler_q, "doppler_q": doppler_q,
"detection": detection, "detection": detection,
"frame_start": frame_start,
} }
@staticmethod @staticmethod
@@ -437,191 +433,7 @@ class FT2232HConnection:
pkt += struct.pack(">h", np.clip(range_i, -32768, 32767)) pkt += struct.pack(">h", np.clip(range_i, -32768, 32767))
pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767)) pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767))
pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767)) pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767))
# Bit 7 = frame_start (sample_counter == 0), bit 0 = detection pkt.append(detection & 0x01)
det_byte = (detection & 0x01) | (0x80 if idx == 0 else 0x00)
pkt.append(det_byte)
pkt.append(FOOTER_BYTE)
buf += pkt
self._mock_seq_idx = (start_idx + num_packets) % NUM_CELLS
return bytes(buf)
# ============================================================================
# FT601 USB 3.0 Connection (premium board only)
# ============================================================================
# Optional ftd3xx import (FTDI's proprietary driver for FT60x USB 3.0 chips).
# pyftdi does NOT support FT601 — it only handles USB 2.0 chips (FT232H, etc.)
try:
import ftd3xx # type: ignore[import-untyped]
FTD3XX_AVAILABLE = True
_Ftd3xxError: type = ftd3xx.FTD3XXError # type: ignore[attr-defined]
except ImportError:
FTD3XX_AVAILABLE = False
_Ftd3xxError = OSError # fallback for type-checking; never raised
class FT601Connection:
"""
FT601 USB 3.0 SuperSpeed FIFO bridge premium board only.
The FT601 has a 32-bit data bus and runs at 100 MHz.
VID:PID = 0x0403:0x6030 or 0x6031 (FTDI FT60x).
Requires the ``ftd3xx`` library (``pip install ftd3xx`` on Windows,
or ``libft60x`` on Linux). This is FTDI's proprietary USB 3.0 driver;
``pyftdi`` only supports USB 2.0 and will NOT work with FT601.
Public contract matches FT2232HConnection so callers can swap freely.
"""
VID = 0x0403
PID_LIST: ClassVar[list[int]] = [0x6030, 0x6031]
def __init__(self, mock: bool = True):
self._mock = mock
self._dev = None
self._lock = threading.Lock()
self.is_open = False
# Mock state (reuses same synthetic data pattern)
self._mock_frame_num = 0
self._mock_rng = np.random.RandomState(42)
def open(self, device_index: int = 0) -> bool:
if self._mock:
self.is_open = True
log.info("FT601 mock device opened (no hardware)")
return True
if not FTD3XX_AVAILABLE:
log.error(
"ftd3xx library required for FT601 hardware — "
"install with: pip install ftd3xx"
)
return False
try:
self._dev = ftd3xx.create(device_index, ftd3xx.OPEN_BY_INDEX)
if self._dev is None:
log.error("No FT601 device found at index %d", device_index)
return False
# Verify chip configuration — only reconfigure if needed.
# setChipConfiguration triggers USB re-enumeration, which
# invalidates the device handle and requires a re-open cycle.
cfg = self._dev.getChipConfiguration()
needs_reconfig = (
cfg.FIFOMode != 0 # 245 FIFO mode
or cfg.ChannelConfig != 0 # 1 channel, 32-bit
or cfg.OptionalFeatureSupport != 0
)
if needs_reconfig:
cfg.FIFOMode = 0
cfg.ChannelConfig = 0
cfg.OptionalFeatureSupport = 0
self._dev.setChipConfiguration(cfg)
# Device re-enumerates — close stale handle, wait, re-open
self._dev.close()
self._dev = None
import time
time.sleep(2.0) # wait for USB re-enumeration
self._dev = ftd3xx.create(device_index, ftd3xx.OPEN_BY_INDEX)
if self._dev is None:
log.error("FT601 not found after reconfiguration")
return False
log.info("FT601 reconfigured and re-opened (index %d)", device_index)
self.is_open = True
log.info("FT601 device opened (index %d)", device_index)
return True
except (OSError, _Ftd3xxError) as e:
log.error("FT601 open failed: %s", e)
self._dev = None
return False
def close(self):
if self._dev is not None:
with contextlib.suppress(Exception):
self._dev.close()
self._dev = None
self.is_open = False
def read(self, size: int = 4096) -> bytes | None:
"""Read raw bytes from FT601. Returns None on error/timeout."""
if not self.is_open:
return None
if self._mock:
return self._mock_read(size)
with self._lock:
try:
data = self._dev.readPipe(0x82, size, raw=True)
return bytes(data) if data else None
except (OSError, _Ftd3xxError) as e:
log.error("FT601 read error: %s", e)
return None
def write(self, data: bytes) -> bool:
"""Write raw bytes to FT601. Data must be 4-byte aligned for 32-bit bus."""
if not self.is_open:
return False
if self._mock:
log.info(f"FT601 mock write: {data.hex()}")
return True
# Pad to 4-byte alignment (FT601 32-bit bus requirement).
# NOTE: Radar commands are already 4 bytes, so this should be a no-op.
remainder = len(data) % 4
if remainder:
data = data + b"\x00" * (4 - remainder)
with self._lock:
try:
written = self._dev.writePipe(0x02, data, raw=True)
return written == len(data)
except (OSError, _Ftd3xxError) as e:
log.error("FT601 write error: %s", e)
return False
def _mock_read(self, size: int) -> bytes:
"""Generate synthetic radar packets (same pattern as FT2232H mock)."""
time.sleep(0.05)
self._mock_frame_num += 1
buf = bytearray()
num_packets = min(NUM_CELLS, size // DATA_PACKET_SIZE)
start_idx = getattr(self, "_mock_seq_idx", 0)
for n in range(num_packets):
idx = (start_idx + n) % NUM_CELLS
rbin = idx // NUM_DOPPLER_BINS
dbin = idx % NUM_DOPPLER_BINS
range_i = int(self._mock_rng.normal(0, 100))
range_q = int(self._mock_rng.normal(0, 100))
if abs(rbin - 20) < 3:
range_i += 5000
range_q += 3000
dop_i = int(self._mock_rng.normal(0, 50))
dop_q = int(self._mock_rng.normal(0, 50))
if abs(rbin - 20) < 3 and abs(dbin - 8) < 2:
dop_i += 8000
dop_q += 4000
detection = 1 if (abs(rbin - 20) < 2 and abs(dbin - 8) < 2) else 0
pkt = bytearray()
pkt.append(HEADER_BYTE)
pkt += struct.pack(">h", np.clip(range_q, -32768, 32767))
pkt += struct.pack(">h", np.clip(range_i, -32768, 32767))
pkt += struct.pack(">h", np.clip(dop_i, -32768, 32767))
pkt += struct.pack(">h", np.clip(dop_q, -32768, 32767))
# Bit 7 = frame_start (sample_counter == 0), bit 0 = detection
det_byte = (detection & 0x01) | (0x80 if idx == 0 else 0x00)
pkt.append(det_byte)
pkt.append(FOOTER_BYTE) pkt.append(FOOTER_BYTE)
buf += pkt buf += pkt
@@ -788,12 +600,6 @@ class RadarAcquisition(threading.Thread):
if sample.get("detection", 0): if sample.get("detection", 0):
self._frame.detections[rbin, dbin] = 1 self._frame.detections[rbin, dbin] = 1
self._frame.detection_count += 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)
self._sample_idx += 1 self._sample_idx += 1
@@ -801,11 +607,11 @@ class RadarAcquisition(threading.Thread):
self._finalize_frame() self._finalize_frame()
def _finalize_frame(self): def _finalize_frame(self):
"""Complete frame: push to queue, record.""" """Complete frame: compute range profile, push to queue, record."""
self._frame.timestamp = time.time() self._frame.timestamp = time.time()
self._frame.frame_number = self._frame_num self._frame.frame_number = self._frame_num
# range_profile is already accumulated from FPGA range_i/range_q # Range profile = sum of magnitude across Doppler bins
# data in _ingest_sample(). No need to synthesize from doppler magnitude. self._frame.range_profile = np.sum(self._frame.magnitude, axis=1)
# Push to display queue (drop old if backed up) # Push to display queue (drop old if backed up)
try: try:
+1 -56
View File
@@ -16,7 +16,7 @@ import unittest
import numpy as np import numpy as np
from radar_protocol import ( from radar_protocol import (
RadarProtocol, FT2232HConnection, FT601Connection, DataRecorder, RadarAcquisition, RadarProtocol, FT2232HConnection, DataRecorder, RadarAcquisition,
RadarFrame, StatusResponse, Opcode, RadarFrame, StatusResponse, Opcode,
HEADER_BYTE, FOOTER_BYTE, STATUS_HEADER_BYTE, HEADER_BYTE, FOOTER_BYTE, STATUS_HEADER_BYTE,
NUM_RANGE_BINS, NUM_DOPPLER_BINS, NUM_RANGE_BINS, NUM_DOPPLER_BINS,
@@ -312,61 +312,6 @@ class TestFT2232HConnection(unittest.TestCase):
self.assertFalse(conn.write(b"\x00\x00\x00\x00")) self.assertFalse(conn.write(b"\x00\x00\x00\x00"))
class TestFT601Connection(unittest.TestCase):
"""Test mock FT601 connection (mirrors FT2232H tests)."""
def test_mock_open_close(self):
conn = FT601Connection(mock=True)
self.assertTrue(conn.open())
self.assertTrue(conn.is_open)
conn.close()
self.assertFalse(conn.is_open)
def test_mock_read_returns_data(self):
conn = FT601Connection(mock=True)
conn.open()
data = conn.read(4096)
self.assertIsNotNone(data)
self.assertGreater(len(data), 0)
conn.close()
def test_mock_read_contains_valid_packets(self):
"""Mock data should contain parseable data packets."""
conn = FT601Connection(mock=True)
conn.open()
raw = conn.read(4096)
packets = RadarProtocol.find_packet_boundaries(raw)
self.assertGreater(len(packets), 0)
for start, end, ptype in packets:
if ptype == "data":
result = RadarProtocol.parse_data_packet(raw[start:end])
self.assertIsNotNone(result)
conn.close()
def test_mock_write(self):
conn = FT601Connection(mock=True)
conn.open()
cmd = RadarProtocol.build_command(0x01, 1)
self.assertTrue(conn.write(cmd))
conn.close()
def test_write_pads_to_4_bytes(self):
"""FT601 write() should pad data to 4-byte alignment."""
conn = FT601Connection(mock=True)
conn.open()
# 3-byte payload should be padded internally (no error)
self.assertTrue(conn.write(b"\x01\x02\x03"))
conn.close()
def test_read_when_closed(self):
conn = FT601Connection(mock=True)
self.assertIsNone(conn.read())
def test_write_when_closed(self):
conn = FT601Connection(mock=True)
self.assertFalse(conn.write(b"\x00\x00\x00\x00"))
class TestDataRecorder(unittest.TestCase): class TestDataRecorder(unittest.TestCase):
"""Test HDF5 recording (skipped if h5py not available).""" """Test HDF5 recording (skipped if h5py not available)."""
+26 -17
View File
@@ -58,9 +58,9 @@ class TestRadarSettings(unittest.TestCase):
def test_has_physical_conversion_fields(self): def test_has_physical_conversion_fields(self):
s = _models().RadarSettings() s = _models().RadarSettings()
self.assertIsInstance(s.range_resolution, float) self.assertIsInstance(s.range_bin_spacing, float)
self.assertIsInstance(s.velocity_resolution, float) self.assertIsInstance(s.velocity_resolution, float)
self.assertGreater(s.range_resolution, 0) self.assertGreater(s.range_bin_spacing, 0)
self.assertGreater(s.velocity_resolution, 0) self.assertGreater(s.velocity_resolution, 0)
def test_defaults(self): def test_defaults(self):
@@ -432,27 +432,35 @@ class TestWaveformConfig(unittest.TestCase):
self.assertEqual(wc.center_freq_hz, 10.5e9) self.assertEqual(wc.center_freq_hz, 10.5e9)
self.assertEqual(wc.n_range_bins, 64) self.assertEqual(wc.n_range_bins, 64)
self.assertEqual(wc.n_doppler_bins, 32) self.assertEqual(wc.n_doppler_bins, 32)
self.assertEqual(wc.chirps_per_subframe, 16)
self.assertEqual(wc.fft_size, 1024) self.assertEqual(wc.fft_size, 1024)
self.assertEqual(wc.decimation_factor, 16) self.assertEqual(wc.decimation_factor, 16)
def test_range_resolution(self): def test_range_resolution(self):
"""range_resolution_m should be ~23.98 m/bin (matched filter, 100 MSPS).""" """bin_spacing_m should be ~24.0 m/bin with PLFM defaults."""
from v7.models import WaveformConfig from v7.models import WaveformConfig
wc = WaveformConfig() wc = WaveformConfig()
self.assertAlmostEqual(wc.range_resolution_m, 23.983, places=1) self.assertAlmostEqual(wc.bin_spacing_m, 23.98, places=1)
def test_range_resolution_physical(self):
"""range_resolution_m = c/(2*BW), ~7.5 m at 20 MHz BW."""
from v7.models import WaveformConfig
wc = WaveformConfig()
self.assertAlmostEqual(wc.range_resolution_m, 7.49, places=1)
# 30 MHz BW → 5.0 m resolution
wc30 = WaveformConfig(bandwidth_hz=30e6)
self.assertAlmostEqual(wc30.range_resolution_m, 4.996, places=1)
def test_velocity_resolution(self): def test_velocity_resolution(self):
"""velocity_resolution_mps should be ~5.34 m/s/bin (PRI=167us, 16 chirps).""" """velocity_resolution_mps should be ~2.67 m/s/bin."""
from v7.models import WaveformConfig from v7.models import WaveformConfig
wc = WaveformConfig() wc = WaveformConfig()
self.assertAlmostEqual(wc.velocity_resolution_mps, 5.343, places=1) self.assertAlmostEqual(wc.velocity_resolution_mps, 2.67, places=1)
def test_max_range(self): def test_max_range(self):
"""max_range_m = range_resolution * n_range_bins.""" """max_range_m = bin_spacing * n_range_bins."""
from v7.models import WaveformConfig from v7.models import WaveformConfig
wc = WaveformConfig() wc = WaveformConfig()
self.assertAlmostEqual(wc.max_range_m, wc.range_resolution_m * 64, places=1) self.assertAlmostEqual(wc.max_range_m, wc.bin_spacing_m * 64, places=1)
def test_max_velocity(self): def test_max_velocity(self):
"""max_velocity_mps = velocity_resolution * n_doppler_bins / 2.""" """max_velocity_mps = velocity_resolution * n_doppler_bins / 2."""
@@ -468,8 +476,9 @@ class TestWaveformConfig(unittest.TestCase):
"""Non-default parameters correctly change derived values.""" """Non-default parameters correctly change derived values."""
from v7.models import WaveformConfig from v7.models import WaveformConfig
wc1 = WaveformConfig() wc1 = WaveformConfig()
wc2 = WaveformConfig(sample_rate_hz=200e6) # double Fs → halve range bin # Matched-filter: bin_spacing = c/(2*fs)*dec — proportional to 1/fs
self.assertAlmostEqual(wc2.range_resolution_m, wc1.range_resolution_m / 2, places=2) wc2 = WaveformConfig(sample_rate_hz=200e6) # double fs → halve bin spacing
self.assertAlmostEqual(wc2.bin_spacing_m, wc1.bin_spacing_m / 2, places=2)
def test_zero_center_freq_velocity(self): def test_zero_center_freq_velocity(self):
"""Zero center freq should cause ZeroDivisionError in velocity calc.""" """Zero center freq should cause ZeroDivisionError in velocity calc."""
@@ -927,18 +936,18 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
"""Detection at range bin 10 → range = 10 * range_resolution.""" """Detection at range bin 10 → range = 10 * range_resolution."""
from v7.processing import extract_targets_from_frame from v7.processing import extract_targets_from_frame
frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0 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, bin_spacing=23.98)
self.assertEqual(len(targets), 1) self.assertEqual(len(targets), 1)
self.assertAlmostEqual(targets[0].range, 10 * 23.983, places=1) self.assertAlmostEqual(targets[0].range, 10 * 23.98, places=1)
self.assertAlmostEqual(targets[0].velocity, 0.0, places=2) self.assertAlmostEqual(targets[0].velocity, 0.0, places=2)
def test_velocity_sign(self): def test_velocity_sign(self):
"""Doppler bin < center → negative velocity, > center → positive.""" """Doppler bin < center → negative velocity, > center → positive."""
from v7.processing import extract_targets_from_frame from v7.processing import extract_targets_from_frame
frame = self._make_frame(det_cells=[(5, 10), (5, 20)]) frame = self._make_frame(det_cells=[(5, 10), (5, 20)])
targets = extract_targets_from_frame(frame, velocity_resolution=1.484) targets = extract_targets_from_frame(frame, velocity_resolution=2.67)
# dbin=10: vel = (10-16)*1.484 = -8.904 (approaching) # dbin=10: vel = (10-16)*2.67 = -16.02 (approaching)
# dbin=20: vel = (20-16)*1.484 = +5.936 (receding) # dbin=20: vel = (20-16)*2.67 = +10.68 (receding)
self.assertLess(targets[0].velocity, 0) self.assertLess(targets[0].velocity, 0)
self.assertGreater(targets[1].velocity, 0) self.assertGreater(targets[1].velocity, 0)
@@ -956,7 +965,7 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
pitch=0.0, heading=90.0) pitch=0.0, heading=90.0)
frame = self._make_frame(det_cells=[(10, 16)]) frame = self._make_frame(det_cells=[(10, 16)])
targets = extract_targets_from_frame( targets = extract_targets_from_frame(
frame, range_resolution=100.0, gps=gps) frame, bin_spacing=100.0, gps=gps)
# Should be roughly east of radar position # Should be roughly east of radar position
self.assertAlmostEqual(targets[0].latitude, 41.9, places=2) self.assertAlmostEqual(targets[0].latitude, 41.9, places=2)
self.assertGreater(targets[0].longitude, 12.5) self.assertGreater(targets[0].longitude, 12.5)
+1 -2
View File
@@ -26,7 +26,6 @@ from .models import (
# Hardware interfaces — production protocol via radar_protocol.py # Hardware interfaces — production protocol via radar_protocol.py
from .hardware import ( from .hardware import (
FT2232HConnection, FT2232HConnection,
FT601Connection,
RadarProtocol, RadarProtocol,
Opcode, Opcode,
RadarAcquisition, RadarAcquisition,
@@ -90,7 +89,7 @@ __all__ = [ # noqa: RUF022
"USB_AVAILABLE", "FTDI_AVAILABLE", "SCIPY_AVAILABLE", "USB_AVAILABLE", "FTDI_AVAILABLE", "SCIPY_AVAILABLE",
"SKLEARN_AVAILABLE", "FILTERPY_AVAILABLE", "SKLEARN_AVAILABLE", "FILTERPY_AVAILABLE",
# hardware — production FPGA protocol # hardware — production FPGA protocol
"FT2232HConnection", "FT601Connection", "RadarProtocol", "Opcode", "FT2232HConnection", "RadarProtocol", "Opcode",
"RadarAcquisition", "RadarFrame", "StatusResponse", "DataRecorder", "RadarAcquisition", "RadarFrame", "StatusResponse", "DataRecorder",
"STM32USBInterface", "STM32USBInterface",
# processing # processing
+8 -37
View File
@@ -13,14 +13,13 @@ RadarDashboard is a QMainWindow with six tabs:
6. Settings Host-side DSP parameters + About section 6. Settings Host-side DSP parameters + About section
Uses production radar_protocol.py for all FPGA communication: Uses production radar_protocol.py for all FPGA communication:
- FT2232HConnection for production board (FT2232H USB 2.0) - FT2232HConnection for real hardware
- FT601Connection for premium board (FT601 USB 3.0) selectable from GUI
- Unified replay via SoftwareFPGA + ReplayEngine + ReplayWorker - Unified replay via SoftwareFPGA + ReplayEngine + ReplayWorker
- Mock mode (FT2232HConnection(mock=True)) for development - Mock mode (FT2232HConnection(mock=True)) for development
The old STM32 magic-packet start flow has been removed. FPGA registers The old STM32 magic-packet start flow has been removed. FPGA registers
are controlled directly via 4-byte {opcode, addr, value_hi, value_lo} are controlled directly via 4-byte {opcode, addr, value_hi, value_lo}
commands sent over FT2232H or FT601. commands sent over FT2232H.
""" """
from __future__ import annotations from __future__ import annotations
@@ -56,7 +55,6 @@ from .models import (
) )
from .hardware import ( from .hardware import (
FT2232HConnection, FT2232HConnection,
FT601Connection,
RadarProtocol, RadarProtocol,
RadarFrame, RadarFrame,
StatusResponse, StatusResponse,
@@ -144,7 +142,7 @@ class RadarDashboard(QMainWindow):
) )
# Hardware interfaces — production protocol # Hardware interfaces — production protocol
self._connection: FT2232HConnection | FT601Connection | None = None self._connection: FT2232HConnection | None = None
self._stm32 = STM32USBInterface() self._stm32 = STM32USBInterface()
self._recorder = DataRecorder() self._recorder = DataRecorder()
@@ -366,7 +364,7 @@ class RadarDashboard(QMainWindow):
# Row 0: connection mode + device combos + buttons # Row 0: connection mode + device combos + buttons
ctrl_layout.addWidget(QLabel("Mode:"), 0, 0) ctrl_layout.addWidget(QLabel("Mode:"), 0, 0)
self._mode_combo = QComboBox() self._mode_combo = QComboBox()
self._mode_combo.addItems(["Mock", "Live", "Replay"]) self._mode_combo.addItems(["Mock", "Live FT2232H", "Replay"])
self._mode_combo.setCurrentIndex(0) self._mode_combo.setCurrentIndex(0)
ctrl_layout.addWidget(self._mode_combo, 0, 1) ctrl_layout.addWidget(self._mode_combo, 0, 1)
@@ -379,13 +377,6 @@ class RadarDashboard(QMainWindow):
refresh_btn.clicked.connect(self._refresh_devices) refresh_btn.clicked.connect(self._refresh_devices)
ctrl_layout.addWidget(refresh_btn, 0, 4) ctrl_layout.addWidget(refresh_btn, 0, 4)
# USB Interface selector (production FT2232H / premium FT601)
ctrl_layout.addWidget(QLabel("USB Interface:"), 0, 5)
self._usb_iface_combo = QComboBox()
self._usb_iface_combo.addItems(["FT2232H (Production)", "FT601 (Premium)"])
self._usb_iface_combo.setCurrentIndex(0)
ctrl_layout.addWidget(self._usb_iface_combo, 0, 6)
self._start_btn = QPushButton("Start Radar") self._start_btn = QPushButton("Start Radar")
self._start_btn.setStyleSheet( self._start_btn.setStyleSheet(
f"QPushButton {{ background-color: {DARK_SUCCESS}; color: white; font-weight: bold; }}" f"QPushButton {{ background-color: {DARK_SUCCESS}; color: white; font-weight: bold; }}"
@@ -1010,8 +1001,7 @@ class RadarDashboard(QMainWindow):
self._conn_ft2232h = self._make_status_label("FT2232H") self._conn_ft2232h = self._make_status_label("FT2232H")
self._conn_stm32 = self._make_status_label("STM32 USB") self._conn_stm32 = self._make_status_label("STM32 USB")
self._conn_usb_label = QLabel("USB Data:") conn_layout.addWidget(QLabel("FT2232H:"), 0, 0)
conn_layout.addWidget(self._conn_usb_label, 0, 0)
conn_layout.addWidget(self._conn_ft2232h, 0, 1) conn_layout.addWidget(self._conn_ft2232h, 0, 1)
conn_layout.addWidget(QLabel("STM32 USB:"), 1, 0) conn_layout.addWidget(QLabel("STM32 USB:"), 1, 0)
conn_layout.addWidget(self._conn_stm32, 1, 1) conn_layout.addWidget(self._conn_stm32, 1, 1)
@@ -1177,7 +1167,7 @@ class RadarDashboard(QMainWindow):
about_lbl = QLabel( about_lbl = QLabel(
"<b>AERIS-10 Radar System V7</b><br>" "<b>AERIS-10 Radar System V7</b><br>"
"PyQt6 Edition with Embedded Leaflet Map<br><br>" "PyQt6 Edition with Embedded Leaflet Map<br><br>"
"<b>Data Interface:</b> FT2232H USB 2.0 (production) / FT601 USB 3.0 (premium)<br>" "<b>Data Interface:</b> FT2232H USB 2.0 (production protocol)<br>"
"<b>FPGA Protocol:</b> 4-byte register commands, 0xAA/0xBB packets<br>" "<b>FPGA Protocol:</b> 4-byte register commands, 0xAA/0xBB packets<br>"
"<b>Map:</b> OpenStreetMap + Leaflet.js<br>" "<b>Map:</b> OpenStreetMap + Leaflet.js<br>"
"<b>Framework:</b> PyQt6 + QWebEngine<br>" "<b>Framework:</b> PyQt6 + QWebEngine<br>"
@@ -1234,7 +1224,7 @@ class RadarDashboard(QMainWindow):
# ===================================================================== # =====================================================================
def _send_fpga_cmd(self, opcode: int, value: int): def _send_fpga_cmd(self, opcode: int, value: int):
"""Send a 4-byte register command to the FPGA via USB (FT2232H or FT601).""" """Send a 4-byte register command to the FPGA via FT2232H."""
if self._connection is None or not self._connection.is_open: if self._connection is None or not self._connection.is_open:
logger.warning(f"Cannot send 0x{opcode:02X}={value}: no connection") logger.warning(f"Cannot send 0x{opcode:02X}={value}: no connection")
return return
@@ -1297,26 +1287,16 @@ class RadarDashboard(QMainWindow):
if "Mock" in mode: if "Mock" in mode:
self._replay_mode = False self._replay_mode = False
iface = self._usb_iface_combo.currentText()
if "FT601" in iface:
self._connection = FT601Connection(mock=True)
else:
self._connection = FT2232HConnection(mock=True) self._connection = FT2232HConnection(mock=True)
if not self._connection.open(): if not self._connection.open():
QMessageBox.critical(self, "Error", "Failed to open mock connection.") QMessageBox.critical(self, "Error", "Failed to open mock connection.")
return return
elif "Live" in mode: elif "Live" in mode:
self._replay_mode = False self._replay_mode = False
iface = self._usb_iface_combo.currentText()
if "FT601" in iface:
self._connection = FT601Connection(mock=False)
iface_name = "FT601"
else:
self._connection = FT2232HConnection(mock=False) self._connection = FT2232HConnection(mock=False)
iface_name = "FT2232H"
if not self._connection.open(): if not self._connection.open():
QMessageBox.critical(self, "Error", QMessageBox.critical(self, "Error",
f"Failed to open {iface_name}. Check USB connection.") "Failed to open FT2232H. Check USB connection.")
return return
elif "Replay" in mode: elif "Replay" in mode:
self._replay_mode = True self._replay_mode = True
@@ -1388,7 +1368,6 @@ class RadarDashboard(QMainWindow):
self._start_btn.setEnabled(False) self._start_btn.setEnabled(False)
self._stop_btn.setEnabled(True) self._stop_btn.setEnabled(True)
self._mode_combo.setEnabled(False) self._mode_combo.setEnabled(False)
self._usb_iface_combo.setEnabled(False)
self._demo_btn_main.setEnabled(False) self._demo_btn_main.setEnabled(False)
self._demo_btn_map.setEnabled(False) self._demo_btn_map.setEnabled(False)
n_frames = self._replay_engine.total_frames n_frames = self._replay_engine.total_frames
@@ -1438,7 +1417,6 @@ class RadarDashboard(QMainWindow):
self._start_btn.setEnabled(False) self._start_btn.setEnabled(False)
self._stop_btn.setEnabled(True) self._stop_btn.setEnabled(True)
self._mode_combo.setEnabled(False) self._mode_combo.setEnabled(False)
self._usb_iface_combo.setEnabled(False)
self._demo_btn_main.setEnabled(False) self._demo_btn_main.setEnabled(False)
self._demo_btn_map.setEnabled(False) self._demo_btn_map.setEnabled(False)
self._status_label_main.setText(f"Status: Running ({mode})") self._status_label_main.setText(f"Status: Running ({mode})")
@@ -1484,7 +1462,6 @@ class RadarDashboard(QMainWindow):
self._start_btn.setEnabled(True) self._start_btn.setEnabled(True)
self._stop_btn.setEnabled(False) self._stop_btn.setEnabled(False)
self._mode_combo.setEnabled(True) self._mode_combo.setEnabled(True)
self._usb_iface_combo.setEnabled(True)
self._demo_btn_main.setEnabled(True) self._demo_btn_main.setEnabled(True)
self._demo_btn_map.setEnabled(True) self._demo_btn_map.setEnabled(True)
self._status_label_main.setText("Status: Radar stopped") self._status_label_main.setText("Status: Radar stopped")
@@ -1977,12 +1954,6 @@ class RadarDashboard(QMainWindow):
self._set_conn_indicator(self._conn_ft2232h, conn_open) self._set_conn_indicator(self._conn_ft2232h, conn_open)
self._set_conn_indicator(self._conn_stm32, self._stm32.is_open) self._set_conn_indicator(self._conn_stm32, self._stm32.is_open)
# Update USB label to reflect which interface is active
if isinstance(self._connection, FT601Connection):
self._conn_usb_label.setText("FT601:")
else:
self._conn_usb_label.setText("FT2232H:")
gps_count = self._gps_packet_count gps_count = self._gps_packet_count
if self._gps_worker: if self._gps_worker:
gps_count = self._gps_worker.gps_count gps_count = self._gps_worker.gps_count
+2 -4
View File
@@ -25,7 +25,6 @@ if USB_AVAILABLE:
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from radar_protocol import ( # noqa: F401 — re-exported for v7 package from radar_protocol import ( # noqa: F401 — re-exported for v7 package
FT2232HConnection, FT2232HConnection,
FT601Connection,
RadarProtocol, RadarProtocol,
Opcode, Opcode,
RadarAcquisition, RadarAcquisition,
@@ -47,9 +46,8 @@ class STM32USBInterface:
Used ONLY for receiving GPS data from the MCU. Used ONLY for receiving GPS data from the MCU.
FPGA register commands are sent via the USB data interface either FPGA register commands are sent via FT2232H (see FT2232HConnection
FT2232HConnection (production) or FT601Connection (premium), both from radar_protocol.py). The old send_start_flag() / send_settings()
from radar_protocol.py. The old send_start_flag() / send_settings()
methods have been removed they used an incompatible magic-packet methods have been removed they used an incompatible magic-packet
protocol that the FPGA does not understand. protocol that the FPGA does not understand.
""" """
+1 -1
View File
@@ -98,7 +98,7 @@ class RadarMapWidget(QWidget):
) )
self._targets: list[RadarTarget] = [] self._targets: list[RadarTarget] = []
self._pending_targets: list[RadarTarget] | None = None self._pending_targets: list[RadarTarget] | None = None
self._coverage_radius = 1_536 # metres (64 bins x ~24 m/bin) self._coverage_radius = 1_536 # metres (64 bins x 24 m, 3 km mode)
self._tile_server = TileServer.OPENSTREETMAP self._tile_server = TileServer.OPENSTREETMAP
self._show_coverage = True self._show_coverage = True
self._show_trails = False self._show_trails = False
+37 -30
View File
@@ -105,14 +105,14 @@ class RadarSettings:
tab and Opcode enum in radar_protocol.py. This dataclass holds only tab and Opcode enum in radar_protocol.py. This dataclass holds only
host-side display/map settings and physical-unit conversion factors. host-side display/map settings and physical-unit conversion factors.
range_resolution and velocity_resolution should be calibrated to range_bin_spacing and velocity_resolution should be calibrated to
the actual waveform parameters. the actual waveform parameters.
""" """
system_frequency: float = 10.5e9 # Hz (carrier, used for velocity calc) system_frequency: float = 10.5e9 # Hz (PLFM TX LO, verified from ADF4382 config)
range_resolution: float = 24.0 # Meters per range bin (c/(2*Fs)*decim) range_bin_spacing: float = 24.0 # Meters per decimated range bin (c/(2*100MSPS)*16)
velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform) velocity_resolution: float = 2.67 # m/s per Doppler bin (lam/(2*32*167us))
max_distance: float = 1536 # Max detection range (m) max_distance: float = 1536 # Max detection range (m) -- 64 bins x 24 m (3 km mode)
map_size: float = 2000 # Map display size (m) map_size: float = 1536 # Map display size (m)
coverage_radius: float = 1536 # Map coverage radius (m) coverage_radius: float = 1536 # Map coverage radius (m)
@@ -196,54 +196,61 @@ class TileServer(Enum):
class WaveformConfig: class WaveformConfig:
"""Physical waveform parameters for converting bins to SI units. """Physical waveform parameters for converting bins to SI units.
Encapsulates the radar waveform so that range/velocity resolution Encapsulates the PLFM radar waveform so that range/velocity resolution
can be derived automatically instead of hardcoded in RadarSettings. can be derived automatically instead of hardcoded in RadarSettings.
Defaults match the AERIS-10 production system parameters from Defaults match the PLFM hardware: 100 MSPS post-DDC processing rate,
radar_scene.py / plfm_chirp_controller.v: 20 MHz chirp bandwidth, 30 us long chirp, 167 us PRI, 10.5 GHz carrier.
100 MSPS DDC output, 20 MHz chirp BW, 30 us long chirp, The receiver uses matched-filter pulse compression (NOT deramped FMCW),
167 us long-chirp PRI, X-band 10.5 GHz carrier. so range-per-bin = c / (2 * fs_processing) * decimation_factor.
""" """
sample_rate_hz: float = 100e6 # DDC output I/Q rate (matched filter input) sample_rate_hz: float = 100e6 # Post-DDC processing rate (400 MSPS / 4)
bandwidth_hz: float = 20e6 # Chirp bandwidth (not used in range calc; bandwidth_hz: float = 20e6 # Chirp bandwidth (Phase 1 target: 30 MHz)
# retained for time-bandwidth product / display) chirp_duration_s: float = 30e-6 # Long chirp ramp (informational only)
chirp_duration_s: float = 30e-6 # Long chirp ramp time
pri_s: float = 167e-6 # Pulse repetition interval (chirp + listen) pri_s: float = 167e-6 # Pulse repetition interval (chirp + listen)
center_freq_hz: float = 10.5e9 # Carrier frequency (radar_scene.py: F_CARRIER) center_freq_hz: float = 10.5e9 # TX LO carrier (verified: ADF4382 config)
n_range_bins: int = 64 # After decimation n_range_bins: int = 64 # After decimation (3 km mode)
n_doppler_bins: int = 32 # Total Doppler bins (2 sub-frames x 16) n_doppler_bins: int = 32 # After Doppler FFT
chirps_per_subframe: int = 16 # Chirps in one Doppler sub-frame
fft_size: int = 1024 # Pre-decimation FFT length fft_size: int = 1024 # Pre-decimation FFT length
decimation_factor: int = 16 # 1024 → 64 decimation_factor: int = 16 # 1024 → 64
@property @property
def range_resolution_m(self) -> float: def bin_spacing_m(self) -> float:
"""Meters per decimated range bin (matched-filter pulse compression). """Meters per decimated range bin (matched-filter receiver).
For FFT-based matched filtering, each IFFT output bin spans For matched-filter pulse compression: bin spacing = c / (2 * fs).
c / (2 * Fs) in range, where Fs is the I/Q sample rate at the After decimation the bin spacing grows by *decimation_factor*.
matched-filter input (DDC output). After decimation the bin This is independent of chirp bandwidth (BW affects physical
spacing grows by *decimation_factor*. resolution, not bin spacing).
""" """
c = 299_792_458.0 c = 299_792_458.0
raw_bin = c / (2.0 * self.sample_rate_hz) raw_bin = c / (2.0 * self.sample_rate_hz)
return raw_bin * self.decimation_factor return raw_bin * self.decimation_factor
@property @property
def velocity_resolution_mps(self) -> float: def range_resolution_m(self) -> float:
"""m/s per Doppler bin. """Physical range resolution in meters, set by chirp bandwidth.
lambda / (2 * chirps_per_subframe * PRI), matching radar_scene.py. range_resolution = c / (2 * BW).
At 20 MHz BW 7.5 m; at 30 MHz BW 5.0 m.
This is distinct from bin_spacing_m (which depends on sample rate
and decimation factor, not bandwidth).
""" """
c = 299_792_458.0 c = 299_792_458.0
return c / (2.0 * self.bandwidth_hz)
@property
def velocity_resolution_mps(self) -> float:
"""m/s per Doppler bin. lambda / (2 * n_doppler * PRI)."""
c = 299_792_458.0
wavelength = c / self.center_freq_hz wavelength = c / self.center_freq_hz
return wavelength / (2.0 * self.chirps_per_subframe * self.pri_s) return wavelength / (2.0 * self.n_doppler_bins * self.pri_s)
@property @property
def max_range_m(self) -> float: def max_range_m(self) -> float:
"""Maximum unambiguous range in meters.""" """Maximum unambiguous range in meters."""
return self.range_resolution_m * self.n_range_bins return self.bin_spacing_m * self.n_range_bins
@property @property
def max_velocity_mps(self) -> float: def max_velocity_mps(self) -> float:
+4 -4
View File
@@ -490,7 +490,7 @@ def polar_to_geographic(
def extract_targets_from_frame( def extract_targets_from_frame(
frame, frame,
range_resolution: float = 1.0, bin_spacing: float = 1.0,
velocity_resolution: float = 1.0, velocity_resolution: float = 1.0,
gps: GPSData | None = None, gps: GPSData | None = None,
) -> list[RadarTarget]: ) -> list[RadarTarget]:
@@ -503,8 +503,8 @@ def extract_targets_from_frame(
---------- ----------
frame : RadarFrame frame : RadarFrame
Frame with populated ``detections``, ``magnitude``, ``range_doppler_i/q``. Frame with populated ``detections``, ``magnitude``, ``range_doppler_i/q``.
range_resolution : float bin_spacing : float
Meters per range bin. Meters per range bin (bin spacing, NOT bandwidth-limited resolution).
velocity_resolution : float velocity_resolution : float
m/s per Doppler bin. m/s per Doppler bin.
gps : GPSData | None gps : GPSData | None
@@ -525,7 +525,7 @@ def extract_targets_from_frame(
mag = float(frame.magnitude[rbin, dbin]) mag = float(frame.magnitude[rbin, dbin])
snr = 10.0 * math.log10(max(mag, 1.0)) if mag > 0 else 0.0 snr = 10.0 * math.log10(max(mag, 1.0)) if mag > 0 else 0.0
range_m = float(rbin) * range_resolution range_m = float(rbin) * bin_spacing
velocity_ms = float(dbin - doppler_center) * velocity_resolution velocity_ms = float(dbin - doppler_center) * velocity_resolution
lat, lon, azimuth, elevation = 0.0, 0.0, 0.0, 0.0 lat, lon, azimuth, elevation = 0.0, 0.0, 0.0, 0.0
+5 -5
View File
@@ -169,7 +169,7 @@ class RadarDataWorker(QThread):
The FPGA already does: FFT, MTI, CFAR, DC notch. The FPGA already does: FFT, MTI, CFAR, DC notch.
Host-side DSP adds: clustering, tracking, geo-coordinate mapping. Host-side DSP adds: clustering, tracking, geo-coordinate mapping.
Bin-to-physical conversion uses RadarSettings.range_resolution Bin-to-physical conversion uses RadarSettings.range_bin_spacing
and velocity_resolution (should be calibrated to actual waveform). and velocity_resolution (should be calibrated to actual waveform).
""" """
targets: list[RadarTarget] = [] targets: list[RadarTarget] = []
@@ -180,7 +180,7 @@ class RadarDataWorker(QThread):
# Extract detections from FPGA CFAR flags # Extract detections from FPGA CFAR flags
det_indices = np.argwhere(frame.detections > 0) det_indices = np.argwhere(frame.detections > 0)
r_res = self._settings.range_resolution r_res = self._settings.range_bin_spacing
v_res = self._settings.velocity_resolution v_res = self._settings.velocity_resolution
for idx in det_indices: for idx in det_indices:
@@ -334,7 +334,7 @@ class TargetSimulator(QObject):
self._add_random_target() self._add_random_target()
def _add_random_target(self): def _add_random_target(self):
range_m = random.uniform(50, 1400) range_m = random.uniform(5000, 40000)
azimuth = random.uniform(0, 360) azimuth = random.uniform(0, 360)
velocity = random.uniform(-100, 100) velocity = random.uniform(-100, 100)
elevation = random.uniform(-5, 45) elevation = random.uniform(-5, 45)
@@ -368,7 +368,7 @@ class TargetSimulator(QObject):
for t in self._targets: for t in self._targets:
new_range = t.range - t.velocity * 0.5 new_range = t.range - t.velocity * 0.5
if new_range < 10 or new_range > 1536: if new_range < 50 or new_range > 1536:
continue # target exits coverage — drop it continue # target exits coverage — drop it
new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2))) new_vel = max(-150, min(150, t.velocity + random.uniform(-2, 2)))
@@ -559,7 +559,7 @@ class ReplayWorker(QThread):
# Target extraction # Target extraction
targets = self._extract_targets( targets = self._extract_targets(
frame, frame,
range_resolution=self._waveform.range_resolution_m, bin_spacing=self._waveform.bin_spacing_m,
velocity_resolution=self._waveform.velocity_resolution_mps, velocity_resolution=self._waveform.velocity_resolution_mps,
gps=self._gps, gps=self._gps,
) )
+57 -42
View File
@@ -188,7 +188,7 @@ def parse_python_data_packet_fields(filepath: Path | None = None) -> list[DataPa
width_bits=size * 8 width_bits=size * 8
)) ))
# Match detection = raw[9] & 0x01 (direct access) # Match detection = raw[9] & 0x01
for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]\s*&\s*(0x[0-9a-fA-F]+|\d+)', body): for m in re.finditer(r'(\w+)\s*=\s*raw\[(\d+)\]\s*&\s*(0x[0-9a-fA-F]+|\d+)', body):
name = m.group(1) name = m.group(1)
offset = int(m.group(2)) offset = int(m.group(2))
@@ -196,24 +196,6 @@ def parse_python_data_packet_fields(filepath: Path | None = None) -> list[DataPa
name=name, byte_start=offset, byte_end=offset, width_bits=1 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) fields.sort(key=lambda f: f.byte_start)
return fields return fields
@@ -601,28 +583,12 @@ def parse_verilog_data_mux(
for m in re.finditer( for m in re.finditer(
r"5'd(\d+)\s*:\s*data_pkt_byte\s*=\s*(.+?);", r"5'd(\d+)\s*:\s*data_pkt_byte\s*=\s*(.+?);",
mux_body, re.DOTALL mux_body
): ):
idx = int(m.group(1)) idx = int(m.group(1))
expr = m.group(2).strip() expr = m.group(2).strip()
entries.append((idx, expr)) 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 # Group consecutive bytes by signal root name
fields: list[DataPacketField] = [] fields: list[DataPacketField] = []
i = 0 i = 0
@@ -632,21 +598,22 @@ def parse_verilog_data_mux(
i += 1 i += 1
continue continue
signal = _extract_signal(expr) # Extract signal name (e.g., range_profile_cap from range_profile_cap[31:24])
if not signal: sig_match = re.match(r'(\w+?)(?:\[|$)', expr)
if not sig_match:
i += 1 i += 1
continue continue
signal = sig_match.group(1)
start_byte = idx start_byte = idx
end_byte = idx end_byte = idx
# Find consecutive bytes of the same signal # Find consecutive bytes of the same signal
j = i + 1 j = i + 1
while j < len(entries): while j < len(entries):
_next_idx, next_expr = entries[j] next_idx, next_expr = entries[j]
next_sig = _extract_signal(next_expr) if next_expr.startswith(signal):
if next_sig == signal: end_byte = next_idx
end_byte = _next_idx
j += 1 j += 1
else: else:
break break
@@ -826,3 +793,51 @@ def parse_stm32_gpio_init(filepath: Path | None = None) -> list[GpioPin]:
)) ))
return pins return pins
# ===================================================================
# FPGA radar_params.vh parser
# ===================================================================
def parse_radar_params_vh() -> dict[str, int]:
"""
Parse `define values from radar_params.vh.
Returns dict like {"RP_FFT_SIZE": 1024, "RP_DECIMATION_FACTOR": 16, ...}.
Only parses defines with simple integer or Verilog literal values.
Skips bit-width prefixed literals (e.g. 2'b00) — returns the numeric value.
"""
path = FPGA_DIR / "radar_params.vh"
text = path.read_text()
params: dict[str, int] = {}
for m in re.finditer(
r'`define\s+(RP_\w+)\s+(\S+)', text
):
name = m.group(1)
val_str = m.group(2).rstrip()
# Skip non-numeric defines (like RADAR_PARAMS_VH guard)
if name == "RADAR_PARAMS_VH":
continue
# Handle Verilog bit-width literals: 2'b00, 8'h30, etc.
verilog_lit = re.match(r"\d+'([bhd])(\w+)", val_str)
if verilog_lit:
base_char = verilog_lit.group(1)
digits = verilog_lit.group(2)
base = {"b": 2, "h": 16, "d": 10}[base_char]
params[name] = int(digits, base)
continue
# Handle parenthesized expressions like (`RP_X * `RP_Y)
if "(" in val_str or "`" in val_str:
continue # Skip computed defines
# Plain integer
try:
params[name] = int(val_str)
except ValueError:
continue
return params
@@ -620,10 +620,8 @@ module tb_cross_layer_ft2232h;
"Data pkt: byte 7 = 0x56 (doppler_imag MSB)"); "Data pkt: byte 7 = 0x56 (doppler_imag MSB)");
check(captured_bytes[8] === 8'h78, check(captured_bytes[8] === 8'h78,
"Data pkt: byte 8 = 0x78 (doppler_imag LSB)"); "Data pkt: byte 8 = 0x78 (doppler_imag LSB)");
// Byte 9 = {frame_start, 6'b0, cfar_detection} check(captured_bytes[9] === 8'h01,
// After reset sample_counter==0, so frame_start=1 → 0x81 "Data pkt: byte 9 = 0x01 (cfar_detection=1)");
check(captured_bytes[9] === 8'h81,
"Data pkt: byte 9 = 0x81 (frame_start=1, cfar_detection=1)");
check(captured_bytes[10] === 8'h55, check(captured_bytes[10] === 8'h55,
"Data pkt: byte 10 = 0x55 (footer)"); "Data pkt: byte 10 = 0x55 (footer)");
@@ -27,10 +27,12 @@ layers agree (because both could be wrong).
from __future__ import annotations from __future__ import annotations
import os import os
import re
import struct import struct
import subprocess import subprocess
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import ClassVar
import pytest import pytest
@@ -202,6 +204,58 @@ class TestTier1OpcodeContract:
f"but ground truth says '{expected_reg}'" f"but ground truth says '{expected_reg}'"
) )
def test_opcode_count_exact_match(self):
"""
Total opcode count must match ground truth exactly.
This is a CANARY test: if an LLM agent or developer adds/removes
an opcode in one layer without updating ground truth, this fails
immediately. Update GROUND_TRUTH_OPCODES when intentionally
changing the register map.
"""
expected_count = len(GROUND_TRUTH_OPCODES) # 25
py_count = len(cp.parse_python_opcodes())
v_count = len(cp.parse_verilog_opcodes())
assert py_count == expected_count, (
f"Python has {py_count} opcodes but ground truth has {expected_count}. "
f"If intentional, update GROUND_TRUTH_OPCODES in this test file."
)
assert v_count == expected_count, (
f"Verilog has {v_count} opcodes but ground truth has {expected_count}. "
f"If intentional, update GROUND_TRUTH_OPCODES in this test file."
)
def test_no_extra_verilog_opcodes(self):
"""
Verilog must not have opcodes absent from ground truth.
Catches the case where someone adds a new case entry in
radar_system_top.v but forgets to update the contract.
"""
v_opcodes = cp.parse_verilog_opcodes()
extra = set(v_opcodes.keys()) - set(GROUND_TRUTH_OPCODES.keys())
assert not extra, (
f"Verilog has opcodes not in ground truth: "
f"{[f'0x{x:02X} ({v_opcodes[x].register})' for x in extra]}. "
f"Add them to GROUND_TRUTH_OPCODES if intentional."
)
def test_no_extra_python_opcodes(self):
"""
Python must not have opcodes absent from ground truth.
Catches phantom opcodes (like the 0x06 incident) added by
LLM agents that assume without verifying.
"""
py_opcodes = cp.parse_python_opcodes()
extra = set(py_opcodes.keys()) - set(GROUND_TRUTH_OPCODES.keys())
assert not extra, (
f"Python has opcodes not in ground truth: "
f"{[f'0x{x:02X} ({py_opcodes[x].name})' for x in extra]}. "
f"Remove phantom opcodes or add to GROUND_TRUTH_OPCODES."
)
class TestTier1BitWidths: class TestTier1BitWidths:
"""Verify register widths and opcode bit slices match ground truth.""" """Verify register widths and opcode bit slices match ground truth."""
@@ -300,6 +354,122 @@ class TestTier1StatusFieldPositions:
) )
class TestTier1ArchitecturalParams:
"""
Verify radar_params.vh (FPGA single source of truth) matches Python
WaveformConfig defaults and frozen architectural constants.
These tests catch:
- LLM agents changing FFT size, bin counts, or sample rates in one
layer without updating the other
- Accidental parameter drift between FPGA and GUI
- Unauthorized changes to critical architectural constants
When intentionally changing a parameter (e.g. FFT 10242048),
update BOTH radar_params.vh AND WaveformConfig, then update the
FROZEN_PARAMS dict below.
"""
# Frozen architectural constants — update deliberately when changing arch
FROZEN_PARAMS: ClassVar[dict[str, int]] = {
"RP_FFT_SIZE": 1024,
"RP_DECIMATION_FACTOR": 16,
"RP_BINS_PER_SEGMENT": 64,
"RP_OUTPUT_RANGE_BINS_3KM": 64,
"RP_DOPPLER_FFT_SIZE": 16,
"RP_NUM_DOPPLER_BINS": 32,
"RP_CHIRPS_PER_FRAME": 32,
"RP_CHIRPS_PER_SUBFRAME": 16,
"RP_DATA_WIDTH": 16,
"RP_PROCESSING_RATE_MHZ": 100,
"RP_RANGE_PER_BIN_DM": 240, # 24.0 m in decimeters
}
def test_radar_params_vh_parseable(self):
"""radar_params.vh must exist and parse without error."""
params = cp.parse_radar_params_vh()
assert len(params) > 10, (
f"Only parsed {len(params)} defines from radar_params.vh — "
f"expected > 10. Parser may be broken."
)
def test_frozen_constants_unchanged(self):
"""
Critical architectural constants must match frozen values.
If this test fails, someone changed a fundamental parameter.
Verify the change is intentional, update FROZEN_PARAMS, AND
update all downstream consumers (GUI, testbenches, docs).
"""
params = cp.parse_radar_params_vh()
for name, expected in self.FROZEN_PARAMS.items():
assert name in params, (
f"{name} missing from radar_params.vh! "
f"Was it renamed or removed?"
)
assert params[name] == expected, (
f"ARCHITECTURAL CHANGE DETECTED: {name} = {params[name]}, "
f"expected {expected}. If intentional, update FROZEN_PARAMS "
f"in this test AND all downstream consumers."
)
def test_fpga_python_fft_size_match(self):
"""FPGA FFT size must match Python WaveformConfig.fft_size."""
params = cp.parse_radar_params_vh()
sys.path.insert(0, str(cp.GUI_DIR))
from v7.models import WaveformConfig
wc = WaveformConfig()
assert params["RP_FFT_SIZE"] == wc.fft_size, (
f"FFT size mismatch: radar_params.vh={params['RP_FFT_SIZE']}, "
f"WaveformConfig={wc.fft_size}"
)
def test_fpga_python_decimation_match(self):
"""FPGA decimation factor must match Python WaveformConfig."""
params = cp.parse_radar_params_vh()
sys.path.insert(0, str(cp.GUI_DIR))
from v7.models import WaveformConfig
wc = WaveformConfig()
assert params["RP_DECIMATION_FACTOR"] == wc.decimation_factor, (
f"Decimation mismatch: radar_params.vh={params['RP_DECIMATION_FACTOR']}, "
f"WaveformConfig={wc.decimation_factor}"
)
def test_fpga_python_range_bins_match(self):
"""FPGA 3km output bins must match Python WaveformConfig.n_range_bins."""
params = cp.parse_radar_params_vh()
sys.path.insert(0, str(cp.GUI_DIR))
from v7.models import WaveformConfig
wc = WaveformConfig()
assert params["RP_OUTPUT_RANGE_BINS_3KM"] == wc.n_range_bins, (
f"Range bins mismatch: radar_params.vh={params['RP_OUTPUT_RANGE_BINS_3KM']}, "
f"WaveformConfig={wc.n_range_bins}"
)
def test_fpga_python_doppler_bins_match(self):
"""FPGA Doppler bins must match Python WaveformConfig.n_doppler_bins."""
params = cp.parse_radar_params_vh()
sys.path.insert(0, str(cp.GUI_DIR))
from v7.models import WaveformConfig
wc = WaveformConfig()
assert params["RP_NUM_DOPPLER_BINS"] == wc.n_doppler_bins, (
f"Doppler bins mismatch: radar_params.vh={params['RP_NUM_DOPPLER_BINS']}, "
f"WaveformConfig={wc.n_doppler_bins}"
)
def test_fpga_python_sample_rate_match(self):
"""FPGA processing rate must match Python WaveformConfig.sample_rate_hz."""
params = cp.parse_radar_params_vh()
sys.path.insert(0, str(cp.GUI_DIR))
from v7.models import WaveformConfig
wc = WaveformConfig()
fpga_rate_hz = params["RP_PROCESSING_RATE_MHZ"] * 1e6
assert fpga_rate_hz == wc.sample_rate_hz, (
f"Sample rate mismatch: radar_params.vh={fpga_rate_hz/1e6} MHz, "
f"WaveformConfig={wc.sample_rate_hz/1e6} MHz"
)
class TestTier1PacketConstants: class TestTier1PacketConstants:
"""Verify packet header/footer/size constants match across layers.""" """Verify packet header/footer/size constants match across layers."""
@@ -724,8 +894,8 @@ class TestTier3CStub:
"freq_max": 30.0e6, "freq_max": 30.0e6,
"prf1": 1000.0, "prf1": 1000.0,
"prf2": 2000.0, "prf2": 2000.0,
"max_distance": 50000.0, "max_distance": 1536.0,
"map_size": 50000.0, "map_size": 1536.0,
} }
pkt = self._build_settings_packet(values) pkt = self._build_settings_packet(values)
result = self._run_stub(stub_binary, pkt) result = self._run_stub(stub_binary, pkt)
@@ -784,11 +954,11 @@ class TestTier3CStub:
def test_bad_markers_rejected(self, stub_binary): def test_bad_markers_rejected(self, stub_binary):
"""Packet with wrong start/end markers must be rejected.""" """Packet with wrong start/end markers must be rejected."""
values = { values = {
"system_frequency": 10.0e9, "chirp_duration_1": 30.0e-6, "system_frequency": 10.5e9, "chirp_duration_1": 30.0e-6,
"chirp_duration_2": 0.5e-6, "chirps_per_position": 32, "chirp_duration_2": 0.5e-6, "chirps_per_position": 32,
"freq_min": 10.0e6, "freq_max": 30.0e6, "freq_min": 10.0e6, "freq_max": 30.0e6,
"prf1": 1000.0, "prf2": 2000.0, "prf1": 1000.0, "prf2": 2000.0,
"max_distance": 50000.0, "map_size": 50000.0, "max_distance": 1536.0, "map_size": 1536.0,
} }
pkt = self._build_settings_packet(values) pkt = self._build_settings_packet(values)
@@ -826,3 +996,107 @@ class TestTier3CStub:
assert result.get("parse_ok") == "true", ( assert result.get("parse_ok") == "true", (
f"Boundary values rejected: {result}" f"Boundary values rejected: {result}"
) )
# ===================================================================
# TIER 4: Stale Value Detection (LLM Agent Guardrails)
# ===================================================================
class TestTier4BannedPatterns:
"""
Scan source files for known-wrong values that have been corrected.
These patterns are stale ADI Phaser defaults, wrong sample rates,
and incorrect range calculations that were cleaned up in commit
d259e5c. If an LLM agent reintroduces them, this test catches it.
IMPORTANT: Allowlist entries exist for legitimate uses (e.g. comments
explaining what was wrong, test files checking for these values).
"""
# (regex_pattern, description, file_extensions_to_scan)
BANNED_PATTERNS: ClassVar[list[tuple[str, str, tuple[str, ...]]]] = [
# Wrong carrier frequency (ADI Phaser default, not PLFM)
(r'10[._]?525\s*e\s*9|10\.525\s*GHz|10525000000',
"Stale ADI Phaser carrier freq — PLFM uses 10.5 GHz",
("*.py", "*.v", "*.vh", "*.cpp", "*.h")),
# Wrong post-DDC sample rate (ADI Phaser uses 4 MSPS)
(r'(?<!\d)4e6(?!\d)|4_?000_?000\.0?\s*#.*(?:sample|samp|rate|fs)',
"Stale ADI 4 MSPS rate — PLFM post-DDC is 100 MSPS",
("*.py",)),
# Wrong range per bin values from old calculations
(r'(?<!\d)4\.8\s*(?:m/bin|m per|meters?\s*per)',
"Stale bin spacing 4.8 m — PLFM is 24.0 m/bin",
("*.py", "*.cpp")),
(r'(?<!\d)5\.6\s*(?:m/bin|m per|meters?\s*per)',
"Stale bin spacing 5.6 m — PLFM is 24.0 m/bin",
("*.py", "*.cpp")),
# Wrong range resolution from deramped FMCW formula
(r'781\.25',
"Stale ADI range value 781.25 — not applicable to PLFM",
("*.py",)),
]
# Files that are allowed to contain banned patterns
# (this test file, historical docs, comparison scripts)
# ADI co-sim files legitimately reference ADI Phaser hardware params
# because they process ADI test stimulus data (10.525 GHz, 4 MSPS).
ALLOWLIST: ClassVar[set[str]] = {
"test_cross_layer_contract.py", # This file — contains the patterns!
"CLAUDE.md", # Context doc may reference old values
"golden_reference.py", # Processes ADI Phaser test data
"tb_fullchain_mti_cfar_realdata.v", # $display of ADI test stimulus info
"tb_doppler_realdata.v", # $display of ADI test stimulus info
"tb_range_fft_realdata.v", # $display of ADI test stimulus info
"tb_fullchain_realdata.v", # $display of ADI test stimulus info
}
def _scan_files(self, pattern_re, extensions):
"""Scan firmware source files for a regex pattern."""
hits = []
firmware_dir = cp.REPO_ROOT / "9_Firmware"
for ext in extensions:
for fpath in firmware_dir.rglob(ext.replace("*", "**/*") if "**" in ext else ext):
# Skip allowlisted files
if fpath.name in self.ALLOWLIST:
continue
# Skip __pycache__, .git, etc.
parts = set(fpath.parts)
if parts & {"__pycache__", ".git", ".venv", "venv", ".ruff_cache"}:
continue
try:
text = fpath.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
for i, line in enumerate(text.splitlines(), 1):
if pattern_re.search(line):
hits.append(f"{fpath.relative_to(cp.REPO_ROOT)}:{i}: {line.strip()[:120]}")
return hits
def test_no_banned_stale_values(self):
"""
No source file should contain known-wrong stale values.
If this fails, an LLM agent likely reintroduced a corrected value.
Check the flagged lines and fix them. If a line is a legitimate
use (e.g. a comment explaining history), add the file to ALLOWLIST.
"""
all_hits = []
for pattern_str, description, extensions in self.BANNED_PATTERNS:
pattern_re = re.compile(pattern_str, re.IGNORECASE)
hits = self._scan_files(pattern_re, extensions)
all_hits.extend(f"[{description}] {hit}" for hit in hits)
assert not all_hits, (
f"Found {len(all_hits)} stale/banned value(s) in source files:\n"
+ "\n".join(f" {h}" for h in all_hits[:20])
+ ("\n ... and more" if len(all_hits) > 20 else "")
)
+1 -2
View File
@@ -111,8 +111,7 @@ The AERIS-10 main sub-systems are:
- Map integration - Map integration
- Radar control interface - Radar control interface
![AERIS-10 Dashboard](https://raw.githubusercontent.com/NawfalMotii79/PLFM_RADAR/main/8_Utils/GUI_V65_Tk.png) ![AERIS-10 GUI Demo](https://raw.githubusercontent.com/NawfalMotii79/PLFM_RADAR/main/8_Utils/GUI_V6.gif)
<!-- V6 GIF removed — V6 is deprecated. V65 Tk and V7 PyQt6 are the active GUIs. -->
## 📊 Technical Specifications ## 📊 Technical Specifications
-5
View File
@@ -32,11 +32,6 @@
</section> </section>
<section class="stats-grid"> <section class="stats-grid">
<article class="card stat notice">
<h2>Production Board USB</h2>
<p class="metric">FT2232H (USB 2.0)</p>
<p class="muted">50T production board uses FT2232H. FT601 USB 3.0 is available on 200T premium dev board only.</p>
</article>
<article class="card stat"> <article class="card stat">
<h2>Tracked Timing Baseline</h2> <h2>Tracked Timing Baseline</h2>
<p class="metric">WNS +0.058 ns</p> <p class="metric">WNS +0.058 ns</p>