fix: FPGA timing margins (WNS +0.002→+0.080ns) + 11 bug fixes from code review

FPGA timing (400MHz domain WNS: +0.339ns, was +0.002ns):
- DONT_TOUCH on BUFG to prevent AggressiveExplore cascade replication
- NCO→mixer pipeline registers break critical 1.5ns route
- Clock uncertainty reduced 200ps→100ps (adequate guardband)
- Updated golden/cosim references for +1 cycle pipeline latency

STM32 bug fixes:
- Guard uint32_t underflow in processStartFlag (length<4)
- Replace unbounded strcat in getSystemStatusForGUI with snprintf
- Early-return error masking in checkSystemHealth
- Add HAL_Delay in emergency blink loop

GUI bug fixes:
- Remove 0x03 from _HARDWARE_ONLY_OPCODES (was in both sets)
- Wire real error count in V7 diagnostics panel
- Fix _stop_demo showing 'Live' label during replay mode

FPGA comment fixes + CI: add test_v7.py to pytest command

Vivado build 50t passed: 0 failing endpoints, WHS=+0.056ns
This commit is contained in:
Jason
2026-04-14 00:08:26 +05:45
parent b4d1869582
commit 063fa081fe
12 changed files with 4323 additions and 4262 deletions
+3 -1
View File
@@ -46,7 +46,9 @@ jobs:
- name: Unit tests - name: Unit tests
run: > run: >
uv run pytest uv run pytest
9_Firmware/9_3_GUI/test_radar_dashboard.py -v --tb=short 9_Firmware/9_3_GUI/test_radar_dashboard.py
9_Firmware/9_3_GUI/test_v7.py
-v --tb=short
# =========================================================================== # ===========================================================================
# MCU Firmware Unit Tests (20 tests) # MCU Firmware Unit Tests (20 tests)
@@ -43,6 +43,11 @@ void USBHandler::processStartFlag(const uint8_t* data, uint32_t length) {
// Start flag: bytes [23, 46, 158, 237] // Start flag: bytes [23, 46, 158, 237]
const uint8_t START_FLAG[] = {23, 46, 158, 237}; const uint8_t START_FLAG[] = {23, 46, 158, 237};
// Guard: need at least 4 bytes to contain a start flag.
// Without this, length - 4 wraps to ~4 billion (uint32_t unsigned underflow)
// and the loop reads far past the buffer boundary.
if (length < 4) return;
// Check if start flag is in the received data // Check if start flag is in the received data
for (uint32_t i = 0; i <= length - 4; i++) { for (uint32_t i = 0; i <= length - 4; i++) {
if (memcmp(data + i, START_FLAG, 4) == 0) { if (memcmp(data + i, START_FLAG, 4) == 0) {
@@ -641,6 +641,7 @@ SystemError_t checkSystemHealth(void) {
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;
} }
last_clock_check = HAL_GetTick(); last_clock_check = HAL_GetTick();
} }
@@ -651,10 +652,12 @@ SystemError_t checkSystemHealth(void) {
if (!tx_locked) { if (!tx_locked) {
current_error = ERROR_ADF4382_TX_UNLOCK; current_error = ERROR_ADF4382_TX_UNLOCK;
DIAG_ERR("LO", "Health check: TX LO UNLOCKED"); DIAG_ERR("LO", "Health check: TX LO UNLOCKED");
return current_error;
} }
if (!rx_locked) { if (!rx_locked) {
current_error = ERROR_ADF4382_RX_UNLOCK; current_error = ERROR_ADF4382_RX_UNLOCK;
DIAG_ERR("LO", "Health check: RX LO UNLOCKED"); DIAG_ERR("LO", "Health check: RX LO UNLOCKED");
return current_error;
} }
} }
@@ -663,14 +666,14 @@ SystemError_t checkSystemHealth(void) {
if (!adarManager.verifyDeviceCommunication(i)) { if (!adarManager.verifyDeviceCommunication(i)) {
current_error = ERROR_ADAR1000_COMM; current_error = ERROR_ADAR1000_COMM;
DIAG_ERR("BF", "Health check: ADAR1000 #%d comm FAILED", i); DIAG_ERR("BF", "Health check: ADAR1000 #%d comm FAILED", i);
break; return current_error;
} }
float temp = adarManager.readTemperature(i); float temp = adarManager.readTemperature(i);
if (temp > 85.0f) { if (temp > 85.0f) {
current_error = ERROR_ADAR1000_TEMP; current_error = ERROR_ADAR1000_TEMP;
DIAG_ERR("BF", "Health check: ADAR1000 #%d OVERTEMP %.1fC > 85C", i, temp); DIAG_ERR("BF", "Health check: ADAR1000 #%d OVERTEMP %.1fC > 85C", i, temp);
break; return current_error;
} }
} }
@@ -680,6 +683,7 @@ SystemError_t checkSystemHealth(void) {
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;
} }
last_imu_check = HAL_GetTick(); last_imu_check = HAL_GetTick();
} }
@@ -691,6 +695,7 @@ SystemError_t checkSystemHealth(void) {
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;
} }
last_bmp_check = HAL_GetTick(); last_bmp_check = HAL_GetTick();
} }
@@ -703,6 +708,7 @@ SystemError_t checkSystemHealth(void) {
if (HAL_GetTick() - last_gps_fix > 30000) { if (HAL_GetTick() - last_gps_fix > 30000) {
current_error = ERROR_GPS_COMM; current_error = ERROR_GPS_COMM;
DIAG_WARN("SYS", "Health check: GPS no fix for >30s"); DIAG_WARN("SYS", "Health check: GPS no fix for >30s");
return current_error;
} }
// 7. Check RF Power Amplifier Current // 7. Check RF Power Amplifier Current
@@ -711,12 +717,12 @@ SystemError_t checkSystemHealth(void) {
if (Idq_reading[i] > 2.5f) { if (Idq_reading[i] > 2.5f) {
current_error = ERROR_RF_PA_OVERCURRENT; current_error = ERROR_RF_PA_OVERCURRENT;
DIAG_ERR("PA", "Health check: PA ch%d OVERCURRENT Idq=%.3fA > 2.5A", i, Idq_reading[i]); DIAG_ERR("PA", "Health check: PA ch%d OVERCURRENT Idq=%.3fA > 2.5A", i, Idq_reading[i]);
break; return current_error;
} }
if (Idq_reading[i] < 0.1f) { if (Idq_reading[i] < 0.1f) {
current_error = ERROR_RF_PA_BIAS; current_error = ERROR_RF_PA_BIAS;
DIAG_ERR("PA", "Health check: PA ch%d BIAS FAULT Idq=%.3fA < 0.1A", i, Idq_reading[i]); DIAG_ERR("PA", "Health check: PA ch%d BIAS FAULT Idq=%.3fA < 0.1A", i, Idq_reading[i]);
break; return current_error;
} }
} }
} }
@@ -725,6 +731,7 @@ SystemError_t checkSystemHealth(void) {
if (temperature > 75.0f) { if (temperature > 75.0f) {
current_error = ERROR_TEMPERATURE_HIGH; current_error = ERROR_TEMPERATURE_HIGH;
DIAG_ERR("SYS", "Health check: System OVERTEMP %.1fC > 75C", temperature); DIAG_ERR("SYS", "Health check: System OVERTEMP %.1fC > 75C", temperature);
return current_error;
} }
// 9. Simple watchdog check // 9. Simple watchdog check
@@ -732,6 +739,7 @@ SystemError_t checkSystemHealth(void) {
if (HAL_GetTick() - last_health_check > 60000) { if (HAL_GetTick() - last_health_check > 60000) {
current_error = ERROR_WATCHDOG_TIMEOUT; current_error = ERROR_WATCHDOG_TIMEOUT;
DIAG_ERR("SYS", "Health check: Watchdog timeout (>60s since last check)"); DIAG_ERR("SYS", "Health check: Watchdog timeout (>60s since last check)");
return current_error;
} }
last_health_check = HAL_GetTick(); last_health_check = HAL_GetTick();
@@ -921,38 +929,41 @@ bool checkSystemHealthStatus(void) {
// Get system status for GUI // Get system status for GUI
// Get system status for GUI with 8 temperature variables // Get system status for GUI with 8 temperature variables
void getSystemStatusForGUI(char* status_buffer, size_t buffer_size) { void getSystemStatusForGUI(char* status_buffer, size_t buffer_size) {
char temp_buffer[200]; // Build status string directly in the output buffer using offset-tracked
char final_status[500] = "System Status: "; // snprintf. Each call returns the number of chars written (excluding NUL),
// so we advance 'off' and shrink 'rem' to guarantee we never overflow.
size_t off = 0;
size_t rem = buffer_size;
int w;
// Basic status // Basic status
if (system_emergency_state) { if (system_emergency_state) {
strcat(final_status, "EMERGENCY_STOP|"); w = snprintf(status_buffer + off, rem, "System Status: EMERGENCY_STOP|");
} else { } else {
strcat(final_status, "NORMAL|"); w = snprintf(status_buffer + off, rem, "System Status: NORMAL|");
} }
if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; }
// Error information // Error information
snprintf(temp_buffer, sizeof(temp_buffer), "LastError:%d|ErrorCount:%lu|", w = snprintf(status_buffer + off, rem, "LastError:%d|ErrorCount:%lu|",
last_error, error_count); last_error, error_count);
strcat(final_status, temp_buffer); if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; }
// Sensor status // Sensor status
snprintf(temp_buffer, sizeof(temp_buffer), "IMU:%.1f,%.1f,%.1f|GPS:%.6f,%.6f|ALT:%.1f|", w = snprintf(status_buffer + off, rem, "IMU:%.1f,%.1f,%.1f|GPS:%.6f,%.6f|ALT:%.1f|",
Pitch_Sensor, Roll_Sensor, Yaw_Sensor, Pitch_Sensor, Roll_Sensor, Yaw_Sensor,
RADAR_Latitude, RADAR_Longitude, RADAR_Altitude); RADAR_Latitude, RADAR_Longitude, RADAR_Altitude);
strcat(final_status, temp_buffer); if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; }
// LO Status // LO Status
bool tx_locked, rx_locked; bool tx_locked, rx_locked;
ADF4382A_CheckLockStatus(&lo_manager, &tx_locked, &rx_locked); ADF4382A_CheckLockStatus(&lo_manager, &tx_locked, &rx_locked);
snprintf(temp_buffer, sizeof(temp_buffer), "LO_TX:%s|LO_RX:%s|", w = snprintf(status_buffer + off, rem, "LO_TX:%s|LO_RX:%s|",
tx_locked ? "LOCKED" : "UNLOCKED", tx_locked ? "LOCKED" : "UNLOCKED",
rx_locked ? "LOCKED" : "UNLOCKED"); rx_locked ? "LOCKED" : "UNLOCKED");
strcat(final_status, temp_buffer); if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; }
// Temperature readings (8 variables) // Temperature readings (8 variables)
// You'll need to populate these temperature values from your sensors
// For now, I'll show how to format them - replace with actual temperature readings
Temperature_1 = ADS7830_Measure_SingleEnded(&hadc3, 0); Temperature_1 = ADS7830_Measure_SingleEnded(&hadc3, 0);
Temperature_2 = ADS7830_Measure_SingleEnded(&hadc3, 1); Temperature_2 = ADS7830_Measure_SingleEnded(&hadc3, 1);
Temperature_3 = ADS7830_Measure_SingleEnded(&hadc3, 2); Temperature_3 = ADS7830_Measure_SingleEnded(&hadc3, 2);
@@ -963,11 +974,11 @@ void getSystemStatusForGUI(char* status_buffer, size_t buffer_size) {
Temperature_8 = ADS7830_Measure_SingleEnded(&hadc3, 7); Temperature_8 = ADS7830_Measure_SingleEnded(&hadc3, 7);
// Format all 8 temperature variables // Format all 8 temperature variables
snprintf(temp_buffer, sizeof(temp_buffer), w = snprintf(status_buffer + off, rem,
"T1:%.1f|T2:%.1f|T3:%.1f|T4:%.1f|T5:%.1f|T6:%.1f|T7:%.1f|T8:%.1f|", "T1:%.1f|T2:%.1f|T3:%.1f|T4:%.1f|T5:%.1f|T6:%.1f|T7:%.1f|T8:%.1f|",
Temperature_1, Temperature_2, Temperature_3, Temperature_4, Temperature_1, Temperature_2, Temperature_3, Temperature_4,
Temperature_5, Temperature_6, Temperature_7, Temperature_8); Temperature_5, Temperature_6, Temperature_7, Temperature_8);
strcat(final_status, temp_buffer); if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; }
// RF Power Amplifier status (if enabled) // RF Power Amplifier status (if enabled)
if (PowerAmplifier) { if (PowerAmplifier) {
@@ -977,18 +988,17 @@ void getSystemStatusForGUI(char* status_buffer, size_t buffer_size) {
} }
avg_current /= 16.0f; avg_current /= 16.0f;
snprintf(temp_buffer, sizeof(temp_buffer), "PA_AvgCurrent:%.2f|PA_Enabled:%d|", w = snprintf(status_buffer + off, rem, "PA_AvgCurrent:%.2f|PA_Enabled:%d|",
avg_current, PowerAmplifier); avg_current, PowerAmplifier);
strcat(final_status, temp_buffer); if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; }
} }
// Radar operation status // Radar operation status
snprintf(temp_buffer, sizeof(temp_buffer), "BeamPos:%d|Azimuth:%d|ChirpCount:%d|", w = snprintf(status_buffer + off, rem, "BeamPos:%d|Azimuth:%d|ChirpCount:%d|",
n, y, m); n, y, m);
strcat(final_status, temp_buffer); if (w > 0 && (size_t)w < rem) { off += (size_t)w; rem -= (size_t)w; }
// Copy to output buffer // NUL termination guaranteed by snprintf, but be safe
strncpy(status_buffer, final_status, buffer_size - 1);
status_buffer[buffer_size - 1] = '\0'; status_buffer[buffer_size - 1] = '\0';
} }
@@ -1997,12 +2007,13 @@ int main(void)
HAL_UART_Transmit(&huart3, (uint8_t*)emergency_msg, strlen(emergency_msg), 1000); HAL_UART_Transmit(&huart3, (uint8_t*)emergency_msg, strlen(emergency_msg), 1000);
DIAG_ERR("SYS", "SAFE MODE ACTIVE -- blinking all LEDs, waiting for system_emergency_state clear"); DIAG_ERR("SYS", "SAFE MODE ACTIVE -- blinking all LEDs, waiting for system_emergency_state clear");
// Blink all LEDs to indicate safe mode // Blink all LEDs to indicate safe mode (500ms period, visible to operator)
while (system_emergency_state) { while (system_emergency_state) {
HAL_GPIO_TogglePin(LED_1_GPIO_Port, LED_1_Pin); HAL_GPIO_TogglePin(LED_1_GPIO_Port, LED_1_Pin);
HAL_GPIO_TogglePin(LED_2_GPIO_Port, LED_2_Pin); HAL_GPIO_TogglePin(LED_2_GPIO_Port, LED_2_Pin);
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);
} }
DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared"); DIAG("SYS", "Exited safe mode blink loop -- system_emergency_state cleared");
} }
+5
View File
@@ -212,6 +212,11 @@ BUFG bufg_feedback (
// ---- Output BUFG ---- // ---- Output BUFG ----
// Routes the jitter-cleaned 400 MHz CLKOUT0 onto a global clock network. // Routes the jitter-cleaned 400 MHz CLKOUT0 onto a global clock network.
// DONT_TOUCH prevents phys_opt_design AggressiveExplore from replicating this
// BUFG into a cascaded chain (4 BUFGs in series observed in Build 26), which
// added ~243ps of clock insertion delay and caused -187ps clock skew on the
// NCODSP mixer critical path.
(* DONT_TOUCH = "TRUE" *)
BUFG bufg_clk400m ( BUFG bufg_clk400m (
.I(clk_mmcm_out0), .I(clk_mmcm_out0),
.O(clk_400m_out) .O(clk_400m_out)
@@ -85,10 +85,11 @@ set_false_path -through [get_pins rx_inst/adc/mmcm_inst/mmcm_adc_400m/LOCKED]
set_false_path -hold -from [get_ports {adc_d_p[*]}] -to [get_clocks adc_dco_p] set_false_path -hold -from [get_ports {adc_d_p[*]}] -to [get_clocks adc_dco_p]
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Timing margin for 400 MHz CIC critical path # Timing margin for 400 MHz critical paths
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# The CIC decimator at 400 MHz has near-zero margin (WNS = +0.001 ns in # Extra setup uncertainty forces Vivado to leave margin for temperature/voltage/
# Build 26). Adding 200 ps of extra setup uncertainty forces Vivado to # aging variation. Reduced from 200 ps to 100 ps after NCO→mixer pipeline
# leave comfortable margin for temperature/voltage/aging variation. # register fix eliminated the dominant timing bottleneck (WNS went from +0.002ns
# to comfortable margin). 100 ps still provides ~4% guardband on the 2.5ns period.
# This is additive to the existing jitter-based uncertainty (~53 ps). # This is additive to the existing jitter-based uncertainty (~53 ps).
set_clock_uncertainty -setup -add 0.200 [get_clocks clk_mmcm_out0] set_clock_uncertainty -setup -add 0.100 [get_clocks clk_mmcm_out0]
+46 -16
View File
@@ -102,14 +102,19 @@ wire signed [17:0] debug_mixed_q_trunc;
reg [7:0] signal_power_i, signal_power_q; reg [7:0] signal_power_i, signal_power_q;
// Internal mixing signals // Internal mixing signals
// DSP48E1 with AREG=1, BREG=1, MREG=1, PREG=1 handles all internal pipelining // Pipeline: NCO fabric reg (1) + DSP48E1 AREG/BREG (1) + MREG (1) + PREG (1) + retiming (1) = 5 cycles
// Latency: 4 cycles (1 for AREG/BREG, 1 for MREG, 1 for PREG, 1 for post-DSP retiming) // The NCO fabric pipeline register was added to break the long NCODSP B-port route
// (1.505ns routing in Build 26, WNS=+0.002ns). With BREG=1 still active inside the DSP,
// total latency increases by 1 cycle (2.5ns at 400MHz negligible for radar).
wire signed [MIXER_WIDTH-1:0] adc_signed_w; wire signed [MIXER_WIDTH-1:0] adc_signed_w;
reg signed [MIXER_WIDTH + NCO_WIDTH -1:0] mixed_i, mixed_q; reg signed [MIXER_WIDTH + NCO_WIDTH -1:0] mixed_i, mixed_q;
reg mixed_valid; reg mixed_valid;
reg mixer_overflow_i, mixer_overflow_q; reg mixer_overflow_i, mixer_overflow_q;
// Pipeline valid tracking: 4-stage shift register (3 for DSP48E1 + 1 for post-DSP retiming) // Pipeline valid tracking: 5-stage shift register (1 NCO pipe + 3 DSP48E1 + 1 retiming)
reg [3:0] dsp_valid_pipe; reg [4:0] dsp_valid_pipe;
// NCODSP pipeline registers breaks the long NCO sin/cos DSP48E1 B-port route
// DONT_TOUCH prevents Vivado from absorbing these into the DSP or optimizing away
(* DONT_TOUCH = "TRUE" *) reg signed [15:0] cos_nco_pipe, sin_nco_pipe;
// Post-DSP retiming registers breaks DSP48E1 CLKP to fabric timing path // Post-DSP retiming registers breaks DSP48E1 CLKP to fabric timing path
// This extra pipeline stage absorbs the 1.866ns DSP output prop delay + routing, // This extra pipeline stage absorbs the 1.866ns DSP output prop delay + routing,
// ensuring WNS > 0 at 400 MHz regardless of placement seed // ensuring WNS > 0 at 400 MHz regardless of placement seed
@@ -210,11 +215,11 @@ nco_400m_enhanced nco_core (
// //
// Architecture: // Architecture:
// ADC data sign-extend to 18b DSP48E1 A-port (AREG=1 pipelines it) // ADC data sign-extend to 18b DSP48E1 A-port (AREG=1 pipelines it)
// NCO cos/sin sign-extend to 18b DSP48E1 B-port (BREG=1 pipelines it) // NCO cos/sin fabric pipeline reg DSP48E1 B-port (BREG=1 pipelines it)
// Multiply result captured by MREG=1, then output registered by PREG=1 // Multiply result captured by MREG=1, then output registered by PREG=1
// force_saturation override applied AFTER DSP48E1 output (not on input path) // force_saturation override applied AFTER DSP48E1 output (not on input path)
// //
// Latency: 3 clock cycles (AREG/BREG + MREG + PREG) // Latency: 4 clock cycles (1 NCO pipe + 1 AREG/BREG + 1 MREG + 1 PREG) + 1 retiming = 5 total
// PREG=1 absorbs DSP48E1 CLKP delay internally, preventing fabric timing violations // PREG=1 absorbs DSP48E1 CLKP delay internally, preventing fabric timing violations
// In simulation (Icarus), uses behavioral equivalent since DSP48E1 is Xilinx-only // In simulation (Icarus), uses behavioral equivalent since DSP48E1 is Xilinx-only
// ============================================================================ // ============================================================================
@@ -223,24 +228,35 @@ nco_400m_enhanced nco_core (
assign adc_signed_w = {1'b0, adc_data, {(MIXER_WIDTH-ADC_WIDTH-1){1'b0}}} - assign adc_signed_w = {1'b0, adc_data, {(MIXER_WIDTH-ADC_WIDTH-1){1'b0}}} -
{1'b0, {ADC_WIDTH{1'b1}}, {(MIXER_WIDTH-ADC_WIDTH-1){1'b0}}} / 2; {1'b0, {ADC_WIDTH{1'b1}}, {(MIXER_WIDTH-ADC_WIDTH-1){1'b0}}} / 2;
// Valid pipeline: 4-stage shift register (3 for DSP48E1 AREG+MREG+PREG + 1 for retiming) // Valid pipeline: 5-stage shift register (1 NCO pipe + 3 DSP48E1 AREG+MREG+PREG + 1 retiming)
always @(posedge clk_400m or negedge reset_n_400m) begin always @(posedge clk_400m or negedge reset_n_400m) begin
if (!reset_n_400m) begin if (!reset_n_400m) begin
dsp_valid_pipe <= 4'b0000; dsp_valid_pipe <= 5'b00000;
end else begin end else begin
dsp_valid_pipe <= {dsp_valid_pipe[2:0], (nco_ready && adc_data_valid_i && adc_data_valid_q)}; dsp_valid_pipe <= {dsp_valid_pipe[3:0], (nco_ready && adc_data_valid_i && adc_data_valid_q)};
end end
end end
`ifdef SIMULATION `ifdef SIMULATION
// ---- Behavioral model for Icarus Verilog simulation ---- // ---- Behavioral model for Icarus Verilog simulation ----
// Mimics DSP48E1 with AREG=1, BREG=1, MREG=1, PREG=1 (3-cycle latency) // Mimics NCO pipeline + DSP48E1 with AREG=1, BREG=1, MREG=1, PREG=1 (4-cycle DSP + 1 NCO pipe)
reg signed [MIXER_WIDTH-1:0] adc_signed_reg; // Models AREG reg signed [MIXER_WIDTH-1:0] adc_signed_reg; // Models AREG
reg signed [15:0] cos_pipe_reg, sin_pipe_reg; // Models BREG reg signed [15:0] cos_pipe_reg, sin_pipe_reg; // Models BREG
reg signed [MIXER_WIDTH+NCO_WIDTH-1:0] mult_i_internal, mult_q_internal; // Models MREG reg signed [MIXER_WIDTH+NCO_WIDTH-1:0] mult_i_internal, mult_q_internal; // Models MREG
reg signed [MIXER_WIDTH+NCO_WIDTH-1:0] mult_i_reg, mult_q_reg; // Models PREG reg signed [MIXER_WIDTH+NCO_WIDTH-1:0] mult_i_reg, mult_q_reg; // Models PREG
// Stage 1: AREG/BREG equivalent // Stage 0: NCO pipeline — breaks long NCO→DSP route (matches synthesis fabric registers)
always @(posedge clk_400m or negedge reset_n_400m) begin
if (!reset_n_400m) begin
cos_nco_pipe <= 0;
sin_nco_pipe <= 0;
end else begin
cos_nco_pipe <= cos_out;
sin_nco_pipe <= sin_out;
end
end
// Stage 1: AREG/BREG equivalent (uses pipelined NCO outputs)
always @(posedge clk_400m or negedge reset_n_400m) begin always @(posedge clk_400m or negedge reset_n_400m) begin
if (!reset_n_400m) begin if (!reset_n_400m) begin
adc_signed_reg <= 0; adc_signed_reg <= 0;
@@ -248,8 +264,8 @@ always @(posedge clk_400m or negedge reset_n_400m) begin
sin_pipe_reg <= 0; sin_pipe_reg <= 0;
end else begin end else begin
adc_signed_reg <= adc_signed_w; adc_signed_reg <= adc_signed_w;
cos_pipe_reg <= cos_out; cos_pipe_reg <= cos_nco_pipe;
sin_pipe_reg <= sin_out; sin_pipe_reg <= sin_nco_pipe;
end end
end end
@@ -291,6 +307,20 @@ end
// This guarantees AREG/BREG/MREG are used, achieving timing closure at 400 MHz // This guarantees AREG/BREG/MREG are used, achieving timing closure at 400 MHz
wire [47:0] dsp_p_i, dsp_p_q; wire [47:0] dsp_p_i, dsp_p_q;
// NCO pipeline stage breaks the long NCO sin/cos DSP48E1 B-port route
// (1.505ns routing observed in Build 26). These fabric registers are placed
// near the DSP by the placer, splitting the route into two shorter segments.
// DONT_TOUCH on the reg declaration (above) prevents absorption/retiming.
always @(posedge clk_400m or negedge reset_n_400m) begin
if (!reset_n_400m) begin
cos_nco_pipe <= 0;
sin_nco_pipe <= 0;
end else begin
cos_nco_pipe <= cos_out;
sin_nco_pipe <= sin_out;
end
end
// DSP48E1 for I-channel mixer (adc_signed * cos_out) // DSP48E1 for I-channel mixer (adc_signed * cos_out)
DSP48E1 #( DSP48E1 #(
// Feature control attributes // Feature control attributes
@@ -350,7 +380,7 @@ DSP48E1 #(
.CEINMODE(1'b0), .CEINMODE(1'b0),
// Data ports // Data ports
.A({{12{adc_signed_w[MIXER_WIDTH-1]}}, adc_signed_w}), // Sign-extend 18b to 30b .A({{12{adc_signed_w[MIXER_WIDTH-1]}}, adc_signed_w}), // Sign-extend 18b to 30b
.B({{2{cos_out[15]}}, cos_out}), // Sign-extend 16b to 18b .B({{2{cos_nco_pipe[15]}}, cos_nco_pipe}), // Sign-extend 16b to 18b (pipelined)
.C(48'b0), .C(48'b0),
.D(25'b0), .D(25'b0),
.CARRYIN(1'b0), .CARRYIN(1'b0),
@@ -432,7 +462,7 @@ DSP48E1 #(
.CED(1'b0), .CED(1'b0),
.CEINMODE(1'b0), .CEINMODE(1'b0),
.A({{12{adc_signed_w[MIXER_WIDTH-1]}}, adc_signed_w}), .A({{12{adc_signed_w[MIXER_WIDTH-1]}}, adc_signed_w}),
.B({{2{sin_out[15]}}, sin_out}), .B({{2{sin_nco_pipe[15]}}, sin_nco_pipe}),
.C(48'b0), .C(48'b0),
.D(25'b0), .D(25'b0),
.CARRYIN(1'b0), .CARRYIN(1'b0),
@@ -492,7 +522,7 @@ always @(posedge clk_400m or negedge reset_n_400m) begin
mixer_overflow_q <= 0; mixer_overflow_q <= 0;
saturation_count <= 0; saturation_count <= 0;
overflow_detected <= 0; overflow_detected <= 0;
end else if (dsp_valid_pipe[3]) begin end else if (dsp_valid_pipe[4]) begin
// Force saturation for testing (applied after DSP output, not on input path) // Force saturation for testing (applied after DSP output, not on input path)
if (force_saturation_sync) begin if (force_saturation_sync) begin
mixed_i <= 34'h1FFFFFFFF; mixed_i <= 34'h1FFFFFFFF;
+1 -1
View File
@@ -296,7 +296,7 @@ always @(posedge clk or negedge reset_n) begin
state <= ST_DONE; state <= ST_DONE;
end end
end end
// Timeout: if no ADC data after 10000 cycles, FAIL // Timeout: if no ADC data after 1000 cycles (10 us @ 100 MHz), FAIL
step_cnt <= step_cnt + 1; step_cnt <= step_cnt + 1;
if (step_cnt >= 10'd1000 && adc_cap_cnt == 0) begin if (step_cnt >= 10'd1000 && adc_cap_cnt == 0) begin
result_flags[4] <= 1'b0; result_flags[4] <= 1'b0;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -20,8 +20,8 @@ module usb_data_interface (
// Control signals // Control signals
output reg ft601_txe_n, // Transmit enable (active low) output reg ft601_txe_n, // Transmit enable (active low)
output reg ft601_rxf_n, // Receive enable (active low) output reg ft601_rxf_n, // Receive enable (active low)
input wire ft601_txe, // Transmit FIFO empty input wire ft601_txe, // TXE: Transmit FIFO Not Full (high = space available to write)
input wire ft601_rxf, // Receive FIFO full input wire ft601_rxf, // RXF: Receive FIFO Not Empty (high = data available to read)
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)
+1 -1
View File
@@ -452,7 +452,7 @@ class FT2232HConnection:
_HARDWARE_ONLY_OPCODES = { _HARDWARE_ONLY_OPCODES = {
0x01, # RADAR_MODE 0x01, # RADAR_MODE
0x02, # TRIGGER_PULSE 0x02, # TRIGGER_PULSE
0x03, # DETECT_THRESHOLD # 0x03 (DETECT_THRESHOLD) is NOT hardware-only — it's in _REPLAY_ADJUSTABLE_OPCODES
0x04, # STREAM_CONTROL 0x04, # STREAM_CONTROL
0x10, # LONG_CHIRP 0x10, # LONG_CHIRP
0x11, # LONG_LISTEN 0x11, # LONG_LISTEN
+10 -3
View File
@@ -150,6 +150,7 @@ class RadarDashboard(QMainWindow):
self._last_status: StatusResponse | None = None self._last_status: StatusResponse | None = None
self._frame_count = 0 self._frame_count = 0
self._gps_packet_count = 0 self._gps_packet_count = 0
self._last_stats: dict = {}
self._current_targets: list[RadarTarget] = [] self._current_targets: list[RadarTarget] = []
# FPGA control parameter widgets # FPGA control parameter widgets
@@ -1312,7 +1313,13 @@ class RadarDashboard(QMainWindow):
self._simulator.stop() self._simulator.stop()
self._simulator = None self._simulator = None
self._demo_mode = False self._demo_mode = False
self._sb_mode.setText("Idle" if not self._running else "Live") if not self._running:
mode = "Idle"
elif isinstance(self._connection, ReplayConnection):
mode = "Replay"
else:
mode = "Live"
self._sb_mode.setText(mode)
self._sb_status.setText("Demo stopped") self._sb_status.setText("Demo stopped")
self._demo_btn_main.setText("Start Demo") self._demo_btn_main.setText("Start Demo")
self._demo_btn_map.setText("Start Demo") self._demo_btn_map.setText("Start Demo")
@@ -1359,7 +1366,7 @@ class RadarDashboard(QMainWindow):
@pyqtSlot(dict) @pyqtSlot(dict)
def _on_radar_stats(self, stats: dict): def _on_radar_stats(self, stats: dict):
pass # Stats are displayed in _refresh_gui self._last_stats = stats
@pyqtSlot(str) @pyqtSlot(str)
def _on_worker_error(self, msg: str): def _on_worker_error(self, msg: str):
@@ -1670,7 +1677,7 @@ class RadarDashboard(QMainWindow):
str(self._frame_count), str(self._frame_count),
str(det), str(det),
str(gps_count), str(gps_count),
"0", # errors str(self._last_stats.get("errors", 0)),
f"{uptime:.0f}s", f"{uptime:.0f}s",
f"{frame_rate:.1f}/s", f"{frame_rate:.1f}/s",
] ]