Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8aefc4f61 | |||
| 5b84af68f6 | |||
| 846a0debe8 | |||
| e979363730 | |||
| 2e9a848908 | |||
| 3366ac6417 | |||
| 607399ec28 | |||
| f48448970b | |||
| ebd96c90ce | |||
| db80baf34d | |||
| 33d21da7f2 | |||
| 18901be04a | |||
| 9f899b96e9 | |||
| f895c0244c | |||
| 88ca1910ec |
Binary file not shown.
|
After Width: | Height: | Size: 378 KiB |
@@ -24,6 +24,7 @@ ADAR1000_AGC::ADAR1000_AGC()
|
|||||||
, saturation_event_count(0)
|
, saturation_event_count(0)
|
||||||
{
|
{
|
||||||
memset(cal_offset, 0, sizeof(cal_offset));
|
memset(cal_offset, 0, sizeof(cal_offset));
|
||||||
|
if (holdoff_frames == 0) holdoff_frames = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ void USBHandler::reset() {
|
|||||||
start_flag_received = false;
|
start_flag_received = false;
|
||||||
buffer_index = 0;
|
buffer_index = 0;
|
||||||
current_settings.resetToDefaults();
|
current_settings.resetToDefaults();
|
||||||
|
fault_ack_received = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void USBHandler::processUSBData(const uint8_t* data, uint32_t length) {
|
void USBHandler::processUSBData(const uint8_t* data, uint32_t length) {
|
||||||
@@ -23,6 +24,18 @@ void USBHandler::processUSBData(const uint8_t* data, uint32_t length) {
|
|||||||
|
|
||||||
DIAG("USB", "processUSBData: %lu bytes, state=%d", (unsigned long)length, (int)current_state);
|
DIAG("USB", "processUSBData: %lu bytes, state=%d", (unsigned long)length, (int)current_state);
|
||||||
|
|
||||||
|
// FAULT_ACK: host sends exactly 4 bytes [0x40, 0x00, 0x00, 0x00].
|
||||||
|
// Requires exact 4-byte packet length: settings packets are always
|
||||||
|
// >= 82 bytes, so a lone 4-byte payload is unambiguous. Scanning
|
||||||
|
// inside larger packets would false-trigger on the IEEE 754
|
||||||
|
// encoding of 2.0 (0x4000000000000000) embedded in settings doubles.
|
||||||
|
static const uint8_t FAULT_ACK_SEQ[4] = {0x40, 0x00, 0x00, 0x00};
|
||||||
|
if (length == 4 && memcmp(data, FAULT_ACK_SEQ, 4) == 0) {
|
||||||
|
fault_ack_received = true;
|
||||||
|
DIAG("USB", "FAULT_ACK received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (current_state) {
|
switch (current_state) {
|
||||||
case USBState::WAITING_FOR_START:
|
case USBState::WAITING_FOR_START:
|
||||||
processStartFlag(data, length);
|
processStartFlag(data, length);
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ public:
|
|||||||
// Reset USB handler
|
// Reset USB handler
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
|
// Fault-acknowledgement: host sends FAULT_ACK (0x40) to clear
|
||||||
|
// system_emergency_state and exit the safe-mode blink loop.
|
||||||
|
bool isFaultAckReceived() const { return fault_ack_received; }
|
||||||
|
void clearFaultAck() { fault_ack_received = false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
RadarSettings current_settings;
|
RadarSettings current_settings;
|
||||||
USBState current_state;
|
USBState current_state;
|
||||||
@@ -38,6 +43,7 @@ private:
|
|||||||
static constexpr uint32_t MAX_BUFFER_SIZE = 256;
|
static constexpr uint32_t MAX_BUFFER_SIZE = 256;
|
||||||
uint8_t usb_buffer[MAX_BUFFER_SIZE];
|
uint8_t usb_buffer[MAX_BUFFER_SIZE];
|
||||||
uint32_t buffer_index;
|
uint32_t buffer_index;
|
||||||
|
bool fault_ack_received;
|
||||||
|
|
||||||
void processStartFlag(const uint8_t* data, uint32_t length);
|
void processStartFlag(const uint8_t* data, uint32_t length);
|
||||||
void processSettingsData(const uint8_t* data, uint32_t length);
|
void processSettingsData(const uint8_t* data, uint32_t length);
|
||||||
|
|||||||
@@ -627,7 +627,7 @@ typedef enum {
|
|||||||
|
|
||||||
static SystemError_t last_error = ERROR_NONE;
|
static SystemError_t last_error = ERROR_NONE;
|
||||||
static uint32_t error_count = 0;
|
static uint32_t error_count = 0;
|
||||||
static bool system_emergency_state = false;
|
static volatile bool system_emergency_state = false;
|
||||||
|
|
||||||
// Error handler function
|
// Error handler function
|
||||||
SystemError_t checkSystemHealth(void) {
|
SystemError_t checkSystemHealth(void) {
|
||||||
@@ -2054,6 +2054,10 @@ int main(void)
|
|||||||
HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin);
|
HAL_GPIO_TogglePin(LED_3_GPIO_Port, LED_3_Pin);
|
||||||
HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin);
|
HAL_GPIO_TogglePin(LED_4_GPIO_Port, LED_4_Pin);
|
||||||
HAL_Delay(250);
|
HAL_Delay(250);
|
||||||
|
if (usbHandler.isFaultAckReceived()) {
|
||||||
|
system_emergency_state = false;
|
||||||
|
usbHandler.clearFaultAck();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared");
|
DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ TESTS_STANDALONE := test_bug12_pa_cal_loop_inverted \
|
|||||||
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_overtemp_emergency_stop \
|
||||||
test_gap3_health_watchdog_cold_start
|
test_gap3_health_watchdog_cold_start \
|
||||||
|
test_gap3_fault_ack_clears_emergency
|
||||||
|
|
||||||
# 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
|
||||||
@@ -178,6 +179,9 @@ test_gap3_overtemp_emergency_stop: test_gap3_overtemp_emergency_stop.c
|
|||||||
test_gap3_health_watchdog_cold_start: test_gap3_health_watchdog_cold_start.c
|
test_gap3_health_watchdog_cold_start: test_gap3_health_watchdog_cold_start.c
|
||||||
$(CC) $(CFLAGS) $< -o $@
|
$(CC) $(CFLAGS) $< -o $@
|
||||||
|
|
||||||
|
test_gap3_fault_ack_clears_emergency: test_gap3_fault_ack_clears_emergency.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 $@
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*******************************************************************************
|
||||||
|
* test_gap3_fault_ack_clears_emergency.c
|
||||||
|
*
|
||||||
|
* Verifies the FAULT_ACK clear path for system_emergency_state:
|
||||||
|
* - USBHandler detects exactly [0x40, 0x00, 0x00, 0x00] in a 4-byte packet
|
||||||
|
* - Detection is false-positive-free: larger packets (settings data) carrying
|
||||||
|
* the same bytes as a subsequence must NOT trigger the ack
|
||||||
|
* - Main-loop blink logic clears system_emergency_state on receipt
|
||||||
|
*
|
||||||
|
* Logic extracted from USBHandler.cpp + main.cpp to mirror the actual code
|
||||||
|
* paths without requiring HAL headers.
|
||||||
|
******************************************************************************/
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* ── Simulated USBHandler state ─────────────────────────────────────────── */
|
||||||
|
static bool fault_ack_received = false;
|
||||||
|
static volatile bool system_emergency_state = false;
|
||||||
|
|
||||||
|
static const uint8_t FAULT_ACK_SEQ[4] = {0x40, 0x00, 0x00, 0x00};
|
||||||
|
|
||||||
|
/* Mirrors USBHandler::processUSBData() detection logic */
|
||||||
|
static void sim_processUSBData(const uint8_t *data, uint32_t length)
|
||||||
|
{
|
||||||
|
if (data == NULL || length == 0) return;
|
||||||
|
if (length == 4 && memcmp(data, FAULT_ACK_SEQ, 4) == 0) {
|
||||||
|
fault_ack_received = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/* (normal state machine omitted — not under test here) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mirrors one iteration of the blink loop in main.cpp */
|
||||||
|
static void sim_blink_iteration(void)
|
||||||
|
{
|
||||||
|
/* HAL_GPIO_TogglePin + HAL_Delay omitted */
|
||||||
|
if (fault_ack_received) {
|
||||||
|
system_emergency_state = false;
|
||||||
|
fault_ack_received = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
printf("=== Gap-3 FAULT_ACK clears system_emergency_state ===\n");
|
||||||
|
|
||||||
|
/* Test 1: exact 4-byte FAULT_ACK packet sets the flag */
|
||||||
|
printf(" Test 1: exact FAULT_ACK packet detected... ");
|
||||||
|
fault_ack_received = false;
|
||||||
|
const uint8_t ack_pkt[4] = {0x40, 0x00, 0x00, 0x00};
|
||||||
|
sim_processUSBData(ack_pkt, 4);
|
||||||
|
assert(fault_ack_received == true);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 2: flag cleared and system_emergency_state exits blink loop */
|
||||||
|
printf(" Test 2: blink loop exits on FAULT_ACK... ");
|
||||||
|
system_emergency_state = true;
|
||||||
|
fault_ack_received = true;
|
||||||
|
sim_blink_iteration();
|
||||||
|
assert(system_emergency_state == false);
|
||||||
|
assert(fault_ack_received == false);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 3: blink loop does NOT exit without ack */
|
||||||
|
printf(" Test 3: blink loop holds without ack... ");
|
||||||
|
system_emergency_state = true;
|
||||||
|
fault_ack_received = false;
|
||||||
|
sim_blink_iteration();
|
||||||
|
assert(system_emergency_state == true);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 4: settings-sized packet carrying [0x40,0x00,0x00,0x00] as first
|
||||||
|
* 4 bytes does NOT trigger ack (IEEE 754 double 2.0 = 0x4000000000000000) */
|
||||||
|
printf(" Test 4: settings packet with 2.0 double does not false-trigger... ");
|
||||||
|
fault_ack_received = false;
|
||||||
|
uint8_t settings_pkt[82];
|
||||||
|
memset(settings_pkt, 0, sizeof(settings_pkt));
|
||||||
|
/* First 4 bytes look like FAULT_ACK but packet length is 82 */
|
||||||
|
settings_pkt[0] = 0x40; settings_pkt[1] = 0x00;
|
||||||
|
settings_pkt[2] = 0x00; settings_pkt[3] = 0x00;
|
||||||
|
sim_processUSBData(settings_pkt, sizeof(settings_pkt));
|
||||||
|
assert(fault_ack_received == false);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 5: 3-byte packet (truncated) does not trigger */
|
||||||
|
printf(" Test 5: truncated 3-byte packet ignored... ");
|
||||||
|
fault_ack_received = false;
|
||||||
|
const uint8_t short_pkt[3] = {0x40, 0x00, 0x00};
|
||||||
|
sim_processUSBData(short_pkt, 3);
|
||||||
|
assert(fault_ack_received == false);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 6: wrong opcode byte in 4-byte packet does not trigger */
|
||||||
|
printf(" Test 6: wrong opcode (0x28 AGC_ENABLE) not detected as FAULT_ACK... ");
|
||||||
|
fault_ack_received = false;
|
||||||
|
const uint8_t agc_pkt[4] = {0x28, 0x00, 0x00, 0x01};
|
||||||
|
sim_processUSBData(agc_pkt, 4);
|
||||||
|
assert(fault_ack_received == false);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
/* Test 7: multiple blink iterations — loop stays active until ack */
|
||||||
|
printf(" Test 7: loop stays active across multiple iterations until ack... ");
|
||||||
|
system_emergency_state = true;
|
||||||
|
fault_ack_received = false;
|
||||||
|
sim_blink_iteration();
|
||||||
|
assert(system_emergency_state == true);
|
||||||
|
sim_blink_iteration();
|
||||||
|
assert(system_emergency_state == true);
|
||||||
|
/* Now ack arrives */
|
||||||
|
sim_processUSBData(ack_pkt, 4);
|
||||||
|
assert(fault_ack_received == true);
|
||||||
|
sim_blink_iteration();
|
||||||
|
assert(system_emergency_state == false);
|
||||||
|
printf("PASS\n");
|
||||||
|
|
||||||
|
printf("\n=== Gap-3 FAULT_ACK: ALL 7 TESTS PASSED ===\n\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -18,23 +18,8 @@
|
|||||||
# Bank 35: VCCO = 3.3V (FT2232H USB 2.0 FIFO — 15 signals)
|
# Bank 35: VCCO = 3.3V (FT2232H USB 2.0 FIFO — 15 signals)
|
||||||
#
|
#
|
||||||
# DRC Fix History:
|
# DRC Fix History:
|
||||||
# - PLIO-9 (REVERTED): Previously moved clk_120m_dac from C13 (N-type) to
|
# - PLIO-9: Moved clk_120m_dac from C13 (N-type) to D13 (P-type MRCC).
|
||||||
# D13 (P-type MRCC) to satisfy the MRCC preference. However, a schematic
|
# Clock inputs must use the P-type pin of a Multi-Region Clock-Capable pair.
|
||||||
# audit (KiCad netlist export from the Eagle schematic, U42 pad->net map)
|
|
||||||
# revealed that D13 is UNCONNECTED on the physical PCB. The real
|
|
||||||
# /FPGA_DAC_CLOCK net from AD9523 OUT11 lands on C13 (IO_L11N_T1_SRCC_15,
|
|
||||||
# N-type). Moved back to C13 and added CLOCK_DEDICATED_ROUTE FALSE,
|
|
||||||
# matching the ft_clkout treatment on C4 (N-type MRCC).
|
|
||||||
# - Schematic audit added pin constraints for previously-unconstrained
|
|
||||||
# signals connected to the FPGA in hardware: ADC_OR_P/N (M6/N6, AD9484
|
|
||||||
# overflow flag), /FPGA_ADC_CLOCK_P/N (N11/N12, 400 MHz observation tap
|
|
||||||
# of the AD9523->AD9484 sample clock). Added to 50T wrapper as
|
|
||||||
# anchored-but-unused inputs to secure pin assignment and prevent
|
|
||||||
# accidental future contention; full RTL consumers are a follow-up.
|
|
||||||
# - PLIO-9 (original, historical): FT2232H CLKOUT routed to C4
|
|
||||||
# (IO_L12N_T1_MRCC_35, N-type). Clock inputs normally use P-type MRCC
|
|
||||||
# pins, but IBUFG works correctly on N-type. Demote PLIO-9 to warning
|
|
||||||
# in build script.
|
|
||||||
# - BIVC-1 / Place 30-372: Bank 14 must have a single VCCO. LVDS_25 forces
|
# - BIVC-1 / Place 30-372: Bank 14 must have a single VCCO. LVDS_25 forces
|
||||||
# VCCO=2.5V, so adc_pwdn was changed from LVCMOS33 to LVCMOS25 to match.
|
# VCCO=2.5V, so adc_pwdn was changed from LVCMOS33 to LVCMOS25 to match.
|
||||||
# IBUFDS input buffers are VCCO-independent. BIVC-1 also waived via
|
# IBUFDS input buffers are VCCO-independent. BIVC-1 also waived via
|
||||||
@@ -43,6 +28,9 @@
|
|||||||
# - UCIO/NSTD: Unconstrained ports (FT601 ports inactive with USB_MODE=1,
|
# - UCIO/NSTD: Unconstrained ports (FT601 ports inactive with USB_MODE=1,
|
||||||
# status/debug outputs have no physical pins). Handled with SEVERITY
|
# status/debug outputs have no physical pins). Handled with SEVERITY
|
||||||
# demotion + default IOSTANDARD.
|
# demotion + default IOSTANDARD.
|
||||||
|
# - PLIO-9: FT2232H CLKOUT routed to C4 (IO_L12N_T1_MRCC_35, N-type).
|
||||||
|
# Clock inputs normally use P-type MRCC pins, but IBUFG works correctly
|
||||||
|
# on N-type. Demote PLIO-9 to warning in build script.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -78,7 +66,7 @@ set_property IOSTANDARD LVCMOS33 [get_ports {clk_100m}]
|
|||||||
create_clock -name clk_100m -period 10.0 [get_ports {clk_100m}]
|
create_clock -name clk_100m -period 10.0 [get_ports {clk_100m}]
|
||||||
set_input_jitter [get_clocks clk_100m] 0.1
|
set_input_jitter [get_clocks clk_100m] 0.1
|
||||||
|
|
||||||
# 120MHz DAC Clock (AD9523 OUT11 → /FPGA_DAC_CLOCK → Bank 15 pin C13)
|
# 120MHz DAC Clock (AD9523 OUT11 → FPGA_DAC_CLOCK → Bank 15 MRCC pin D13)
|
||||||
# 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
|
||||||
@@ -86,19 +74,12 @@ set_input_jitter [get_clocks clk_100m] 0.1
|
|||||||
# physical pin on the 50T board and is left unconnected here. The port
|
# physical pin on the 50T board and is left unconnected here. The port
|
||||||
# CANNOT be removed from the RTL because the 200T board uses it with
|
# CANNOT be removed from the RTL because the 200T board uses it with
|
||||||
# ODDR clock forwarding (pin H17, see xc7a200t_fbg484.xdc).
|
# ODDR clock forwarding (pin H17, see xc7a200t_fbg484.xdc).
|
||||||
#
|
# FIX: Moved from C13 (IO_L12N = N-type) to D13 (IO_L12P = P-type MRCC).
|
||||||
# PIN: C13 is IO_L11N_T1_SRCC_15 (N-type SRCC). A prior commit attempted to
|
# Clock inputs must use the P-type pin of an MRCC pair (PLIO-9 DRC).
|
||||||
# move this to D13 (MRCC P-type) to satisfy PLIO-9, but the schematic audit
|
set_property PACKAGE_PIN D13 [get_ports {clk_120m_dac}]
|
||||||
# showed D13 is UNCONNECTED on the PCB — the /FPGA_DAC_CLOCK net physically
|
|
||||||
# lands on C13. Moving to D13 made the DAC clock input float. Restored to
|
|
||||||
# C13 and forced CLOCK_DEDICATED_ROUTE FALSE (same mechanism as ft_clkout on
|
|
||||||
# C4), which routes the IBUFG output through general fabric to a BUFG.
|
|
||||||
set_property PACKAGE_PIN C13 [get_ports {clk_120m_dac}]
|
|
||||||
set_property IOSTANDARD LVCMOS33 [get_ports {clk_120m_dac}]
|
set_property IOSTANDARD LVCMOS33 [get_ports {clk_120m_dac}]
|
||||||
create_clock -name clk_120m_dac -period 8.333 [get_ports {clk_120m_dac}]
|
create_clock -name clk_120m_dac -period 8.333 [get_ports {clk_120m_dac}]
|
||||||
set_input_jitter [get_clocks clk_120m_dac] 0.1
|
set_input_jitter [get_clocks clk_120m_dac] 0.1
|
||||||
# C13 is N-type SRCC (not dedicated-clock-capable); override the DRC check.
|
|
||||||
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets {clk_120m_dac_IBUF}]
|
|
||||||
|
|
||||||
# ADC DCO Clock (400MHz LVDS — AD9523 OUT5 → AD9484 → FPGA, Bank 14 MRCC)
|
# ADC DCO Clock (400MHz LVDS — AD9523 OUT5 → AD9484 → FPGA, Bank 14 MRCC)
|
||||||
# NOTE: LVDS_25 is the only valid differential input standard on 7-series HR
|
# NOTE: LVDS_25 is the only valid differential input standard on 7-series HR
|
||||||
@@ -302,45 +283,6 @@ set_input_delay -clock [get_clocks adc_dco_p] -min 0.2 [get_ports {adc_d_p[*]}]
|
|||||||
set_input_delay -clock [get_clocks adc_dco_p] -max 1.0 -clock_fall [get_ports {adc_d_p[*]}] -add_delay
|
set_input_delay -clock [get_clocks adc_dco_p] -max 1.0 -clock_fall [get_ports {adc_d_p[*]}] -add_delay
|
||||||
set_input_delay -clock [get_clocks adc_dco_p] -min 0.2 -clock_fall [get_ports {adc_d_p[*]}] -add_delay
|
set_input_delay -clock [get_clocks adc_dco_p] -min 0.2 -clock_fall [get_ports {adc_d_p[*]}] -add_delay
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# AD9484 Overflow / Out-Of-Range flag (schematic nets ADC_OR_P / ADC_OR_N)
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# AD9484 differential OR output on FPGA pads M6 (OR_P) / N6 (OR_N), Bank 14.
|
|
||||||
# This is the AD9484's full-scale overflow indicator, useful for AGC /
|
|
||||||
# gain-ranging feedback. The 50T RTL wrapper anchors this with an IBUFDS
|
|
||||||
# (DONT_TOUCH) so the pads cannot be accidentally driven as outputs (which
|
|
||||||
# would cause contention with the AD9484 driver). A future PR should wire
|
|
||||||
# the buffered signal into the receive-path status flags.
|
|
||||||
set_property PACKAGE_PIN M6 [get_ports {adc_or_p}]
|
|
||||||
set_property PACKAGE_PIN N6 [get_ports {adc_or_n}]
|
|
||||||
set_property IOSTANDARD LVDS_25 [get_ports {adc_or_p}]
|
|
||||||
set_property IOSTANDARD LVDS_25 [get_ports {adc_or_n}]
|
|
||||||
set_property DIFF_TERM TRUE [get_ports {adc_or_p}]
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# FPGA observation of AD9523->AD9484 sample clock (/FPGA_ADC_CLOCK_P/N)
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# AD9523 drives the AD9484 sample clock directly; the same differential
|
|
||||||
# pair is tapped to FPGA pads N11 (P) / N12 (N), Bank 14, MRCC-capable.
|
|
||||||
# This is an INPUT-ONLY tap (FPGA must never drive these pads — that would
|
|
||||||
# contend with the AD9523 driver feeding the ADC). The 50T wrapper anchors
|
|
||||||
# with IBUFDS + DONT_TOUCH so the pad assignment is preserved across all
|
|
||||||
# synthesis/optimization stages. The buffered net is unconsumed for now;
|
|
||||||
# create_clock and clock_groups are deferred until an RTL consumer exists
|
|
||||||
# (see commented template below).
|
|
||||||
set_property PACKAGE_PIN N11 [get_ports {fpga_adc_clock_p}]
|
|
||||||
set_property PACKAGE_PIN N12 [get_ports {fpga_adc_clock_n}]
|
|
||||||
set_property IOSTANDARD LVDS_25 [get_ports {fpga_adc_clock_p}]
|
|
||||||
set_property IOSTANDARD LVDS_25 [get_ports {fpga_adc_clock_n}]
|
|
||||||
set_property DIFF_TERM TRUE [get_ports {fpga_adc_clock_p}]
|
|
||||||
# No create_clock here on purpose: the IBUFDS output is unconsumed (anchored
|
|
||||||
# via DONT_TOUCH only), so declaring it as a clock would only generate
|
|
||||||
# "clock has no registered destinations" warnings. When a follow-up PR adds
|
|
||||||
# an actual consumer, add:
|
|
||||||
# create_clock -name fpga_adc_clock -period 2.5 [get_ports {fpga_adc_clock_p}]
|
|
||||||
# set_input_jitter [get_clocks fpga_adc_clock] 0.05
|
|
||||||
# set_clock_groups -asynchronous -group [get_clocks fpga_adc_clock] ...
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# FT2232H USB 2.0 INTERFACE (Bank 35, VCCO=3.3V)
|
# FT2232H USB 2.0 INTERFACE (Bank 35, VCCO=3.3V)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -405,49 +347,29 @@ set_property DRIVE 8 [get_ports {ft_data[*]}]
|
|||||||
# FPGA Write Path (FPGA drives data, FT2232H samples):
|
# FPGA Write Path (FPGA drives data, FT2232H samples):
|
||||||
# - Data setup before next CLKOUT rising: t_su = 5.0 ns
|
# - Data setup before next CLKOUT rising: t_su = 5.0 ns
|
||||||
# - Data hold after CLKOUT rising: t_hd = 0.0 ns
|
# - Data hold after CLKOUT rising: t_hd = 0.0 ns
|
||||||
# - Board trace skew budget: ~0.5 ns
|
# - Output delay max = period - t_su = 16.667 - 5.0 = 11.667 ns
|
||||||
# - Output delay max = t_su + trace_max = 5.0 + 0.5 = 5.5 ns
|
# - Output delay min = t_hd = 0.0 ns
|
||||||
# - Output delay min = t_hd - trace_min = 0.0 - 0.0 = 0.0 ns
|
|
||||||
#
|
|
||||||
# NOTE: Historical XDC used 'period - t_su = 11.667 ns' for output_delay -max,
|
|
||||||
# which is the wrong interpretation: set_output_delay takes the external setup
|
|
||||||
# requirement (+trace), not the remaining timing budget. The old value forced
|
|
||||||
# Vivado to close a path assuming FT2232H requires 11.667 ns of setup, which
|
|
||||||
# it does not, and caused WNS=-5.350 ns failures on ft_data/ft_rd_n/ft_wr_n/
|
|
||||||
# ft_oe_n/ft_siwu paths given the 5.513 ns clock insertion delay on the
|
|
||||||
# non-dedicated C4 routing.
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
# Input delays: FT2232H → FPGA (data bus and status signals)
|
# Input delays: FT2232H → FPGA (data bus and status signals)
|
||||||
#
|
|
||||||
# -min revision (Build N+1): was 0.0 ns, now 1.0 ns.
|
|
||||||
# Rationale: set_input_delay -min is the EARLIEST time data can change at the
|
|
||||||
# FPGA pin after the launch clock edge, i.e. FT2232H Tco_min + trace_min.
|
|
||||||
# Setting -min 0.0 claimed data could change simultaneously with the clock
|
|
||||||
# edge, which is pessimistically tight for hold analysis and caused a
|
|
||||||
# -0.079 ns hold violation on ft_rxf_n → FSM_sequential_wr_state in Build N
|
|
||||||
# (due to 2.895 ns clock insertion delay on non-dedicated C4 routing).
|
|
||||||
# FT2232H Sync FIFO Tco is spec'd 1–4 ns; using 1.0 ns is conservative and
|
|
||||||
# still covers worst-case silicon. Invariant preserved: hold_margin =
|
|
||||||
# Tco_min + trace_min - clk_insertion_delay - Th_fpga ≥ 0.
|
|
||||||
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_data[*]}]
|
set_input_delay -clock [get_clocks ft_clkout] -max 9.667 [get_ports {ft_data[*]}]
|
||||||
set_input_delay -clock [get_clocks ft_clkout] -min 1.0 [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] -max 9.667 [get_ports {ft_rxf_n}]
|
||||||
set_input_delay -clock [get_clocks ft_clkout] -min 1.0 [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] -max 9.667 [get_ports {ft_txe_n}]
|
||||||
set_input_delay -clock [get_clocks ft_clkout] -min 1.0 [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)
|
# Output delays: FPGA → FT2232H (control strobes and data bus when writing)
|
||||||
set_output_delay -clock [get_clocks ft_clkout] -max 5.5 [get_ports {ft_data[*]}]
|
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] -min 0.0 [get_ports {ft_data[*]}]
|
||||||
set_output_delay -clock [get_clocks ft_clkout] -max 5.5 [get_ports {ft_rd_n}]
|
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] -min 0.0 [get_ports {ft_rd_n}]
|
||||||
set_output_delay -clock [get_clocks ft_clkout] -max 5.5 [get_ports {ft_wr_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] -min 0.0 [get_ports {ft_wr_n}]
|
||||||
set_output_delay -clock [get_clocks ft_clkout] -max 5.5 [get_ports {ft_oe_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] -min 0.0 [get_ports {ft_oe_n}]
|
||||||
set_output_delay -clock [get_clocks ft_clkout] -max 5.5 [get_ports {ft_siwu}]
|
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}]
|
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
|
||||||
@@ -489,42 +411,24 @@ set_false_path -from [get_ports {stm32_mixers_enable}]
|
|||||||
set_false_path -from [get_cells reset_sync_reg[*]] -to [get_pins -filter {REF_PIN_NAME == CLR} -of_objects [get_cells -hierarchical -filter {PRIMITIVE_TYPE =~ REGISTER.*.*}]]
|
set_false_path -from [get_cells reset_sync_reg[*]] -to [get_pins -filter {REF_PIN_NAME == CLR} -of_objects [get_cells -hierarchical -filter {PRIMITIVE_TYPE =~ REGISTER.*.*}]]
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Clock Domain Crossing — asynchronous clock groups
|
# Clock Domain Crossing false paths
|
||||||
#
|
|
||||||
# Rationale: prefer `set_clock_groups -asynchronous` over pairwise
|
|
||||||
# `set_false_path -from CLK -to CLK`. The latter is an STA antipattern:
|
|
||||||
# it disables *all* paths between the two domains, including the
|
|
||||||
# synchronizer paths themselves and any future inadvertent crossings,
|
|
||||||
# which can mask real CDC bugs that only show up at temperature/voltage
|
|
||||||
# corners. Clock-groups is the idiomatic way to declare domains async
|
|
||||||
# while still letting STA flag newly-introduced unrelated paths.
|
|
||||||
#
|
|
||||||
# Register-level false_paths (e.g. reset_sync_reg above) remain
|
|
||||||
# appropriate — those restrict the waiver to specific, audited endpoints.
|
|
||||||
#
|
|
||||||
# Groups declared here mirror the pairwise false_paths that existed
|
|
||||||
# previously; no new pair is declared async.
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
# clk_100m ↔ adc_dco_p (400 MHz): DDC has internal CDC synchronizers
|
# clk_100m ↔ adc_dco_p (400 MHz): DDC has internal CDC synchronizers
|
||||||
set_clock_groups -asynchronous \
|
set_false_path -from [get_clocks clk_100m] -to [get_clocks adc_dco_p]
|
||||||
-group [get_clocks clk_100m] \
|
set_false_path -from [get_clocks adc_dco_p] -to [get_clocks clk_100m]
|
||||||
-group [get_clocks adc_dco_p]
|
|
||||||
|
|
||||||
# clk_100m ↔ clk_120m_dac: CDC via synchronizers in radar_system_top
|
# clk_100m ↔ clk_120m_dac: CDC via synchronizers in radar_system_top
|
||||||
set_clock_groups -asynchronous \
|
set_false_path -from [get_clocks clk_100m] -to [get_clocks clk_120m_dac]
|
||||||
-group [get_clocks clk_100m] \
|
set_false_path -from [get_clocks clk_120m_dac] -to [get_clocks clk_100m]
|
||||||
-group [get_clocks clk_120m_dac]
|
|
||||||
|
|
||||||
# FT2232H CDC: clk_100m ↔ ft_clkout (60 MHz), toggle CDC in RTL
|
# FT2232H CDC: clk_100m ↔ ft_clkout (60 MHz), toggle CDC in RTL
|
||||||
set_clock_groups -asynchronous \
|
set_false_path -from [get_clocks clk_100m] -to [get_clocks ft_clkout]
|
||||||
-group [get_clocks clk_100m] \
|
set_false_path -from [get_clocks ft_clkout] -to [get_clocks clk_100m]
|
||||||
-group [get_clocks ft_clkout]
|
|
||||||
|
|
||||||
# FT2232H CDC: clk_120m_dac ↔ ft_clkout (no direct crossing, but belt-and-suspenders)
|
# FT2232H CDC: clk_120m_dac ↔ ft_clkout (no direct crossing, but belt-and-suspenders)
|
||||||
set_clock_groups -asynchronous \
|
set_false_path -from [get_clocks clk_120m_dac] -to [get_clocks ft_clkout]
|
||||||
-group [get_clocks clk_120m_dac] \
|
set_false_path -from [get_clocks ft_clkout] -to [get_clocks clk_120m_dac]
|
||||||
-group [get_clocks ft_clkout]
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PHYSICAL CONSTRAINTS
|
# PHYSICAL CONSTRAINTS
|
||||||
|
|||||||
@@ -62,20 +62,6 @@ module radar_system_top_50t (
|
|||||||
input wire adc_dco_n,
|
input wire adc_dco_n,
|
||||||
output wire adc_pwdn,
|
output wire adc_pwdn,
|
||||||
|
|
||||||
// ----- AD9484 overflow flag (differential) -----
|
|
||||||
// Schematic pads M6 (OR_P) / N6 (OR_N). Anchored-only for now; a future
|
|
||||||
// PR will wire this into the receive-path status flags for AGC feedback.
|
|
||||||
input wire adc_or_p,
|
|
||||||
input wire adc_or_n,
|
|
||||||
|
|
||||||
// ----- Tap of AD9523 -> AD9484 sample clock (differential) -----
|
|
||||||
// Schematic pads N11 (P) / N12 (N). Must remain input-only — driving
|
|
||||||
// these pads as outputs would contend with the AD9523 driver feeding
|
|
||||||
// the ADC. Anchored with an IBUFDS (DONT_TOUCH) below; buffered net is
|
|
||||||
// unconsumed pending a follow-up PR.
|
|
||||||
input wire fpga_adc_clock_p,
|
|
||||||
input wire fpga_adc_clock_n,
|
|
||||||
|
|
||||||
// ===== STM32 Control (Bank 15: 3.3V) =====
|
// ===== STM32 Control (Bank 15: 3.3V) =====
|
||||||
input wire stm32_new_chirp,
|
input wire stm32_new_chirp,
|
||||||
input wire stm32_new_elevation,
|
input wire stm32_new_elevation,
|
||||||
@@ -98,38 +84,6 @@ module radar_system_top_50t (
|
|||||||
output wire gpio_dig7 // DIG_7 (H12→PD15): reserved
|
output wire gpio_dig7 // DIG_7 (H12→PD15): reserved
|
||||||
);
|
);
|
||||||
|
|
||||||
// =====================================================================
|
|
||||||
// Anchored-but-unused schematic inputs (secured via IBUFDS + DONT_TOUCH)
|
|
||||||
// =====================================================================
|
|
||||||
// Without these buffer instantiations, synthesis would remove the
|
|
||||||
// orphan input ports (UCIO / NSTD warnings) and the XDC pin constraints
|
|
||||||
// would fail to bind. DONT_TOUCH forces Vivado to retain the buffer
|
|
||||||
// primitives and their package-pin connections across all optimization
|
|
||||||
// stages. The buffered nets are intentionally left unconsumed here;
|
|
||||||
// they will be wired into the RTL in a follow-up PR once the ADC
|
|
||||||
// status-flag and sample-clock-tap features are implemented.
|
|
||||||
(* DONT_TOUCH = "TRUE" *) wire adc_or_buf;
|
|
||||||
(* DONT_TOUCH = "TRUE" *) IBUFDS #(
|
|
||||||
.DIFF_TERM ("TRUE"),
|
|
||||||
.IBUF_LOW_PWR("FALSE"),
|
|
||||||
.IOSTANDARD ("LVDS_25")
|
|
||||||
) u_ibufds_adc_or (
|
|
||||||
.O (adc_or_buf),
|
|
||||||
.I (adc_or_p),
|
|
||||||
.IB (adc_or_n)
|
|
||||||
);
|
|
||||||
|
|
||||||
(* DONT_TOUCH = "TRUE" *) wire fpga_adc_clock_buf;
|
|
||||||
(* DONT_TOUCH = "TRUE" *) IBUFDS #(
|
|
||||||
.DIFF_TERM ("TRUE"),
|
|
||||||
.IBUF_LOW_PWR("FALSE"),
|
|
||||||
.IOSTANDARD ("LVDS_25")
|
|
||||||
) u_ibufds_fpga_adc_clk (
|
|
||||||
.O (fpga_adc_clock_buf),
|
|
||||||
.I (fpga_adc_clock_p),
|
|
||||||
.IB (fpga_adc_clock_n)
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== Tie-off wires for unconstrained FT601 inputs (inactive with USB_MODE=1) =====
|
// ===== Tie-off wires for unconstrained FT601 inputs (inactive with USB_MODE=1) =====
|
||||||
wire ft601_txe_tied = 1'b0;
|
wire ft601_txe_tied = 1'b0;
|
||||||
wire ft601_rxf_tied = 1'b0;
|
wire ft601_rxf_tied = 1'b0;
|
||||||
|
|||||||
@@ -169,11 +169,11 @@ endfunction
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Clamp a wider signed value to [-7, +7]
|
// Clamp a wider signed value to [-7, +7]
|
||||||
function signed [3:0] clamp_gain;
|
function signed [3:0] clamp_gain;
|
||||||
input signed [4:0] val; // 5-bit to handle overflow from add
|
input signed [5:0] val; // 6-bit: covers [-22,+22] (max |gain|+step = 7+15)
|
||||||
begin
|
begin
|
||||||
if (val > 5'sd7)
|
if (val > 6'sd7)
|
||||||
clamp_gain = 4'sd7;
|
clamp_gain = 4'sd7;
|
||||||
else if (val < -5'sd7)
|
else if (val < -6'sd7)
|
||||||
clamp_gain = -4'sd7;
|
clamp_gain = -4'sd7;
|
||||||
else
|
else
|
||||||
clamp_gain = val[3:0];
|
clamp_gain = val[3:0];
|
||||||
@@ -246,15 +246,15 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
// Use inclusive counts/peaks (accounting for simultaneous valid_in)
|
// Use inclusive counts/peaks (accounting for simultaneous valid_in)
|
||||||
if (wire_frame_sat_incr || frame_sat_count > 8'd0) begin
|
if (wire_frame_sat_incr || frame_sat_count > 8'd0) begin
|
||||||
// Clipping detected: reduce gain immediately (attack)
|
// Clipping detected: reduce gain immediately (attack)
|
||||||
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) -
|
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain[3], agc_gain}) -
|
||||||
$signed({1'b0, agc_attack}));
|
$signed({2'b00, agc_attack}));
|
||||||
holdoff_counter <= agc_holdoff; // Reset holdoff
|
holdoff_counter <= agc_holdoff; // Reset holdoff
|
||||||
end else if ((wire_frame_peak_update ? max_iq[14:7] : frame_peak[14:7])
|
end else if ((wire_frame_peak_update ? max_iq[14:7] : frame_peak[14:7])
|
||||||
< agc_target) begin
|
< agc_target) begin
|
||||||
// Signal too weak: increase gain after holdoff expires
|
// Signal too weak: increase gain after holdoff expires
|
||||||
if (holdoff_counter == 4'd0) begin
|
if (holdoff_counter == 4'd0) begin
|
||||||
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain}) +
|
agc_gain <= clamp_gain($signed({agc_gain[3], agc_gain[3], agc_gain}) +
|
||||||
$signed({1'b0, agc_decay}));
|
$signed({2'b00, agc_decay}));
|
||||||
end else begin
|
end else begin
|
||||||
holdoff_counter <= holdoff_counter - 4'd1;
|
holdoff_counter <= holdoff_counter - 4'd1;
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -103,6 +103,15 @@ class Opcode(IntEnum):
|
|||||||
STATUS_REQUEST = 0xFF
|
STATUS_REQUEST = 0xFF
|
||||||
|
|
||||||
|
|
||||||
|
# MCU-only commands — NOT dispatched to the FPGA opcode switch.
|
||||||
|
# These values have no corresponding case in radar_system_top.v.
|
||||||
|
# Listed here so the GUI can build and send them via build_command().
|
||||||
|
# contract_parser.py filters MCU_ONLY_OPCODES out of the Python/Verilog
|
||||||
|
# bidirectional check.
|
||||||
|
FAULT_ACK = 0x40 # Exact 4-byte CDC packet; clears system_emergency_state
|
||||||
|
MCU_ONLY_OPCODES: frozenset[int] = frozenset({0x40})
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Data Structures
|
# Data Structures
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -586,6 +586,135 @@ class TestSoftwareFPGA(unittest.TestCase):
|
|||||||
self.assertEqual(fpga.agc_holdoff, 0x0F)
|
self.assertEqual(fpga.agc_holdoff, 0x0F)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Test: live vs replay physical-unit parity — regression guard for unit drift
|
||||||
|
#
|
||||||
|
# Uses AST parse of workers.py (not inspect.getsource / import) so the test
|
||||||
|
# runs in headless CI without PyQt6 — v7.workers imports PyQt6 unconditionally
|
||||||
|
# at workers.py:24, and other worker tests here already use skipUnless(
|
||||||
|
# _pyqt6_available()). Contract enforcement must not be gated on GUI deps.
|
||||||
|
#
|
||||||
|
# Asserts on AST nodes (Call / Attribute / BinOp), not source substrings, so
|
||||||
|
# false-pass on comments or docstring wording is impossible.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class TestLiveReplayPhysicalUnitsParity(unittest.TestCase):
|
||||||
|
"""Contract: live path (RadarDataWorker._run_host_dsp) and replay path
|
||||||
|
(ReplayWorker._emit_frame) both derive bin-to-physical conversion from
|
||||||
|
WaveformConfig — same source of truth, identical (range_m, velocity_ms)
|
||||||
|
for identical detections.
|
||||||
|
|
||||||
|
Regression context: before the fix, live path used
|
||||||
|
RadarSettings.velocity_resolution (default 1.0 in models.py:113) while
|
||||||
|
replay used WaveformConfig.velocity_resolution_mps (~5.343). Live GUI
|
||||||
|
therefore under-reported velocity by factor ~5.34x vs replay for
|
||||||
|
identical frames. See test_v7.py:449 for the WaveformConfig pin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_method(class_name: str, method_name: str):
|
||||||
|
"""Return AST FunctionDef for class_name.method_name from workers.py,
|
||||||
|
without importing v7.workers (PyQt6-independent)."""
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
path = Path(__file__).parent / "v7" / "workers.py"
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||||
|
for node in tree.body:
|
||||||
|
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
||||||
|
for item in node.body:
|
||||||
|
if isinstance(item, ast.FunctionDef) and item.name == method_name:
|
||||||
|
return item
|
||||||
|
raise RuntimeError(f"{class_name}.{method_name} not found in workers.py")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_attribute_chain(tree, chain):
|
||||||
|
"""True if AST tree contains a dotted attribute access matching chain.
|
||||||
|
|
||||||
|
Chain ('self', '_settings', 'range_resolution') matches
|
||||||
|
``self._settings.range_resolution`` exactly.
|
||||||
|
"""
|
||||||
|
import ast
|
||||||
|
for n in ast.walk(tree):
|
||||||
|
if isinstance(n, ast.Attribute):
|
||||||
|
parts = [n.attr]
|
||||||
|
cur = n.value
|
||||||
|
while isinstance(cur, ast.Attribute):
|
||||||
|
parts.append(cur.attr)
|
||||||
|
cur = cur.value
|
||||||
|
if isinstance(cur, ast.Name):
|
||||||
|
parts.append(cur.id)
|
||||||
|
parts.reverse()
|
||||||
|
if tuple(parts) == tuple(chain):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_call_to(tree, func_name):
|
||||||
|
"""True if AST tree contains a call to a bare name (func_name())."""
|
||||||
|
import ast
|
||||||
|
for n in ast.walk(tree):
|
||||||
|
if (isinstance(n, ast.Call) and isinstance(n.func, ast.Name)
|
||||||
|
and n.func.id == func_name):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_dbin_minus(tree, literal):
|
||||||
|
"""True if AST tree contains ``dbin - <literal>`` binary op."""
|
||||||
|
import ast
|
||||||
|
for n in ast.walk(tree):
|
||||||
|
if (isinstance(n, ast.BinOp) and isinstance(n.op, ast.Sub)
|
||||||
|
and isinstance(n.left, ast.Name) and n.left.id == "dbin"
|
||||||
|
and isinstance(n.right, ast.Constant)
|
||||||
|
and n.right.value == literal):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_live_path_uses_waveform_config(self):
|
||||||
|
"""RadarDataWorker.__init__ must instantiate WaveformConfig() into
|
||||||
|
self._waveform; _run_host_dsp must read self._waveform.range_resolution_m
|
||||||
|
/ velocity_resolution_mps — not self._settings equivalents."""
|
||||||
|
init = self._parse_method("RadarDataWorker", "__init__")
|
||||||
|
self.assertTrue(self._has_call_to(init, "WaveformConfig"),
|
||||||
|
"RadarDataWorker.__init__ must instantiate WaveformConfig() into self._waveform.")
|
||||||
|
method = self._parse_method("RadarDataWorker", "_run_host_dsp")
|
||||||
|
self.assertTrue(
|
||||||
|
self._has_attribute_chain(method, ("self", "_waveform", "range_resolution_m")),
|
||||||
|
"Live path must read self._waveform.range_resolution_m.")
|
||||||
|
self.assertTrue(
|
||||||
|
self._has_attribute_chain(method, ("self", "_waveform", "velocity_resolution_mps")),
|
||||||
|
"Live path must read self._waveform.velocity_resolution_mps. "
|
||||||
|
"RadarSettings.velocity_resolution default 1.0 caused ~5.34x "
|
||||||
|
"underreport vs replay (test_v7.py:449 pins ~5.343).")
|
||||||
|
self.assertFalse(self._has_attribute_chain(
|
||||||
|
method, ("self", "_settings", "range_resolution")),
|
||||||
|
"Live path still reads stale RadarSettings.range_resolution.")
|
||||||
|
self.assertFalse(self._has_attribute_chain(
|
||||||
|
method, ("self", "_settings", "velocity_resolution")),
|
||||||
|
"Live path still reads stale RadarSettings.velocity_resolution.")
|
||||||
|
|
||||||
|
def test_live_path_doppler_center_not_hardcoded(self):
|
||||||
|
"""_run_host_dsp must derive doppler_center from frame shape, not
|
||||||
|
use hardcoded ``dbin - 16`` — mirrors processing.py:520."""
|
||||||
|
method = self._parse_method("RadarDataWorker", "_run_host_dsp")
|
||||||
|
self.assertFalse(self._has_dbin_minus(method, 16),
|
||||||
|
"Hardcoded doppler_center=16 breaks if frame shape changes. "
|
||||||
|
"Use frame.detections.shape[1] // 2 like processing.py:520.")
|
||||||
|
|
||||||
|
def test_replay_path_still_uses_waveform_config(self):
|
||||||
|
"""Parity half: replay path (ReplayWorker._emit_frame) must keep
|
||||||
|
reading self._waveform.range_resolution_m / velocity_resolution_mps —
|
||||||
|
guards against someone breaking the replay side of the invariant."""
|
||||||
|
method = self._parse_method("ReplayWorker", "_emit_frame")
|
||||||
|
self.assertTrue(self._has_attribute_chain(
|
||||||
|
method, ("self", "_waveform", "range_resolution_m")),
|
||||||
|
"Replay path lost WaveformConfig range source of truth.")
|
||||||
|
self.assertTrue(self._has_attribute_chain(
|
||||||
|
method, ("self", "_waveform", "velocity_resolution_mps")),
|
||||||
|
"Replay path lost WaveformConfig velocity source of truth.")
|
||||||
|
|
||||||
|
|
||||||
class TestSoftwareFPGASignalChain(unittest.TestCase):
|
class TestSoftwareFPGASignalChain(unittest.TestCase):
|
||||||
"""SoftwareFPGA.process_chirps with real co-sim data."""
|
"""SoftwareFPGA.process_chirps with real co-sim data."""
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import numpy as np
|
|||||||
|
|
||||||
from PyQt6.QtCore import QThread, QObject, QTimer, pyqtSignal
|
from PyQt6.QtCore import QThread, QObject, QTimer, pyqtSignal
|
||||||
|
|
||||||
from .models import RadarTarget, GPSData, RadarSettings
|
from .models import RadarTarget, GPSData, RadarSettings, WaveformConfig
|
||||||
from .hardware import (
|
from .hardware import (
|
||||||
RadarAcquisition,
|
RadarAcquisition,
|
||||||
RadarFrame,
|
RadarFrame,
|
||||||
@@ -84,6 +84,7 @@ class RadarDataWorker(QThread):
|
|||||||
self._recorder = recorder
|
self._recorder = recorder
|
||||||
self._gps = gps_data_ref
|
self._gps = gps_data_ref
|
||||||
self._settings = settings or RadarSettings()
|
self._settings = settings or RadarSettings()
|
||||||
|
self._waveform = WaveformConfig()
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
# Frame queue for production RadarAcquisition → this thread
|
# Frame queue for production RadarAcquisition → this thread
|
||||||
@@ -97,6 +98,9 @@ class RadarDataWorker(QThread):
|
|||||||
self._byte_count = 0
|
self._byte_count = 0
|
||||||
self._error_count = 0
|
self._error_count = 0
|
||||||
|
|
||||||
|
def set_waveform(self, wf: "WaveformConfig") -> None:
|
||||||
|
self._waveform = wf
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self._running = False
|
self._running = False
|
||||||
if self._acquisition:
|
if self._acquisition:
|
||||||
@@ -169,8 +173,8 @@ 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 self._waveform (WaveformConfig) to keep
|
||||||
and velocity_resolution (should be calibrated to actual waveform).
|
live and replay units aligned. Override via set_waveform() if needed.
|
||||||
"""
|
"""
|
||||||
targets: list[RadarTarget] = []
|
targets: list[RadarTarget] = []
|
||||||
|
|
||||||
@@ -180,8 +184,11 @@ 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._waveform.range_resolution_m
|
||||||
v_res = self._settings.velocity_resolution
|
v_res = self._waveform.velocity_resolution_mps
|
||||||
|
n_doppler = (frame.detections.shape[1] if frame.detections.ndim == 2
|
||||||
|
else self._waveform.n_doppler_bins)
|
||||||
|
doppler_center = n_doppler // 2
|
||||||
|
|
||||||
for idx in det_indices:
|
for idx in det_indices:
|
||||||
rbin, dbin = idx
|
rbin, dbin = idx
|
||||||
@@ -190,8 +197,9 @@ class RadarDataWorker(QThread):
|
|||||||
|
|
||||||
# Convert bin indices to physical units
|
# Convert bin indices to physical units
|
||||||
range_m = float(rbin) * r_res
|
range_m = float(rbin) * r_res
|
||||||
# Doppler: centre bin (16) = 0 m/s; positive bins = approaching
|
# Doppler: centre bin = 0 m/s; positive bins = approaching.
|
||||||
velocity_ms = float(dbin - 16) * v_res
|
# Derived from frame shape — mirrors processing.py:520.
|
||||||
|
velocity_ms = float(dbin - doppler_center) * v_res
|
||||||
|
|
||||||
# Apply pitch correction if GPS data available
|
# Apply pitch correction if GPS data available
|
||||||
raw_elev = 0.0 # FPGA doesn't send elevation per-detection
|
raw_elev = 0.0 # FPGA doesn't send elevation per-detection
|
||||||
|
|||||||
@@ -108,12 +108,23 @@ class ConcatWidth:
|
|||||||
|
|
||||||
def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]:
|
def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]:
|
||||||
"""Parse the Opcode enum from radar_protocol.py.
|
"""Parse the Opcode enum from radar_protocol.py.
|
||||||
Returns {opcode_value: OpcodeEntry}.
|
Returns {opcode_value: OpcodeEntry}, excluding MCU_ONLY_OPCODES.
|
||||||
|
MCU-only opcodes have no FPGA case statement and must not appear in
|
||||||
|
the bidirectional Python/Verilog contract check.
|
||||||
"""
|
"""
|
||||||
if filepath is None:
|
if filepath is None:
|
||||||
filepath = GUI_DIR / "radar_protocol.py"
|
filepath = GUI_DIR / "radar_protocol.py"
|
||||||
text = filepath.read_text()
|
text = filepath.read_text()
|
||||||
|
|
||||||
|
# Extract MCU_ONLY_OPCODES set so we can exclude those values below.
|
||||||
|
mcu_only: set[int] = set()
|
||||||
|
m_set = re.search(r'MCU_ONLY_OPCODES[^=]*=\s*frozenset\(\{([^}]*)\}\)', text)
|
||||||
|
if m_set:
|
||||||
|
for tok in m_set.group(1).split(','):
|
||||||
|
tok = tok.strip()
|
||||||
|
if tok.startswith(('0x', '0X')):
|
||||||
|
mcu_only.add(int(tok, 16))
|
||||||
|
|
||||||
# Find the Opcode class body
|
# Find the Opcode class body
|
||||||
match = re.search(r'class Opcode\b.*?(?=\nclass |\Z)', text, re.DOTALL)
|
match = re.search(r'class Opcode\b.*?(?=\nclass |\Z)', text, re.DOTALL)
|
||||||
if not match:
|
if not match:
|
||||||
@@ -123,7 +134,8 @@ def parse_python_opcodes(filepath: Path | None = None) -> dict[int, OpcodeEntry]
|
|||||||
for m in re.finditer(r'(\w+)\s*=\s*(0x[0-9a-fA-F]+)', match.group()):
|
for m in re.finditer(r'(\w+)\s*=\s*(0x[0-9a-fA-F]+)', match.group()):
|
||||||
name = m.group(1)
|
name = m.group(1)
|
||||||
value = int(m.group(2), 16)
|
value = int(m.group(2), 16)
|
||||||
opcodes[value] = OpcodeEntry(name=name, value=value)
|
if value not in mcu_only:
|
||||||
|
opcodes[value] = OpcodeEntry(name=name, value=value)
|
||||||
return opcodes
|
return opcodes
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
[](https://github.com/NawfalMotii79/PLFM_RADAR)
|
[](https://github.com/NawfalMotii79/PLFM_RADAR)
|
||||||
[](https://github.com/NawfalMotii79/PLFM_RADAR/pulls)
|
[](https://github.com/NawfalMotii79/PLFM_RADAR/pulls)
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
AERIS-10 is an open-source, low-cost 10.5 GHz phased array radar system featuring Pulse Linear Frequency Modulated (LFM) modulation. Available in two versions (3km and 20km range), it's designed for researchers, drone developers, and serious SDR enthusiasts who want to explore and experiment with phased array radar technology.
|
AERIS-10 is an open-source, low-cost 10.5 GHz phased array radar system featuring Pulse Linear Frequency Modulated (LFM) modulation. Available in two versions (3km and 20km range), it's designed for researchers, drone developers, and serious SDR enthusiasts who want to explore and experiment with phased array radar technology.
|
||||||
|
|
||||||
@@ -47,7 +46,7 @@ The AERIS-10 main sub-systems are:
|
|||||||
|
|
||||||
- **Main Board** containing:
|
- **Main Board** containing:
|
||||||
- **DAC** - Generates the RADAR Chirps
|
- **DAC** - Generates the RADAR Chirps
|
||||||
- **2x Microwave Mixers (LT5552)** - For up-conversion and IF-down-conversion
|
- **2x Microwave Mixers (LTC5552)** - For up-conversion and IF-down-conversion
|
||||||
- **4x 4-Channel Phase Shifters (ADAR1000)** - For RX and TX chain beamforming
|
- **4x 4-Channel Phase Shifters (ADAR1000)** - For RX and TX chain beamforming
|
||||||
- **16x Front End Chips (ADTR1107)** - Used for both Low Noise Amplifying (RX) and Power Amplifying (TX) stages
|
- **16x Front End Chips (ADTR1107)** - Used for both Low Noise Amplifying (RX) and Power Amplifying (TX) stages
|
||||||
- **XC7A50T FPGA** - Handles RADAR Signal Processing on the upstream FTG256 board:
|
- **XC7A50T FPGA** - Handles RADAR Signal Processing on the upstream FTG256 board:
|
||||||
@@ -92,7 +91,7 @@ The AERIS-10 main sub-systems are:
|
|||||||
### Processing Pipeline
|
### Processing Pipeline
|
||||||
|
|
||||||
1. **Waveform Generation** - DAC creates LFM chirps
|
1. **Waveform Generation** - DAC creates LFM chirps
|
||||||
2. **Up/Down Conversion** - LT5552 mixers handle frequency translation
|
2. **Up/Down Conversion** - LTC5552 mixers handle frequency translation
|
||||||
3. **Beam Steering** - ADAR1000 phase shifters control 16 elements
|
3. **Beam Steering** - ADAR1000 phase shifters control 16 elements
|
||||||
4. **Signal Processing (FPGA)**:
|
4. **Signal Processing (FPGA)**:
|
||||||
- Raw ADC data capture
|
- Raw ADC data capture
|
||||||
|
|||||||
Reference in New Issue
Block a user