Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cb7688814 | |||
| 86b493a780 | |||
| c023337949 | |||
| d259e5c106 |
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,41 +631,20 @@ 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) {
|
||||||
GPIO_PinState s0 = HAL_GPIO_ReadPin(AD9523_STATUS0_GPIO_Port, AD9523_STATUS0_Pin);
|
GPIO_PinState s0 = HAL_GPIO_ReadPin(AD9523_STATUS0_GPIO_Port, AD9523_STATUS0_Pin);
|
||||||
GPIO_PinState s1 = HAL_GPIO_ReadPin(AD9523_STATUS1_GPIO_Port, AD9523_STATUS1_Pin);
|
GPIO_PinState s1 = HAL_GPIO_ReadPin(AD9523_STATUS1_GPIO_Port, AD9523_STATUS1_Pin);
|
||||||
DIAG_GPIO("CLK", "AD9523 STATUS0", s0);
|
DIAG_GPIO("CLK", "AD9523 STATUS0", s0);
|
||||||
DIAG_GPIO("CLK", "AD9523 STATUS1", s1);
|
DIAG_GPIO("CLK", "AD9523 STATUS1", s1);
|
||||||
if (s0 == GPIO_PIN_RESET || s1 == GPIO_PIN_RESET) {
|
if (s0 == GPIO_PIN_RESET || s1 == GPIO_PIN_RESET) {
|
||||||
current_error = ERROR_AD9523_CLOCK;
|
current_error = ERROR_AD9523_CLOCK;
|
||||||
DIAG_ERR("CLK", "AD9523 clock health check FAILED (STATUS0=%d STATUS1=%d)", s0, s1);
|
DIAG_ERR("CLK", "AD9523 clock health check FAILED (STATUS0=%d STATUS1=%d)", s0, s1);
|
||||||
return current_error;
|
return current_error;
|
||||||
}
|
}
|
||||||
last_clock_check = HAL_GetTick();
|
last_clock_check = HAL_GetTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check ADF4382 Lock Status
|
// 2. Check ADF4382 Lock Status
|
||||||
bool tx_locked, rx_locked;
|
bool tx_locked, rx_locked;
|
||||||
@@ -701,26 +679,26 @@ SystemError_t checkSystemHealth(void) {
|
|||||||
|
|
||||||
// 4. Check IMU Communication
|
// 4. Check IMU Communication
|
||||||
static uint32_t last_imu_check = 0;
|
static uint32_t last_imu_check = 0;
|
||||||
if (HAL_GetTick() - last_imu_check > 10000) {
|
if (HAL_GetTick() - last_imu_check > 10000) {
|
||||||
if (!GY85_Update(&imu)) {
|
if (!GY85_Update(&imu)) {
|
||||||
current_error = ERROR_IMU_COMM;
|
current_error = ERROR_IMU_COMM;
|
||||||
DIAG_ERR("IMU", "Health check: GY85_Update() FAILED");
|
DIAG_ERR("IMU", "Health check: GY85_Update() FAILED");
|
||||||
return current_error;
|
return current_error;
|
||||||
}
|
}
|
||||||
last_imu_check = HAL_GetTick();
|
last_imu_check = HAL_GetTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Check BMP180 Communication
|
// 5. Check BMP180 Communication
|
||||||
static uint32_t last_bmp_check = 0;
|
static uint32_t last_bmp_check = 0;
|
||||||
if (HAL_GetTick() - last_bmp_check > 15000) {
|
if (HAL_GetTick() - last_bmp_check > 15000) {
|
||||||
double pressure = myBMP.getPressure();
|
double pressure = myBMP.getPressure();
|
||||||
if (pressure < 30000.0 || pressure > 110000.0 || isnan(pressure)) {
|
if (pressure < 30000.0 || pressure > 110000.0 || isnan(pressure)) {
|
||||||
current_error = ERROR_BMP180_COMM;
|
current_error = ERROR_BMP180_COMM;
|
||||||
DIAG_ERR("SYS", "Health check: BMP180 pressure out of range: %.0f", pressure);
|
DIAG_ERR("SYS", "Health check: BMP180 pressure out of range: %.0f", pressure);
|
||||||
return current_error;
|
return current_error;
|
||||||
}
|
}
|
||||||
last_bmp_check = HAL_GetTick();
|
last_bmp_check = HAL_GetTick();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Check GPS Communication
|
// 6. Check GPS Communication
|
||||||
static uint32_t last_gps_fix = 0;
|
static uint32_t last_gps_fix = 0;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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...");
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)");
|
||||||
|
|||||||
@@ -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 0→1 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 1→2.
|
// 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 2→0,
|
|
||||||
// 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},
|
||||||
|
|||||||
@@ -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,66 +424,124 @@ 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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -536,19 +437,17 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin
|
|||||||
rd_shift_reg <= {rd_shift_reg[23:0], ft_data};
|
rd_shift_reg <= {rd_shift_reg[23:0], ft_data};
|
||||||
if (rd_byte_cnt == 2'd3) begin
|
if (rd_byte_cnt == 2'd3) 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;
|
||||||
// Keep reading if more data available
|
// Keep reading if more data available
|
||||||
if (ft_rxf_n) begin
|
if (ft_rxf_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
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {}
|
||||||
|
|||||||
@@ -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')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,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 (FPGA→Host):
|
TX (FPGA→Host):
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)."""
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
self._connection = FT2232HConnection(mock=True)
|
||||||
if "FT601" in iface:
|
|
||||||
self._connection = FT601Connection(mock=True)
|
|
||||||
else:
|
|
||||||
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()
|
self._connection = FT2232HConnection(mock=False)
|
||||||
if "FT601" in iface:
|
|
||||||
self._connection = FT601Connection(mock=False)
|
|
||||||
iface_name = "FT601"
|
|
||||||
else:
|
|
||||||
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
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -105,15 +105,15 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 1024→2048),
|
||||||
|
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 "")
|
||||||
|
)
|
||||||
|
|||||||
@@ -111,8 +111,7 @@ The AERIS-10 main sub-systems are:
|
|||||||
- Map integration
|
- Map integration
|
||||||
- Radar control interface
|
- Radar control interface
|
||||||
|
|
||||||

|

|
||||||
<!-- V6 GIF removed — V6 is deprecated. V65 Tk and V7 PyQt6 are the active GUIs. -->
|
|
||||||
|
|
||||||
## 📊 Technical Specifications
|
## 📊 Technical Specifications
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user