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 5ea90b2..a697272 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 @@ -626,6 +626,10 @@ void runRadarPulseSequence() { DIAG("SYS", "Broadside reference (vector_0) — 1× per azimuth"); adarManager.setCustomBeamPattern16(vector_0, ADAR1000Manager::BeamDirection::TX); adarManager.setCustomBeamPattern16(vector_0, ADAR1000Manager::BeamDirection::RX); + // PR-AB.b expanded commit 5: announce "beam pattern ready" to the FPGA so + // chirp_scheduler can release S_BEAM_WAIT. Harmless when the FPGA's + // host_handshake_enable=0 (legacy open-loop default). + HAL_GPIO_TogglePin(FPGA_BEAM_READY_GPIO_Port, FPGA_BEAM_READY_Pin); waitForFramePulse(FRAME_PULSE_TIMEOUT_MS); m += m_max/2; @@ -636,12 +640,14 @@ 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_GPIO_TogglePin(FPGA_BEAM_READY_GPIO_Port, FPGA_BEAM_READY_Pin); // commit 5 handshake 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_GPIO_TogglePin(FPGA_BEAM_READY_GPIO_Port, FPGA_BEAM_READY_Pin); // commit 5 handshake waitForFramePulse(FRAME_PULSE_TIMEOUT_MS); m += m_max/2; 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 057eaec..2c6e968 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 @@ -162,6 +162,17 @@ void Error_Handler(void); #define FPGA_FRAME_PULSE_GPIO_Port FPGA_DIG6_GPIO_Port #define FPGA_FRAME_PULSE_EXTI_IRQn EXTI15_10_IRQn +/* PR-AB.b expanded commit 5: DIG_0 (PD8) carries the MCU per-beam-pattern + * ready toggle into the FPGA's chirp_scheduler beam-ready handshake. + * runRadarPulseSequence toggles this pin once per setCustomBeamPattern16(RX) + * call after the SPI burst completes, so the FPGA only starts the next 48- + * chirp frame once the new beam pattern is in place. The FPGA-side stall is + * gated by opcode 0x1A (host_handshake_enable, cold-reset = 0) so flashing + * the bitstream before the GUI enables the feature leaves the radar in its + * legacy open-loop cadence. */ +#define FPGA_BEAM_READY_Pin GPIO_PIN_8 +#define FPGA_BEAM_READY_GPIO_Port GPIOD + #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_2_FPGA/chirp_scheduler.v b/9_Firmware/9_2_FPGA/chirp_scheduler.v index f827314..a029615 100644 --- a/9_Firmware/9_2_FPGA/chirp_scheduler.v +++ b/9_Firmware/9_2_FPGA/chirp_scheduler.v @@ -24,6 +24,15 @@ * Pulse outputs (chirp_pulse, subframe_pulse, frame_pulse) are 1-cycle * positive pulses, not toggles. * + * PR-AB.b expanded commit 5 — beam-ready handshake: when + * host_handshake_enable=1, the FSM enters S_BEAM_WAIT after frame_pulse + * and only fires the next frame's first chirp once it observes an edge on + * beam_ready_async (MCU PD8 toggle) or the ~80 ms watchdog expires. + * Watchdog timeout sets the sticky output beam_handshake_watchdog_fired + * (cleared only by reset_n) so the host can spot patterns falling behind + * the chirp ladder. host_handshake_enable=0 preserves the legacy + * always-on chirp cadence. + * * Clock domain: clk (100 MHz), async-low reset. */ @@ -55,6 +64,15 @@ module chirp_scheduler ( // TX-side cdc_async_fifo before mixers come up. input wire mixers_enable, + // PR-AB.b expanded commit 5: beam-ready handshake. beam_ready_async is the + // raw MCU PD8 GPIO toggle (CDC-synchronized inside this module on `clk`). + // host_handshake_enable gates whether the FSM stalls in S_BEAM_WAIT after + // each frame_pulse. Cold-reset default at the top level is 1'b0 (legacy + // open-loop cadence) — host enables via opcode 0x1A once the MCU's PD8 + // toggle wiring is in place. + input wire beam_ready_async, + input wire host_handshake_enable, + // ====== Outputs ====== output reg [1:0] wave_sel, // canonical waveform identity output reg chirp_pulse, // 1-cycle pulse: chirp begins this clk @@ -66,7 +84,11 @@ module chirp_scheduler ( // Currently selected timing for the in-flight chirp (PR-E TX async FIFO) output wire [15:0] cfg_chirp_cycles, output wire [15:0] cfg_listen_cycles, - output wire [15:0] cfg_guard_cycles + output wire [15:0] cfg_guard_cycles, + + // PR-AB.b expanded commit 5: sticky handshake watchdog flag, cleared + // only by reset_n. Plumbed into status_words[4][1] at the top level. + output reg beam_handshake_watchdog_fired ); // ============================================================================ @@ -140,17 +162,46 @@ assign cfg_chirp_cycles = sel_chirp_cycles; assign cfg_listen_cycles = sel_listen_cycles; assign cfg_guard_cycles = host_guard_cycles; +// ============================================================================ +// Beam-ready CDC + edge detection (PR-AB.b expanded commit 5). +// beam_ready_async is a slow MCU GPIO toggle (PD8). Two ASYNC_REG flops bring +// it into clk, then a one-cycle delay lets us detect any transition (rising or +// falling) — the MCU drives via HAL_GPIO_TogglePin once per beam pattern, so +// successive frames see alternating polarities. +// ============================================================================ +(* ASYNC_REG = "TRUE" *) reg [1:0] beam_ready_sync; +reg beam_ready_q_prev; +always @(posedge clk or negedge reset_n) begin + if (!reset_n) begin + beam_ready_sync <= 2'b00; + beam_ready_q_prev <= 1'b0; + end else begin + beam_ready_sync <= {beam_ready_sync[0], beam_ready_async}; + beam_ready_q_prev <= beam_ready_sync[1]; + end +end +wire beam_ready_q = beam_ready_sync[1]; +wire beam_ready_edge = (beam_ready_q != beam_ready_q_prev); + // ============================================================================ // Main FSM — auto-scan over enabled sub-frames. // ============================================================================ -localparam S_IDLE = 3'd0; -localparam S_CHIRP = 3'd1; -localparam S_LISTEN = 3'd2; -localparam S_GUARD = 3'd3; -localparam S_ADVANCE = 3'd4; +localparam S_IDLE = 3'd0; +localparam S_CHIRP = 3'd1; +localparam S_LISTEN = 3'd2; +localparam S_GUARD = 3'd3; +localparam S_ADVANCE = 3'd4; +localparam S_BEAM_WAIT = 3'd5; + +// Beam-ready watchdog: 23 bits at 100 MHz → ~83.9 ms = ~8 nominal frames +// (frame ≈ 8.05 ms full 3-PRI ladder, less when subframes are masked). Long +// enough to absorb MCU SPI bursts + scheduling jitter without auto-advancing, +// short enough to keep the radar moving when a pattern write actually drops. +localparam [22:0] BEAM_WATCHDOG_MAX = 23'd8_000_000; reg [2:0] state; reg [16:0] timer; // 17 bits cover LONG+listen+guard worst case +reg [22:0] beam_watchdog; // counts clk cycles while in S_BEAM_WAIT // Pre-computed wires used inside the FSM advance logic so non-blocking // updates to subframe_id / wave_sel see the correct next value in the same @@ -168,14 +219,19 @@ always @(posedge clk or negedge reset_n) begin frame_pulse <= 1'b0; chirp_counter <= 6'd0; subframe_id <= 2'd0; + beam_watchdog <= 23'd0; + beam_handshake_watchdog_fired <= 1'b0; end else if (!mixers_enable) begin // Master disable — quiesce the FSM so chirp_pulse never asserts and - // the TX side stays at idle. + // the TX side stays at idle. beam_handshake_watchdog_fired is sticky + // across mixers_enable cycles so the host can see late patterns even + // after a soft restart. state <= S_IDLE; timer <= 17'd0; chirp_pulse <= 1'b0; subframe_pulse <= 1'b0; frame_pulse <= 1'b0; + beam_watchdog <= 23'd0; end else begin // Pulses default low — set high for one cycle on relevant transitions. chirp_pulse <= 1'b0; @@ -219,10 +275,39 @@ always @(posedge clk or negedge reset_n) begin subframe_pulse <= 1'b1; subframe_id <= next_sf; wave_sel <= subframe_to_wave(next_sf); - if (next_sf == first_sf) + if (next_sf == first_sf) begin + // Frame wrap — emit frame_pulse and (if enabled) stall + // in S_BEAM_WAIT until the MCU acknowledges via PD8. frame_pulse <= 1'b1; - chirp_pulse <= 1'b1; - state <= S_CHIRP; + if (host_handshake_enable) begin + beam_watchdog <= 23'd0; + state <= S_BEAM_WAIT; + end else begin + chirp_pulse <= 1'b1; + state <= S_CHIRP; + end + end else begin + chirp_pulse <= 1'b1; + state <= S_CHIRP; + end + end + end + S_BEAM_WAIT: begin + // Wait for an MCU PD8 toggle (any edge) OR the watchdog. + // host_handshake_enable can drop mid-wait — release the FSM in + // that case so disabling the handshake never strands the + // radar between frames. + if (beam_ready_edge || !host_handshake_enable) begin + beam_watchdog <= 23'd0; + chirp_pulse <= 1'b1; + state <= S_CHIRP; + end else if (beam_watchdog >= BEAM_WATCHDOG_MAX) begin + beam_handshake_watchdog_fired <= 1'b1; + beam_watchdog <= 23'd0; + chirp_pulse <= 1'b1; + state <= S_CHIRP; + end else begin + beam_watchdog <= beam_watchdog + 23'd1; end end default: state <= S_IDLE; diff --git a/9_Firmware/9_2_FPGA/constraints/xc7a200t_fbg484.xdc b/9_Firmware/9_2_FPGA/constraints/xc7a200t_fbg484.xdc index d5a6fa0..272ff31 100644 --- a/9_Firmware/9_2_FPGA/constraints/xc7a200t_fbg484.xdc +++ b/9_Firmware/9_2_FPGA/constraints/xc7a200t_fbg484.xdc @@ -229,13 +229,13 @@ set_property IOSTANDARD LVCMOS33 [get_ports {stm32_*_3v3}] # STM32 DIG BUS — Control signals (Bank 15, VCCO = 3.3V) # ============================================================================ # Pin: L18 = IO_L16N_T2_A27_15 -set_property PACKAGE_PIN L18 [get_ports {stm32_new_chirp}] +set_property PACKAGE_PIN L18 [get_ports {stm32_beam_ready}] # N18 / N19 retired in PR-AB.b expanded — formerly stm32_new_elevation / # stm32_new_azimuth. MCU side init is also stripped (see commit 3); the # pins default to high-Z inputs after MCU reset. # Pin: N20 = IO_L18P_T2_A24_15 set_property PACKAGE_PIN N20 [get_ports {stm32_mixers_enable}] -set_property IOSTANDARD LVCMOS33 [get_ports {stm32_new_chirp}] +set_property IOSTANDARD LVCMOS33 [get_ports {stm32_beam_ready}] set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}] # ============================================================================ diff --git a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc index afa1596..21efa41 100644 --- a/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc +++ b/9_Firmware/9_2_FPGA/constraints/xc7a50t_ftg256.xdc @@ -222,12 +222,12 @@ set_property IOSTANDARD LVCMOS18 [get_ports {stm32_*_1v8}] # 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 F13 [get_ports {stm32_beam_ready}] ;# DIG_0 (PD8) # DIG_1 (PD9) + DIG_2 (PD10) retired in PR-AB.b expanded — formerly # stm32_new_elevation / stm32_new_azimuth. MCU side init is also stripped # (see commit 3); the pins default to high-Z inputs after MCU reset. set_property PACKAGE_PIN F15 [get_ports {stm32_mixers_enable}] ;# DIG_3 (PD11) -set_property IOSTANDARD LVCMOS33 [get_ports {stm32_new_chirp}] +set_property IOSTANDARD LVCMOS33 [get_ports {stm32_beam_ready}] set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}] # reset_n is DIG_4 (PD12) — constrained above in the RESET section diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index 41463e2..e4def92 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -67,6 +67,16 @@ module radar_receiver_final ( // so it stays in S_IDLE until the operator turns the radar on. input wire mixers_enable_100m, + // PR-AB.b expanded commit 5: beam-ready handshake plumbing. + // stm32_beam_ready_async is the raw MCU PD8 toggle (CDC-synchronized + // inside chirp_scheduler on `clk`). host_handshake_enable gates whether + // the scheduler stalls in S_BEAM_WAIT after each frame_pulse. + // beam_handshake_watchdog_fired is sticky (cleared only on reset_n) and + // surfaces in status_words[4][1] at the top level. + input wire stm32_beam_ready_async, + input wire host_handshake_enable, + output wire beam_handshake_watchdog_fired, + // CFAR integration: expose Doppler frame_complete to top level output wire doppler_frame_done_out, @@ -236,6 +246,10 @@ chirp_scheduler sched ( .host_long_listen_cycles(host_long_listen_cycles), .host_guard_cycles(host_guard_cycles), .host_chirps_per_subframe(6'd`RP_DEF_CHIRPS_PER_SUBFRAME), + // PR-AB.b expanded commit 5: beam-ready handshake. CDC-sync happens + // inside chirp_scheduler so the raw async GPIO can be routed directly. + .beam_ready_async(stm32_beam_ready_async), + .host_handshake_enable(host_handshake_enable), .wave_sel(wave_sel), .chirp_pulse(chirp_pulse), .subframe_pulse(subframe_pulse), @@ -244,7 +258,8 @@ chirp_scheduler sched ( .subframe_id(sched_subframe_id), .cfg_chirp_cycles (sched_cfg_chirp_cycles), .cfg_listen_cycles(sched_cfg_listen_cycles), - .cfg_guard_cycles (sched_cfg_guard_cycles) + .cfg_guard_cycles (sched_cfg_guard_cycles), + .beam_handshake_watchdog_fired(beam_handshake_watchdog_fired) ); // PR-E: forward scheduler pulses + wave_sel to the TX-side CDC bridge in diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index c1208fa..0e36f0c 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -78,9 +78,12 @@ module radar_system_top ( // STM32 → FPGA control (PD8/PD11). PD9/PD10 (was stm32_new_elevation / // stm32_new_azimuth) retired in PR-AB.b expanded — see commit 3 for the - // XDC + MCU GPIO scrub. stm32_new_chirp will be renamed to - // stm32_beam_ready in commit 5 (beam-ready handshake). - input wire stm32_new_chirp, + // XDC + MCU GPIO scrub. stm32_beam_ready (PD8) is the MCU's per-pattern + // ready toggle for the beam-ready handshake added in PR-AB.b expanded + // commit 5; CDC-sync is owned by chirp_scheduler.v on `clk`. The + // handshake itself is gated by opcode 0x1A (host_handshake_enable); + // cold-reset default 1'b0 keeps the open-loop legacy cadence in place. + input wire stm32_beam_ready, input wire stm32_mixers_enable, // ========== FT601 USB 3.0 INTERFACE ========== @@ -248,6 +251,12 @@ end // 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; +// PR-AB.b expanded commit 5: sticky from chirp_scheduler. High = at least one +// frame fired without an MCU PD8 ack within the ~80 ms watchdog. Packed into +// status_words[4][1] so the host can spot patterns falling behind. Sticky is +// owned by the scheduler (clk_100m source domain); CDC across to ft_clk is +// done inside usb_data_interface_ft2232h alongside the other status stickies. +wire rx_beam_handshake_watchdog; // Data packing for USB wire [31:0] usb_range_profile; @@ -297,6 +306,12 @@ reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 48 = RP_CHIRPS_PER // bit 1 MEDIUM, bit 2 LONG. Default 3'b111 keeps the production 3-PRI ladder. // Mirrored into v2 frame byte 2 bits[5:3] (usb_data_interface_ft2232h.v). reg [2:0] host_subframe_enable; // Opcode 0x19 (default RP_DEF_SUBFRAME_ENABLE = 3'b111) +// PR-AB.b expanded commit 5: opcode 0x1A enables the beam-ready handshake +// inside chirp_scheduler. Cold-reset default = 1'b0 (legacy open-loop +// cadence) so existing TBs and any production deployment without the MCU +// PD8 toggle wired up keep their old behavior. Host (GUI) writes 1 once +// the MCU has been verified to be toggling PD8 each beam pattern. +reg host_handshake_enable; reg host_status_request; // Opcode 0xFF (self-clearing pulse) // Fix 4: Doppler/chirps mismatch protection @@ -627,6 +642,14 @@ radar_receiver_final rx_inst ( .host_agc_holdoff(host_agc_holdoff), // PR-E: master enable for the scheduler (CDC-sync'd to clk_100m above) .mixers_enable_100m(stm32_mixers_enable_100m), + // PR-AB.b expanded commit 5: beam-ready handshake plumbing. stm32_beam_ready + // is the raw MCU PD8 GPIO (sync'd inside chirp_scheduler on `clk`). + // host_handshake_enable is the opcode-0x1A register, sampled on clk_100m + // (quasi-static, no extra CDC). beam_handshake_watchdog_fired is sticky in + // the source domain and packed into status_words[4][1] downstream. + .stm32_beam_ready_async(stm32_beam_ready), + .host_handshake_enable(host_handshake_enable), + .beam_handshake_watchdog_fired(rx_beam_handshake_watchdog), // CFAR: Doppler frame-complete pulse .doppler_frame_done_out(rx_frame_complete), // Ground clutter removal @@ -903,7 +926,13 @@ if (USB_MODE == 0) begin : gen_ft601 // (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) + .status_ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun), + + // PR-AB.b expanded commit 5: beam-ready handshake watchdog sticky. + // Packed into status_words[4][1] in the FT601 path too so the host + // sees the same fault bit regardless of which USB build it's talking + // to (FT601 200T premium vs FT2232H 50T production). + .status_beam_handshake_watchdog(rx_beam_handshake_watchdog) ); // FT2232H ports unused in FT601 mode — tie off @@ -1002,7 +1031,15 @@ end else begin : gen_ft2232h // PR-G: 2-tier CFAR telemetry (status_words[6]) .status_cfar_alpha_soft(host_cfar_alpha_soft), .status_detect_threshold_soft(cfar_detect_threshold_soft), - .status_detect_count_cand(cfar_detect_count_cand) + .status_detect_count_cand(cfar_detect_count_cand), + + // PR-AB.b expanded commit 5: beam-ready handshake watchdog sticky. + // Source domain is clk_100m; usb_data_interface_ft2232h hands it to + // ft_clk through the same status-bit CDC convention used for the + // F-6.4 watchdog and F-1.2 overrun stickies above. Packed into + // status_words[4][1] downstream — see word-4 layout note in + // usb_data_interface_ft2232h.v. + .status_beam_handshake_watchdog(rx_beam_handshake_watchdog) ); // FT601 ports unused in FT2232H mode — tie off @@ -1096,6 +1133,10 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin host_chirps_per_elev <= 6'd48; // PR-U / M-8: 3'b111 = SHORT|MEDIUM|LONG all on (production 3-PRI ladder). host_subframe_enable <= `RP_DEF_SUBFRAME_ENABLE; + // PR-AB.b expanded commit 5: handshake disabled at cold-reset so any + // host or TB that doesn't drive PD8 keeps the legacy open-loop cadence. + // Production GUI writes 0x1A=1 after MCU has been verified. + host_handshake_enable <= 1'b0; host_status_request <= 1'b0; chirps_mismatch_error <= 1'b0; // CFAR defaults (disabled by default — backward-compatible) @@ -1150,6 +1191,11 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin // bits[5:3] so host CRT can detect mask != 3'b111 and degrade // confidence rather than mis-attribute the SF axis. 8'h19: host_subframe_enable <= usb_cmd_value[2:0]; + // PR-AB.b expanded commit 5: beam-ready handshake enable. + // value[0]=1 makes chirp_scheduler stall in S_BEAM_WAIT after + // each frame_pulse until the MCU toggles PD8 (or watchdog + // ~80 ms). value[0]=0 reverts to the legacy open-loop cadence. + 8'h1A: host_handshake_enable <= usb_cmd_value[0]; 8'h15: begin // Fix 4: Clamp chirps_per_elev to the fixed Doppler frame size. // If host requests a different value, clamp and set error flag. 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 ce4a868..ac2a1a9 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top_50t.v +++ b/9_Firmware/9_2_FPGA/radar_system_top_50t.v @@ -68,8 +68,10 @@ module radar_system_top_50t ( // ===== STM32 Control (Bank 15: 3.3V) ===== // PR-AB.b expanded: stm32_new_elevation/azimuth retired (no consumer). - // stm32_new_chirp becomes stm32_beam_ready in commit 5. - input wire stm32_new_chirp, + // stm32_beam_ready (PD8) is the MCU per-pattern ready toggle for the + // beam-ready handshake landed in commit 5; gated by opcode 0x1A inside + // radar_system_top (host_handshake_enable). Cold-reset = legacy off. + input wire stm32_beam_ready, input wire stm32_mixers_enable, // ===== FT2232H USB 2.0 Interface (Bank 35: 3.3V) ===== @@ -178,7 +180,7 @@ module radar_system_top_50t ( .adc_pwdn (adc_pwdn), // ----- STM32 Control ----- - .stm32_new_chirp (stm32_new_chirp), + .stm32_beam_ready (stm32_beam_ready), .stm32_mixers_enable (stm32_mixers_enable), // ----- FT2232H USB 2.0 (active on 50T, USB_MODE=1) ----- diff --git a/9_Firmware/9_2_FPGA/run_regression.sh b/9_Firmware/9_2_FPGA/run_regression.sh index cddea1a..96aa4f9 100755 --- a/9_Firmware/9_2_FPGA/run_regression.sh +++ b/9_Firmware/9_2_FPGA/run_regression.sh @@ -574,6 +574,10 @@ run_test "DIG6 frame-pulse stretcher (PR-AB.b)" \ tb/tb_dig6_frame_pulse.vvp \ tb/tb_dig6_frame_pulse.v +run_test "Chirp Scheduler beam-ready handshake (PR-AB.b expanded c5)" \ + tb/tb_chirp_scheduler_handshake.vvp \ + tb/tb_chirp_scheduler_handshake.v chirp_scheduler.v + run_test "NUM_CELLS sizing 50T (AUDIT-C16)" \ tb/tb_audit_c16_num_cells_50t.vvp \ tb/tb_audit_c16_num_cells.v diff --git a/9_Firmware/9_2_FPGA/tb/cosim/compare_independent.py b/9_Firmware/9_2_FPGA/tb/cosim/compare_independent.py index 647d4bc..bba48ce 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/compare_independent.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/compare_independent.py @@ -358,7 +358,7 @@ def check_mf_invariants(result: CheckResult): f"twin={twin_peak}, ref={ref_peak}" ) - # Sidelobe behaviour: peak should be ≥ 5× the median magnitude. Under + # Sidelobe behaviour: peak should be at least 5x the median magnitude. Under # scaled-mode at amp=4000 the peak rises to ~977 while sidelobes stay # near the LSB floor, easily clearing the threshold. twin_peak_val = float(twin_mag[delay]) diff --git a/9_Firmware/9_2_FPGA/tb/cosim/gen_e2e_expected.py b/9_Firmware/9_2_FPGA/tb/cosim/gen_e2e_expected.py index 24ca77c..e66bca8 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/gen_e2e_expected.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/gen_e2e_expected.py @@ -62,7 +62,7 @@ import numpy as np THIS_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, THIS_DIR) -from fpga_model import DopplerProcessor, run_cfar_ca +from fpga_model import DopplerProcessor, run_cfar_ca # noqa: E402 # Pull stimulus configuration verbatim so dimensions stay aligned. from gen_e2e_stimulus import ( # noqa: E402 @@ -72,8 +72,6 @@ from gen_e2e_stimulus import ( # noqa: E402 CHIRPS_PER_FRAME, RANGE_BINS, HOST_DC_NOTCH_WIDTH, - EXPECTED_RANGE_BIN, - EXPECTED_DOPPLER_BIN_PER_SF, EXPECTED_DETECT_CELLS, ) @@ -263,10 +261,8 @@ def pack_bulk_frame(frame_number: int, flags: int, packed = 0 for slot in range(4): db = byte_idx * 4 + slot - if db < DOPPLER_TOTAL_BINS: - code = int(cfar_class[rb, db]) & 0x3 - else: - code = 0 # padding + # padding for db >= DOPPLER_TOTAL_BINS lands on slot 3 + code = int(cfar_class[rb, db]) & 0x3 if db < DOPPLER_TOTAL_BINS else 0 packed |= code << ((3 - slot) * 2) out.append(packed) diff --git a/9_Firmware/9_2_FPGA/tb/cosim/gen_e2e_stimulus.py b/9_Firmware/9_2_FPGA/tb/cosim/gen_e2e_stimulus.py index 167cb8b..6bd5c84 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/gen_e2e_stimulus.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/gen_e2e_stimulus.py @@ -98,16 +98,15 @@ HOST_DC_NOTCH_WIDTH = 1 # ============================================================================ # Target placement -> expected bin coordinates # ============================================================================ -# range_bin = round(2 * R / c * fs / decim) -# = round(2 * 100 / 3e8 * 400e6 / 4) -# = round(66.667) = 67 -EXPECTED_RANGE_BIN = int(round(2.0 * TARGET_RANGE_M / C_LIGHT * RANGE_BIN_HZ)) +# Range bin formula: round(2 * R / c * fs / decim). For R=100m, fs=400 MHz, +# decim=4 -> round(2 * 100 / 3e8 * 100e6) = round(66.667) = 67. +EXPECTED_RANGE_BIN = round(2.0 * TARGET_RANGE_M / C_LIGHT * RANGE_BIN_HZ) # Per-sub-frame doppler bin (folding into 16-pt FFT). For our 5 m/s target # this is intentionally non-folding -> 1 in all three sub-frames. F_DOPPLER_HZ = 2.0 * TARGET_VEL_MPS * F_CARRIER / C_LIGHT EXPECTED_DOPPLER_BIN_PER_SF = tuple( - int(round(F_DOPPLER_HZ * DOPPLER_FFT_SIZE * pri)) % DOPPLER_FFT_SIZE + round(F_DOPPLER_HZ * DOPPLER_FFT_SIZE * pri) % DOPPLER_FFT_SIZE for pri in PRI_BY_SF ) # Flat 48-bin doppler-axis expected cells (sub_frame << 4 | bin). @@ -160,8 +159,8 @@ def generate_range_decim_frame(seed: int = SCENE_SEED) -> tuple[np.ndarray, np.n # Target injection at the expected range bin. phi = _target_phase_rad(c) - sig_i = int(round(TARGET_AMPLITUDE * np.cos(phi))) - sig_q = int(round(TARGET_AMPLITUDE * np.sin(phi))) + sig_i = round(TARGET_AMPLITUDE * np.cos(phi)) + sig_q = round(TARGET_AMPLITUDE * np.sin(phi)) frame_i[c, EXPECTED_RANGE_BIN] += sig_i frame_q[c, EXPECTED_RANGE_BIN] += sig_q @@ -231,7 +230,7 @@ def main() -> int: f"shape={frame_i.shape}") if n_lines != expected_lines: - print(f" ERROR: line count mismatch", file=sys.stderr) + print(" ERROR: line count mismatch", file=sys.stderr) return 1 # Sanity: target peak should dominate at the expected range bin. diff --git a/9_Firmware/9_2_FPGA/tb/cosim/tb_e2e_dsp_to_host_parse.py b/9_Firmware/9_2_FPGA/tb/cosim/tb_e2e_dsp_to_host_parse.py index 59eecb9..9a24253 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/tb_e2e_dsp_to_host_parse.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/tb_e2e_dsp_to_host_parse.py @@ -95,7 +95,7 @@ class TestState: def load_captured_frame_hex(path: str) -> bytes: """Read iverilog $writememh output (one byte per line, 2-hex-digit).""" out = bytearray() - with open(path, 'r') as f: + with open(path) as f: for line in f: tok = line.strip() if not tok or tok.startswith('//'): diff --git a/9_Firmware/9_2_FPGA/tb/tb_chirp_scheduler_handshake.v b/9_Firmware/9_2_FPGA/tb/tb_chirp_scheduler_handshake.v new file mode 100644 index 0000000..9db1e23 --- /dev/null +++ b/9_Firmware/9_2_FPGA/tb/tb_chirp_scheduler_handshake.v @@ -0,0 +1,279 @@ +`timescale 1ns / 1ps +// ============================================================================ +// tb_chirp_scheduler_handshake.v — PR-AB.b expanded commit 5 unit TB +// +// Exercises the beam-ready handshake added to chirp_scheduler.v: +// - S_BEAM_WAIT entered on frame_pulse when host_handshake_enable=1 +// - Exit on any beam_ready_async edge (toggle semantic) +// - Watchdog auto-advances + sets beam_handshake_watchdog_fired sticky +// - host_handshake_enable=0 keeps legacy open-loop cadence +// - Mid-wait disable releases the FSM +// - Reset clears the sticky +// +// Uses compressed cycle counts so a full 48-chirp frame completes in <1 ms +// of sim time. The production cycle counts (175/161/167 µs PRIs) would push +// the sim past the iverilog regression budget; the FSM logic exercised here +// is independent of the cycle-count values. +// ============================================================================ +`include "radar_params.vh" + +module tb_chirp_scheduler_handshake; + +// ---- Clock (100 MHz, 10 ns) ---- +reg clk = 1'b0; +always #5 clk = ~clk; + +// ---- Compressed timing — 6 chirp/listen/guard cycles each per PRI ---- +localparam [15:0] T_CHIRP = 16'd6; +localparam [15:0] T_LISTEN = 16'd6; +localparam [15:0] T_GUARD = 16'd6; +// Single chirp + listen + guard ≈ 20 cycles. With chirps_per_subframe=2 and +// 3 sub-frames active, a frame lands at ~120 cycles → 1.2 µs/frame in sim. +localparam [5:0] CHIRPS_PER_SUBFRAME = 6'd2; + +// ---- DUT signals ---- +reg reset_n = 1'b0; +reg mixers_enable = 1'b0; +reg [2:0] subframe_enable = 3'b111; +reg beam_ready_async = 1'b0; +reg handshake_enable = 1'b0; + +wire [1:0] wave_sel; +wire chirp_pulse; +wire subframe_pulse; +wire frame_pulse; +wire [5:0] chirp_counter; +wire [1:0] subframe_id; +wire [15:0] cfg_chirp_cycles; +wire [15:0] cfg_listen_cycles; +wire [15:0] cfg_guard_cycles; +wire watchdog_fired; + +chirp_scheduler dut ( + .clk (clk), + .reset_n (reset_n), + .host_subframe_enable (subframe_enable), + .host_short_chirp_cycles (T_CHIRP), + .host_short_listen_cycles (T_LISTEN), + .host_medium_chirp_cycles (T_CHIRP), + .host_medium_listen_cycles (T_LISTEN), + .host_long_chirp_cycles (T_CHIRP), + .host_long_listen_cycles (T_LISTEN), + .host_guard_cycles (T_GUARD), + .host_chirps_per_subframe (CHIRPS_PER_SUBFRAME), + .mixers_enable (mixers_enable), + .beam_ready_async (beam_ready_async), + .host_handshake_enable (handshake_enable), + .wave_sel (wave_sel), + .chirp_pulse (chirp_pulse), + .subframe_pulse (subframe_pulse), + .frame_pulse (frame_pulse), + .chirp_counter (chirp_counter), + .subframe_id (subframe_id), + .cfg_chirp_cycles (cfg_chirp_cycles), + .cfg_listen_cycles (cfg_listen_cycles), + .cfg_guard_cycles (cfg_guard_cycles), + .beam_handshake_watchdog_fired(watchdog_fired) +); + +// ---- Bookkeeping ---- +integer pass = 0; +integer fail = 0; +integer frame_pulse_count = 0; +always @(posedge clk) if (frame_pulse) frame_pulse_count = frame_pulse_count + 1; + +task check; + input [255:0] label; + input cond; + begin + if (cond) begin + $display(" [PASS] %0s", label); + pass = pass + 1; + end else begin + $display(" [FAIL] %0s", label); + fail = fail + 1; + end + end +endtask + +// Wait for the FSM to enter S_BEAM_WAIT (state == 3'd5). +task wait_for_beam_wait; + input integer timeout_cycles; + integer i; + begin + i = 0; + while (dut.state !== 3'd5 && i < timeout_cycles) begin + @(posedge clk); + i = i + 1; + end + end +endtask + +// Wait for state to leave S_BEAM_WAIT (timeout reports failure to caller). +task wait_for_beam_wait_exit; + input integer timeout_cycles; + integer i; + begin + i = 0; + while (dut.state === 3'd5 && i < timeout_cycles) begin + @(posedge clk); + i = i + 1; + end + end +endtask + +// Wait for at least N frames to complete (frame_pulse_count >= target). +task wait_frames; + input integer target; + input integer timeout_cycles; + integer i; + begin + i = 0; + while (frame_pulse_count < target && i < timeout_cycles) begin + @(posedge clk); + i = i + 1; + end + end +endtask + +// ========================================================================= +// MAIN +// ========================================================================= +initial begin + $dumpfile("tb_chirp_scheduler_handshake.vcd"); + $dumpvars(0, tb_chirp_scheduler_handshake); + + $display("============================================================"); + $display(" CHIRP_SCHEDULER beam-ready handshake (PR-AB.b expanded c5)"); + $display("============================================================"); + + // Reset + reset_n = 1'b0; + mixers_enable = 1'b0; + handshake_enable = 1'b0; + beam_ready_async = 1'b0; + repeat (4) @(posedge clk); + reset_n = 1'b1; + @(posedge clk); + check("T1: post-reset watchdog sticky low", watchdog_fired == 1'b0); + + // ==================================================================== + // T2: Legacy mode (handshake_enable=0) — frames advance back-to-back, + // S_BEAM_WAIT must never be visited. + // ==================================================================== + $display("--- T2: legacy open-loop (handshake_enable=0) ---"); + mixers_enable = 1'b1; + frame_pulse_count = 0; + wait_frames(3, 5000); + check("T2: at least 3 frames fired without handshake", frame_pulse_count >= 3); + check("T2: watchdog sticky still low", watchdog_fired == 1'b0); + + // Park the scheduler in IDLE so the next test starts clean. + mixers_enable = 1'b0; + repeat (5) @(posedge clk); + frame_pulse_count = 0; + + // ==================================================================== + // T3: Handshake enabled — scheduler should enter S_BEAM_WAIT after + // the next frame_pulse and only exit on a beam_ready edge. + // ==================================================================== + $display("--- T3: handshake enabled, MCU toggles before watchdog ---"); + handshake_enable = 1'b1; + mixers_enable = 1'b1; + wait_for_beam_wait(5000); + check("T3a: FSM entered S_BEAM_WAIT after a frame_pulse", + dut.state == 3'd5); + + // Sit in S_BEAM_WAIT for a deliberate number of cycles; the scheduler + // must stay parked until we toggle beam_ready_async. + repeat (200) @(posedge clk); + check("T3b: FSM still in S_BEAM_WAIT after 200 idle cycles", + dut.state == 3'd5); + check("T3b: watchdog has not fired", watchdog_fired == 1'b0); + + // Toggle beam_ready_async and verify exit within a handful of clk + // edges (2-FF sync + 1-cycle edge latch + S_BEAM_WAIT → S_CHIRP). + @(posedge clk); beam_ready_async = 1'b1; + wait_for_beam_wait_exit(50); + check("T3c: FSM left S_BEAM_WAIT after PD8 toggle (rising)", + dut.state != 3'd5); + + // ==================================================================== + // T4: Second toggle exits a later wait (verifies edge-detect handles + // falling edges symmetrically — HAL_GPIO_TogglePin gives both). + // ==================================================================== + $display("--- T4: second wait, falling-edge ack ---"); + wait_for_beam_wait(5000); + check("T4a: FSM re-entered S_BEAM_WAIT on next frame", dut.state == 3'd5); + @(posedge clk); beam_ready_async = 1'b0; // falling edge + wait_for_beam_wait_exit(50); + check("T4b: FSM left S_BEAM_WAIT after PD8 toggle (falling)", + dut.state != 3'd5); + + // ==================================================================== + // T5: Watchdog timeout. The real 23-bit terminal value (8M cycles ≈ + // 80 ms) is unreachable in iverilog sim time, so we force the + // FSM's counter to the terminal value and let one clk edge + // resolve the (counter >= BEAM_WATCHDOG_MAX) branch. + // ==================================================================== + $display("--- T5: watchdog auto-advance + sticky latch ---"); + wait_for_beam_wait(5000); + check("T5a: FSM in S_BEAM_WAIT (pre-force)", dut.state == 3'd5); + // Force the counter to BEAM_WATCHDOG_MAX so the FSM's >= comparison + // trips on the next posedge; then release immediately so the always + // block can drive normally on the same edge. + @(negedge clk); + force dut.beam_watchdog = 23'd8_000_000; + @(posedge clk); #1; + release dut.beam_watchdog; + check("T5b: watchdog sticky latched after timeout", + watchdog_fired == 1'b1); + check("T5c: FSM left S_BEAM_WAIT after watchdog", + dut.state != 3'd5); + + // ==================================================================== + // T6: Sticky survives mixers_enable=0 cycle (only reset_n clears it). + // ==================================================================== + $display("--- T6: sticky watchdog is reset-only ---"); + mixers_enable = 1'b0; + repeat (20) @(posedge clk); + check("T6a: watchdog sticky stays high across mixers_enable=0", + watchdog_fired == 1'b1); + mixers_enable = 1'b1; + + // ==================================================================== + // T7: Mid-wait disable releases the FSM (handshake_enable→0). + // ==================================================================== + $display("--- T7: mid-wait host_handshake_enable=0 releases FSM ---"); + wait_for_beam_wait(5000); + check("T7a: FSM in S_BEAM_WAIT for disable test", dut.state == 3'd5); + @(posedge clk); handshake_enable = 1'b0; + wait_for_beam_wait_exit(20); + check("T7b: FSM left S_BEAM_WAIT after handshake disable", + dut.state != 3'd5); + + // ==================================================================== + // T8: Full reset clears sticky. + // ==================================================================== + $display("--- T8: reset_n clears watchdog sticky ---"); + reset_n = 1'b0; + repeat (4) @(posedge clk); + check("T8: watchdog sticky cleared by reset_n", watchdog_fired == 1'b0); + reset_n = 1'b1; + repeat (4) @(posedge clk); + + $display("============================================================"); + $display("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; // 1 ms wall-clock safety + $display("[FATAL] timeout"); + $finish; +end + +endmodule diff --git a/9_Firmware/9_2_FPGA/tb/tb_e2e_dsp_to_host.v b/9_Firmware/9_2_FPGA/tb/tb_e2e_dsp_to_host.v index ad693ac..44d8ee6 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_e2e_dsp_to_host.v +++ b/9_Firmware/9_2_FPGA/tb/tb_e2e_dsp_to_host.v @@ -361,6 +361,7 @@ module tb_e2e_dsp_to_host; .status_agc_enable (1'b0), .status_range_decim_watchdog(1'b0), .status_ddc_cic_fir_overrun (1'b0), + .status_beam_handshake_watchdog(1'b0), // commit 5 — tied off, e2e path doesn't model handshake .status_cfar_alpha_soft (TEST_CFAR_ALPHA_SOFT), .status_detect_threshold_soft(17'd0), .status_detect_count_cand (16'd0) diff --git a/9_Firmware/9_2_FPGA/tb/tb_ft2232h_frame_drop.v b/9_Firmware/9_2_FPGA/tb/tb_ft2232h_frame_drop.v index 006da78..8e45ce6 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_ft2232h_frame_drop.v +++ b/9_Firmware/9_2_FPGA/tb/tb_ft2232h_frame_drop.v @@ -150,6 +150,8 @@ module tb_ft2232h_frame_drop; // AUDIT-S10: control-fault flags tied off (frame-drop TB scope) .status_range_decim_watchdog(1'b0), .status_ddc_cic_fir_overrun(1'b0), + // PR-AB.b expanded commit 5: beam-handshake watchdog tied off + .status_beam_handshake_watchdog(1'b0), // PR-G: 2-tier CFAR telemetry tied off .status_cfar_alpha_soft(8'h18), // RP_DEF_CFAR_ALPHA_SOFT .status_detect_threshold_soft(17'd0), diff --git a/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v b/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v index 4cc45df..e43dbe3 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/tb/tb_radar_receiver_final.v @@ -185,7 +185,14 @@ radar_receiver_final dut ( .doppler_frame_done_out(), // PR-E: pin mixers_enable HIGH so the scheduler runs in this TB - .mixers_enable_100m(1'b1) + .mixers_enable_100m(1'b1), + + // PR-AB.b expanded commit 5: beam-ready handshake — tie off for this TB + // (legacy open-loop cadence). Tests for the handshake live in their own + // dedicated tb_chirp_scheduler_handshake.v unit TB. + .stm32_beam_ready_async(1'b0), + .host_handshake_enable(1'b0), + .beam_handshake_watchdog_fired() ); // ============================================================================ diff --git a/9_Firmware/9_2_FPGA/tb/tb_system_mechanics.v b/9_Firmware/9_2_FPGA/tb/tb_system_mechanics.v index eaa3361..5c76876 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_system_mechanics.v +++ b/9_Firmware/9_2_FPGA/tb/tb_system_mechanics.v @@ -16,7 +16,11 @@ // mixer-disable propagation) // G7.3 TX chirp counter CDC (120MHz -> 100MHz) // — G7.1 (STM32→FPGA chirp toggle CDC stress) retired in PR-AB.b -// expanded; stm32_new_chirp port is gone. +// expanded; the stm32_new_chirp port was renamed to +// stm32_beam_ready in commit 5 (beam-ready handshake) and is +// tied 1'b0 here so the cold-reset default of host_handshake_enable +// (=0) keeps the FSM out of S_BEAM_WAIT and the open-loop cadence +// intact. // // DUT is radar_system_top with USB_MODE=1 (production FT2232H path); the // FT2232H ports are wired so a stream_control opcode (0x04) can be sent at @@ -155,6 +159,7 @@ radar_system_top #(.USB_MODE(1)) dut ( .adc_or_p(1'b0), .adc_or_n(1'b1), .adc_pwdn(adc_pwdn), + .stm32_beam_ready(1'b0), // commit 5: handshake disabled by cold-reset default .stm32_mixers_enable(stm32_mixers_enable), .ft601_data(ft601_data), diff --git a/9_Firmware/9_2_FPGA/tb/tb_system_opcodes.v b/9_Firmware/9_2_FPGA/tb/tb_system_opcodes.v index f282095..d686045 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_system_opcodes.v +++ b/9_Firmware/9_2_FPGA/tb/tb_system_opcodes.v @@ -158,6 +158,7 @@ radar_system_top #( .adc_or_p(1'b0), .adc_or_n(1'b1), .adc_pwdn(adc_pwdn), + .stm32_beam_ready(1'b0), // commit 5: handshake gated off by host_handshake_enable cold-reset = 0 .stm32_mixers_enable(stm32_mixers_enable), // FT601 ports — tied off / unused in USB_MODE=1 diff --git a/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v b/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v index 6c3466c..55d8f54 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v @@ -144,7 +144,9 @@ module tb_usb_data_interface; .status_agc_enable (status_agc_enable), // AUDIT-S10: control-fault flags tied off (pre-existing TB scope) .status_range_decim_watchdog(1'b0), - .status_ddc_cic_fir_overrun (1'b0) + .status_ddc_cic_fir_overrun (1'b0), + // PR-AB.b expanded commit 5: beam-handshake watchdog tied off + .status_beam_handshake_watchdog(1'b0) ); // ── Test bookkeeping ─────────────────────────────────────── diff --git a/9_Firmware/9_2_FPGA/tb/tb_usb_protocol_v2.v b/9_Firmware/9_2_FPGA/tb/tb_usb_protocol_v2.v index 6ad652e..a3c376b 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_usb_protocol_v2.v +++ b/9_Firmware/9_2_FPGA/tb/tb_usb_protocol_v2.v @@ -157,6 +157,10 @@ module tb_usb_protocol_v2; .status_agc_enable(status_agc_enable), .status_range_decim_watchdog(status_range_decim_watchdog), .status_ddc_cic_fir_overrun(status_ddc_cic_fir_overrun), + // PR-AB.b expanded commit 5: beam-handshake watchdog tied off here; + // exercised by tb_chirp_scheduler_handshake.v and word-4 layout test + // refreshed below. + .status_beam_handshake_watchdog(1'b0), .status_cfar_alpha_soft(status_cfar_alpha_soft), .status_detect_threshold_soft(status_detect_threshold_soft), .status_detect_count_cand(status_detect_count_cand) diff --git a/9_Firmware/9_2_FPGA/usb_data_interface.v b/9_Firmware/9_2_FPGA/usb_data_interface.v index 7ff28e2..b1e43c4 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface.v @@ -113,7 +113,12 @@ module usb_data_interface ( // sticky/slow-changing in the source domain (set on event, cleared by // monitor reset), so 2-stage level CDC into ft601_clk_in is sufficient. input wire status_range_decim_watchdog, // audit F-6.4 - input wire status_ddc_cic_fir_overrun // audit F-1.2 + input wire status_ddc_cic_fir_overrun, // audit F-1.2 + + // PR-AB.b expanded commit 5: beam-ready handshake watchdog (clk domain). + // Sticky in chirp_scheduler; same 2-FF level CDC convention as the F-6.4 + // and F-1.2 stickies above. Packed into status_words[4][1] downstream. + input wire status_beam_handshake_watchdog ); // USB packet structure (same as before) @@ -339,6 +344,9 @@ reg status_req_sync_prev; reg range_decim_watchdog_sync_1; (* ASYNC_REG = "TRUE" *) reg ddc_cic_fir_overrun_sync_0; reg ddc_cic_fir_overrun_sync_1; +// PR-AB.b expanded commit 5: 2-FF level CDC for beam-handshake watchdog sticky. +(* ASYNC_REG = "TRUE" *) reg beam_handshake_wd_sync_0; +reg beam_handshake_wd_sync_1; wire status_req_ft601 = status_req_sync[1] ^ status_req_sync_prev; // Status snapshot: captured in ft601_clk domain when status request arrives. @@ -376,6 +384,8 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin range_decim_watchdog_sync_1 <= 1'b0; ddc_cic_fir_overrun_sync_0 <= 1'b0; ddc_cic_fir_overrun_sync_1 <= 1'b0; + beam_handshake_wd_sync_0 <= 1'b0; + beam_handshake_wd_sync_1 <= 1'b0; end else begin // Synchronize valid strobes (2-stage sync chain) range_valid_sync <= {range_valid_sync[0], range_valid}; @@ -395,6 +405,8 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin range_decim_watchdog_sync_1 <= range_decim_watchdog_sync_0; ddc_cic_fir_overrun_sync_0 <= status_ddc_cic_fir_overrun; ddc_cic_fir_overrun_sync_1 <= ddc_cic_fir_overrun_sync_0; + beam_handshake_wd_sync_0 <= status_beam_handshake_watchdog; + beam_handshake_wd_sync_1 <= beam_handshake_wd_sync_0; // Gap 2: Capture status snapshot when request arrives in ft601 domain if (status_req_ft601) begin @@ -409,14 +421,23 @@ always @(posedge ft601_clk_in or negedge ft601_effective_reset_n) begin status_words[2] <= {status_guard, status_short_chirp}; // Word 3: {short_listen_cycles[15:0], chirps_per_elev[5:0], 10'b0} status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev}; - // Word 4: AGC metrics (range_mode retired in PR-AB.b expanded) - status_words[4] <= {status_agc_current_gain, // [31:28] - status_agc_peak_magnitude, // [27:20] - status_agc_saturation_count, // [19:12] 8-bit saturation count - status_agc_enable, // [11] - status_chirps_mismatch, // [10] TX-G mismatch flag - 8'd0, // [9:2] reserved - 2'd0}; // [1:0] reserved (was range_mode) + // Word 4 layout (post PR-AB.b expanded commit 5): + // [31:28] agc_current_gain + // [27:20] agc_peak_magnitude + // [19:12] agc_saturation_count + // [11] agc_enable + // [10] chirps_mismatch (TX-G) + // [9:2] reserved 0 (alpha_soft echo lives in ft2232h-only path) + // [1] beam_handshake_watchdog_fired (sticky, reset_n clear) + // [0] reserved 0 (range_mode bit retired PR-AB.b expanded) + status_words[4] <= {status_agc_current_gain, + status_agc_peak_magnitude, + status_agc_saturation_count, + status_agc_enable, + status_chirps_mismatch, + 8'd0, + beam_handshake_wd_sync_1, + 1'd0}; // Word 5: {reserved[6:0], self_test_busy[24], reserved[23:16], // self_test_detail[15:8], reserved[7], cic_fir_overrun[6], // range_decim_watchdog[5], self_test_flags[4:0]} diff --git a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v index 45f4e44..dbb4539 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v @@ -173,6 +173,11 @@ module usb_data_interface_ft2232h ( input wire status_range_decim_watchdog, // audit F-6.4 input wire status_ddc_cic_fir_overrun, // audit F-1.2 + // PR-AB.b expanded commit 5: beam-ready handshake watchdog (clk domain). + // Sticky in chirp_scheduler; 2-FF level CDC into ft_clk. Packed into + // status_words[4][1] — see word-4 layout below. + input wire status_beam_handshake_watchdog, + // PR-G: 2-tier CFAR telemetry (clk domain → status_words[6]). // Slow-changing per-frame values; 2-stage level CDC into ft_clk. input wire [7:0] status_cfar_alpha_soft, // current host_cfar_alpha_soft (Q4.4) @@ -674,6 +679,10 @@ reg [6:0] frame_drop_sync_1; reg range_decim_watchdog_sync_1; (* ASYNC_REG = "TRUE" *) reg ddc_cic_fir_overrun_sync_0; reg ddc_cic_fir_overrun_sync_1; +// PR-AB.b expanded commit 5: 2-FF level CDC for the beam-handshake watchdog +// sticky. Same convention as the F-6.4 / F-1.2 stickies above. +(* ASYNC_REG = "TRUE" *) reg beam_handshake_wd_sync_0; +reg beam_handshake_wd_sync_1; wire stream_range_en = stream_ctrl_sync_1[0]; wire stream_doppler_en = stream_ctrl_sync_1[1]; @@ -803,6 +812,8 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin range_decim_watchdog_sync_1 <= 1'b0; ddc_cic_fir_overrun_sync_0 <= 1'b0; ddc_cic_fir_overrun_sync_1 <= 1'b0; + beam_handshake_wd_sync_0 <= 1'b0; + beam_handshake_wd_sync_1 <= 1'b0; // PR-G: 2-tier CFAR telemetry CDC reset alpha_soft_sync_0 <= 8'd0; alpha_soft_sync_1 <= 8'd0; @@ -861,6 +872,8 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin range_decim_watchdog_sync_1 <= range_decim_watchdog_sync_0; ddc_cic_fir_overrun_sync_0 <= status_ddc_cic_fir_overrun; ddc_cic_fir_overrun_sync_1 <= ddc_cic_fir_overrun_sync_0; + beam_handshake_wd_sync_0 <= status_beam_handshake_watchdog; + beam_handshake_wd_sync_1 <= beam_handshake_wd_sync_0; // PR-G: 2-tier CFAR telemetry CDC (clk → ft_clk for status read) alpha_soft_sync_0 <= status_cfar_alpha_soft; @@ -879,13 +892,23 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin status_words[1] <= {status_long_chirp, status_long_listen}; status_words[2] <= {status_guard, status_short_chirp}; status_words[3] <= {status_short_listen, 10'd0, status_chirps_per_elev}; - status_words[4] <= {status_agc_current_gain, // [31:28] - status_agc_peak_magnitude, // [27:20] - status_agc_saturation_count, // [19:12] - status_agc_enable, // [11] - status_chirps_mismatch, // [10] TX-G mismatch flag - alpha_soft_sync_1, // [9:2] PR-G: host_cfar_alpha_soft echo (Q4.4) - 2'd0}; // [1:0] reserved (was range_mode, retired PR-AB.b expanded) + // Word 4 layout (post PR-AB.b expanded commit 5): + // [31:28] agc_current_gain + // [27:20] agc_peak_magnitude + // [19:12] agc_saturation_count + // [11] agc_enable + // [10] chirps_mismatch (TX-G) + // [9:2] alpha_soft echo (Q4.4) + // [1] beam_handshake_watchdog_fired (sticky, reset_n clear) + // [0] reserved 0 (range_mode bit retired PR-AB.b expanded) + status_words[4] <= {status_agc_current_gain, + status_agc_peak_magnitude, + status_agc_saturation_count, + status_agc_enable, + status_chirps_mismatch, + alpha_soft_sync_1, + beam_handshake_wd_sync_1, + 1'd0}; // Word 5: {frame_drop_count[31:25], self_test_busy[24], // reserved[23:16], self_test_detail[15:8], reserved[7], // cic_fir_overrun[6], range_decim_watchdog[5], diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index 52f7de6..f3e1731 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -136,6 +136,7 @@ class Opcode(IntEnum): 0x17 host_medium_chirp_cycles (PR-G G2) 0x18 host_medium_listen_cycles (PR-G G2) 0x19 host_subframe_enable (PR-U / M-8 — 3-bit {LONG, MED, SHORT} mask) + 0x1A host_handshake_enable (PR-AB.b expanded commit 5 — beam-ready stall) PR-AB.b expanded retired opcodes 0x01 (host_radar_mode), 0x02 (host_trigger_pulse), 0x20 (host_range_mode). @@ -165,6 +166,12 @@ class Opcode(IntEnum): # host CRT downgrades confidence to UNKNOWN (dbin // 16 attribution would # otherwise be wrong when the scheduler skips a sub-frame). SUBFRAME_ENABLE = 0x19 + # PR-AB.b expanded commit 5: beam-ready handshake enable. value[0]=1 makes + # chirp_scheduler stall in S_BEAM_WAIT after frame_pulse until the MCU + # toggles PD8, or the ~80 ms watchdog expires (status word 4 bit [1] is + # the sticky watchdog flag). FPGA cold-reset = 0 — host opts in once the + # MCU PD8 wiring is verified on the bench. + HANDSHAKE_ENABLE = 0x1A # --- Signal processing (0x21-0x27; # 0x20 host_range_mode retired in PR-AB.b expanded) --- @@ -260,14 +267,20 @@ class StatusResponse: agc_saturation_count: int = 0 # 8-bit saturation count [7:0] agc_enable: int = 0 # 1-bit AGC enable readback chirps_mismatch: int = 0 # TX-G: 1 if FPGA clamped/rejected host chirps_per_elev + # PR-AB.b expanded commit 5: sticky watchdog from chirp_scheduler S_BEAM_WAIT. + # 1 means at least one frame elapsed without an MCU PD8 ack within ~80 ms; + # cleared only by full reset_n on the FPGA. Reserved 0 when host has not + # enabled the handshake (opcode 0x1A=1). + beam_handshake_watchdog: int = 0 # word 4 bit [1] # PR-G 2-tier CFAR telemetry (word 6) detect_count_cand: int = 0 # 16-bit count of CAND-tier detections per frame detect_threshold_soft: int = 0 # 16-bit soft-CFAR threshold readback (saturates 0xFFFF) # AUDIT-S10 control-fault flags (word 5 high half) frame_drop_count: int = 0 # frame-drop counter from RTL # M-5 MEDIUM PRI readback (word 7) — closes 161-µs MEDIUM visibility gap. - medium_chirp: int = 0 # opcode 0x17 readback (16-bit, default RP_DEF_MEDIUM_CHIRP_CYCLES) - medium_listen: int = 0 # opcode 0x18 readback (16-bit, default RP_DEF_MEDIUM_LISTEN_CYCLES) + # opcode 0x17/0x18 readback (16-bit each, default RP_DEF_MEDIUM_*_CYCLES). + medium_chirp: int = 0 + medium_listen: int = 0 # ============================================================================ @@ -373,9 +386,12 @@ class RadarProtocol: # Word 3: {short_listen[31:16], 10'd0, chirps_per_elev[5:0]} sr.chirps_per_elev = words[3] & 0x3F sr.short_listen = (words[3] >> 16) & 0xFFFF - # Word 4 layout: gain[31:28] peak[27:20] sat[19:12] agc_en[11] mismatch[10] reserved[1:0] - # PR-AB.b expanded: bits [1:0] formerly range_mode, now reserved 0. + # Word 4 layout: gain[31:28] peak[27:20] sat[19:12] agc_en[11] + # mismatch[10] alpha_soft[9:2] beam_handshake_watchdog[1] reserved[0] + # PR-AB.b expanded commit 5 reclaimed bit [1] for the handshake watchdog + # sticky (FPGA chirp_scheduler S_BEAM_WAIT, ~80 ms timeout). sr.chirps_mismatch = (words[4] >> 10) & 0x01 + sr.beam_handshake_watchdog = (words[4] >> 1) & 0x01 sr.agc_enable = (words[4] >> 11) & 0x01 sr.agc_saturation_count = (words[4] >> 12) & 0xFF sr.agc_peak_magnitude = (words[4] >> 20) & 0xFF diff --git a/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py b/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py index 4838969..8cea6b5 100644 --- a/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py +++ b/9_Firmware/9_3_GUI/test_GUI_V65_Tk.py @@ -133,6 +133,7 @@ class TestRadarProtocol(unittest.TestCase): st_flags=0, st_detail=0, st_busy=0, agc_gain=0, agc_peak=0, agc_sat=0, agc_enable=0, chirps_mismatch=0, + beam_handshake_watchdog=0, cand_count=0, thr_soft=0, frame_drop=0, medium_chirp=0, medium_listen=0): """Build an M-5 34-byte status response matching FPGA format.""" @@ -156,13 +157,18 @@ class TestRadarProtocol(unittest.TestCase): w3 = ((short_listen & 0xFFFF) << 16) | (chirps & 0x3F) pkt += struct.pack(">I", w3) - # Word 4: {agc_current_gain[3:0], agc_peak_magnitude[7:0], - # agc_saturation_count[7:0], agc_enable, - # chirps_mismatch[10], 10'd0 reserved [9:0]} - # PR-AB.b expanded: bits [1:0] formerly range_mode, now reserved 0. + # Word 4: {agc_current_gain[31:28], agc_peak_magnitude[27:20], + # agc_saturation_count[19:12], agc_enable[11], + # chirps_mismatch[10], reserved[9:2], + # beam_handshake_watchdog[1], reserved[0]} + # PR-AB.b expanded commit 5: bit [1] is the chirp_scheduler + # S_BEAM_WAIT sticky watchdog flag (reset_n clear). The 8-bit + # alpha_soft echo at [9:2] is FT2232H-only; this co-spec + # builder leaves [9:2] reserved-0 like the FT601 path. w4 = (((agc_gain & 0x0F) << 28) | ((agc_peak & 0xFF) << 20) | ((agc_sat & 0xFF) << 12) | ((agc_enable & 0x01) << 11) | - ((chirps_mismatch & 0x01) << 10)) + ((chirps_mismatch & 0x01) << 10) | + ((beam_handshake_watchdog & 0x01) << 1)) pkt += struct.pack(">I", w4) # Word 5: {frame_drop[31:25], self_test_busy[24], 8'd0, @@ -196,6 +202,7 @@ class TestRadarProtocol(unittest.TestCase): self.assertEqual(sr.short_listen, 17450) self.assertEqual(sr.chirps_per_elev, 32) self.assertEqual(sr.chirps_mismatch, 0) + self.assertEqual(sr.beam_handshake_watchdog, 0) def test_parse_status_chirps_mismatch(self): # TX-G: bit 10 of word 4 must round-trip without disturbing neighbours. @@ -203,6 +210,16 @@ class TestRadarProtocol(unittest.TestCase): sr = RadarProtocol.parse_status_packet(raw) self.assertEqual(sr.chirps_mismatch, 1) self.assertEqual(sr.agc_enable, 1) + self.assertEqual(sr.beam_handshake_watchdog, 0) + + def test_parse_status_beam_handshake_watchdog(self): + # PR-AB.b expanded commit 5: bit 1 of word 4 must round-trip without + # bleeding into chirps_mismatch (bit 10) or the reserved [0] bit. + raw = self._make_status_packet(beam_handshake_watchdog=1) + sr = RadarProtocol.parse_status_packet(raw) + self.assertEqual(sr.beam_handshake_watchdog, 1) + self.assertEqual(sr.chirps_mismatch, 0) + self.assertEqual(sr.agc_enable, 0) def test_parse_status_too_short(self): # Anything under STATUS_PACKET_SIZE (34 post-M-5) must be rejected. @@ -262,14 +279,20 @@ class TestRadarProtocol(unittest.TestCase): usb_data_interface_ft2232h.v:675-679 — exactly one source of truth in this test, so any future drift between FPGA and GUI trips here: - [31:28] agc_current_gain (4-bit) - [27:20] agc_peak_magnitude (8-bit) - [19:12] agc_saturation_count (8-bit) - [11] agc_enable (1-bit) - [10] chirps_mismatch (1-bit, TX-G) - [9:0] reserved (10 bits, must be zero from builder) - (was [9:2] + range_mode[1:0]; range_mode retired in - PR-AB.b expanded) + [31:28] agc_current_gain (4-bit) + [27:20] agc_peak_magnitude (8-bit) + [19:12] agc_saturation_count (8-bit) + [11] agc_enable (1-bit) + [10] chirps_mismatch (1-bit, TX-G) + [9:2] reserved / alpha_soft (8 bits — FT601 leaves zero; FT2232H + echoes host_cfar_alpha_soft. This + co-spec builder uses the FT601 + layout so the builder remains + path-agnostic.) + [1] beam_handshake_watchdog (1-bit sticky, PR-AB.b expanded + commit 5) + [0] reserved (1-bit; was range_mode[0], retired + in PR-AB.b expanded) For each field we set ONLY that field to its max, build the packet, parse, and assert (a) the field reads back correctly and (b) every @@ -278,16 +301,19 @@ class TestRadarProtocol(unittest.TestCase): """ layout = [ # (field_name, builder_kwarg, lsb, width, parsed_attr) - ("agc_current_gain", "agc_gain", 28, 4, "agc_current_gain"), - ("agc_peak_magnitude", "agc_peak", 20, 8, "agc_peak_magnitude"), - ("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"), - ("agc_enable", "agc_enable", 11, 1, "agc_enable"), - ("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"), + ("agc_current_gain", "agc_gain", 28, 4, "agc_current_gain"), + ("agc_peak_magnitude", "agc_peak", 20, 8, "agc_peak_magnitude"), + ("agc_saturation_count", "agc_sat", 12, 8, "agc_saturation_count"), + ("agc_enable", "agc_enable", 11, 1, "agc_enable"), + ("chirps_mismatch", "chirps_mismatch", 10, 1, "chirps_mismatch"), + ("beam_handshake_watchdog", "beam_handshake_watchdog", + 1, 1, "beam_handshake_watchdog"), ] - # Sanity: layout fields + reserved [9:0] must cover exactly 32 bits. + # Sanity: layout fields + reserved [9:2] (8 bits) + reserved [0] (1 bit) + # must cover exactly 32 bits. used = sum(width for _, _, _, width, _ in layout) - self.assertEqual(used + 10, 32, - "word 4 layout (incl. reserved [9:0]) must total 32 bits") + self.assertEqual(used + 9, 32, + "word 4 layout (incl. reserved [9:2] + [0]) must total 32 bits") # No two fields may overlap. occupied = set() @@ -1002,6 +1028,10 @@ class TestOpcodeEnum(unittest.TestCase): self.assertEqual(Opcode.DETECT_THRESHOLD, 0x03) self.assertEqual(Opcode.STREAM_CONTROL, 0x04) + def test_handshake_enable_opcode(self): + """PR-AB.b expanded commit 5: beam-ready handshake opcode = 0x1A.""" + self.assertEqual(Opcode.HANDSHAKE_ENABLE, 0x1A) + def test_all_rtl_opcodes_present(self): """Every RTL opcode (from radar_system_top.v) has a matching Opcode enum member. @@ -1010,6 +1040,7 @@ class TestOpcodeEnum(unittest.TestCase): """ expected = {0x03, 0x04, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, + 0x17, 0x18, 0x19, 0x1A, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x30, 0x31, 0xFF} diff --git a/9_Firmware/9_3_GUI/v7/dashboard.py b/9_Firmware/9_3_GUI/v7/dashboard.py index 05acf33..87f11fc 100644 --- a/9_Firmware/9_3_GUI/v7/dashboard.py +++ b/9_Firmware/9_3_GUI/v7/dashboard.py @@ -994,7 +994,7 @@ class RadarDashboard(QMainWindow): spin.setValue(default) # 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 + # longer than its peers (the AGC Enable hint is ~3x longer than the # others and was rendering at ~90 px while siblings hit ~160). spin.setFixedWidth(120) row.addWidget(spin) @@ -2119,7 +2119,7 @@ class RadarDashboard(QMainWindow): # 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"): + 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