From ada170ef1f2de1938aba6eb4a658b5e5d39d8c77 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 7 May 2026 13:29:48 +0545 Subject: [PATCH] =?UTF-8?q?feat(fpga,mcu,gui):=20PR-AB.b=20=E2=80=94=20dri?= =?UTF-8?q?ft-free=20dwell=20sync=20via=20DIG=5F6=20frame=5Fpulse=20+=20AG?= =?UTF-8?q?C=20always-on=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FPGA (Phase 1+2): - gpio_dig6 (PD14) now carries chirp_scheduler frame_pulse, FPGA-stretched to ~100 ns so the STM32 EXTI on PD14 can latch reliably. - gpio_dig7 (PD15) returns to its pre-PR-AB.b role: control-fault OR (range_decim_watchdog | CDC overrun); MCU stuck-high sampler unchanged. - rx_range_decim_watchdog gains a sticky in source clock domain so a slow status poll cannot miss a 1-cycle assertion (Phase 1). - New tb_dig6_frame_pulse.v (13 checks); tb_status_words_stickies.v extended with DIG_7 fault-OR coverage (14 checks); retired tb_audit_s10_gpio_split.v. - Port comments in radar_system_top.v / _50t.v and XDC roles refreshed. MCU (Phase 3): - PD14 reconfigured to GPIO_MODE_IT_RISING + GPIO_PULLDOWN; new EXTI15_10_IRQHandler in stm32f7xx_it.c dispatches to HAL_GPIO_EXTI_Callback that bumps a volatile g_frame_pulse_count. - runRadarPulseSequence dwell loop replaces 3x HAL_Delay(8) with waitForFramePulse(20) — per-pattern dwell now tracks the actual mask-aware ladder length (drift-free, mask-aware), with a 20 ms timeout safety net. - AGC outer loop is ALWAYS-ON in production (compile-time policy); bench builds compile the body out via -DMCU_AGC_FORCE_DISABLED. The runtime enable/debounce + DIG_6 polling that previously gated AGC are removed. - main.h adds FPGA_FRAME_PULSE_* aliases pointing at FPGA_DIG6_*. GUI (Phase 4): - Settings tab gains a Bench / Diagnostics group with a BENCH-MODE checkbox (off by default, persisted via QSettings). - AGC group header swaps between a green "AGC: ALWAYS-ON" badge (production) and Enable/Disable AGC buttons (bench), pinned to the top of the group. The redundant 0/1 spinbox row for opcode 0x28 is removed — buttons send the same opcode and cannot accept invalid input. - Both the FPGA Control AGC Status box and the AGC Monitor strip share a helper that honours bench-mode in production (always shows ALWAYS-ON in green so the two views never disagree with the badge). - _add_fpga_param_row uses setFixedWidth on label and Set button + explicit stretch=1 on the hint, so all rows align column-wise whether they sit directly in a QVBoxLayout or inside a wrapper QWidget. Regression: FPGA 42/0/0 (PR-M.4 baseline) - MCU 34/34 - GPS extended 51/51 - GUI v7 150/150 - BENCH-MODE flip behaviorally verified. Hardware-blocked steps deferred: bench-scope verify (PD14 dwell pulse, counter advance, PD15 stuck-high recovery still triggers). Closes #182. --- .../9_1_1_C_Cpp_Libraries/stm32f7xx_it.c | 19 ++ .../9_1_3_C_Cpp_Code/main.cpp | 119 +++++--- .../9_1_3_C_Cpp_Code/main.h | 14 +- .../tests/test_agc_outer_loop.cpp | 2 +- .../9_2_FPGA/constraints/xc7a50t_ftg256.xdc | 15 +- 9_Firmware/9_2_FPGA/radar_system_top.v | 79 +++++- 9_Firmware/9_2_FPGA/radar_system_top_50t.v | 4 +- 9_Firmware/9_2_FPGA/run_regression.sh | 10 +- 9_Firmware/9_2_FPGA/tb/tb_dig6_frame_pulse.v | 205 ++++++++++++++ ...pio_split.v => tb_status_words_stickies.v} | 256 ++++++------------ 9_Firmware/9_3_GUI/v7/dashboard.py | 189 +++++++++++-- 11 files changed, 655 insertions(+), 257 deletions(-) create mode 100644 9_Firmware/9_2_FPGA/tb/tb_dig6_frame_pulse.v rename 9_Firmware/9_2_FPGA/tb/{tb_audit_s10_gpio_split.v => tb_status_words_stickies.v} (50%) diff --git a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/stm32f7xx_it.c b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/stm32f7xx_it.c index 4e8cbd6..604adb3 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/stm32f7xx_it.c +++ b/9_Firmware/9_1_Microcontroller/9_1_1_C_Cpp_Libraries/stm32f7xx_it.c @@ -212,6 +212,25 @@ void OTG_FS_IRQHandler(void) /* USER CODE END OTG_FS_IRQn 1 */ } +/** + * @brief This function handles EXTI lines [15:10] interrupts. + * + * PR-AB.b: PD14 (FPGA frame_pulse / DIG_6) is the only EXTI source on lines + * 10-15. HAL_GPIO_EXTI_IRQHandler clears the pending bit and dispatches to + * HAL_GPIO_EXTI_Callback in main.cpp, which increments g_frame_pulse_count + * to release the runRadarPulseSequence dwell loop. + */ +void EXTI15_10_IRQHandler(void) +{ + /* USER CODE BEGIN EXTI15_10_IRQn 0 */ + + /* USER CODE END EXTI15_10_IRQn 0 */ + HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_14); + /* USER CODE BEGIN EXTI15_10_IRQn 1 */ + + /* USER CODE END EXTI15_10_IRQn 1 */ +} + /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ diff --git a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp index 67a2823..2ba8af0 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp +++ b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.cpp @@ -300,6 +300,25 @@ void systemPowerUpSequence(); void systemPowerDownSequence(); void initializeBeamMatrices(); void runRadarPulseSequence(); + +/* PR-AB.b: FPGA chirp_scheduler frame_pulse (DIG_6 / PD14, ~100 ns stretched) + * is wired to EXTI15_10. Each rising edge bumps g_frame_pulse_count from the + * ISR; the dwell loop in runRadarPulseSequence waits for the counter to + * advance instead of HAL_Delay-padding, so per-pattern dwell tracks the + * actual mask-aware ladder length (drift-free, mask-aware). The ladder is + * ~8 ms; we use a 20 ms timeout so a missed pulse cannot stall the loop. */ +static volatile uint32_t g_frame_pulse_count = 0; + +static inline void waitForFramePulse(uint32_t timeout_ms) +{ + uint32_t prev = g_frame_pulse_count; + uint32_t t0 = HAL_GetTick(); + while (g_frame_pulse_count == prev) { + if ((HAL_GetTick() - t0) >= timeout_ms) { + return; // timeout — caller continues; missed-pulse degrades to wall-clock cadence + } + } +} /* F-2.1: executeChirpSequence() removed. The MCU is no longer in the chirp * dispatch path -- production runs FPGA mode 2'b01 (auto-scan) where * chirp_scheduler.v owns chirp timing and adar_tr_x. See runRadarPulseSequence @@ -376,6 +395,17 @@ extern "C" { USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &usb_rx_buffer[0]); USBD_CDC_ReceivePacket(&hUsbDeviceFS); } + + // PR-AB.b: EXTI callback for FPGA frame_pulse on PD14 (DIG_6). + // EXTI15_10_IRQHandler in stm32f7xx_it.c calls HAL_GPIO_EXTI_IRQHandler, + // which dispatches here. Bumping g_frame_pulse_count releases the dwell + // loop's waitForFramePulse spin. PD15 (DIG_7 fault flag) stays polled + // by checkSystemHealth — no EXTI on that line. + void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { + if (GPIO_Pin == FPGA_FRAME_PULSE_Pin) { + g_frame_pulse_count++; + } + } } void systemPowerUpSequence() { @@ -577,25 +607,26 @@ void runRadarPulseSequence() { * reference; if multiple per-pos broadside frames are ever needed * they should re-enter behind a runtime switch. * - * Per-pattern dwell: + * Per-pattern dwell tracks the actual chirp_scheduler ladder length + * (drift-free, mask-aware) by waiting on the FPGA frame_pulse on + * DIG_6 / PD14 — see waitForFramePulse + HAL_GPIO_EXTI_Callback above. + * Nominal ladder is: * 16 × PRI_SHORT (175 us) + * 16 × PRI_MEDIUM (161 us) + - * 16 × PRI_LONG (167 us) = 8048 us per ladder, rounded to 8 ms. - * HAL_Delay yields to SysTick / IRQs, so unlike the removed - * executeChirpSequence busy-loop the MCU stays responsive to USB CDC, - * UART, and I2C peripherals during the dwell. For drift-immune sync - * we'd wait on an FPGA `subframe_pulse` GPIO instead — DIG_7 - * (H12→PD15) is wired in the schematic but currently driven by the - * F-6.4 watchdog OR; reassigning it is a tracked follow-up - * (PR-AB.b). */ - static const uint32_t BEAM_PATTERN_DWELL_MS = 8u; + * 16 × PRI_LONG (167 us) = 8048 us + * but with subframe_enable masking a single PRI it can drop to ~2.7 ms, + * which a fixed HAL_Delay(8) used to over-pad. The 20 ms timeout below + * is a safety floor — if a frame_pulse ever goes missing (FPGA reset, + * PD14 disconnected) the loop degrades to wall-clock cadence rather + * than hanging. */ + static const uint32_t FRAME_PULSE_TIMEOUT_MS = 20u; // One broadside (vector_0) reference frame per azimuth — replaces the // 15 in-loop fires that PR-AB.a removed. DIAG("SYS", "Broadside reference (vector_0) — 1× per azimuth"); adarManager.setCustomBeamPattern16(vector_0, ADAR1000Manager::BeamDirection::TX); adarManager.setCustomBeamPattern16(vector_0, ADAR1000Manager::BeamDirection::RX); - HAL_Delay(BEAM_PATTERN_DWELL_MS); + waitForFramePulse(FRAME_PULSE_TIMEOUT_MS); m += m_max/2; for(int beam_pos = 0; beam_pos < 15; beam_pos++) { @@ -604,13 +635,13 @@ void runRadarPulseSequence() { // Pattern 1: matrix1 (negative-θ scan, peak at -62°..-3°) adarManager.setCustomBeamPattern16(matrix1[beam_pos], ADAR1000Manager::BeamDirection::TX); adarManager.setCustomBeamPattern16(matrix1[beam_pos], ADAR1000Manager::BeamDirection::RX); - HAL_Delay(BEAM_PATTERN_DWELL_MS); + waitForFramePulse(FRAME_PULSE_TIMEOUT_MS); m += m_max/2; // Pattern 2: matrix2 (positive-θ scan, peak at +3°..+62°) adarManager.setCustomBeamPattern16(matrix2[beam_pos], ADAR1000Manager::BeamDirection::TX); adarManager.setCustomBeamPattern16(matrix2[beam_pos], ADAR1000Manager::BeamDirection::RX); - HAL_Delay(BEAM_PATTERN_DWELL_MS); + waitForFramePulse(FRAME_PULSE_TIMEOUT_MS); m += m_max/2; // Reset chirp counter if needed @@ -1718,6 +1749,17 @@ int main(void) DIAG("SYS", "DWT cycle counter initialized, TIM1 started"); DIAG("SYS", "HAL tick at init start: %lu ms", (unsigned long)HAL_GetTick()); + /* PR-AB.b: production AGC policy is ALWAYS-ON. Bench builds compile this + * out via -DMCU_AGC_FORCE_DISABLED so engineers can drive the ADAR VGA + * by hand. Default class state is enabled=false (see ADAR1000_AGC ctor), + * so we have to flip it here for the production polling loop to do work. */ +#ifndef MCU_AGC_FORCE_DISABLED + outerAgc.enabled = true; + DIAG("AGC", "Outer-loop AGC ALWAYS-ON (production policy)"); +#else + DIAG("AGC", "Outer-loop AGC compiled out (MCU_AGC_FORCE_DISABLED bench build)"); +#endif + /* MCU-A4: skip the full 180 s OCXO warmup on warm restart. BKPSRAM * survives MCU-only resets (IWDG, SYSRESETREQ, brown-out) so the warmup * flag from the previous boot tells us the OCXO oven is still hot and @@ -2582,30 +2624,23 @@ int main(void) runRadarPulseSequence(); - /* [AGC] Outer-loop AGC: sync enable from FPGA via DIG_6 (PD14), - * then read saturation flag (DIG_5 / PD13) and adjust ADAR1000 VGA - * common gain once per radar frame (~258 ms). - * FPGA register host_agc_enable is the single source of truth — - * DIG_6 propagates it to MCU every frame. - * 2-frame confirmation debounce: only change outerAgc.enabled when - * two consecutive frames read the same DIG_6 value. Prevents a - * single-sample glitch from causing a spurious AGC state transition. - * Added latency: 1 extra frame (~258 ms), acceptable for control plane. */ + /* [AGC] Outer-loop AGC: read saturation flag (DIG_5 / PD13) once per + * radar frame and adjust the ADAR1000 VGA common gain. PR-AB.b + * repurposed DIG_6 to carry the FPGA frame_pulse, so the runtime + * enable/debounce path is gone — production firmware runs AGC + * unconditionally (always-on policy). Bench builds compile this body + * out by defining MCU_AGC_FORCE_DISABLED, which lets engineers drive + * the ADAR VGA manually via adarManager / GUI gain widgets without + * the AGC fighting them. The host opcode-0x28 register stays plumbed + * for telemetry display; the MCU just no longer reads it. */ +#ifndef MCU_AGC_FORCE_DISABLED { - bool dig6_now = (HAL_GPIO_ReadPin(FPGA_DIG6_GPIO_Port, - FPGA_DIG6_Pin) == GPIO_PIN_SET); - static bool dig6_prev = false; // matches boot default (AGC off) - if (dig6_now == dig6_prev) { - outerAgc.enabled = dig6_now; - } - dig6_prev = dig6_now; - } - if (outerAgc.enabled) { bool sat = HAL_GPIO_ReadPin(FPGA_DIG5_SAT_GPIO_Port, FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET; outerAgc.update(sat); outerAgc.applyGain(adarManager); } +#endif /* [GAP-3 FIX 2] Kick hardware watchdog — if we don't reach here within * ~4 s, the IWDG resets the MCU automatically. */ @@ -3143,12 +3178,30 @@ static void MX_GPIO_Init(void) GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); - /*Configure GPIO pins : PD13 PD14 PD15 */ - GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15; + /*Configure GPIO pins : PD13 PD15 — polled inputs + * PD13 (FPGA_DIG5_SAT): saturation flag for AGC outer loop + * PD15 (FPGA_DIG7): control-fault OR, polled by checkSystemHealth */ + GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_15; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); + /*Configure GPIO pin : PD14 — PR-AB.b FPGA frame_pulse, EXTI rising edge. + * Pulldown so a disconnected FPGA can't ring the line and starve the dwell + * loop with phantom pulses (the FPGA-side stretcher drives 3.3V CMOS active + * high for ~100 ns). NVIC enable below routes EXTI15_10_IRQn to the + * handler in stm32f7xx_it.c. */ + GPIO_InitStruct.Pin = FPGA_FRAME_PULSE_Pin; + GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; + GPIO_InitStruct.Pull = GPIO_PULLDOWN; + HAL_GPIO_Init(FPGA_FRAME_PULSE_GPIO_Port, &GPIO_InitStruct); + + /* Priority below USB_OTG_FS (0) and SysTick — frame_pulse ISR is short + * (one volatile increment) and a couple of ms of jitter is invisible to + * the dwell loop, so it doesn't need top priority. */ + HAL_NVIC_SetPriority(FPGA_FRAME_PULSE_EXTI_IRQn, 5, 0); + HAL_NVIC_EnableIRQ(FPGA_FRAME_PULSE_EXTI_IRQn); + /*Configure GPIO pins : ADF4382_RX_LKDET_Pin ADF4382_TX_LKDET_Pin */ GPIO_InitStruct.Pin = ADF4382_RX_LKDET_Pin|ADF4382_TX_LKDET_Pin; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; diff --git a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h index f5b8d0d..057eaec 100644 --- a/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h +++ b/9_Firmware/9_1_Microcontroller/9_1_3_C_Cpp_Code/main.h @@ -142,7 +142,14 @@ void Error_Handler(void); #define EN_DIS_COOLING_Pin GPIO_PIN_7 #define EN_DIS_COOLING_GPIO_Port GPIOD -/* FPGA digital I/O (directly connected GPIOs) */ +/* FPGA digital I/O (directly connected GPIOs) + * DIG_5 (PD13): signal-saturation flag — input, polled by AGC outer loop + * (gated by !MCU_AGC_FORCE_DISABLED). + * DIG_6 (PD14): chirp_scheduler frame_pulse, ~100 ns FPGA-stretched — + * EXTI rising + pulldown; ISR increments g_frame_pulse_count + * (PR-AB.b drift-free dwell sync). + * DIG_7 (PD15): control-fault OR (range_decim_watchdog | CIC→FIR overrun) — + * polled by checkSystemHealth stuck-high sampler. */ #define FPGA_DIG5_SAT_Pin GPIO_PIN_13 #define FPGA_DIG5_SAT_GPIO_Port GPIOD #define FPGA_DIG6_Pin GPIO_PIN_14 @@ -150,6 +157,11 @@ void Error_Handler(void); #define FPGA_DIG7_Pin GPIO_PIN_15 #define FPGA_DIG7_GPIO_Port GPIOD +/* PR-AB.b: alias names that match the post-Phase-3 role of DIG_6. */ +#define FPGA_FRAME_PULSE_Pin FPGA_DIG6_Pin +#define FPGA_FRAME_PULSE_GPIO_Port FPGA_DIG6_GPIO_Port +#define FPGA_FRAME_PULSE_EXTI_IRQn EXTI15_10_IRQn + #define ADF4382_RX_CE_Pin GPIO_PIN_9 #define ADF4382_RX_CE_GPIO_Port GPIOG #define ADF4382_RX_CS_Pin GPIO_PIN_10 diff --git a/9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop.cpp b/9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop.cpp index 8eb5292..d8ac2d0 100644 --- a/9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop.cpp +++ b/9_Firmware/9_1_Microcontroller/tests/test_agc_outer_loop.cpp @@ -50,7 +50,7 @@ static void test_defaults() assert(agc.min_gain == 0); assert(agc.max_gain == 127); assert(agc.holdoff_frames == 4); - assert(agc.enabled == false); // disabled by default — FPGA DIG_6 is source of truth + assert(agc.enabled == false); // class default; main.cpp flips this on under !MCU_AGC_FORCE_DISABLED assert(agc.holdoff_counter == 0); assert(agc.last_saturated == false); assert(agc.saturation_event_count == 0); diff --git a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc index 42b2bd5..7cb7d94 100644 --- a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc +++ b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc @@ -219,8 +219,8 @@ set_property IOSTANDARD LVCMOS18 [get_ports {stm32_*_1v8}] # ============================================================================ # STM32 CONTROL INTERFACE (DIG bus, Bank 15, VCCO=3.3V) # ============================================================================ -# DIG_0..DIG_4 are STM32 outputs (PD8-PD12) → FPGA inputs -# DIG_5..DIG_7 are STM32 inputs (PD13-PD15) ← FPGA outputs (unused in RTL) +# DIG_0..DIG_4 are STM32 outputs (PD8-PD12) → FPGA inputs (control) +# DIG_5..DIG_7 are STM32 inputs (PD13-PD15) ← FPGA outputs (status / sync) set_property PACKAGE_PIN F13 [get_ports {stm32_new_chirp}] ;# DIG_0 (PD8) set_property PACKAGE_PIN E16 [get_ports {stm32_new_elevation}] ;# DIG_1 (PD9) @@ -230,10 +230,13 @@ set_property IOSTANDARD LVCMOS33 [get_ports {stm32_new_*}] set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}] # reset_n is DIG_4 (PD12) — constrained above in the RESET section -# DIG_5 = H11, DIG_6 = G12, DIG_7 = H12 — FPGA→STM32 status outputs -# DIG_5: AGC saturation flag (PD13 on STM32) -# DIG_6: AGC enable flag (PD14) — mirrors FPGA host_agc_enable to STM32 -# DIG_7: reserved (PD15) +# DIG_5 = H11, DIG_6 = G12, DIG_7 = H12 — FPGA→STM32 status / sync outputs +# DIG_5 (PD13): signal-chain saturation flag — drives MCU outer-loop AGC. +# DIG_6 (PD14): stretched chirp_scheduler frame_pulse (~100 ns) — PR-AB.b +# STM32 EXTI rising edge for drift-free dwell sync. +# DIG_7 (PD15): control-chain fault OR — F-6.4 range_decim_watchdog +# | F-1.2 CIC→FIR CDC overrun. MCU PD15 stuck-high sampler +# triggers attemptErrorRecovery(ERROR_FPGA_DSP_STALL). set_property PACKAGE_PIN H11 [get_ports {gpio_dig5}] set_property PACKAGE_PIN G12 [get_ports {gpio_dig6}] set_property PACKAGE_PIN H12 [get_ports {gpio_dig7}] diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index b82a0f6..b33ffe6 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -135,8 +135,8 @@ module radar_system_top ( // FPGA→STM32 GPIO outputs (DIG_5..DIG_7 on 50T board) // Used by STM32 outer AGC loop to read saturation state without USB polling. output wire gpio_dig5, // DIG_5 (H11→PD13): AGC saturation flag (1=clipping detected) - output wire gpio_dig6, // DIG_6 (G12→PD14): AGC enable flag (mirrors host_agc_enable) - output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low) + output wire gpio_dig6, // DIG_6 (G12→PD14): stretched chirp_scheduler frame_pulse (~100 ns) for STM32 dwell sync + output wire gpio_dig7 // DIG_7 (H12→PD15): control-fault OR (F-6.4 watchdog | F-1.2 CDC overrun) ); // ============================================================================ @@ -227,10 +227,29 @@ wire rx_ddc_overflow_any; wire [2:0] rx_ddc_saturation_count; // MTI saturation count (audit F-6.3). OR'd into gpio_dig5 for MCU visibility. wire [7:0] rx_mti_saturation_count; -// Range-bin decimator watchdog (audit F-6.4). High = decimator stalled. +// Range-bin decimator watchdog (audit F-6.4). 1-cycle pulse from +// range_bin_decimator (~10 ns @ 100 MHz). Drives gpio_dig7 directly so +// the MCU can see the raw fault edge. wire rx_range_decim_watchdog; +// PR-AB.b Step 1: sticky latch over the watchdog pulse so a slow host +// status poll never misses the event. The 100 MHz → ft_clk synchronizer +// inside usb_data_interface_ft2232h cannot reliably capture a 10 ns +// pulse, so we register the level here in the source (clk) domain and +// feed the level — not the pulse — into status_words[5][5]. Cleared +// only by reset_n: the FPGA exposes no host-driven clear opcode today. +// See project_aeris10_clear_monitors_opcode_future.md for the bundled-PR +// shape that would activate a host clear here and across the DDC + AD9484 +// overrange stickies in radar_receiver_final.v. +reg rx_range_decim_watchdog_sticky; +always @(posedge clk_100m_buf or negedge sys_reset_n) begin + if (!sys_reset_n) + rx_range_decim_watchdog_sticky <= 1'b0; + else if (rx_range_decim_watchdog) + rx_range_decim_watchdog_sticky <= 1'b1; +end // CIC→FIR CDC overrun sticky (audit F-1.2). High = at least one baseband // sample has been silently dropped between the 400 MHz CIC and 100 MHz FIR. +// Already sticky in source (ddc_400m.v:697-702), no extra latch needed. wire rx_ddc_cic_fir_overrun; // Data packing for USB @@ -906,8 +925,11 @@ if (USB_MODE == 0) begin : gen_ft601 .status_agc_enable(host_agc_enable), // AUDIT-S10: control-fault flags exposed in status_words[5][6:5] - // for host-side observability (paired with gpio_dig7 split) - .status_range_decim_watchdog(rx_range_decim_watchdog), + // for host-side observability (paired with gpio_dig7 split). + // PR-AB.b Step 1: feed the sticky version of the F-6.4 watchdog + // (level signal) so the ft_clk synchronizer cannot miss a 10 ns + // source-domain pulse. F-1.2 overrun is already sticky in source. + .status_range_decim_watchdog(rx_range_decim_watchdog_sticky), .status_ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun) ); @@ -996,8 +1018,11 @@ end else begin : gen_ft2232h .status_agc_enable(host_agc_enable), // AUDIT-S10: control-fault flags exposed in status_words[5][6:5] - // for host-side observability (paired with gpio_dig7 split) - .status_range_decim_watchdog(rx_range_decim_watchdog), + // for host-side observability (paired with gpio_dig7 split). + // PR-AB.b Step 1: feed the sticky version of the F-6.4 watchdog + // (level signal) so the ft_clk synchronizer cannot miss a 10 ns + // source-domain pulse. F-1.2 overrun is already sticky in source. + .status_range_decim_watchdog(rx_range_decim_watchdog_sticky), .status_ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun), // PR-G: 2-tier CFAR telemetry (status_words[6]) @@ -1238,7 +1263,7 @@ end assign system_status = status_reg; // ============================================================================ -// FPGA→STM32 GPIO OUTPUTS (DIG_5, DIG_6, DIG_7) — AUDIT-S10 SPLIT +// FPGA→STM32 GPIO OUTPUTS (DIG_5, DIG_6, DIG_7) — AUDIT-S10 SPLIT + PR-AB.b // ============================================================================ // AUDIT-S10: gpio_dig5 previously OR'd six unrelated flags (signal-saturation // AND control-faults), so the MCU outer-loop AGC could not distinguish @@ -1251,19 +1276,49 @@ assign system_status = status_reg; // usb_data_interface[*_ft2232h].v so the host can graph each fault class // regardless of MCU consumption. // +// PR-AB.b: gpio_dig6 is reassigned from a host_agc_enable mirror to the +// chirp_scheduler frame_pulse, stretched to ~100 ns for clean STM32 EXTI +// capture on PD14. The STM32 firmware swaps HAL_Delay(BEAM_PATTERN_DWELL_MS) +// for an EXTI-driven semaphore acquire so per-pattern dwell tracks the +// actual ladder length — drift-free, mask-aware (a 2-subframe 3 km variant +// runs at the shorter cadence automatically because chirp_scheduler.frame_pulse +// fires at the wrap of whatever subframes are enabled). Outer-loop AGC moves +// to always-on in production firmware (saturation handling shouldn't be +// optional); host_agc_enable register stays for status display + bench build +// flag MCU_AGC_FORCE_DISABLED keeps a manual-gain path for ADAR1000 +// calibration / level checks / RX gain sweeps. +// // DIG_5 (PD13): Signal-chain saturation — outer-loop AGC reduces RF gain. // Asserts on AGC clipping, DDC mixer/filter overflow, or MTI // 2-pulse saturation (audit F-6.1/F-6.3). -// DIG_6 (PD14): AGC enable mirror (host_agc_enable) — single source of truth. +// DIG_6 (PD14): Stretched chirp_scheduler frame_pulse for STM32 dwell sync. +// 1-cycle source pulse @ 100 MHz, stretched to 10 cycles +// (100 ns). Period tracks the enabled-subframe ladder +// (8.05 ms full 3-PRI, 5.38 ms SHORT+MEDIUM, 2.80 ms SHORT-only). // DIG_7 (PD15): Control-chain fault — MCU should log + consider FPGA reset. // Asserts on range-decimator watchdog (audit F-6.4) or CIC→FIR -// CDC overrun (audit F-1.2). MCU consumption is a tracked -// follow-up; until then the host telemetry path covers it. +// CDC overrun (audit F-1.2). PD15 stuck-high sampler in MCU +// main loop fires attemptErrorRecovery(ERROR_FPGA_DSP_STALL). assign gpio_dig5 = (rx_agc_saturation_count != 8'd0) | rx_ddc_overflow_any | (rx_ddc_saturation_count != 3'd0) | (rx_mti_saturation_count != 8'd0); -assign gpio_dig6 = host_agc_enable; + +// PR-AB.b: 10-cycle stretcher on sched_frame_pulse. STM32 EXTI on PD14 needs +// ≥2 APB clocks (~12 ns @ 168 MHz APB) to latch the rising edge; 100 ns gives +// generous margin while staying well below the shortest dwell (2.80 ms = +// SHORT-only) so back-to-back pulses never overlap. +reg [3:0] frame_pulse_stretch_count; +always @(posedge clk_100m_buf or negedge sys_reset_n) begin + if (!sys_reset_n) + frame_pulse_stretch_count <= 4'd0; + else if (sched_frame_pulse) + frame_pulse_stretch_count <= 4'd10; // 10 cycles = 100 ns @ 100 MHz + else if (frame_pulse_stretch_count != 4'd0) + frame_pulse_stretch_count <= frame_pulse_stretch_count - 4'd1; +end +assign gpio_dig6 = (frame_pulse_stretch_count != 4'd0); + assign gpio_dig7 = rx_range_decim_watchdog // audit F-6.4 | rx_ddc_cic_fir_overrun; // audit F-1.2 diff --git a/9_Firmware/9_2_FPGA/radar_system_top_50t.v b/9_Firmware/9_2_FPGA/radar_system_top_50t.v index d77caf8..dbc3673 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top_50t.v +++ b/9_Firmware/9_2_FPGA/radar_system_top_50t.v @@ -84,8 +84,8 @@ module radar_system_top_50t ( // ===== FPGA→STM32 GPIO (Bank 15: 3.3V) ===== output wire gpio_dig5, // DIG_5 (H11→PD13): AGC saturation flag - output wire gpio_dig6, // DIG_6 (G12→PD14): reserved - output wire gpio_dig7 // DIG_7 (H12→PD15): reserved + output wire gpio_dig6, // DIG_6 (G12→PD14): stretched chirp_scheduler frame_pulse (PR-AB.b dwell sync) + output wire gpio_dig7 // DIG_7 (H12→PD15): control-fault OR (F-6.4 watchdog | F-1.2 CDC overrun) ); // ===== Tie-off wires for unconstrained FT601 inputs (inactive with USB_MODE=1) ===== diff --git a/9_Firmware/9_2_FPGA/run_regression.sh b/9_Firmware/9_2_FPGA/run_regression.sh index 7aa5baa..cddea1a 100755 --- a/9_Firmware/9_2_FPGA/run_regression.sh +++ b/9_Firmware/9_2_FPGA/run_regression.sh @@ -566,9 +566,13 @@ run_test "ADC PWDN opcode 0x32 (AUDIT-S25)" \ tb/tb_adc_pwdn_opcode.vvp \ tb/tb_adc_pwdn_opcode.v -run_test "GPIO dig5/dig7 split (AUDIT-S10)" \ - tb/tb_audit_s10_gpio_split.vvp \ - tb/tb_audit_s10_gpio_split.v +run_test "Status-word stickies CDC + DIG7 fault-OR (AUDIT-S10 + PR-AB.b Step 1)" \ + tb/tb_status_words_stickies.vvp \ + tb/tb_status_words_stickies.v + +run_test "DIG6 frame-pulse stretcher (PR-AB.b)" \ + tb/tb_dig6_frame_pulse.vvp \ + tb/tb_dig6_frame_pulse.v run_test "NUM_CELLS sizing 50T (AUDIT-C16)" \ tb/tb_audit_c16_num_cells_50t.vvp \ diff --git a/9_Firmware/9_2_FPGA/tb/tb_dig6_frame_pulse.v b/9_Firmware/9_2_FPGA/tb/tb_dig6_frame_pulse.v new file mode 100644 index 0000000..a25dbb1 --- /dev/null +++ b/9_Firmware/9_2_FPGA/tb/tb_dig6_frame_pulse.v @@ -0,0 +1,205 @@ +// ============================================================================ +// tb_dig6_frame_pulse.v +// +// PR-AB.b: gpio_dig6 (PD14) carries the chirp_scheduler frame_pulse, stretched +// to ~100 ns so the STM32 EXTI on PD14 can latch the rising edge reliably. +// The MCU dwell loop (runRadarPulseSequence) replaces +// HAL_Delay(BEAM_PATTERN_DWELL_MS) with osSemaphoreAcquire so per-pattern +// dwell tracks the actual ladder length — drift-free, mask-aware. +// +// Companion to tb_status_words_stickies.v which covers the gpio_dig7 fault-OR +// (watchdog | CDC overrun) semantic and the status_words[5][6:5] CDC packing. +// +// This TB mirrors the production stretcher fragment from radar_system_top.v +// and asserts: +// +// T1 Reset → count=0, dig6=0 +// T2 Single 1-cycle pulse → dig6 high for exactly 10 cycles, low on 11 +// T3 Pulse during stretch → counter reloads to 10 (longer total high time) +// T4 Two pulses spaced > 10 cycles → two clean rising edges +// T5 Pulse asserted continuously for many cycles → counter pinned high +// T6 No pulse activity → dig6 stays low forever +// T7 Reset mid-stretch → counter and dig6 drop immediately +// ============================================================================ +`timescale 1ns/1ps + +module tb_dig6_frame_pulse; + + reg clk_100m = 1'b0; + reg sys_reset_n = 1'b0; + reg frame_pulse = 1'b0; + wire dig6; + + frame_pulse_stretcher_block dut ( + .clk_100m_buf (clk_100m), + .sys_reset_n (sys_reset_n), + .frame_pulse_in(frame_pulse), + .gpio_dig6 (dig6) + ); + + // 100 MHz clock (10 ns period) + always #5 clk_100m = ~clk_100m; + + integer pass = 0; + integer fail = 0; + + task check (input [127:0] label, input expected); + begin + #1; + if (dig6 === expected) begin + $display(" [PASS] %0s: dig6=%b", label, dig6); + pass = pass + 1; + end else begin + $display(" [FAIL] %0s: dig6=%b (exp %b)", label, dig6, expected); + fail = fail + 1; + end + end + endtask + + task fire_pulse; + begin + @(posedge clk_100m); #1; + frame_pulse = 1'b1; + @(posedge clk_100m); #1; + frame_pulse = 1'b0; + end + endtask + + integer i; + + initial begin + $display("============================================================"); + $display("PR-AB.b: gpio_dig6 = stretched(frame_pulse)"); + $display("============================================================"); + + // ---- T1: reset state ---- + sys_reset_n = 1'b0; + repeat (4) @(posedge clk_100m); + check("T1 reset asserted, dig6 low", 1'b0); + @(posedge clk_100m); #1; + sys_reset_n = 1'b1; + @(posedge clk_100m); #1; + check("T1b after deassert, dig6 still low", 1'b0); + + // ---- T2: single pulse → exactly 10 cycles high ---- + fire_pulse(); + // After fire_pulse, frame_pulse was high for 1 clk; the always block + // sampled it on that posedge and loaded count=10. dig6 is high while + // count != 0. Count decrements each cycle: 10,9,...,1,0. So dig6 is + // high for 10 posedges starting on the cycle the load took effect. + for (i = 0; i < 10; i = i + 1) begin + #1; + if (dig6 !== 1'b1) begin + $display(" [FAIL] T2 cycle %0d: dig6=%b (exp 1)", i, dig6); + fail = fail + 1; + end + @(posedge clk_100m); + end + // 11th cycle: count has wrapped to 0, dig6 should be low. + #1; + check("T2 dig6 low on 11th cycle", 1'b0); + // Make sure it stays low. + repeat (5) @(posedge clk_100m); + check("T2 dig6 stays low after stretch", 1'b0); + + // ---- T3: pulse during stretch reloads counter ---- + fire_pulse(); + @(posedge clk_100m); #1; // count = 9 + @(posedge clk_100m); #1; // count = 8 + @(posedge clk_100m); #1; // count = 7 + // Reload by firing another pulse mid-stretch. + @(posedge clk_100m); #1; + frame_pulse = 1'b1; + @(posedge clk_100m); #1; + frame_pulse = 1'b0; + // Counter is now reloaded to 10. dig6 stays high for another 10 cycles. + for (i = 0; i < 10; i = i + 1) begin + #1; + if (dig6 !== 1'b1) begin + $display(" [FAIL] T3 reload cycle %0d: dig6=%b (exp 1)", i, dig6); + fail = fail + 1; + end + @(posedge clk_100m); + end + #1; + check("T3 dig6 low after reloaded stretch", 1'b0); + + // ---- T4: spaced pulses produce two distinct rising edges ---- + fire_pulse(); + repeat (15) @(posedge clk_100m); // > 10 cycles, dig6 must be 0 + check("T4a dig6 low between pulses", 1'b0); + fire_pulse(); + #1; + check("T4b dig6 high after second pulse", 1'b1); + repeat (10) @(posedge clk_100m); + #1; + check("T4c dig6 low after second stretch", 1'b0); + + // ---- T5: continuous pulse pins counter at 10 ---- + @(posedge clk_100m); #1; + frame_pulse = 1'b1; + repeat (50) @(posedge clk_100m); + #1; + check("T5 dig6 high under continuous pulse", 1'b1); + @(posedge clk_100m); #1; + frame_pulse = 1'b0; + // After deassert, dig6 should drain over 10 cycles. + repeat (10) @(posedge clk_100m); + #1; + check("T5 dig6 drains after pulse deassert", 1'b0); + + // ---- T6: idle line stays low ---- + repeat (200) @(posedge clk_100m); + check("T6 dig6 idle stays low", 1'b0); + + // ---- T7: reset mid-stretch drops dig6 immediately ---- + fire_pulse(); + @(posedge clk_100m); #1; + @(posedge clk_100m); #1; + check("T7a dig6 high pre-reset", 1'b1); + sys_reset_n = 1'b0; + @(posedge clk_100m); #1; + check("T7b reset drops dig6", 1'b0); + sys_reset_n = 1'b1; + @(posedge clk_100m); #1; + + $display("============================================================"); + $display("DIG6 FRAME PULSE RESULTS: pass=%0d fail=%0d", pass, fail); + $display("============================================================"); + if (fail == 0) $display("[OVERALL] PASS"); + else $display("[OVERALL] FAIL"); + $finish; + end + + initial begin + #1_000_000; + $display("[FATAL] timeout"); + $finish; + end + +endmodule + +// ============================================================================ +// frame_pulse_stretcher_block — mirrors the production stretcher in +// radar_system_top.v (PR-AB.b). 1-cycle frame_pulse_in loads count=10; +// counter decrements each clock; gpio_dig6 = (count != 0). reset_n async +// clear. Reloads on subsequent pulses. Result: dig6 high for 10 clk_100m +// cycles = 100 ns starting on the cycle after the input pulse is sampled. +// ============================================================================ +module frame_pulse_stretcher_block ( + input wire clk_100m_buf, + input wire sys_reset_n, + input wire frame_pulse_in, + output wire gpio_dig6 +); + reg [3:0] frame_pulse_stretch_count; + always @(posedge clk_100m_buf or negedge sys_reset_n) begin + if (!sys_reset_n) + frame_pulse_stretch_count <= 4'd0; + else if (frame_pulse_in) + frame_pulse_stretch_count <= 4'd10; + else if (frame_pulse_stretch_count != 4'd0) + frame_pulse_stretch_count <= frame_pulse_stretch_count - 4'd1; + end + assign gpio_dig6 = (frame_pulse_stretch_count != 4'd0); +endmodule diff --git a/9_Firmware/9_2_FPGA/tb/tb_audit_s10_gpio_split.v b/9_Firmware/9_2_FPGA/tb/tb_status_words_stickies.v similarity index 50% rename from 9_Firmware/9_2_FPGA/tb/tb_audit_s10_gpio_split.v rename to 9_Firmware/9_2_FPGA/tb/tb_status_words_stickies.v index d701447..d7b623e 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_audit_s10_gpio_split.v +++ b/9_Firmware/9_2_FPGA/tb/tb_status_words_stickies.v @@ -1,68 +1,35 @@ // ============================================================================ -// tb_audit_s10_gpio_split.v +// tb_status_words_stickies.v // -// AUDIT-S10: gpio_dig5 previously OR'd six unrelated flags — four signal- -// saturation classes (AGC, DDC overflow, DDC saturation, MTI saturation) and -// two control-fault classes (range-decimator watchdog, CIC->FIR CDC overrun) -// — into the single MCU-visible bit at PD13. The MCU outer-loop AGC reduces -// RF gain on PD13 assertion, which is the wrong response to a watchdog or -// CDC stall. gpio_dig7 (PD15) was tied 1'b0 (reserved). +// AUDIT-S10 + PR-AB.b: status_words[5][6:5] CDC packing for the two +// control-fault flags (range-decimator watchdog F-6.4, CIC->FIR CDC overrun +// F-1.2) AND the gpio_dig7 fault-OR semantic. PR-AB.b Step 1 made the F-6.4 +// half sticky in the source clock domain so a slow host poll cannot miss +// the event; F-1.2 is already sticky inside ddc_400m.v. PR-AB.b drives +// gpio_dig7 = (watchdog | cic_fir_overrun) so the MCU stuck-high sampler +// (main.cpp:880-1000) can fire attemptErrorRecovery(ERROR_FPGA_DSP_STALL). +// status_words[5][7] is reserved (must stay 0). // -// Fix: split the OR-network so gpio_dig5 carries only signal-saturation flags -// (AGC continues to react correctly) and gpio_dig7 carries control-fault -// flags (MCU follow-up will log + reset; until then host telemetry covers). -// Status words[5][6:5] expose the two control-fault classes so host-side -// can graph them regardless of MCU consumption. +// Replaces the GROUP B tests in the retired tb_audit_s10_gpio_split.v and +// also restores the GROUP A dig7 fault-OR coverage (the dig5 saturation +// portion of GROUP A lives in the radar_receiver_final TBs). gpio_dig6 +// stretcher (chirp_scheduler frame_pulse) coverage is in +// tb_dig6_frame_pulse.v. // -// This TB mirrors the production fragments from radar_system_top.v and -// usb_data_interface[*_ft2232h].v and asserts: -// -// GROUP A GPIO split (combinational) -// T1 All inputs 0 -> dig5=0, dig7=0 -// T2 Each signal-sat input -> dig5=1, dig7=0 (no cross-route to dig7) -// T3 Each control-fault -> dig5=0, dig7=1 (no cross-route to dig5) -// T4 Mixed sat + fault -> dig5=1, dig7=1 (independent) -// T5 AGC count >0 (boundary) -> dig5=1 -// T6 DDC count =0 (boundary) -> dig5=0 -// -// GROUP B Status-word CDC packing (sequential) -// T7 Reset state -> sync regs 0, status_word_bits 0 -// T8 Watchdog asserted in src -> after 2 ft_clk edges, status[5]=1 -// T9 CIC overrun asserted in src -> after 2 ft_clk edges, status[6]=1 -// T10 Both asserted, both cleared -> status[6:5] tracks (sticky in src; -// TB drives explicit clears) -// T11 status_words[5][7] stays 0 (reserved bit, not stomped by sync) -// T12 status_words[5][4:0] (self_test_flags) stays = src input +// T1 Reset state -> sync regs 0, status[6:5]=0, dig7=0 +// T2 Watchdog asserted -> after 2 ft_clk edges, status[5]=1, dig7=1 +// T3 CIC overrun asserted -> after 2 ft_clk edges, status[6]=1, dig7=1 +// T4a Both asserted -> status[6:5]=11, dig7=1 +// T4b Both cleared -> status[6:5]=00, dig7=0 +// T5 status_words[5][7] stays 0 (reserved bit not stomped by sync); dig7=1 +// T6 status_words[5][4:0] (self_test_flags) pass through; dig7=0 // ============================================================================ `timescale 1ns/1ps -module tb_audit_s10_gpio_split; +module tb_status_words_stickies; - // ===== GROUP A: GPIO split inputs/outputs ===== - reg [7:0] agc_saturation_count; - reg ddc_overflow_any; - reg [2:0] ddc_saturation_count; - reg [7:0] mti_saturation_count; - reg range_decim_watchdog; - reg ddc_cic_fir_overrun; - - wire dig5; - wire dig7; - - gpio_split_block gpio_dut ( - .agc_saturation_count (agc_saturation_count), - .ddc_overflow_any (ddc_overflow_any), - .ddc_saturation_count (ddc_saturation_count), - .mti_saturation_count (mti_saturation_count), - .range_decim_watchdog (range_decim_watchdog), - .ddc_cic_fir_overrun (ddc_cic_fir_overrun), - .gpio_dig5 (dig5), - .gpio_dig7 (dig7) - ); - - // ===== GROUP B: status-word CDC packing ===== reg clk_src = 1'b0; // 100 MHz radar domain - reg ft_clk = 1'b0; // 60/100 MHz USB domain + reg ft_clk = 1'b0; // 60 MHz USB domain reg reset_n = 1'b0; reg src_watchdog; reg src_overrun; @@ -70,6 +37,7 @@ module tb_audit_s10_gpio_split; reg status_req_pulse; wire [31:0] status_word_5; + wire gpio_dig7; // PR-AB.b: production OR of fault flags status_packing_block status_dut ( .clk (clk_src), @@ -82,29 +50,20 @@ module tb_audit_s10_gpio_split; .status_word_5 (status_word_5) ); - // 100 MHz src clock + // Mirrors production combinational OR in radar_system_top.v: + // assign gpio_dig7 = rx_range_decim_watchdog | rx_ddc_cic_fir_overrun; + gpio_dig7_or_block dig7_dut ( + .watchdog (src_watchdog), + .overrun (src_overrun), + .gpio_dig7(gpio_dig7) + ); + always #5 clk_src = ~clk_src; - // 60 MHz ft_clk (~16.67 ns) always #8 ft_clk = ~ft_clk; - // ----- bookkeeping ----- integer pass = 0; integer fail = 0; - task check_dig (input [127:0] label, input expected_dig5, input expected_dig7); - begin - #1; // settle combinational - if (dig5 === expected_dig5 && dig7 === expected_dig7) begin - $display(" [PASS] %0s: dig5=%b dig7=%b", label, dig5, dig7); - pass = pass + 1; - end else begin - $display(" [FAIL] %0s: dig5=%b (exp %b) dig7=%b (exp %b)", - label, dig5, expected_dig5, dig7, expected_dig7); - fail = fail + 1; - end - end - endtask - task check_status (input [127:0] label, input [31:0] mask, input [31:0] expected); begin if ((status_word_5 & mask) === (expected & mask)) begin @@ -119,155 +78,117 @@ module tb_audit_s10_gpio_split; end endtask + task check_dig7 (input [127:0] label, input expected); + begin + if (gpio_dig7 === expected) begin + $display(" [PASS] %0s: dig7=%b", label, gpio_dig7); + pass = pass + 1; + end else begin + $display(" [FAIL] %0s: dig7=%b (exp %b)", label, gpio_dig7, expected); + fail = fail + 1; + end + end + endtask + task pulse_status_req; begin @(posedge ft_clk); #1; status_req_pulse = 1'b1; @(posedge ft_clk); #1; status_req_pulse = 1'b0; - // Allow the registered status_words update to land. @(posedge ft_clk); #1; end endtask initial begin $display("============================================================"); - $display("AUDIT-S10: gpio_dig split + status_words[5][6:5] visibility"); + $display("status_words[5][6:5] CDC + gpio_dig7 fault-OR (AUDIT-S10 + PR-AB.b)"); $display("============================================================"); - // ---- GROUP A: GPIO split ---- - agc_saturation_count = 8'd0; - ddc_overflow_any = 1'b0; - ddc_saturation_count = 3'd0; - mti_saturation_count = 8'd0; - range_decim_watchdog = 1'b0; - ddc_cic_fir_overrun = 1'b0; - - // T1 - check_dig("T1 all zero", 1'b0, 1'b0); - - // T2 each signal-sat individually - agc_saturation_count = 8'd1; - check_dig("T2a agc_sat>0", 1'b1, 1'b0); - agc_saturation_count = 8'd0; - - ddc_overflow_any = 1'b1; - check_dig("T2b ddc_overflow", 1'b1, 1'b0); - ddc_overflow_any = 1'b0; - - ddc_saturation_count = 3'd1; - check_dig("T2c ddc_sat>0", 1'b1, 1'b0); - ddc_saturation_count = 3'd0; - - mti_saturation_count = 8'd1; - check_dig("T2d mti_sat>0", 1'b1, 1'b0); - mti_saturation_count = 8'd0; - - // T3 each control-fault individually - range_decim_watchdog = 1'b1; - check_dig("T3a watchdog", 1'b0, 1'b1); - range_decim_watchdog = 1'b0; - - ddc_cic_fir_overrun = 1'b1; - check_dig("T3b cic_fir_overrun", 1'b0, 1'b1); - ddc_cic_fir_overrun = 1'b0; - - // T4 mixed - agc_saturation_count = 8'd5; - range_decim_watchdog = 1'b1; - check_dig("T4 mixed sat+fault", 1'b1, 1'b1); - agc_saturation_count = 8'd0; - range_decim_watchdog = 1'b0; - - // T5 boundary: largest agc count - agc_saturation_count = 8'hFF; - check_dig("T5 agc_sat=FF", 1'b1, 1'b0); - agc_saturation_count = 8'd0; - - // T6 boundary: ddc_sat=0 stays low - ddc_saturation_count = 3'd0; - check_dig("T6 ddc_sat=0", 1'b0, 1'b0); - - // ---- GROUP B: status-word CDC packing ---- src_watchdog = 1'b0; src_overrun = 1'b0; src_self_test_flags = 5'b00000; status_req_pulse = 1'b0; - // Apply reset reset_n = 1'b0; repeat (5) @(posedge ft_clk); reset_n = 1'b1; repeat (3) @(posedge ft_clk); - // T7 reset state + // T1 reset state pulse_status_req(); - check_status("T7 reset state", + check_status("T1 reset state", 32'h000000E0, // [7:5] 32'h00000000); + check_dig7("T1 dig7 idle low", 1'b0); - // T8 watchdog asserted only + // T2 watchdog asserted only @(posedge clk_src); #1; src_watchdog = 1'b1; - // give 4 ft_clk for sync chain to settle repeat (5) @(posedge ft_clk); pulse_status_req(); - check_status("T8 watchdog asserted", + check_status("T2 watchdog asserted", 32'h00000060, // [6:5] 32'h00000020); // [5]=1 + check_dig7("T2 dig7 watchdog -> high", 1'b1); - // T9 cic_fir_overrun asserted only (clear watchdog first) + // T3 cic_fir_overrun asserted only (clear watchdog first) @(posedge clk_src); #1; src_watchdog = 1'b0; src_overrun = 1'b1; repeat (5) @(posedge ft_clk); pulse_status_req(); - check_status("T9 cic_fir_overrun asserted", + check_status("T3 cic_fir_overrun asserted", 32'h00000060, 32'h00000040); // [6]=1 + check_dig7("T3 dig7 overrun -> high", 1'b1); - // T10 both, then both cleared + // T4 both, then both cleared @(posedge clk_src); #1; src_watchdog = 1'b1; src_overrun = 1'b1; repeat (5) @(posedge ft_clk); pulse_status_req(); - check_status("T10a both asserted", + check_status("T4a both asserted", 32'h00000060, 32'h00000060); // [6:5]=11 + check_dig7("T4a dig7 both -> high", 1'b1); @(posedge clk_src); #1; src_watchdog = 1'b0; src_overrun = 1'b0; repeat (5) @(posedge ft_clk); pulse_status_req(); - check_status("T10b both cleared", + check_status("T4b both cleared", 32'h00000060, 32'h00000000); + check_dig7("T4b dig7 both cleared -> low", 1'b0); - // T11 reserved bit [7] stays 0 even when neighbours are 1 + // T5 reserved bit [7] stays 0 even when neighbours are 1 @(posedge clk_src); #1; src_watchdog = 1'b1; src_overrun = 1'b1; repeat (5) @(posedge ft_clk); pulse_status_req(); - check_status("T11 [7] reserved stays 0", - 32'h00000080, // [7] only + check_status("T5 [7] reserved stays 0", + 32'h00000080, 32'h00000000); + check_dig7("T5 dig7 both asserted -> high", 1'b1); - // T12 self_test_flags pass through unchanged + // T6 self_test_flags pass through unchanged @(posedge clk_src); #1; src_watchdog = 1'b0; src_overrun = 1'b0; src_self_test_flags = 5'b10110; repeat (5) @(posedge ft_clk); pulse_status_req(); - check_status("T12 self_test_flags untouched", + check_status("T6 self_test_flags untouched", 32'h0000001F, 32'h00000016); + check_dig7("T6 dig7 cleared -> low", 1'b0); $display("============================================================"); - $display("AUDIT-S10 RESULTS: pass=%0d fail=%0d", pass, fail); + $display("STATUS STICKIES + DIG7 OR RESULTS: pass=%0d fail=%0d", pass, fail); $display("============================================================"); if (fail == 0) $display("[OVERALL] PASS"); else $display("[OVERALL] FAIL"); @@ -282,31 +203,6 @@ module tb_audit_s10_gpio_split; endmodule -// ============================================================================ -// gpio_split_block — mirrors the production fragment from radar_system_top.v -// post AUDIT-S10. Two combinational ORs: -// gpio_dig5 = signal-saturation classes (AGC + DDC + MTI) -// gpio_dig7 = control-fault classes (range-decimator watchdog + CIC->FIR -// CDC overrun) -// ============================================================================ -module gpio_split_block ( - input wire [7:0] agc_saturation_count, - input wire ddc_overflow_any, - input wire [2:0] ddc_saturation_count, - input wire [7:0] mti_saturation_count, - input wire range_decim_watchdog, - input wire ddc_cic_fir_overrun, - output wire gpio_dig5, - output wire gpio_dig7 -); - assign gpio_dig5 = (agc_saturation_count != 8'd0) - | ddc_overflow_any - | (ddc_saturation_count != 3'd0) - | (mti_saturation_count != 8'd0); - assign gpio_dig7 = range_decim_watchdog - | ddc_cic_fir_overrun; -endmodule - // ============================================================================ // status_packing_block — mirrors the production CDC fragment from // usb_data_interface.v (and usb_data_interface_ft2232h.v) for the AUDIT-S10 @@ -355,3 +251,21 @@ module status_packing_block ( end end endmodule + +// ============================================================================ +// gpio_dig7_or_block — mirrors the production combinational OR in +// radar_system_top.v: +// +// assign gpio_dig7 = rx_range_decim_watchdog | rx_ddc_cic_fir_overrun; +// +// MCU's PD15 stuck-high sampler at main.cpp:880-1000 fires +// attemptErrorRecovery(ERROR_FPGA_DSP_STALL) → bitstream reload on either +// fault class. +// ============================================================================ +module gpio_dig7_or_block ( + input wire watchdog, + input wire overrun, + output wire gpio_dig7 +); + assign gpio_dig7 = watchdog | overrun; +endmodule diff --git a/9_Firmware/9_3_GUI/v7/dashboard.py b/9_Firmware/9_3_GUI/v7/dashboard.py index e99aa3a..04980b9 100644 --- a/9_Firmware/9_3_GUI/v7/dashboard.py +++ b/9_Firmware/9_3_GUI/v7/dashboard.py @@ -40,7 +40,7 @@ from PyQt6.QtWidgets import ( QTableWidget, QTableWidgetItem, QHeaderView, QPlainTextEdit, QStatusBar, QMessageBox, ) -from PyQt6.QtCore import Qt, QLocale, QTimer, pyqtSignal, pyqtSlot, QObject +from PyQt6.QtCore import Qt, QLocale, QTimer, QSettings, pyqtSignal, pyqtSlot, QObject from PyQt6.QtGui import QColor from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg @@ -208,6 +208,18 @@ class RadarDashboard(QMainWindow): # FPGA control parameter widgets self._param_spins: dict = {} # opcode_hex -> QSpinBox + # PR-AB.b: BENCH-MODE persistent flag (advanced settings checkbox). + # OFF (default, production): AGC is ALWAYS-ON in MCU firmware; AGC + # Enable spinbox + Enable/Disable AGC buttons are hidden so an operator + # cannot send opcode 0x28 and confuse themselves about whether the MCU + # is honouring the register (it doesn't — see main.cpp #ifndef + # MCU_AGC_FORCE_DISABLED). A coloured "ALWAYS-ON" badge replaces them. + # ON: bench / debug build assumed, AGC controls are exposed; the user + # is expected to know the MCU is compiled with MCU_AGC_FORCE_DISABLED + # and that opcode 0x28 only sets the FPGA register (display-only). + self._qsettings = QSettings("AERIS-10", "RadarDashboardV7") + self._bench_mode: bool = bool(self._qsettings.value("bench_mode", False, type=bool)) + # AGC visualization history (ring buffers) self._agc_history_len = 256 self._agc_gain_history: deque[int] = deque(maxlen=self._agc_history_len) @@ -375,6 +387,11 @@ class RadarDashboard(QMainWindow): self._create_diagnostics_tab() self._create_settings_tab() + # PR-AB.b: apply persisted BENCH-MODE state to AGC widget visibility. + # Has to run AFTER _create_fpga_control_tab (creates the AGC widgets) + # AND _create_settings_tab (creates the checkbox). + self._apply_bench_mode_visibility() + # ----------------------------------------------------------------- # TAB 1: Main View # ----------------------------------------------------------------- @@ -816,11 +833,48 @@ class RadarDashboard(QMainWindow): right_layout.addWidget(grp_cfar) # ── AGC (Automatic Gain Control) ────────────────────────────── + # PR-AB.b: The MCU's outer-loop AGC is ALWAYS-ON in production builds + # (compile-time policy, see main.cpp #ifndef MCU_AGC_FORCE_DISABLED). + # Bench/debug builds compile the AGC body out, and the runtime enable + # bit becomes meaningful only as an FPGA-side display register. We + # therefore hide the AGC Enable spinbox + Enable/Disable buttons in + # production (BENCH-MODE OFF) and show a "ALWAYS-ON" badge instead. + # Bench engineers tick the BENCH-MODE checkbox in Settings to expose + # the controls. grp_agc = QGroupBox("AGC (Auto Gain)") agc_layout = QVBoxLayout(grp_agc) + # Header row — exactly one of these is visible at a time: + # production: ALWAYS-ON badge. + # bench: Enable/Disable AGC buttons (send opcode 0x28 with 0|1; + # the bit's only valid values, so a spinbox would add + # nothing but typo-risk). + # Tuning knobs sit below regardless, so the AGC group's "header" + # control is always at the top of the box. + self._agc_always_on_badge = QLabel( + "AGC: ALWAYS-ON (production policy — MCU runs every frame)" + ) + self._agc_always_on_badge.setStyleSheet( + f"background-color: {DARK_SUCCESS}; color: white; " + "padding: 6px; font-weight: bold; border-radius: 3px;" + ) + self._agc_always_on_badge.setWordWrap(True) + agc_layout.addWidget(self._agc_always_on_badge) + + self._agc_toggle_container = QWidget() + agc_toggle_inner = QHBoxLayout(self._agc_toggle_container) + agc_toggle_inner.setContentsMargins(0, 0, 0, 0) + self._btn_agc_on = QPushButton("Enable AGC") + self._btn_agc_on.clicked.connect(lambda: self._send_fpga_cmd(0x28, 1)) + agc_toggle_inner.addWidget(self._btn_agc_on) + self._btn_agc_off = QPushButton("Disable AGC") + self._btn_agc_off.clicked.connect(lambda: self._send_fpga_cmd(0x28, 0)) + agc_toggle_inner.addWidget(self._btn_agc_off) + agc_layout.addWidget(self._agc_toggle_container) + + # Tuning knobs — always visible. Target/attack/decay/holdoff drive the + # FPGA inner-loop register fields regardless of the MCU AGC build flag. agc_params = [ - ("AGC Enable", 0x28, 0, 1, "0=manual, 1=auto"), ("AGC Target", 0x29, 200, 8, "0-255, peak target"), ("AGC Attack", 0x2A, 1, 4, "0-15, atten step"), ("AGC Decay", 0x2B, 1, 4, "0-15, gain-up step"), @@ -829,16 +883,6 @@ class RadarDashboard(QMainWindow): for label, opcode, default, bits, hint in agc_params: self._add_fpga_param_row(agc_layout, label, opcode, default, bits, hint) - # AGC quick toggles - agc_row = QHBoxLayout() - btn_agc_on = QPushButton("Enable AGC") - btn_agc_on.clicked.connect(lambda: self._send_fpga_cmd(0x28, 1)) - agc_row.addWidget(btn_agc_on) - btn_agc_off = QPushButton("Disable AGC") - btn_agc_off.clicked.connect(lambda: self._send_fpga_cmd(0x28, 0)) - agc_row.addWidget(btn_agc_off) - agc_layout.addLayout(agc_row) - # AGC status readback labels agc_st_group = QGroupBox("AGC Status") agc_st_layout = QVBoxLayout(agc_st_group) @@ -927,23 +971,38 @@ class RadarDashboard(QMainWindow): row = QHBoxLayout() lbl = QLabel(label) - lbl.setMinimumWidth(140) + # PR-AB.b: setFixedWidth (not min) so labels line up across rows + # regardless of whether the row sits directly in the group's + # QVBoxLayout or inside an intermediate QWidget container (the AGC + # Enable row uses _agc_enable_container so it can be hidden in + # production — its inner layout reflows differently and the + # minimum-width hint was being ignored, leaving the spinbox start + # ~67 px to the left of its peers). + lbl.setFixedWidth(140) row.addWidget(lbl) max_val = (1 << bits) - 1 spin = QSpinBox() spin.setRange(0, max_val) spin.setValue(default) - spin.setMinimumWidth(80) + # PR-AB.b: setFixedWidth (not min/max) — QHBoxLayout would otherwise + # squeeze the spinbox toward its minimum on rows where the hint is + # longer than its peers (the AGC Enable hint is ~3× longer than the + # others and was rendering at ~90 px while siblings hit ~160). + spin.setFixedWidth(120) row.addWidget(spin) self._param_spins[f"0x{opcode:02X}"] = spin + # PR-AB.b: hint is the only flex element in the row — explicit stretch=1 + # so it absorbs leftover space instead of competing with the Set button. + # Without this, a long hint (e.g. AGC Enable's "0=manual, 1=auto …") + # would shrink the Set button below its 60 px cap on the same row. hint_lbl = QLabel(hint) hint_lbl.setStyleSheet(f"color: {DARK_INFO}; font-size: 10px;") - row.addWidget(hint_lbl) + row.addWidget(hint_lbl, 1) btn = QPushButton("Set") - btn.setMaximumWidth(60) + btn.setFixedWidth(60) # Capture opcode and spin by value btn.clicked.connect(lambda _, op=opcode, sp=spin, b=bits: self._send_fpga_validated(op, sp.value(), b)) @@ -1245,6 +1304,41 @@ class RadarDashboard(QMainWindow): layout.addWidget(proc_group) + # ---- Bench / Diagnostics ------------------------------------------ + # PR-AB.b: tucked-away toggle that only matters when the MCU is built + # with -DMCU_AGC_FORCE_DISABLED (bench / debug build). Production + # operators should leave this OFF — the AGC runs always-on at the + # firmware level and exposing the Enable/Disable buttons would let + # someone send opcode 0x28 without changing observable behaviour, + # which would just create confusion. + bench_group = QGroupBox("Bench / Diagnostics") + bench_layout = QVBoxLayout(bench_group) + + self._bench_mode_check = QCheckBox( + "BENCH-MODE: expose AGC Enable controls (debug-build firmware only)" + ) + self._bench_mode_check.setChecked(self._bench_mode) + self._bench_mode_check.setToolTip( + "OFF (default): production firmware runs AGC every frame. " + "AGC Enable buttons are hidden so opcode 0x28 cannot be sent.\n" + "ON: bench / debug firmware (built with -DMCU_AGC_FORCE_DISABLED) " + "is assumed. AGC Enable buttons become visible — they only set " + "the FPGA-side display register, the MCU does not honour them." + ) + self._bench_mode_check.toggled.connect(self._on_bench_mode_toggled) + bench_layout.addWidget(self._bench_mode_check) + + bench_note = QLabel( + "Future: this checkbox will go away once the MCU broadcasts " + "a one-shot USB-CDC boot announce identifying production vs " + "bench firmware build." + ) + bench_note.setStyleSheet(f"color: {DARK_INFO}; font-size: 10px;") + bench_note.setWordWrap(True) + bench_layout.addWidget(bench_note) + + layout.addWidget(bench_group) + # ---- About group --------------------------------------------------- about_group = QGroupBox("About") about_layout = QVBoxLayout(about_group) @@ -1868,13 +1962,10 @@ class RadarDashboard(QMainWindow): self._st_labels["t4"].setText( f"T4 ADC: {'PASS' if flags & 0x10 else 'FAIL'}") - # AGC status readback + # AGC status readback. The 'enable' line is owned by + # _refresh_agc_mode_labels so production stays honest with the badge. if hasattr(self, '_agc_labels'): - agc_str = "AUTO" if st.agc_enable else "MANUAL" - agc_color = DARK_SUCCESS if st.agc_enable else DARK_INFO - self._agc_labels["enable"].setStyleSheet( - f"color: {agc_color}; font-weight: bold;") - self._agc_labels["enable"].setText(f"AGC: {agc_str}") + self._refresh_agc_mode_labels(st) self._agc_labels["gain"].setText( f"Gain: {st.agc_current_gain}") self._agc_labels["peak"].setText( @@ -1903,12 +1994,9 @@ class RadarDashboard(QMainWindow): self._agc_peak_history.append(st.agc_peak_magnitude) self._agc_sat_history.append(st.agc_saturation_count) - # Update indicator labels (cheap Qt calls) - agc_str = "AUTO" if st.agc_enable else "MANUAL" - agc_color = DARK_SUCCESS if st.agc_enable else DARK_INFO - self._agc_mode_lbl.setStyleSheet( - f"color: {agc_color}; font-size: 16px; font-weight: bold;") - self._agc_mode_lbl.setText(f"AGC: {agc_str}") + # The mode label honours bench-mode in production — same shared helper + # the FPGA Control tab uses, so the two views can never disagree. + self._refresh_agc_mode_labels(st) self._agc_gain_lbl.setText(f"Gain: {st.agc_current_gain}") self._agc_peak_lbl.setText(f"Peak: {st.agc_peak_magnitude}") @@ -2001,6 +2089,51 @@ class RadarDashboard(QMainWindow): f"Failed to apply DSP settings: {e}") logger.error(f"DSP config error: {e}") + def _on_bench_mode_toggled(self, checked: bool): + """Persist BENCH-MODE flag and refresh AGC widget visibility.""" + self._bench_mode = bool(checked) + self._qsettings.setValue("bench_mode", self._bench_mode) + self._apply_bench_mode_visibility() + logger.info(f"BENCH-MODE {'ON' if self._bench_mode else 'OFF'}") + + def _apply_bench_mode_visibility(self): + """Show or hide AGC Enable controls based on self._bench_mode and + re-sync the AGC mode labels so they don't contradict the badge.""" + production = not self._bench_mode + self._agc_always_on_badge.setVisible(production) + self._agc_toggle_container.setVisible(self._bench_mode) + # Push current bench-mode state through the AGC mode labels — uses + # the last StatusResponse if any, otherwise the static defaults. + self._refresh_agc_mode_labels(self._last_status) + + def _refresh_agc_mode_labels(self, st: "StatusResponse | None"): + """Update the AGC enable text on both the FPGA Control Status box + (self._agc_labels['enable']) and the AGC Monitor strip + (self._agc_mode_lbl). In production the firmware ignores the FPGA + register and runs AGC every frame, so both labels show 'ALWAYS-ON' + in green — keeps them honest with the production badge. In bench + the labels follow the StatusResponse register, falling back to '--' + before the first status arrives.""" + if self._bench_mode: + if st is None: + text, color = "AGC: --", DARK_INFO + elif st.agc_enable: + text, color = "AGC: AUTO", DARK_SUCCESS + else: + text, color = "AGC: MANUAL", DARK_INFO + else: + text, color = "AGC: ALWAYS-ON", DARK_SUCCESS + + if hasattr(self, "_agc_labels") and "enable" in self._agc_labels: + self._agc_labels["enable"].setStyleSheet( + f"color: {color}; font-weight: bold;") + self._agc_labels["enable"].setText(text) + + if hasattr(self, "_agc_mode_lbl"): + self._agc_mode_lbl.setStyleSheet( + f"color: {color}; font-size: 16px; font-weight: bold;") + self._agc_mode_lbl.setText(text) + # ===================================================================== # Periodic GUI refresh (100 ms timer) # =====================================================================