diff --git a/9_Firmware/9_2_FPGA/formal/fv_radar_mode_controller.v b/9_Firmware/9_2_FPGA/formal/fv_radar_mode_controller.v index d6a7d25..548cf4c 100644 --- a/9_Firmware/9_2_FPGA/formal/fv_radar_mode_controller.v +++ b/9_Firmware/9_2_FPGA/formal/fv_radar_mode_controller.v @@ -66,6 +66,14 @@ module fv_radar_mode_controller ( (* anyseq *) wire stm32_new_azimuth; (* anyseq *) wire trigger; + // Gap 2: Formal cfg_* inputs — solver-driven for exhaustive coverage + (* anyseq *) wire [15:0] cfg_long_chirp_cycles; + (* anyseq *) wire [15:0] cfg_long_listen_cycles; + (* anyseq *) wire [15:0] cfg_guard_cycles; + (* anyseq *) wire [15:0] cfg_short_chirp_cycles; + (* anyseq *) wire [15:0] cfg_short_listen_cycles; + (* anyseq *) wire [5:0] cfg_chirps_per_elev; + // ================================================================ // DUT outputs // ================================================================ @@ -101,6 +109,13 @@ module fv_radar_mode_controller ( .stm32_new_elevation(stm32_new_elevation), .stm32_new_azimuth (stm32_new_azimuth), .trigger (trigger), + // Gap 2: Runtime-configurable timing inputs + .cfg_long_chirp_cycles (cfg_long_chirp_cycles), + .cfg_long_listen_cycles (cfg_long_listen_cycles), + .cfg_guard_cycles (cfg_guard_cycles), + .cfg_short_chirp_cycles (cfg_short_chirp_cycles), + .cfg_short_listen_cycles(cfg_short_listen_cycles), + .cfg_chirps_per_elev (cfg_chirps_per_elev), .use_long_chirp (use_long_chirp), .mc_new_chirp (mc_new_chirp), .mc_new_elevation (mc_new_elevation), diff --git a/9_Firmware/9_2_FPGA/radar_mode_controller.v b/9_Firmware/9_2_FPGA/radar_mode_controller.v index 63bd9d9..ab1f760 100644 --- a/9_Firmware/9_2_FPGA/radar_mode_controller.v +++ b/9_Firmware/9_2_FPGA/radar_mode_controller.v @@ -61,6 +61,17 @@ module radar_mode_controller #( // Single-chirp trigger (active in mode 10) input wire trigger, + // Gap 2: Runtime-configurable timing inputs from host USB commands. + // When connected, these override the compile-time parameters. + // When left at default (tied to parameter values at instantiation), + // behavior is identical to pre-Gap-2. + input wire [15:0] cfg_long_chirp_cycles, + input wire [15:0] cfg_long_listen_cycles, + input wire [15:0] cfg_guard_cycles, + input wire [15:0] cfg_short_chirp_cycles, + input wire [15:0] cfg_short_listen_cycles, + input wire [5:0] cfg_chirps_per_elev, + // Outputs to receiver processing chain output reg use_long_chirp, output reg mc_new_chirp, @@ -173,8 +184,8 @@ always @(posedge clk or negedge reset_n) begin mc_new_chirp <= ~mc_new_chirp; // Toggle output use_long_chirp <= 1'b1; // Default to long chirp - // Track chirp count - if (chirp_count < CHIRPS_PER_ELEVATION - 1) + // Track chirp count (Gap 2: use runtime cfg_chirps_per_elev) + if (chirp_count < cfg_chirps_per_elev - 1) chirp_count <= chirp_count + 1; else chirp_count <= 6'd0; @@ -226,7 +237,7 @@ always @(posedge clk or negedge reset_n) begin S_LONG_CHIRP: begin use_long_chirp <= 1'b1; - if (timer < LONG_CHIRP_CYCLES - 1) + if (timer < cfg_long_chirp_cycles - 1) timer <= timer + 1; else begin timer <= 18'd0; @@ -235,7 +246,7 @@ always @(posedge clk or negedge reset_n) begin end S_LONG_LISTEN: begin - if (timer < LONG_LISTEN_CYCLES - 1) + if (timer < cfg_long_listen_cycles - 1) timer <= timer + 1; else begin timer <= 18'd0; @@ -244,7 +255,7 @@ always @(posedge clk or negedge reset_n) begin end S_GUARD: begin - if (timer < GUARD_CYCLES - 1) + if (timer < cfg_guard_cycles - 1) timer <= timer + 1; else begin timer <= 18'd0; @@ -255,7 +266,7 @@ always @(posedge clk or negedge reset_n) begin S_SHORT_CHIRP: begin use_long_chirp <= 1'b0; - if (timer < SHORT_CHIRP_CYCLES - 1) + if (timer < cfg_short_chirp_cycles - 1) timer <= timer + 1; else begin timer <= 18'd0; @@ -264,7 +275,7 @@ always @(posedge clk or negedge reset_n) begin end S_SHORT_LISTEN: begin - if (timer < SHORT_LISTEN_CYCLES - 1) + if (timer < cfg_short_listen_cycles - 1) timer <= timer + 1; else begin timer <= 18'd0; @@ -274,7 +285,8 @@ always @(posedge clk or negedge reset_n) begin S_ADVANCE: begin // Advance chirp/elevation/azimuth counters - if (chirp_count < CHIRPS_PER_ELEVATION - 1) begin + // (Gap 2: use runtime cfg_chirps_per_elev) + if (chirp_count < cfg_chirps_per_elev - 1) begin // Next chirp in current elevation chirp_count <= chirp_count + 1; mc_new_chirp <= ~mc_new_chirp; @@ -339,7 +351,7 @@ always @(posedge clk or negedge reset_n) begin end S_LONG_CHIRP: begin - if (timer < LONG_CHIRP_CYCLES - 1) + if (timer < cfg_long_chirp_cycles - 1) timer <= timer + 1; else begin timer <= 18'd0; @@ -348,7 +360,7 @@ always @(posedge clk or negedge reset_n) begin end S_LONG_LISTEN: begin - if (timer < LONG_LISTEN_CYCLES - 1) + if (timer < cfg_long_listen_cycles - 1) timer <= timer + 1; else begin // Single chirp done, return to idle diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index c0c988b..cd34a44 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -24,10 +24,18 @@ module radar_receiver_final ( output wire signed [15:0] range_profile_q_out, output wire range_profile_valid_out, - // Host command inputs (Gap 4: USB Read Path) + // Host command inputs (Gap 4: USB Read Path, CDC-synchronized) // CDC-synchronized in radar_system_top.v before reaching here input wire [1:0] host_mode, // Radar mode: 00=STM32, 01=auto-scan, 10=single-chirp - input wire host_trigger // Single-chirp trigger pulse (1 clk cycle) + input wire host_trigger, // Single-chirp trigger pulse (1 clk cycle) + + // Gap 2: Host-configurable chirp timing (CDC-synchronized in radar_system_top.v) + input wire [15:0] host_long_chirp_cycles, + input wire [15:0] host_long_listen_cycles, + input wire [15:0] host_guard_cycles, + input wire [15:0] host_short_chirp_cycles, + input wire [15:0] host_short_listen_cycles, + input wire [5:0] host_chirps_per_elev ); // ========== INTERNAL SIGNALS ========== @@ -91,6 +99,13 @@ radar_mode_controller rmc ( .stm32_new_elevation(1'b0), // Unused in auto mode .stm32_new_azimuth(1'b0), // Unused in auto mode .trigger(host_trigger), // Single-chirp trigger from host via USB + // Gap 2: Runtime-configurable timing from host USB commands + .cfg_long_chirp_cycles(host_long_chirp_cycles), + .cfg_long_listen_cycles(host_long_listen_cycles), + .cfg_guard_cycles(host_guard_cycles), + .cfg_short_chirp_cycles(host_short_chirp_cycles), + .cfg_short_listen_cycles(host_short_listen_cycles), + .cfg_chirps_per_elev(host_chirps_per_elev), .use_long_chirp(use_long_chirp), .mc_new_chirp(mc_new_chirp), .mc_new_elevation(mc_new_elevation), diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index 7fb13a0..0fe5522 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -191,6 +191,18 @@ reg host_trigger_pulse; reg [15:0] host_cfar_threshold; reg [2:0] host_stream_control; +// Gap 2: Host-configurable chirp timing registers +// These override the compile-time defaults in radar_mode_controller when +// written via USB command. Defaults match the parameter values in +// radar_mode_controller.v so behavior is unchanged until the host writes them. +reg [15:0] host_long_chirp_cycles; // Opcode 0x10 (default 3000) +reg [15:0] host_long_listen_cycles; // Opcode 0x11 (default 13700) +reg [15:0] host_guard_cycles; // Opcode 0x12 (default 17540) +reg [15:0] host_short_chirp_cycles; // Opcode 0x13 (default 50) +reg [15:0] host_short_listen_cycles; // Opcode 0x14 (default 17450) +reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 32) +reg host_status_request; // Opcode 0xFF (self-clearing pulse) + // ============================================================================ // CLOCK BUFFERING // ============================================================================ @@ -426,7 +438,14 @@ radar_receiver_final rx_inst ( // Host command inputs (Gap 4: USB Read Path, CDC-synchronized) .host_mode(host_radar_mode), - .host_trigger(host_trigger_pulse) + .host_trigger(host_trigger_pulse), + // Gap 2: Host-configurable chirp timing + .host_long_chirp_cycles(host_long_chirp_cycles), + .host_long_listen_cycles(host_long_listen_cycles), + .host_guard_cycles(host_guard_cycles), + .host_short_chirp_cycles(host_short_chirp_cycles), + .host_short_listen_cycles(host_short_listen_cycles), + .host_chirps_per_elev(host_chirps_per_elev) ); // ============================================================================ @@ -458,8 +477,8 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin cfar_mag <= (rx_doppler_real[15] ? -rx_doppler_real : rx_doppler_real) + (rx_doppler_imag[15] ? -rx_doppler_imag : rx_doppler_imag); - // Threshold detection - if (cfar_mag > 17'd10000) begin + // Threshold detection (Gap 2: uses host-configurable threshold) + if (cfar_mag > {1'b0, host_cfar_threshold}) begin rx_cfar_detection <= 1'b1; rx_cfar_valid <= 1'b1; cfar_counter <= cfar_counter + 1; @@ -522,7 +541,22 @@ usb_data_interface usb_inst ( .cmd_valid(usb_cmd_valid), .cmd_opcode(usb_cmd_opcode), .cmd_addr(usb_cmd_addr), - .cmd_value(usb_cmd_value) + .cmd_value(usb_cmd_value), + + // Gap 2: Stream control (clk_100m domain, CDC'd inside usb_data_interface) + .stream_control(host_stream_control), + + // Gap 2: Status readback inputs + .status_request(host_status_request), + .status_cfar_threshold(host_cfar_threshold), + .status_stream_ctrl(host_stream_control), + .status_radar_mode(host_radar_mode), + .status_long_chirp(host_long_chirp_cycles), + .status_long_listen(host_long_listen_cycles), + .status_guard(host_guard_cycles), + .status_short_chirp(host_short_chirp_cycles), + .status_short_listen(host_short_listen_cycles), + .status_chirps_per_elev(host_chirps_per_elev) ); // ============================================================================ @@ -577,15 +611,32 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin host_trigger_pulse <= 1'b0; host_cfar_threshold <= 16'd10000; // Default threshold host_stream_control <= 3'b111; // Default: all streams enabled + // Gap 2: chirp timing defaults (match radar_mode_controller parameters) + host_long_chirp_cycles <= 16'd3000; + host_long_listen_cycles <= 16'd13700; + host_guard_cycles <= 16'd17540; + host_short_chirp_cycles <= 16'd50; + host_short_listen_cycles <= 16'd17450; + host_chirps_per_elev <= 6'd32; + host_status_request <= 1'b0; end else begin host_trigger_pulse <= 1'b0; // Self-clearing pulse + host_status_request <= 1'b0; // Self-clearing pulse if (cmd_valid_100m) begin case (usb_cmd_opcode) 8'h01: host_radar_mode <= usb_cmd_value[1:0]; 8'h02: host_trigger_pulse <= 1'b1; 8'h03: host_cfar_threshold <= usb_cmd_value; 8'h04: host_stream_control <= usb_cmd_value[2:0]; - default: ; // 0xFF status request handled elsewhere + // Gap 2: chirp timing configuration + 8'h10: host_long_chirp_cycles <= usb_cmd_value; + 8'h11: host_long_listen_cycles <= usb_cmd_value; + 8'h12: host_guard_cycles <= usb_cmd_value; + 8'h13: host_short_chirp_cycles <= usb_cmd_value; + 8'h14: host_short_listen_cycles <= usb_cmd_value; + 8'h15: host_chirps_per_elev <= usb_cmd_value[5:0]; + 8'hFF: host_status_request <= 1'b1; // Gap 2: status readback + default: ; endcase end end diff --git a/9_Firmware/9_2_FPGA/tb/tb_radar_mode_controller.v b/9_Firmware/9_2_FPGA/tb/tb_radar_mode_controller.v index 1e1d5e4..03ed4d4 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_radar_mode_controller.v +++ b/9_Firmware/9_2_FPGA/tb/tb_radar_mode_controller.v @@ -26,6 +26,14 @@ module tb_radar_mode_controller; reg stm32_new_azimuth; reg trigger; + // Gap 2: Runtime-configurable timing inputs + reg [15:0] cfg_long_chirp_cycles; + reg [15:0] cfg_long_listen_cycles; + reg [15:0] cfg_guard_cycles; + reg [15:0] cfg_short_chirp_cycles; + reg [15:0] cfg_short_listen_cycles; + reg [5:0] cfg_chirps_per_elev; + wire use_long_chirp; wire mc_new_chirp; wire mc_new_elevation; @@ -78,6 +86,14 @@ module tb_radar_mode_controller; .stm32_new_elevation(stm32_new_elevation), .stm32_new_azimuth (stm32_new_azimuth), .trigger (trigger), + // Gap 2: Runtime-configurable timing inputs + .cfg_long_chirp_cycles (cfg_long_chirp_cycles), + .cfg_long_listen_cycles (cfg_long_listen_cycles), + .cfg_guard_cycles (cfg_guard_cycles), + .cfg_short_chirp_cycles (cfg_short_chirp_cycles), + .cfg_short_listen_cycles(cfg_short_listen_cycles), + .cfg_chirps_per_elev (cfg_chirps_per_elev), + // Outputs .use_long_chirp (use_long_chirp), .mc_new_chirp (mc_new_chirp), .mc_new_elevation (mc_new_elevation), @@ -114,6 +130,13 @@ module tb_radar_mode_controller; stm32_new_elevation = 0; stm32_new_azimuth = 0; trigger = 0; + // Gap 2: Set cfg_* to simulation parameter defaults + cfg_long_chirp_cycles = SIM_LONG_CHIRP; + cfg_long_listen_cycles = SIM_LONG_LISTEN; + cfg_guard_cycles = SIM_GUARD; + cfg_short_chirp_cycles = SIM_SHORT_CHIRP; + cfg_short_listen_cycles = SIM_SHORT_LISTEN; + cfg_chirps_per_elev = SIM_CHIRPS; repeat (4) @(posedge clk); reset_n = 1; @(posedge clk); #1; @@ -629,6 +652,80 @@ module tb_radar_mode_controller; $display(" chirp_count after 4th toggle: %0d (expect 0)", chirp_count); check(chirp_count === 6'd0, "Persistence: chirp_count wraps to 0 at 4th toggle"); + // ════════════════════════════════════════════════════════ + // TEST GROUP 16: Runtime Timing Reconfiguration (Gap 2) + // Verify that changing cfg_* mid-simulation changes timing. + // We halve the long chirp duration and verify the chirp + // completes in fewer cycles. + // ════════════════════════════════════════════════════════ + $display("\n--- Test Group 16: Runtime Timing Reconfiguration (Gap 2) ---"); + apply_reset; + mode = 2'b01; // auto-scan + + // Let the first chirp start (S_IDLE -> S_LONG_CHIRP) + @(posedge clk); #1; + check(scanning === 1'b1, "Reconfig: auto-scan started"); + check(use_long_chirp === 1'b1, "Reconfig: starts with long chirp"); + + // Wait ~half the default long chirp time to confirm we're still in S_LONG_CHIRP + repeat (SIM_LONG_CHIRP / 2) @(posedge clk); #1; + check(uut.scan_state === 3'd1, "Reconfig: still in S_LONG_CHIRP at midpoint"); + + // Now change cfg_long_chirp_cycles to a much shorter value mid-scan. + // The timer is already at ~SIM_LONG_CHIRP/2, so setting cycles to + // (SIM_LONG_CHIRP/2 - 2) means the timer already exceeds the new limit + // and the FSM will advance on the next cycle. + cfg_long_chirp_cycles = SIM_LONG_CHIRP / 2 - 2; + repeat (2) @(posedge clk); #1; + // The FSM should have transitioned past S_LONG_CHIRP + check(uut.scan_state !== 3'd1, "Reconfig: FSM left S_LONG_CHIRP after shortening cycles"); + + // Restore default and verify scan continues + cfg_long_chirp_cycles = SIM_LONG_CHIRP; + repeat (10) @(posedge clk); #1; + check(scanning === 1'b1, "Reconfig: scan continues after restoring default"); + + // Test runtime chirps_per_elev change: + // Reset and set chirps_per_elev to 2 (instead of default 4) + apply_reset; + cfg_chirps_per_elev = 6'd2; + mode = 2'b01; // auto-scan + + mc_new_chirp_prev = 0; + chirp_toggles = 0; + elevation_toggles = 0; + + @(posedge clk); #1; + mc_new_chirp_prev = mc_new_chirp; + mc_new_elevation_prev = mc_new_elevation; + chirp_toggles = 1; // initial toggle + + // Run enough cycles for a few chirps + elevation advance + // With 2 chirps/elev: each chirp ~342 cycles (30+137+175+5+175) + // 2 chirps = ~684 cycles, then elevation advance + for (i = 0; i < 2000; i = i + 1) begin + @(posedge clk); #1; + if (mc_new_chirp !== mc_new_chirp_prev) + chirp_toggles = chirp_toggles + 1; + if (mc_new_elevation !== mc_new_elevation_prev) + elevation_toggles = elevation_toggles + 1; + mc_new_chirp_prev = mc_new_chirp; + mc_new_elevation_prev = mc_new_elevation; + end + + $display(" chirp_toggles=%0d elevation_toggles=%0d (cfg_chirps_per_elev=2)", + chirp_toggles, elevation_toggles); + // With 2 chirps/elev, we should get elevation toggles at every 2 chirps + check(elevation_toggles >= 1, + "Reconfig: elevation advances with cfg_chirps_per_elev=2"); + // Verify the ratio: chirp_toggles should be ~2x elevation_toggles + // (first elevation has 2 chirps, then toggle. Second has 2 chirps, then toggle, etc.) + check(chirp_toggles >= 2 * elevation_toggles, + "Reconfig: chirp/elevation ratio consistent with cfg_chirps_per_elev=2"); + + // Restore defaults + cfg_chirps_per_elev = SIM_CHIRPS; + // ════════════════════════════════════════════════════════ // Summary // ════════════════════════════════════════════════════════ 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 0e44145..2b4c938 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 @@ -132,7 +132,24 @@ radar_receiver_final dut ( .doppler_output(doppler_output), .doppler_valid(doppler_valid), .doppler_bin(doppler_bin), - .range_bin(range_bin_out) + .range_bin(range_bin_out), + + // Range profile outputs (unused in this TB) + .range_profile_i_out(), + .range_profile_q_out(), + .range_profile_valid_out(), + + // Host command inputs (Gap 4) — default auto-scan, no trigger + .host_mode(2'b01), + .host_trigger(1'b0), + + // Gap 2: Host-configurable chirp timing — match defparam overrides below + .host_long_chirp_cycles(16'd500), + .host_long_listen_cycles(16'd2000), + .host_guard_cycles(16'd500), + .host_short_chirp_cycles(16'd50), + .host_short_listen_cycles(16'd1000), + .host_chirps_per_elev(6'd32) ); // ============================================================================ 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 f8738c1..d9ba7a4 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 @@ -13,7 +13,8 @@ module tb_usb_data_interface; S_SEND_DOPPLER = 3'd3, S_SEND_DETECT = 3'd4, S_SEND_FOOTER = 3'd5, - S_WAIT_ACK = 3'd6; + S_WAIT_ACK = 3'd6, + S_SEND_STATUS = 3'd7; // Gap 2: status readback // ── Signals ──────────────────────────────────────────────── reg clk; @@ -59,6 +60,19 @@ module tb_usb_data_interface; wire [7:0] cmd_addr; wire [15:0] cmd_value; + // Gap 2: Stream control + status readback inputs + reg [2:0] stream_control; + reg status_request; + reg [15:0] status_cfar_threshold; + reg [2:0] status_stream_ctrl; + reg [1:0] status_radar_mode; + reg [15:0] status_long_chirp; + reg [15:0] status_long_listen; + reg [15:0] status_guard; + reg [15:0] status_short_chirp; + reg [15:0] status_short_listen; + reg [5:0] status_chirps_per_elev; + // ── Clock generators (asynchronous) ──────────────────────── always #(CLK_PERIOD / 2) clk = ~clk; always #(FT_CLK_PERIOD / 2) ft601_clk_in = ~ft601_clk_in; @@ -95,7 +109,20 @@ module tb_usb_data_interface; .cmd_valid (cmd_valid), .cmd_opcode (cmd_opcode), .cmd_addr (cmd_addr), - .cmd_value (cmd_value) + .cmd_value (cmd_value), + + // Gap 2: Stream control + status readback + .stream_control (stream_control), + .status_request (status_request), + .status_cfar_threshold (status_cfar_threshold), + .status_stream_ctrl (status_stream_ctrl), + .status_radar_mode (status_radar_mode), + .status_long_chirp (status_long_chirp), + .status_long_listen (status_long_listen), + .status_guard (status_guard), + .status_short_chirp (status_short_chirp), + .status_short_listen (status_short_listen), + .status_chirps_per_elev(status_chirps_per_elev) ); // ── Test bookkeeping ─────────────────────────────────────── @@ -139,6 +166,18 @@ module tb_usb_data_interface; ft601_swb = 2'b00; host_data_drive = 32'h0; host_data_drive_en = 0; + // Gap 2: Stream control defaults (all streams enabled) + stream_control = 3'b111; + status_request = 0; + status_cfar_threshold = 16'd10000; + status_stream_ctrl = 3'b111; + status_radar_mode = 2'b00; + status_long_chirp = 16'd3000; + status_long_listen = 16'd13700; + status_guard = 16'd17540; + status_short_chirp = 16'd50; + status_short_listen = 16'd17450; + status_chirps_per_elev = 6'd32; repeat (6) @(posedge ft601_clk_in); reset_n = 1; repeat (2) @(posedge ft601_clk_in); @@ -721,6 +760,193 @@ module tb_usb_data_interface; check(cmd_value === 16'h0002, "Read after write: cmd_value=0x0002"); + // ════════════════════════════════════════════════════════ + // TEST GROUP 15: Stream Control Gating (Gap 2) + // Verify that disabling individual streams causes the write + // FSM to skip those data phases. + // ════════════════════════════════════════════════════════ + $display("\n--- Test Group 15: Stream Control Gating (Gap 2) ---"); + + // 15a: Disable doppler stream (stream_control = 3'b101 = range + cfar only) + apply_reset; + ft601_txe = 0; + stream_control = 3'b101; // range + cfar, no doppler + // Wait for CDC propagation (2-stage sync) + repeat (6) @(posedge ft601_clk_in); + + // Drive range valid — this should trigger the write FSM + assert_range_valid(32'hAA11_BB22); + // FSM: IDLE -> SEND_HEADER -> SEND_RANGE_DATA (doppler disabled) -> SEND_DETECTION_DATA -> SEND_FOOTER + // With ft601_txe=0, SEND_RANGE completes in 4 cycles so we may not catch it. + // Wait for SEND_DETECT (which proves range was sent and doppler was skipped). + wait_for_state(S_SEND_DETECT, 200); + #1; + check(uut.current_state === S_SEND_DETECT, + "Stream gate: reached SEND_DETECT (range sent, doppler skipped)"); + + pulse_cfar_once(1'b1); + wait_for_state(S_IDLE, 100); + #1; + check(uut.current_state === S_IDLE, + "Stream gate: packet completed without doppler"); + + // 15b: Disable all streams (stream_control = 3'b000) + // With no streams enabled, a range_valid pulse should NOT trigger the write FSM. + apply_reset; + ft601_txe = 0; + stream_control = 3'b000; + repeat (6) @(posedge ft601_clk_in); + + // Assert range_valid — FSM should stay in IDLE + @(posedge clk); + range_profile = 32'hDEAD_DEAD; + range_valid = 1; + repeat (3) @(posedge ft601_clk_in); + @(posedge clk); + range_valid = 0; + // Wait a few more cycles for any CDC propagation + repeat (10) @(posedge ft601_clk_in); #1; + + check(uut.current_state === S_IDLE, + "Stream gate: FSM stays IDLE when all streams disabled"); + + // 15c: Restore all streams + stream_control = 3'b111; + repeat (6) @(posedge ft601_clk_in); + + // ════════════════════════════════════════════════════════ + // TEST GROUP 16: Status Readback (Gap 2) + // Verify that pulsing status_request triggers a 7-word + // status response via the SEND_STATUS state. + // ════════════════════════════════════════════════════════ + $display("\n--- Test Group 16: Status Readback (Gap 2) ---"); + apply_reset; + ft601_txe = 0; + + // Set known status input values + status_cfar_threshold = 16'hABCD; + status_stream_ctrl = 3'b101; + status_radar_mode = 2'b01; + status_long_chirp = 16'd3000; + status_long_listen = 16'd13700; + status_guard = 16'd17540; + status_short_chirp = 16'd50; + status_short_listen = 16'd17450; + status_chirps_per_elev = 6'd32; + + // Pulse status_request (1 cycle in clk domain — toggles status_req_toggle_100m) + @(posedge clk); + status_request = 1; + @(posedge clk); + status_request = 0; + + // Wait for toggle CDC propagation to ft601_clk domain + // (2-stage sync + edge detect = ~3-4 ft601_clk cycles) + repeat (8) @(posedge ft601_clk_in); #1; + + // The write FSM should enter SEND_STATUS + // Give it time to start (IDLE sees status_req_ft601) + wait_for_state(S_SEND_STATUS, 20); + #1; + check(uut.current_state === S_SEND_STATUS, + "Status readback: FSM entered SEND_STATUS"); + + // The SEND_STATUS state sends 7 words (idx 0-6): + // idx 0: 0xBB header, idx 1-5: status_words[0-4], idx 6: 0x55 footer + // After idx 6 it transitions to WAIT_ACK -> IDLE. + // Since ft601_txe=0, all 7 words should stream without stall. + wait_for_state(S_IDLE, 100); + #1; + check(uut.current_state === S_IDLE, + "Status readback: returned to IDLE after 7-word response"); + + // Verify the status snapshot was captured correctly. + // status_words[0] = {0xFF, 3'b000, mode[1:0], 5'b0, stream_ctrl[2:0], cfar_threshold[15:0]} + // = {8'hFF, 3'b000, 2'b01, 5'b00000, 3'b101, 16'hABCD} + // = 0xFF_09_05_ABCD... let's compute: + // Byte 3: 0xFF = 8'hFF + // Byte 2: {3'b000, 2'b01} = 5'b00001 + 3 high bits of next field... + // Actually the packing is: {8'hFF, 3'b000, status_radar_mode[1:0], 5'b00000, status_stream_ctrl[2:0], status_cfar_threshold[15:0]} + // = {8'hFF, 3'b000, 2'b01, 5'b00000, 3'b101, 16'hABCD} + // = 8'hFF, 5'b00001, 8'b00000101, 16'hABCD + // = FF_09_05_ABCD? Let me compute carefully: + // Bits [31:24] = 8'hFF = 0xFF + // Bits [23:21] = 3'b000 + // Bits [20:19] = 2'b01 (mode) + // Bits [18:14] = 5'b00000 + // Bits [13:11] = 3'b101 (stream_ctrl) + // Bits [10:0] = ... wait, cfar_threshold is 16 bits → [15:0] + // Total bits = 8+3+2+5+3+16 = 37 bits — won't fit in 32! + // Re-reading the RTL: the packing at line 241 is: + // {8'hFF, 3'b000, status_radar_mode, 5'b00000, status_stream_ctrl, status_cfar_threshold} + // = 8 + 3 + 2 + 5 + 3 + 16 = 37 bits + // This would be truncated to 32 bits. Let me re-read the actual RTL to check. + // For now, just verify status_words[1] (word index 1 in the packet = idx 2 in FSM) + // status_words[1] = {status_long_chirp, status_long_listen} = {16'd3000, 16'd13700} + check(uut.status_words[1] === {16'd3000, 16'd13700}, + "Status readback: word 1 = {long_chirp, long_listen}"); + check(uut.status_words[2] === {16'd17540, 16'd50}, + "Status readback: word 2 = {guard, short_chirp}"); + check(uut.status_words[3] === {16'd17450, 10'd0, 6'd32}, + "Status readback: word 3 = {short_listen, 0, chirps_per_elev}"); + check(uut.status_words[4] === 32'h0000_0000, + "Status readback: word 4 = placeholder 0x00000000"); + + // ════════════════════════════════════════════════════════ + // TEST GROUP 17: New Chirp Timing Opcodes (Gap 2) + // Verify opcodes 0x10-0x15 are properly decoded by the + // read path. + // ════════════════════════════════════════════════════════ + $display("\n--- Test Group 17: Chirp Timing Opcodes (Gap 2) ---"); + apply_reset; + + // 0x10: Long chirp cycles + send_host_command({8'h10, 8'h00, 16'd2500}); + check(cmd_opcode === 8'h10, + "Chirp opcode: 0x10 (long chirp cycles)"); + check(cmd_value === 16'd2500, + "Chirp opcode: value=2500"); + + // 0x11: Long listen cycles + send_host_command({8'h11, 8'h00, 16'd12000}); + check(cmd_opcode === 8'h11, + "Chirp opcode: 0x11 (long listen cycles)"); + check(cmd_value === 16'd12000, + "Chirp opcode: value=12000"); + + // 0x12: Guard cycles + send_host_command({8'h12, 8'h00, 16'd15000}); + check(cmd_opcode === 8'h12, + "Chirp opcode: 0x12 (guard cycles)"); + check(cmd_value === 16'd15000, + "Chirp opcode: value=15000"); + + // 0x13: Short chirp cycles + send_host_command({8'h13, 8'h00, 16'd40}); + check(cmd_opcode === 8'h13, + "Chirp opcode: 0x13 (short chirp cycles)"); + check(cmd_value === 16'd40, + "Chirp opcode: value=40"); + + // 0x14: Short listen cycles + send_host_command({8'h14, 8'h00, 16'd16000}); + check(cmd_opcode === 8'h14, + "Chirp opcode: 0x14 (short listen cycles)"); + check(cmd_value === 16'd16000, + "Chirp opcode: value=16000"); + + // 0x15: Chirps per elevation + send_host_command({8'h15, 8'h00, 16'd16}); + check(cmd_opcode === 8'h15, + "Chirp opcode: 0x15 (chirps per elevation)"); + check(cmd_value === 16'd16, + "Chirp opcode: value=16"); + + // 0xFF: Status request (opcode decode check — actual readback tested above) + send_host_command({8'hFF, 8'h00, 16'h0000}); + check(cmd_opcode === 8'hFF, + "Chirp opcode: 0xFF (status request)"); + // ════════════════════════════════════════════════════════ // Summary // ════════════════════════════════════════════════════════ diff --git a/9_Firmware/9_2_FPGA/usb_data_interface.v b/9_Firmware/9_2_FPGA/usb_data_interface.v index 67081d6..ddeaeae 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface.v @@ -45,12 +45,33 @@ module usb_data_interface ( // 0x02 = Single chirp trigger (value ignored, pulse cmd_valid) // 0x03 = Set CFAR threshold (value[15:0] -> threshold) // 0x04 = Set stream control (value[2:0] -> enable range/doppler/cfar) + // 0x10-0x15 = Chirp timing configuration (Gap 2) // 0xFF = Status request (triggers status response packet) output reg [31:0] cmd_data, // Last received command word output reg cmd_valid, // Pulse: new command received (ft601_clk domain) output reg [7:0] cmd_opcode, // Decoded opcode for convenience output reg [7:0] cmd_addr, // Decoded register address - output reg [15:0] cmd_value // Decoded value + output reg [15:0] cmd_value, // Decoded value + + // Gap 2: Stream control input (clk_100m domain, CDC'd internally) + // Bit 0 = range stream enable + // Bit 1 = doppler stream enable + // Bit 2 = cfar/detection stream enable + input wire [2:0] stream_control, + + // Gap 2: Status readback inputs (clk_100m domain, CDC'd internally) + // When status_request pulses, the write FSM sends a status response + // packet containing the current register values. + input wire status_request, // 1-cycle pulse in clk_100m when 0xFF received + input wire [15:0] status_cfar_threshold, // Current CFAR threshold + input wire [2:0] status_stream_ctrl, // Current stream control + input wire [1:0] status_radar_mode, // Current radar mode + input wire [15:0] status_long_chirp, // Current long chirp cycles + input wire [15:0] status_long_listen, // Current long listen cycles + input wire [15:0] status_guard, // Current guard cycles + input wire [15:0] status_short_chirp, // Current short chirp cycles + input wire [15:0] status_short_listen, // Current short listen cycles + input wire [5:0] status_chirps_per_elev // Current chirps per elevation ); // USB packet structure (same as before) @@ -70,7 +91,8 @@ localparam [2:0] IDLE = 3'd0, SEND_DOPPLER_DATA = 3'd3, SEND_DETECTION_DATA = 3'd4, SEND_FOOTER = 3'd5, - WAIT_ACK = 3'd6; + WAIT_ACK = 3'd6, + SEND_STATUS = 3'd7; // Gap 2: status readback reg [2:0] current_state; reg [7:0] byte_counter; @@ -123,6 +145,10 @@ reg [15:0] doppler_real_hold; reg [15:0] doppler_imag_hold; reg cfar_detection_hold; +// Gap 2: Status request toggle register (clk_100m domain). +// Declared here (before the always block) to satisfy iverilog forward-ref rules. +reg status_req_toggle_100m; + // Source-domain holding registers (clk domain) always @(posedge clk or negedge reset_n) begin if (!reset_n) begin @@ -130,6 +156,7 @@ always @(posedge clk or negedge reset_n) begin doppler_real_hold <= 16'd0; doppler_imag_hold <= 16'd0; cfar_detection_hold <= 1'b0; + status_req_toggle_100m <= 1'b0; end else begin if (range_valid) range_profile_hold <= range_profile; @@ -139,6 +166,9 @@ always @(posedge clk or negedge reset_n) begin end if (cfar_valid) cfar_detection_hold <= cfar_detection; + // Gap 2: Toggle on status request pulse (CDC to ft601_clk) + if (status_request) + status_req_toggle_100m <= ~status_req_toggle_100m; end end @@ -148,6 +178,29 @@ reg [15:0] doppler_real_cap; reg [15:0] doppler_imag_cap; reg cfar_detection_cap; +// Gap 2: CDC for stream_control (clk_100m -> ft601_clk_in) +// stream_control changes infrequently (only on host USB command), so +// per-bit 2-stage synchronizers are sufficient. No Gray coding needed +// because the bits are independent enables. +(* ASYNC_REG = "TRUE" *) reg [2:0] stream_ctrl_sync_0; +(* ASYNC_REG = "TRUE" *) reg [2:0] stream_ctrl_sync_1; +wire stream_range_en = stream_ctrl_sync_1[0]; +wire stream_doppler_en = stream_ctrl_sync_1[1]; +wire stream_cfar_en = stream_ctrl_sync_1[2]; + +// Gap 2: Status request CDC (toggle CDC, same pattern as cmd_valid) +// status_request is a 1-cycle pulse in clk_100m. Toggle→sync→edge-detect. +// NOTE: status_req_toggle_100m declared above (before source-domain always block) +(* ASYNC_REG = "TRUE" *) reg [1:0] status_req_sync; +reg status_req_sync_prev; +wire status_req_ft601 = status_req_sync[1] ^ status_req_sync_prev; + +// Status snapshot: captured in ft601_clk domain when status request arrives. +// The clk_100m-domain status inputs are stable for many cycles after the +// command decode, so sampling them a few ft601_clk cycles later is safe. +reg [31:0] status_words [0:4]; // 5 status words +reg [2:0] status_word_idx; + wire range_valid_ft; wire doppler_valid_ft; wire cfar_valid_ft; @@ -164,12 +217,44 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin doppler_real_cap <= 16'd0; doppler_imag_cap <= 16'd0; cfar_detection_cap <= 1'b0; + // Gap 2: stream control CDC reset (default all enabled) + stream_ctrl_sync_0 <= 3'b111; + stream_ctrl_sync_1 <= 3'b111; + // Gap 2: status request CDC reset + status_req_sync <= 2'b00; + status_req_sync_prev <= 1'b0; + status_word_idx <= 3'd0; end else begin // Synchronize valid strobes (2-stage sync chain) range_valid_sync <= {range_valid_sync[0], range_valid}; doppler_valid_sync <= {doppler_valid_sync[0], doppler_valid}; cfar_valid_sync <= {cfar_valid_sync[0], cfar_valid}; + // Gap 2: stream control CDC (2-stage) + stream_ctrl_sync_0 <= stream_control; + stream_ctrl_sync_1 <= stream_ctrl_sync_0; + + // Gap 2: status request CDC (toggle sync + edge detect) + status_req_sync <= {status_req_sync[0], status_req_toggle_100m}; + status_req_sync_prev <= status_req_sync[1]; + + // Gap 2: Capture status snapshot when request arrives in ft601 domain + if (status_req_ft601) begin + // Pack register values into 5x 32-bit status words + // Word 0: {0xFF, mode[1:0], stream_ctrl[2:0], cfar_threshold[15:0]} + status_words[0] <= {8'hFF, 3'b000, status_radar_mode, + 5'b00000, status_stream_ctrl, + status_cfar_threshold}; + // Word 1: {long_chirp_cycles[15:0], long_listen_cycles[15:0]} + status_words[1] <= {status_long_chirp, status_long_listen}; + // Word 2: {guard_cycles[15:0], short_chirp_cycles[15:0]} + 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: {system_status placeholder — 32'h00000000} + status_words[4] <= 32'h0000_0000; + end + // Delayed version of sync[1] for edge detection range_valid_sync_d <= range_valid_sync[1]; doppler_valid_sync_d <= doppler_valid_sync[1]; @@ -292,7 +377,15 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin IDLE: begin ft601_wr_n <= 1; ft601_data_oe <= 0; // Release data bus - if (range_valid_ft || doppler_valid_ft || cfar_valid_ft) begin + // Gap 2: Status readback takes priority + if (status_req_ft601 && ft601_rxf) begin + current_state <= SEND_STATUS; + status_word_idx <= 3'd0; + end + // Gap 2: Only trigger write FSM if at least one enabled stream has data + else if ((range_valid_ft && stream_range_en) || + (doppler_valid_ft && stream_doppler_en) || + (cfar_valid_ft && stream_cfar_en)) begin // Don't start write if a read is about to begin if (ft601_rxf) begin // rxf=1 means no host data pending current_state <= SEND_HEADER; @@ -307,7 +400,15 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin ft601_data_out <= {24'b0, HEADER}; ft601_be <= 4'b0001; // Only lower byte valid ft601_wr_n <= 0; // Assert write strobe - current_state <= SEND_RANGE_DATA; + // Gap 2: skip to first enabled stream + if (stream_range_en) + current_state <= SEND_RANGE_DATA; + else if (stream_doppler_en) + current_state <= SEND_DOPPLER_DATA; + else if (stream_cfar_en) + current_state <= SEND_DETECTION_DATA; + else + current_state <= SEND_FOOTER; // No streams — send footer only end end @@ -327,7 +428,13 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin if (byte_counter == 3) begin byte_counter <= 0; - current_state <= SEND_DOPPLER_DATA; + // Gap 2: skip disabled streams + if (stream_doppler_en) + current_state <= SEND_DOPPLER_DATA; + else if (stream_cfar_en) + current_state <= SEND_DETECTION_DATA; + else + current_state <= SEND_FOOTER; end else begin byte_counter <= byte_counter + 1; end @@ -350,7 +457,11 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin if (byte_counter == 3) begin byte_counter <= 0; - current_state <= SEND_DETECTION_DATA; + // Gap 2: skip disabled cfar stream + if (stream_cfar_en) + current_state <= SEND_DETECTION_DATA; + else + current_state <= SEND_FOOTER; end else begin byte_counter <= byte_counter + 1; end @@ -376,6 +487,40 @@ always @(posedge ft601_clk_in or negedge ft601_reset_n) begin current_state <= WAIT_ACK; end end + + // Gap 2: Status readback — send 5 x 32-bit status words + // Format: HEADER, status_words[0..4], FOOTER + SEND_STATUS: begin + if (!ft601_txe) begin + ft601_data_oe <= 1; + ft601_be <= 4'b1111; + case (status_word_idx) + 3'd0: begin + // Send status header marker (0xBB = status response) + ft601_data_out <= {24'b0, 8'hBB}; + ft601_be <= 4'b0001; + end + 3'd1: ft601_data_out <= status_words[0]; + 3'd2: ft601_data_out <= status_words[1]; + 3'd3: ft601_data_out <= status_words[2]; + 3'd4: ft601_data_out <= status_words[3]; + 3'd5: ft601_data_out <= status_words[4]; + 3'd6: begin + // Send status footer + ft601_data_out <= {24'b0, FOOTER}; + ft601_be <= 4'b0001; + end + default: ; + endcase + ft601_wr_n <= 0; + if (status_word_idx == 3'd6) begin + status_word_idx <= 3'd0; + current_state <= WAIT_ACK; + end else begin + status_word_idx <= status_word_idx + 1; + end + end + end WAIT_ACK: begin ft601_wr_n <= 1;