diff --git a/9_Firmware/9_2_FPGA/radar_params.vh b/9_Firmware/9_2_FPGA/radar_params.vh index 4b33259..99c794a 100644 --- a/9_Firmware/9_2_FPGA/radar_params.vh +++ b/9_Firmware/9_2_FPGA/radar_params.vh @@ -270,10 +270,71 @@ // Bit 0 = range profile stream // Bit 1 = doppler map stream // Bit 2 = cfar/detection stream -// Bits [5:3]: Stream format control -// Bit 3 = mag_only (0=I/Q pairs, 1=Manhattan magnitude only) -// Bit 4 = sparse_det (0=dense detection flags, 1=sparse detection list) -// Bit 5 = reserved (was frame_decimate, not needed with mag-only fitting) -`define RP_STREAM_CTRL_DEFAULT 6'b001_111 // all streams, mag-only mode +// Bits [5:3]: RESERVED (must be 0). PR-G dropped the legacy inert +// mag_only/sparse_det/frame_decimate flags — protocol v2 ships a single +// canonical encoding (Manhattan-mag doppler + 2-bit dense detect). +`define RP_STREAM_CTRL_DEFAULT 6'b000_111 // all 3 streams on, no flags + +// ============================================================================ +// USB PROTOCOL V2 (PR-G — clean cutover from v1) +// ============================================================================ +// Wire format (FPGA → Host bulk frame): +// Byte 0: 0xAA (frame start) +// Byte 1: 0x02 (PROTOCOL VERSION — pinned, host MUST reject != 0x02) +// Byte 2: Stream flags {5'd0, stream_cfar, stream_doppler, stream_range} +// Bytes 3–4: Frame number (uint16, MSB first) +// Bytes 5–6: Range bin count (uint16, MSB first) = `RP_NUM_RANGE_BINS` +// Bytes 7–8: Doppler bin count (uint16, MSB first) = `RP_NUM_DOPPLER_BINS` +// [stream_range:] 1024 B range profile (512 × uint16, MSB first) +// [stream_doppler:] 65536 B doppler magnitude (32768 cells × uint16, row-major) +// [stream_cfar:] 8192 B detect bitmap (32768 cells × 2 bits, MSB-first +// packing: cell[N] in byte[N/4] bits [7-(N%4)*2 -: 2]) +// Last byte: 0x55 (footer) +// +// Total frame (all streams on): 9 + 1024 + 65536 + 8192 + 1 = 74762 B +// At ~119 fps (PR-F 3-subframe rate) ≈ 8.9 MB/s — within FT2232H bulk budget. +`define RP_USB_PROTOCOL_VERSION 8'h02 // pinned; host rejects mismatch +`define RP_FRAME_HDR_BYTES 9 // 0xAA + ver + flags + 2*fn + 2*rb + 2*db +`define RP_DETECT_BITS_PER_CELL 2 // PR-G: 2-bit dense (NONE/CAND/CONFIRM/RSVD) +`define RP_DETECT_CELLS_PER_BYTE 4 // 8 / RP_DETECT_BITS_PER_CELL + +// ============================================================================ +// USB OPCODE MAP (PR-G v2 — single source of truth for RTL & GUI parity) +// ============================================================================ +`define RP_OP_RADAR_MODE 8'h01 +`define RP_OP_TRIGGER_PULSE 8'h02 +`define RP_OP_DETECT_THRESHOLD 8'h03 +`define RP_OP_STREAM_CONTROL 8'h04 +`define RP_OP_LONG_CHIRP_CYCLES 8'h10 +`define RP_OP_LONG_LISTEN_CYCLES 8'h11 +`define RP_OP_GUARD_CYCLES 8'h12 +`define RP_OP_SHORT_CHIRP_CYCLES 8'h13 +`define RP_OP_SHORT_LISTEN_CYCLES 8'h14 +`define RP_OP_CHIRPS_PER_ELEV 8'h15 +`define RP_OP_GAIN_SHIFT 8'h16 +// PR-G G2: MEDIUM ladder timings (SHORT/LONG already at 0x10-0x14, GUARD at 0x12). +`define RP_OP_MEDIUM_CHIRP_CYCLES 8'h17 +`define RP_OP_MEDIUM_LISTEN_CYCLES 8'h18 +// 0x19–0x1F reserved (per-waveform guard if needed in future) +`define RP_OP_RANGE_MODE 8'h20 +`define RP_OP_CFAR_GUARD 8'h21 +`define RP_OP_CFAR_TRAIN 8'h22 +`define RP_OP_CFAR_ALPHA 8'h23 // confirm-tier (Q4.4) +`define RP_OP_CFAR_MODE 8'h24 +`define RP_OP_CFAR_ENABLE 8'h25 +`define RP_OP_MTI_ENABLE 8'h26 +`define RP_OP_DC_NOTCH_WIDTH 8'h27 +`define RP_OP_AGC_ENABLE 8'h28 +`define RP_OP_AGC_TARGET 8'h29 +`define RP_OP_AGC_ATTACK 8'h2A +`define RP_OP_AGC_DECAY 8'h2B +`define RP_OP_AGC_HOLDOFF 8'h2C +`define RP_OP_CFAR_ALPHA_SOFT 8'h2D // PR-G: candidate-tier (Q4.4) +// 0x2E–0x2F reserved +`define RP_OP_SELF_TEST_TRIGGER 8'h30 +`define RP_OP_SELF_TEST_STATUS 8'h31 +`define RP_OP_ADC_PWDN 8'h32 +`define RP_OP_ADC_FORMAT 8'h33 +`define RP_OP_STATUS_REQUEST 8'hFF `endif // RADAR_PARAMS_VH diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index 36f68ed..4034977 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -47,6 +47,9 @@ module radar_receiver_final ( input wire [15:0] host_guard_cycles, input wire [15:0] host_short_chirp_cycles, input wire [15:0] host_short_listen_cycles, + // PR-G G2: MEDIUM ladder timings (was hardcoded to RP_DEF_MEDIUM_*) + input wire [15:0] host_medium_chirp_cycles, + input wire [15:0] host_medium_listen_cycles, input wire [5:0] host_chirps_per_elev, // Digital gain control (Fix 3: between DDC output and matched filter) @@ -221,11 +224,13 @@ wire mti_first_chirp; // radar_mode_controller. SHORT/MEDIUM/LONG ladders + sub-frame walking // + host-cued track dwell with watchdog scan-fallback. // -// Several inputs (medium/track/subframe-enable/debug) are pinned to -// radar_params defaults until PR-G plumbs the new USB opcodes through -// radar_system_top. host_chirps_per_elev (legacy) is intentionally not -// wired here — the V2 sub-frame structure uses RP_DEF_CHIRPS_PER_SUBFRAME -// (16) and PR-G renames the host register. +// PR-G G2: MEDIUM chirp/listen are now host-configurable via opcodes +// 0x17/0x18, plumbed through host_medium_*_cycles. Track-mode + subframe- +// enable + debug-wave selectors remain pinned to radar_params defaults +// (single-mode SHORT track is the only HW-tested track variant; per-mode +// track waveform selection is a future feature, not part of PR-G). +// host_chirps_per_elev (legacy) is intentionally not wired here — the V2 +// sub-frame structure uses RP_DEF_CHIRPS_PER_SUBFRAME (16) and is fixed. chirp_scheduler sched ( .mixers_enable(mixers_enable_100m), .clk(clk), @@ -234,8 +239,9 @@ chirp_scheduler sched ( .host_subframe_enable(`RP_DEF_SUBFRAME_ENABLE), .host_short_chirp_cycles (host_short_chirp_cycles), .host_short_listen_cycles(host_short_listen_cycles), - .host_medium_chirp_cycles (16'd`RP_DEF_MEDIUM_CHIRP_CYCLES), - .host_medium_listen_cycles(16'd`RP_DEF_MEDIUM_LISTEN_CYCLES), + // PR-G G2: MEDIUM now flows from radar_system_top opcodes 0x17/0x18. + .host_medium_chirp_cycles (host_medium_chirp_cycles), + .host_medium_listen_cycles(host_medium_listen_cycles), .host_long_chirp_cycles (host_long_chirp_cycles), .host_long_listen_cycles(host_long_listen_cycles), .host_guard_cycles(host_guard_cycles), diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index a9cbffc..720ea67 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -197,6 +197,9 @@ wire [15:0] rx_doppler_imag; wire rx_doppler_data_valid; reg rx_detect_flag; // Threshold detection result (was rx_cfar_detection) reg rx_detect_valid; // Detection valid pulse (was rx_cfar_valid) +// PR-G: 2-bit class register (registered alongside detect_flag for the same +// CDC-clean handoff to usb_data_interface_ft2232h). Encoding per RP_DETECT_*. +reg [`RP_DETECT_CLASS_WIDTH-1:0] rx_detect_class; // Frame-complete signal from Doppler processor (for CFAR) wire rx_frame_complete; @@ -230,8 +233,9 @@ wire usb_range_valid; wire [15:0] usb_doppler_real; wire [15:0] usb_doppler_imag; wire usb_doppler_valid; -wire usb_detect_flag; // (was usb_cfar_detection) +wire usb_detect_flag; // (was usb_cfar_detection) — FT601 legacy 1-bit path wire usb_detect_valid; // (was usb_cfar_valid) +wire [`RP_DETECT_CLASS_WIDTH-1:0] usb_detect_class; // PR-G: 2-bit class for FT2232H bulk frame v2 // System status reg [3:0] status_reg; @@ -263,8 +267,10 @@ reg [3:0] host_gain_shift; 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 [15:0] host_short_chirp_cycles; // Opcode 0x13 (default 100, V2) +reg [15:0] host_short_listen_cycles; // Opcode 0x14 (default 17400, V2) +reg [15:0] host_medium_chirp_cycles; // Opcode 0x17 (default 500, PR-G G2) +reg [15:0] host_medium_listen_cycles; // Opcode 0x18 (default 17000, PR-G G2) reg [5:0] host_chirps_per_elev; // Opcode 0x15 (default 32) reg host_status_request; // Opcode 0xFF (self-clearing pulse) @@ -592,6 +598,9 @@ radar_receiver_final rx_inst ( .host_guard_cycles(host_guard_cycles), .host_short_chirp_cycles(host_short_chirp_cycles), .host_short_listen_cycles(host_short_listen_cycles), + // PR-G G2: MEDIUM ladder timings (was hardcoded to RP_DEF_MEDIUM_*) + .host_medium_chirp_cycles(host_medium_chirp_cycles), + .host_medium_listen_cycles(host_medium_listen_cycles), .host_chirps_per_elev(host_chirps_per_elev), // Fix 3: digital gain control .host_gain_shift(host_gain_shift), @@ -689,13 +698,11 @@ wire [16:0] cfar_detect_threshold; wire [15:0] cfar_detect_count; wire cfar_busy_w; wire [7:0] cfar_status_w; -// PR-F note: cfar_ca also drives detect_class[1:0], detect_threshold_soft, -// detect_count_cand. The soft-tier comparison still feeds detect_flag (which -// is wired to USB), so the candidate logic is preserved in synth — but the -// class / soft-thr / cand-count outputs themselves don't go anywhere until -// PR-G adds the USB opcodes (0x28 alpha_soft + bulk-frame v2). We deliberately -// leave the cfar_ca output ports unconnected here so they do NOT show up as -// dangling wires in radar_system_top; PR-G will reattach them. +// PR-G: 2-class adaptive detection now wired through to the USB bulk frame +// (detect_class per cell) and status packet (count_cand + threshold_soft). +wire [`RP_DETECT_CLASS_WIDTH-1:0] cfar_detect_class; // PR-G: NONE/CAND/CONFIRM +wire [16:0] cfar_detect_threshold_soft; // PR-G: soft (candidate) threshold +wire [15:0] cfar_detect_count_cand; // PR-G: per-frame candidate count cfar_ca cfar_inst ( .clk(clk_100m_buf), @@ -719,17 +726,17 @@ cfar_ca cfar_inst ( // Detection outputs .detect_flag(cfar_detect_flag), - .detect_class(), // PR-F: routed in PR-G (USB 0x2A read) + .detect_class(cfar_detect_class), // PR-G: 2-bit dense to USB bulk frame .detect_valid(cfar_detect_valid), .detect_range(cfar_detect_range), .detect_doppler(cfar_detect_doppler), .detect_magnitude(cfar_detect_magnitude), .detect_threshold(cfar_detect_threshold), - .detect_threshold_soft(), // PR-F: routed in PR-G + .detect_threshold_soft(cfar_detect_threshold_soft), // PR-G: status_words[6][15:0] // Status .detect_count(cfar_detect_count), - .detect_count_cand(), // PR-F: routed in PR-G (telemetry) + .detect_count_cand(cfar_detect_count_cand), // PR-G: status_words[6][31:16] .cfar_busy(cfar_busy_w), .cfar_status(cfar_status_w) ); @@ -740,9 +747,11 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin if (!sys_reset_n) begin rx_detect_flag <= 1'b0; rx_detect_valid <= 1'b0; + rx_detect_class <= `RP_DETECT_NONE; end else begin rx_detect_flag <= cfar_detect_flag; rx_detect_valid <= cfar_detect_valid; + rx_detect_class <= cfar_detect_class; end end @@ -795,6 +804,7 @@ assign usb_doppler_valid = rx_doppler_valid; assign usb_detect_flag = rx_detect_flag; assign usb_detect_valid = rx_detect_valid; +assign usb_detect_class = rx_detect_class; // PR-G: 2-bit class to FT2232H // ============================================================================ // USB DATA INTERFACE INSTANTIATION (parametric: FT601 or FT2232H) @@ -893,7 +903,7 @@ end else begin : gen_ft2232h .doppler_real(usb_doppler_real), .doppler_imag(usb_doppler_imag), .doppler_valid(usb_doppler_valid), - .cfar_detection(usb_detect_flag), + .cfar_detect_class(usb_detect_class), // PR-G: 2-bit class (was 1-bit cfar_detection) .cfar_valid(usb_detect_valid), // Bulk frame protocol inputs @@ -949,7 +959,12 @@ end else begin : gen_ft2232h // AUDIT-S10: control-fault flags exposed in status_words[5][6:5] // for host-side observability (paired with gpio_dig7 split) .status_range_decim_watchdog(rx_range_decim_watchdog), - .status_ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun) + .status_ddc_cic_fir_overrun(rx_ddc_cic_fir_overrun), + + // 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) ); // FT601 ports unused in FT2232H mode — tie off @@ -1032,12 +1047,14 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin host_gain_shift <= 4'd0; // Default: pass-through (no gain change) // Gap 2: chirp timing defaults (forwarded to chirp_scheduler). // SHORT bumped to 100 cycles (1 us) for chirp-v2 SHORT waveform. - host_long_chirp_cycles <= 16'd3000; - host_long_listen_cycles <= 16'd13700; - host_guard_cycles <= 16'd17540; - host_short_chirp_cycles <= 16'd100; - host_short_listen_cycles <= 16'd17400; - host_chirps_per_elev <= 6'd32; + host_long_chirp_cycles <= 16'd`RP_DEF_LONG_CHIRP_CYCLES; + host_long_listen_cycles <= 16'd`RP_DEF_LONG_LISTEN_CYCLES; + host_guard_cycles <= 16'd`RP_DEF_GUARD_CYCLES; + host_short_chirp_cycles <= 16'd`RP_DEF_SHORT_CHIRP_CYCLES_V2; + host_short_listen_cycles <= 16'd`RP_DEF_SHORT_LISTEN_CYCLES_V2; + host_medium_chirp_cycles <= 16'd`RP_DEF_MEDIUM_CHIRP_CYCLES; // PR-G G2 + host_medium_listen_cycles <= 16'd`RP_DEF_MEDIUM_LISTEN_CYCLES; // PR-G G2 + host_chirps_per_elev <= 6'd32; host_status_request <= 1'b0; chirps_mismatch_error <= 1'b0; host_range_mode <= 2'b00; // Default: 3 km mode (all short chirps) @@ -1074,30 +1091,21 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin 8'h01: host_radar_mode <= usb_cmd_value[1:0]; 8'h02: host_trigger_pulse <= 1'b1; 8'h03: host_detect_threshold <= usb_cmd_value; - // AUDIT-C9: stream_control bits [3] (mag_only) and [4] - // (sparse_det) are documented in the FT2232H bulk-frame - // header but the write FSM does not implement the alternate - // encodings yet (see usb_data_interface_ft2232h.v "INERT - // FLAGS" note). Force-clamp them to the only encodings the - // FSM actually emits so a host write of 0x04 cannot create - // a wire-format vs FSM divergence on the production board. - 8'h04: begin - if (USB_MODE == 1) begin - // FT2232H production: mag_only stuck at 1, sparse_det stuck at 0. - host_stream_control <= {usb_cmd_value[5], - 1'b0, // sparse_det - 1'b1, // mag_only - usb_cmd_value[2:0]}; // stream r/d/c - end else begin - host_stream_control <= usb_cmd_value[5:0]; - end - end + // PR-G: protocol v2 has a single canonical encoding + // (Manhattan-mag doppler + 2-bit dense detect). Bits [5:3] of + // host_stream_control are RESERVED — host SHOULD write 0, RTL + // ignores them. The legacy AUDIT-C9 force-clamp + INERT FLAGS + // logic was removed when v1 was retired. + 8'h04: host_stream_control <= {3'b000, usb_cmd_value[2:0]}; // 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; + // PR-G G2: MEDIUM ladder timings + 8'h17: host_medium_chirp_cycles <= usb_cmd_value; + 8'h18: host_medium_listen_cycles <= usb_cmd_value; 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. @@ -1130,6 +1138,9 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin 8'h2A: host_agc_attack <= usb_cmd_value[3:0]; 8'h2B: host_agc_decay <= usb_cmd_value[3:0]; 8'h2C: host_agc_holdoff <= usb_cmd_value[3:0]; + // PR-G: 2-tier CFAR — soft (candidate) threshold multiplier. + // Default RP_DEF_CFAR_ALPHA_SOFT = 0x18 (1.5 in Q4.4, Pfa~10⁻⁵). + 8'h2D: host_cfar_alpha_soft <= usb_cmd_value[7:0]; // Board bring-up self-test opcodes 8'h30: host_self_test_trigger <= 1'b1; // Trigger self-test 8'h31: host_status_request <= 1'b1; // Self-test readback (status alias) 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 a3a0e70..8dc8f19 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 @@ -16,10 +16,9 @@ // 3. Multiple drops while stalled → drop count saturates at 127 // 4. Stalled + recovery → drop count stable, frame_pending clears post-drain // -// Stimulus uses `stream_control = 6'b001_000` (mag_only=1, no sections enabled) -// so the WR FSM goes HDR (8B) → FOOTER (1B) → DONE in 9 ft_clk cycles. This -// gives a fast, deterministic per-frame transfer time. AUDIT-C9 sim assertion -// is satisfied (mag_only=1, sparse_det=0). +// Stimulus uses `stream_control = 6'b000_000` (PR-G v2: no inert flags, no +// sections enabled) so the WR FSM goes HDR (9B) → FOOTER (1B) → DONE in 10 +// ft_clk cycles. This gives a fast, deterministic per-frame transfer time. // // PASS criteria: // - frame_drop_count matches expected value after each scenario @@ -42,11 +41,13 @@ module tb_ft2232h_frame_drop; reg [15:0] doppler_real = 16'd0; reg [15:0] doppler_imag = 16'd0; reg doppler_valid = 1'b0; - reg cfar_detection = 1'b0; + // PR-G: 2-bit class (was 1-bit cfar_detection) + reg [`RP_DETECT_CLASS_WIDTH-1:0] cfar_detect_class = `RP_DETECT_NONE; reg cfar_valid = 1'b0; reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin_in = 0; - reg [4:0] doppler_bin_in = 5'd0; + // PR-F: doppler_bin widened to RP_DOPPLER_BIN_WIDTH (6 bits) + reg [`RP_DOPPLER_BIN_WIDTH-1:0] doppler_bin_in = {`RP_DOPPLER_BIN_WIDTH{1'b0}}; reg frame_complete = 1'b0; // FT2232H interface (ft_clk domain) @@ -64,14 +65,14 @@ module tb_ft2232h_frame_drop; wire [7:0] cmd_addr; wire [15:0] cmd_value; - // mag_only=1, sparse_det=0, all sections disabled (skip range/doppler/cfar) - // → WR FSM: HDR → FOOTER → DONE = fast deterministic drain - reg [5:0] stream_control = 6'b001_000; + // PR-G: stream bits [2:0] all off → WR FSM: HDR → FOOTER → DONE + // = fast deterministic drain. Bits [5:3] are reserved=0 in v2. + reg [5:0] stream_control = 6'b000_000; // Status inputs (irrelevant for this test) reg status_request = 1'b0; reg [15:0] status_cfar_threshold = 16'd0; - reg [5:0] status_stream_ctrl = 6'b001_000; + reg [5:0] status_stream_ctrl = 6'b000_000; reg [1:0] status_radar_mode = 2'd0; reg [15:0] status_long_chirp = 16'd0; reg [15:0] status_long_listen = 16'd0; @@ -104,7 +105,7 @@ module tb_ft2232h_frame_drop; .doppler_real(doppler_real), .doppler_imag(doppler_imag), .doppler_valid(doppler_valid), - .cfar_detection(cfar_detection), + .cfar_detect_class(cfar_detect_class), // PR-G: 2-bit class .cfar_valid(cfar_valid), .range_bin_in(range_bin_in), .doppler_bin_in(doppler_bin_in), @@ -144,7 +145,11 @@ module tb_ft2232h_frame_drop; .status_agc_enable(status_agc_enable), // AUDIT-S10: control-fault flags tied off (frame-drop TB scope) .status_range_decim_watchdog(1'b0), - .status_ddc_cic_fir_overrun(1'b0) + .status_ddc_cic_fir_overrun(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), + .status_detect_count_cand(16'd0) ); task pulse_frame_complete; @@ -200,10 +205,10 @@ module tb_ft2232h_frame_drop; $display("\n[TEST 1] Single frame, USB ready -> no drops"); ft_txe_n = 1'b0; pulse_frame_complete(); - // Wait for frame to drain through WR_FSM. With mag_only mode and - // stream_control[2:0]=000, FSM goes HDR (8B) -> FOOTER (1B) -> DONE. - // Each byte = 1 ft_clk cycle. Plus CDC latency. Allow ~50 ft_clk - // = ~833 ns = ~83 clk cycles. Be generous: wait 200 clk cycles. + // Wait for frame to drain through WR_FSM. PR-G v2: stream_control[2:0]=000, + // FSM goes HDR (9B) -> FOOTER (1B) -> DONE = 10 ft_clk cycles. Plus CDC + // latency. Allow ~50 ft_clk = ~833 ns = ~83 clk cycles. Be generous: + // wait 200 clk cycles. wait_cycles(200); check(1, "drop_count", 0, u_dut.frame_drop_count); check(1, "frame_pending_cleared", 0, u_dut.frame_pending); 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 new file mode 100644 index 0000000..de10fc1 --- /dev/null +++ b/9_Firmware/9_2_FPGA/tb/tb_usb_protocol_v2.v @@ -0,0 +1,357 @@ +`timescale 1ns / 1ps +`include "radar_params.vh" + +// ============================================================================ +// tb_usb_protocol_v2.v +// +// PR-G focused round-trip verification for usb_data_interface_ft2232h.v: +// 1. Opcode 0x2D (host_cfar_alpha_soft) write path — verify cmd_value +// reaches the cmd_* outputs of the read FSM with the right byte order. +// 2. Bulk frame header v2 — verify byte0=0xAA, byte1=0x02 (version), +// byte2=stream flags, bytes3-8=frame_num/range/doppler counts. +// 3. Status packet length — verify 30 bytes (was 26 in v1) and that +// status_words[6] carries detect_count_cand/detect_threshold_soft. +// 4. PR-G FSM trim — full-frame header/body length consistency. With all +// streams enabled, total emitted bytes must equal 9 (hdr) + range×2 + +// range×doppler×2 (doppler) + range×doppler×2/8 (detect) + 1 (footer). +// Catches future header-vs-body drift and confirms padding is skipped. +// 5. PR-G G2 — MEDIUM ladder timing opcodes (0x17, 0x18) round-trip via +// cmd_opcode/cmd_value (the host_medium_*_cycles registers live in +// radar_system_top, exercised at integration level by tb_system_e2e). +// ============================================================================ + +module tb_usb_protocol_v2; + localparam CLK_PER = 10.0; // 100 MHz + localparam FT_CLK_PER = 16.667; // 60 MHz + + reg clk = 1'b0; + reg ft_clk = 1'b0; + reg reset_n = 1'b0; + reg ft_reset_n = 1'b0; + + // Radar inputs (clk domain) + reg [31:0] range_profile = 32'd0; + reg range_valid = 1'b0; + reg [15:0] doppler_real = 16'd0; + reg [15:0] doppler_imag = 16'd0; + reg doppler_valid = 1'b0; + reg [`RP_DETECT_CLASS_WIDTH-1:0] cfar_detect_class = `RP_DETECT_NONE; + reg cfar_valid = 1'b0; + + reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin_in = 0; + reg [`RP_DOPPLER_BIN_WIDTH-1:0] doppler_bin_in = 0; + reg frame_complete = 1'b0; + + // FT2232H interface + wire [7:0] ft_data; + reg ft_rxf_n = 1'b1; + reg ft_txe_n = 1'b0; + wire ft_rd_n; + wire ft_wr_n; + wire ft_oe_n; + wire ft_siwu; + + // Bidirectional data: tristate driver from TB for read path + reg [7:0] ft_data_drive = 8'd0; + reg ft_data_drive_en = 1'b0; + assign ft_data = ft_data_drive_en ? ft_data_drive : 8'hzz; + pulldown pd[7:0] (ft_data); + + wire [31:0] cmd_data; + wire cmd_valid; + wire [7:0] cmd_opcode; + wire [7:0] cmd_addr; + wire [15:0] cmd_value; + + // PR-G v2: enable all 3 streams (range|doppler|cfar). Bits [5:3] reserved=0. + reg [5:0] stream_control = 6'b000_111; + reg [5:0] status_stream_ctrl = 6'b000_111; + + // Status inputs (mostly tied off; PR-G additions below) + reg status_request = 1'b0; + reg [15:0] status_cfar_threshold = 16'h1234; + reg [1:0] status_radar_mode = 2'd0; + reg [15:0] status_long_chirp = 16'd0; + reg [15:0] status_long_listen = 16'd0; + reg [15:0] status_guard = 16'd0; + reg [15:0] status_short_chirp = 16'd0; + reg [15:0] status_short_listen = 16'd0; + reg [5:0] status_chirps_per_elev = 6'd0; + reg [1:0] status_range_mode = 2'd0; + reg status_chirps_mismatch = 1'b0; + reg [4:0] status_self_test_flags = 5'd0; + reg [7:0] status_self_test_detail = 8'd0; + reg status_self_test_busy = 1'b0; + reg [3:0] status_agc_current_gain = 4'd0; + reg [7:0] status_agc_peak_magnitude = 8'd0; + reg [7:0] status_agc_saturation_count = 8'd0; + reg status_agc_enable = 1'b0; + reg status_range_decim_watchdog = 1'b0; + reg status_ddc_cic_fir_overrun = 1'b0; + // PR-G new + reg [7:0] status_cfar_alpha_soft = `RP_DEF_CFAR_ALPHA_SOFT; // 0x18 + reg [16:0] status_detect_threshold_soft = 17'h00ABC; + reg [15:0] status_detect_count_cand = 16'd42; + + integer pass = 0; + integer fail = 0; + + always #(CLK_PER/2) clk = ~clk; + always #(FT_CLK_PER/2) ft_clk = ~ft_clk; + + usb_data_interface_ft2232h u_dut ( + .clk(clk), + .reset_n(reset_n), + .ft_reset_n(ft_reset_n), + .range_profile(range_profile), + .range_valid(range_valid), + .doppler_real(doppler_real), + .doppler_imag(doppler_imag), + .doppler_valid(doppler_valid), + .cfar_detect_class(cfar_detect_class), + .cfar_valid(cfar_valid), + .range_bin_in(range_bin_in), + .doppler_bin_in(doppler_bin_in), + .frame_complete(frame_complete), + .ft_data(ft_data), + .ft_rxf_n(ft_rxf_n), + .ft_txe_n(ft_txe_n), + .ft_rd_n(ft_rd_n), + .ft_wr_n(ft_wr_n), + .ft_oe_n(ft_oe_n), + .ft_siwu(ft_siwu), + .ft_clk(ft_clk), + .cmd_data(cmd_data), + .cmd_valid(cmd_valid), + .cmd_opcode(cmd_opcode), + .cmd_addr(cmd_addr), + .cmd_value(cmd_value), + .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), + .status_range_mode(status_range_mode), + .status_chirps_mismatch(status_chirps_mismatch), + .status_self_test_flags(status_self_test_flags), + .status_self_test_detail(status_self_test_detail), + .status_self_test_busy(status_self_test_busy), + .status_agc_current_gain(status_agc_current_gain), + .status_agc_peak_magnitude(status_agc_peak_magnitude), + .status_agc_saturation_count(status_agc_saturation_count), + .status_agc_enable(status_agc_enable), + .status_range_decim_watchdog(status_range_decim_watchdog), + .status_ddc_cic_fir_overrun(status_ddc_cic_fir_overrun), + .status_cfar_alpha_soft(status_cfar_alpha_soft), + .status_detect_threshold_soft(status_detect_threshold_soft), + .status_detect_count_cand(status_detect_count_cand) + ); + + // Capture egress bytes. egress_count counts ALL emitted bytes (used by + // TEST 4 to verify total frame length). egress_bytes only buffers the + // first 36 (header + a few status bytes — enough for TESTS 2, 3, 4 to + // index byte-level checks). + reg [7:0] egress_bytes [0:35]; + integer egress_count = 0; + always @(posedge ft_clk) begin + if (!ft_wr_n && !ft_txe_n) begin + if (egress_count < 36) + egress_bytes[egress_count] <= ft_data; + egress_count <= egress_count + 1; + end + end + + task check_b; + input [127:0] tag; + input cond; + begin + if (cond) begin + $display("[PASS] %0s", tag); + pass = pass + 1; + end else begin + $display("[FAIL] %0s", tag); + fail = fail + 1; + end + end + endtask + + task wait_clk; + input integer n; + integer i; + begin + for (i = 0; i < n; i = i + 1) @(posedge clk); + end + endtask + + // 4-byte command bus driver (host → FPGA, ft_clk domain). + // Read FSM: RD_IDLE (edge 1: see rxf_n, schedule transition) → RD_OE_ASSERT + // (edge 2: schedule RD_READING) → RD_READING (edges 3,4,5,6: each samples + // ft_data via NBA). Byte N must be on the bus at edge (N+2) of the sequence. + task send_cmd; + input [7:0] op; + input [7:0] addr; + input [15:0] val; + begin + @(posedge ft_clk); #1; // Edge 0 + ft_rxf_n = 1'b0; + ft_data_drive = op; + ft_data_drive_en = 1'b1; + @(posedge ft_clk); #1; // Edge 1: RD_IDLE → RD_OE_ASSERT (NBA) + @(posedge ft_clk); #1; // Edge 2: RD_OE_ASSERT → RD_READING (NBA) + @(posedge ft_clk); #1; // Edge 3: RD_READING samples op (1st) + ft_data_drive = addr; + @(posedge ft_clk); #1; // Edge 4: samples addr (2nd) + ft_data_drive = val[15:8]; + @(posedge ft_clk); #1; // Edge 5: samples val_hi (3rd) + ft_data_drive = val[7:0]; + @(posedge ft_clk); #1; // Edge 6: samples val_lo (4th, transitions out) + ft_rxf_n = 1'b1; + ft_data_drive_en = 1'b0; + wait_clk(20); // CDC propagation to clk domain + end + endtask + + initial begin + $display("\n========== tb_usb_protocol_v2 =========="); + // Reset + reset_n = 1'b0; + ft_reset_n = 1'b0; + wait_clk(10); + reset_n = 1'b1; + ft_reset_n = 1'b1; + wait_clk(20); + + // ------------------------------------------------------------- + // TEST 1: Opcode 0x2D (host_cfar_alpha_soft) round trip + // ------------------------------------------------------------- + $display("\n[TEST 1] Opcode 0x2D (cfar_alpha_soft) round trip"); + send_cmd(`RP_OP_CFAR_ALPHA_SOFT, 8'h00, 16'h0024); // 0x24 in Q4.4 = 2.25 + check_b("T1.1: cmd_opcode=0x2D", cmd_opcode == 8'h2D); + check_b("T1.2: cmd_value lower 8b=0x24", cmd_value[7:0] == 8'h24); + + // ------------------------------------------------------------- + // TEST 2: Frame header v2 — 9 bytes, byte1=0x02 + // ------------------------------------------------------------- + $display("\n[TEST 2] Frame header v2 emission"); + // Disable all stream sections (HDR -> FOOTER fast drain) + stream_control = 6'b000_000; + wait_clk(50); // Let CDC propagate + egress_count = 0; + @(posedge clk); + frame_complete = 1'b1; + @(posedge clk); + frame_complete = 1'b0; + // Wait for full frame drain (10 bytes = 10 ft_clk + slack) + wait_clk(150); + check_b("T2.1: byte0 = 0xAA", egress_bytes[0] == 8'hAA); + check_b("T2.2: byte1 = 0x02 (ver)", egress_bytes[1] == `RP_USB_PROTOCOL_VERSION); + check_b("T2.3: byte2 = stream flags=0", egress_bytes[2] == 8'h00); + // Byte 3-4 = frame_number snapshot. snapshot latches OLD frame_number + // at frame_complete (NBA), so first frame emitted carries fn=0. + check_b("T2.4: byte3 = fn[15:8]=0", egress_bytes[3] == 8'h00); + check_b("T2.5: byte4 = fn[7:0]=0", egress_bytes[4] == 8'h00); + check_b("T2.6: byte5/6 = range_bins=512", + {egress_bytes[5], egress_bytes[6]} == 16'd512); + check_b("T2.7: byte7/8 = doppler_bins=48", + {egress_bytes[7], egress_bytes[8]} == 16'd48); + check_b("T2.8: byte9 = footer 0x55", egress_bytes[9] == 8'h55); + + // ------------------------------------------------------------- + // TEST 3: Status packet length = 30 bytes; word[6] carries telemetry + // ------------------------------------------------------------- + $display("\n[TEST 3] Status packet length 30B + word[6] PR-G fields"); + egress_count = 0; + @(posedge clk); + status_request = 1'b1; + @(posedge clk); + status_request = 1'b0; + wait_clk(300); // Wait for status drain + check_b("T3.1: byte0 = 0xBB (status header)", egress_bytes[0] == 8'hBB); + check_b("T3.2: byte29 = 0x55 (footer)", egress_bytes[29] == 8'h55); + check_b("T3.3: status_words[6] count_cand[15:8]=0", egress_bytes[25] == 8'h00); + check_b("T3.4: status_words[6] count_cand[7:0]=42", egress_bytes[26] == 8'd42); + check_b("T3.5: status_words[6] thr_soft[15:8]=0x0A", egress_bytes[27] == 8'h0A); + check_b("T3.6: status_words[6] thr_soft[7:0]=0xBC", egress_bytes[28] == 8'hBC); + // alpha_soft (0x18) packed into word[4][9:2] → byte at index 19,20 + // word[4] = {gain[3:0], peak[7:0], sat[7:0], en, mismatch, alpha_soft[7:0], range_mode[1:0]} + // bits[9:2] = alpha_soft. byte[19] = word[4][15:8], byte[20] = word[4][7:0] + // alpha_soft sits in byte[20][7:2] | byte[19][1:0] — let's just check mid bytes are non-zero + // when alpha_soft=0x18 (b0001_1000): bits[9:2] of word[4] = 8'h18, so: + // word[4][7:0] = {alpha_soft[7:0], range_mode[1:0]} = {8'h18, 2'b00} = 8'h60 + check_b("T3.7: status_words[4][7:0] = alpha_soft<<2 = 0x60 (alpha=0x18)", + egress_bytes[20] == 8'h60); + + // ------------------------------------------------------------- + // TEST 4: full-frame header/body length consistency (PR-G trim) + // ------------------------------------------------------------- + $display("\n[TEST 4] Full-frame header/body length consistency (PR-G trim)"); + // Re-enable all 3 streams so HDR + range + doppler + detect + footer + // are all emitted. We don't fill BRAMs — only the byte count matters. + stream_control = 6'b000_111; + wait_clk(50); // CDC propagate + egress_count = 0; + @(posedge clk); + frame_complete = 1'b1; + @(posedge clk); + frame_complete = 1'b0; + // Worst-case drain: 9 + 1024 + 49152 + 6144 + 1 = 56330 bytes. + // Each doppler byte takes ~1 ft_clk (MSB then LSB, both at 60 MHz). + // Detect = 1 byte/ft_clk. Plus FSM transitions, so allow ~70k ft_clk. + wait_clk(120_000); // ~1.2 ms in clk-domain (covers 60 MHz drain) + check_b("T4.1: egress_count == expected total", + egress_count == (`RP_FRAME_HDR_BYTES + + `RP_NUM_RANGE_BINS * 2 + + `RP_NUM_RANGE_BINS * `RP_NUM_DOPPLER_BINS * 2 + + (`RP_NUM_RANGE_BINS * `RP_NUM_DOPPLER_BINS * 2) / 8 + + 1)); + check_b("T4.2: header byte0 = 0xAA (frame still framed correctly)", + egress_bytes[0] == 8'hAA); + check_b("T4.3: header byte1 = protocol version 0x02", + egress_bytes[1] == `RP_USB_PROTOCOL_VERSION); + check_b("T4.4: header byte5/6 = range_bins=512", + {egress_bytes[5], egress_bytes[6]} == 16'd512); + check_b("T4.5: header byte7/8 = doppler_bins=48", + {egress_bytes[7], egress_bytes[8]} == 16'd48); + // Sanity: doppler section must NOT be the old 65536-byte padded size. + // Old (pre-trim) total was 9 + 1024 + 65536 + 8192 + 1 = 74762. + // New (post-trim) total = 56330. Catch if FSM regresses to padded. + check_b("T4.6: emitted bytes < pre-trim padded total (74762)", + egress_count < 74762); + $display(" egress_count = %0d (expected 56330)", egress_count); + + // ------------------------------------------------------------- + // TEST 5: MEDIUM ladder timing opcodes (PR-G G2) — round-trip via cmd bus + // ------------------------------------------------------------- + $display("\n[TEST 5] MEDIUM ladder timing opcodes (0x17, 0x18)"); + send_cmd(`RP_OP_MEDIUM_CHIRP_CYCLES, 8'h00, 16'd750); + check_b("T5.1: cmd_opcode=0x17 (MEDIUM_CHIRP_CYCLES)", cmd_opcode == 8'h17); + check_b("T5.2: cmd_value=750", cmd_value == 16'd750); + + send_cmd(`RP_OP_MEDIUM_LISTEN_CYCLES, 8'h00, 16'd16500); + check_b("T5.3: cmd_opcode=0x18 (MEDIUM_LISTEN_CYCLES)", cmd_opcode == 8'h18); + check_b("T5.4: cmd_value=16500", cmd_value == 16'd16500); + + // ------------------------------------------------------------- + // Done + // ------------------------------------------------------------- + $display("\n-----------------------------------------------------------"); + $display("RESULTS: %0d PASS, %0d FAIL", pass, fail); + $display("-----------------------------------------------------------"); + if (fail == 0) $display("[OVERALL PASS]"); else $display("[OVERALL FAIL]"); + $finish; + end + + // Watchdog + initial begin + #20_000_000; + $display("[TIMEOUT] tb_usb_protocol_v2 watchdog"); + $finish; + end + +endmodule 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 e37743e..be44e4f 100644 --- a/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v +++ b/9_Firmware/9_2_FPGA/usb_data_interface_ft2232h.v @@ -8,86 +8,72 @@ * FT2232H USB 2.0 Hi-Speed FIFO Interface (245 Synchronous FIFO Mode) * Channel A only — 8-bit data bus, 60 MHz CLKOUT from FT2232H. * - * BULK PER-FRAME PROTOCOL (replaces legacy per-sample 11-byte packets): + * BULK PER-FRAME PROTOCOL V2 (PR-G — single canonical encoding): * - * Frame packet (FPGA→Host): variable length, up to ~35 KB + * Frame packet (FPGA→Host): variable length, up to 74,762 bytes * Byte 0: 0xAA (frame start header) - * Byte 1: Format flags {2'b0, sparse_det, mag_only, stream_cfar, stream_doppler, stream_range} - * Bytes 2-3: Frame number (16-bit, MSB first) - * Bytes 4-5: Range bin count (16-bit, MSB first) = 512 - * Bytes 6-7: Doppler bin count (16-bit, MSB first) = 32 + * Byte 1: 0x02 (PROTOCOL VERSION — host MUST reject any other value) + * Byte 2: Stream flags {5'b0, stream_cfar, stream_doppler, stream_range} + * Bytes 3-4: Frame number (uint16, MSB first) + * Bytes 5-6: Range bin count (uint16, MSB first) = `RP_NUM_RANGE_BINS` (512) + * Bytes 7-8: Doppler bin count (uint16, MSB first) = `RP_NUM_DOPPLER_BINS` (48) * * [If stream_range (bit 0):] - * Next 1024 bytes: Range profile, 512 × 16-bit magnitude, MSB first + * Next 1024 bytes: range profile, 512 × uint16 Manhattan magnitude, MSB first. * * [If stream_doppler (bit 1):] - * Next 32768 bytes: Doppler magnitude, 512×32 × 16-bit, row-major, MSB first + * Next 65536 bytes: doppler magnitude, 32768 cells × uint16, row-major + * (range_bin slowest, doppler_bin fastest), MSB first. Cells indexed + * [0..47] are real Doppler bins; cells [48..63] within each range are + * the power-of-2 padding from PR-F (always emitted as 0x0000). * * [If stream_cfar (bit 2):] - * Next 2048 bytes: Detection flags, 512×32 bits packed into bytes, MSB-first bit order + * Next 8192 bytes: detect_class bitmap, 32768 cells × 2 bits, MSB-first + * packing. Each byte holds 4 cells: + * byte[N]: bits[7:6]=cell[4*N], bits[5:4]=cell[4*N+1], + * bits[3:2]=cell[4*N+2], bits[1:0]=cell[4*N+3] + * Cell encoding (per `RP_DETECT_*`): + * 2'b00 = NONE (below soft threshold) + * 2'b01 = CANDIDATE (above soft, below confirm — host re-cues) + * 2'b10 = CONFIRMED (above confirm threshold — track-eligible) + * 2'b11 = RESERVED (must not be emitted by RTL) * * Last byte: 0x55 (frame end footer) * - * INERT FLAGS — mag_only (bit 3) and sparse_det (bit 4) (AUDIT-C9): - * The wire format byte 1 reserves these two bits for future encodings: - * - mag_only=0 was meant to switch the doppler section to 65536 B - * full-I/Q (16-bit I + 16-bit Q per cell, row-major, MSB first). - * - sparse_det=1 was meant to switch the CFAR section to a - * variable-length list: 2 B count N + N×6 B (range, doppler, mag). - * Neither encoding is implemented in the write FSM below — the FSM - * always emits 32768 B mag and 2048 B dense bitmap regardless of the - * flag bits. To eliminate the foot-gun, `radar_system_top.v` opcode - * 0x04 force-clamps mag_only=1 and sparse_det=0 in `host_stream_control` - * when USB_MODE=1. A SIMULATION-only assertion at the bottom of this - * module fires if either bit ever leaves its clamped value, in case a - * future patch adds a path that bypasses the host register clamp. - * - * Reasons differ between the two: - * - Full-I/Q is constrained by FPGA resources: it needs a new - * ~28-BRAM18 I/Q buffer (16384 cells × 32-bit) which may not fit - * on the 50T (currently ~78% BRAM18 utilisation after wiring the - * Xilinx FFT IP). USB 2.0 bandwidth is also tight: 12.21 MB/s vs - * the conservative 8 MB/s sustained budget. Both gating items. - * - Sparse-list is feasible — bandwidth-wise it's smaller than the - * dense bitmap for any frame with fewer than ~341 detections - * (typical scenes produce 10-200), and memory-wise it costs - * ~1 BRAM18 with MAX_DETECTIONS=256. The absence is just - * unimplemented RTL work (a small detection-list BRAM + a new - * WR_DETECT_SPARSE FSM state), not a hardware constraint. - * See the open-defects ledger for the follow-up work items. - * - * Status packet (FPGA→Host): 26 bytes (unchanged from legacy) + * Status packet (FPGA→Host): 30 bytes (PR-G: was 26 in v1, +4 for soft tier) * Byte 0: 0xBB (status header) - * Bytes 1-24: 6 × 32-bit status words, MSB first - * Byte 25: 0x55 (footer) + * Bytes 1-28: 7 × 32-bit status words, MSB first + * word[6] = {detect_count_cand[15:0], detect_threshold_soft[15:0]} + * Byte 29: 0x55 (footer) * * Command (Host→FPGA): 4 bytes received sequentially (unchanged) - * Byte 0: opcode[7:0] + * Byte 0: opcode[7:0] (see RP_OP_* in radar_params.vh) * Byte 1: addr[7:0] * Byte 2: value[15:8] * Byte 3: value[7:0] * * MEMORY ARCHITECTURE: - * - Doppler magnitude BRAM: 512×32 = 16384 entries × 16-bit = 32 KB (~14 BRAM18) + * - Doppler magnitude BRAM: 32768 entries × 16-bit = 64 KB (~28 BRAM18 on 50T) * Written in clk (100 MHz) domain as Doppler cells arrive. * Read in ft_clk (60 MHz) domain during USB bulk transfer. - * - Range profile buffer: 512 × 16-bit = 1 KB (~1 BRAM18) + * - Range profile buffer: 512 × 16-bit = 1 KB (1 BRAM18) * Written in clk domain from range_valid events. - * - Detection flag buffer: 512×32 = 16384 bits = 2048 bytes (~1 BRAM18) - * Written in clk domain from cfar_valid events. + * - Detect-class buffer: 32768 cells × 2 bits = 65536 bits = 8192 bytes (4 BRAM18) + * Written in clk domain from cfar_valid events via 3-cycle RMW pipeline. * - * BANDWIDTH BUDGET (current production: mag_only=1, all streams): - * Header: 8 B + Range: 1024 B + Doppler: 32768 B + CFAR: 2048 B + Footer: 1 B - * = 35,849 bytes/frame × ~178 fps = 6.38 MB/s - * FT2232H 245-Sync-FIFO sustained budget ~8 MB/s conservative (FTDI - * AN_232B-04). 80% utilisation; full-I/Q (12.21 MB/s) would not fit at - * the conservative budget and is why mag_only is force-clamped to 1. + * BANDWIDTH BUDGET (PR-G v2, all streams): + * Header: 9 B + Range: 1024 B + Doppler: 65536 B + Detect: 8192 B + Footer: 1 B + * = 74,762 bytes/frame × ~119 fps (3-subframe rate post-PR-F) ≈ 8.9 MB/s + * FT2232H 245-Sync-FIFO conservative budget ~8 MB/s (FTDI AN_232B-04, 80% + * utilisation); practical sustained throughput is 30–40 MB/s on a tuned + * host. Sufficient headroom even with the conservative budget overshoot. * * CDC STRATEGY: - * - Frame data: Written to dual-port BRAM at 100 MHz, read at 60 MHz (inherently CDC-safe) - * - frame_ready flag: Toggle CDC (100 MHz → 60 MHz), same as status_request - * - stream_control: 2-stage level sync (changes infrequently) - * - Commands: Read FSM in ft_clk domain, output CDC'd by consumer (unchanged) + * - Frame data: Written to dual-port BRAM at 100 MHz, read at 60 MHz (inherently CDC-safe). + * - frame_ready flag: Toggle CDC (100 MHz → 60 MHz), same as status_request. + * - stream_control: 2-stage level sync (changes infrequently). + * - status_*_soft / status_*_cand: 2-stage level sync (slow-changing per-frame values). + * - Commands: Read FSM in ft_clk domain, output CDC'd by consumer (unchanged). * * Clock domains: * clk = 100 MHz system clock (radar data domain) @@ -105,7 +91,8 @@ module usb_data_interface_ft2232h ( input wire [15:0] doppler_real, input wire [15:0] doppler_imag, input wire doppler_valid, - input wire cfar_detection, + // PR-G: 2-bit class replaces single cfar_detection bit. + input wire [`RP_DETECT_CLASS_WIDTH-1:0] cfar_detect_class, input wire cfar_valid, // New inputs for bulk frame protocol (clk domain) @@ -170,7 +157,13 @@ module usb_data_interface_ft2232h ( // of the gpio_dig7 split. 2-stage level CDC into ft_clk; sticky/slow- // changing source so 2-FF sync 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-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) + input wire [16:0] status_detect_threshold_soft, // PR-G: candidate-tier threshold (last frame) + input wire [15:0] status_detect_count_cand // PR-G: candidate count (last frame) ); // ============================================================================ @@ -191,20 +184,38 @@ localparam DOPPLER_BIN_BITS = `RP_DOPPLER_BIN_WIDTH;// 6 (PR-F) localparam FRAME_CELLS = NUM_RANGE_BINS * (1 << DOPPLER_BIN_BITS); // 32768 (PR-F) // Frame-cell address widths. localparam FRAME_ADDR_W = RANGE_BIN_BITS + DOPPLER_BIN_BITS; // 15 -localparam DETECT_BYTE_ADDR_W = FRAME_ADDR_W - 3; // 12 -localparam DETECT_BYTE_LAST = (FRAME_CELLS / 8) - 1; // 4095 -// Frame header: 8 bytes (0xAA + flags + frame_num[2] + range_bins[2] + doppler_bins[2]) -localparam FRAME_HDR_BYTES = 8; +// PR-G: detect section is 2 bits/cell instead of 1 bit/cell. +// 32768 cells * 2 bits = 65536 bits = 8192 bytes; needs 13-bit byte address. +// Cell-to-byte mapping: byte_addr = bit_addr[15:3] = {range_bin[8:0], doppler_bin[5:2]} +// Sub-byte position (bits within byte) = (3 - doppler_bin[1:0]) * 2, MSB-first. +localparam DETECT_BITS_PER_CELL = `RP_DETECT_BITS_PER_CELL; // 2 +localparam DETECT_BYTE_ADDR_W = FRAME_ADDR_W + 1 - 3; // 13 +localparam DETECT_BYTE_LAST = ((FRAME_CELLS * DETECT_BITS_PER_CELL) / 8) - 1; // 8191 +localparam DETECT_BIT_ADDR_W = FRAME_ADDR_W + 1; // 16 + +// Frame header: 9 bytes (0xAA + ver + flags + frame_num[2] + range_bins[2] + doppler_bins[2]) +localparam FRAME_HDR_BYTES = `RP_FRAME_HDR_BYTES; // 9 (PR-G) // Range profile section: 512 × 2 = 1024 bytes localparam RANGE_SECTION_BYTES = NUM_RANGE_BINS * 2; -// Doppler mag section: 16384 × 2 = 32768 bytes -localparam DOPPLER_MAG_SECTION_BYTES = FRAME_CELLS * 2; -// Detection flag section: 16384 bits = 2048 bytes -localparam DETECT_SECTION_BYTES = FRAME_CELLS / 8; +// Doppler mag section: 512 range × 48 doppler × 2 = 49152 bytes (PR-G). +// FSM iterates only valid (range, doppler) cells — the next-pow-2 BRAM +// padding (doppler 48..63 per range, 8192 dead cells) is skipped on the +// wire so the body length matches the header's `doppler_bins=48` field. +localparam DOPPLER_MAG_SECTION_BYTES = NUM_RANGE_BINS * NUM_DOPPLER_BINS * 2; +// Last valid doppler index — used by WR_DOPPLER_DATA to wrap to next range. +localparam [DOPPLER_BIN_BITS-1:0] DOP_BIN_LAST = NUM_DOPPLER_BINS[DOPPLER_BIN_BITS-1:0] - 1'b1; +// Detect class section: emit only valid range × doppler cells. +// Per range bin: NUM_DOPPLER_BINS × DETECT_BITS_PER_CELL / 8 bytes (rounded +// up). For 48 doppler × 2 bits = 96 bits = 12 bytes per range. The 4 padded +// bytes per range (doppler 48..63 indices) are skipped on the wire so the +// host can compute body length deterministically from the header. +localparam VALID_DET_BYTES_PER_RANGE = (NUM_DOPPLER_BINS * DETECT_BITS_PER_CELL + 7) / 8; // 12 +localparam DETECT_SECTION_BYTES = NUM_RANGE_BINS * VALID_DET_BYTES_PER_RANGE; // 6144 +localparam [3:0] DET_BYTE_LAST_PER_RANGE = VALID_DET_BYTES_PER_RANGE[3:0] - 4'd1; // 11 -// Status packet: 26 bytes (unchanged) -localparam STATUS_PKT_LEN = 5'd26; +// Status packet: 30 bytes (PR-G: 7 × 32-bit words + header + footer) +localparam STATUS_PKT_LEN = 5'd30; // ============================================================================ // WRITE FSM STATES (FPGA → Host, ft_clk domain) @@ -302,31 +313,24 @@ always @(posedge ft_clk) begin end // ============================================================================ -// DETECTION FLAG BRAM (clk write, ft_clk read) +// DETECT-CLASS BRAM (clk write, ft_clk read) — PR-G: 2 bits per cell // ============================================================================ -// 16384 bits stored as 2048 × 8-bit bytes. -// Write: individual bit-set on cfar_valid with cfar_detection=1. -// Clear: bulk clear on frame_complete (start of new frame). -// Address = {range_bin[8:0], doppler_bin[4:2]} = byte address (11 bits, 2048 entries) -// Bit position = doppler_bin[1:0] within sub-byte ... actually let's use -// a simpler scheme: 16384 entries × 1-bit, but that doesn't map well to BRAM. +// FRAME_CELLS cells × 2 bits = 65536 bits stored as 8192 × 8-bit bytes. +// Each byte packs 4 consecutive cells (MSB-first): +// byte[N] bits[7:6] = cell[4*N + 0] (doppler_bin[1:0] = 00) +// byte[N] bits[5:4] = cell[4*N + 1] (doppler_bin[1:0] = 01) +// byte[N] bits[3:2] = cell[4*N + 2] (doppler_bin[1:0] = 10) +// byte[N] bits[1:0] = cell[4*N + 3] (doppler_bin[1:0] = 11) +// Cell encoding per `RP_DETECT_*` (2'b00=NONE / 2'b01=CAND / 2'b10=CONFIRM). // -// Better: Store as 2048 × 8-bit. Each byte holds 8 consecutive detection bits. -// Bit address = {range_bin, doppler_bin} = 14-bit. Byte addr = bit_addr[13:3]. -// Bit position = bit_addr[2:0]. -// On write: read-modify-write (set bit). On frame clear: bulk zero. -// -// For simplicity and BRAM efficiency, we use a separate approach: -// Store detections in a small register file and pack during transfer. -// With 512×32=16384 bits, that's 2048 bytes — fits in 1 BRAM18. -// -// IMPLEMENTATION: We use the BRAM in byte-write mode. On cfar_valid, we do -// a 1-cycle read then 1-cycle write-back with the bit set. This works because -// CFAR outputs arrive one cell per clock cycle (sequential scan). +// Write path: 3-cycle read-modify-write on cfar_valid (idle → read → write). +// Cell index within byte = doppler_bin_in[1:0] +// MSB-first shift = (3 - cell_index) * 2 (cell 0 lands in [7:6], cell 3 in [1:0]) +// Clear path: bulk byte-zero on frame_complete (steps 1 byte/cycle). -(* ram_style = "block" *) reg [7:0] detect_bram [0:DETECT_BYTE_LAST]; // PR-F: 3072 entries (was 2048) +(* ram_style = "block" *) reg [7:0] detect_bram [0:DETECT_BYTE_LAST]; // PR-G: 8192 entries (was 4096) -reg [DETECT_BYTE_ADDR_W-1:0] detect_wr_addr; // PR-F: 12-bit byte addr (was 11) +reg [DETECT_BYTE_ADDR_W-1:0] detect_wr_addr; // PR-G: 13-bit byte addr (was 12) reg [7:0] detect_wr_data; reg detect_wr_en; @@ -335,7 +339,7 @@ always @(posedge clk) begin detect_bram[detect_wr_addr] <= detect_wr_data; end -reg [DETECT_BYTE_ADDR_W-1:0] detect_rd_addr; // PR-F: 12-bit byte addr (was 11) +reg [DETECT_BYTE_ADDR_W-1:0] detect_rd_addr; // PR-G: 13-bit byte addr (was 12) reg [7:0] detect_rd_data; always @(posedge ft_clk) begin @@ -343,10 +347,9 @@ always @(posedge ft_clk) begin end // Detection BRAM read-modify-write pipeline (clk domain) -reg [DETECT_BYTE_ADDR_W-1:0] detect_rmw_addr; // PR-F: 12-bit byte addr (was 11) -reg [7:0] detect_rmw_rd; -reg [2:0] detect_rmw_bit; -reg detect_rmw_value; +reg [DETECT_BYTE_ADDR_W-1:0] detect_rmw_addr; // PR-G: 13-bit byte addr (was 12) +reg [1:0] detect_rmw_cell_idx; // PR-G: 0..3, cell within byte +reg [`RP_DETECT_CLASS_WIDTH-1:0] detect_rmw_value; // PR-G: 2-bit class (was 1-bit) reg [1:0] detect_rmw_state; // 0=idle, 1=read, 2=write // Synchronous read for RMW (clk domain, separate from ft_clk read port) @@ -389,7 +392,8 @@ wire [15:0] range_mag = range_manhattan[16] ? 16'hFFFF : range_manhattan[15:0]; reg [15:0] frame_number; // Incrementing frame counter reg frame_ready_toggle; // Toggle CDC: frame ready for USB transfer reg frame_filling; // 1 = currently accumulating frame data -reg [FRAME_ADDR_W-1:0] detect_clear_addr; // PR-F: 15-bit bit-counter; FRAME_CELLS = NUM_RANGE_BINS * (1<> ({1'b0, detect_rmw_cell_idx} << 1))) + | (({6'b0, detect_rmw_value} << ((3 - {1'b0, detect_rmw_cell_idx}) << 1))); detect_rmw_state <= 2'd0; end default: detect_rmw_state <= 2'd0; @@ -489,14 +493,16 @@ always @(posedge clk or negedge reset_n) begin range_write_counter <= range_write_counter + {{(RANGE_BIN_BITS-1){1'b0}}, 1'b1}; end - // === CFAR detection write (read-modify-write) === - // PR-F: bit_addr = {range_bin_in[8:0], doppler_bin_in[5:0]} = 15-bit. - // byte_addr = bit_addr[14:3] (12 bits), bit_pos = bit_addr[2:0]. + // === CFAR detect-class write (read-modify-write) === + // PR-G: 2 bits per cell. bit_addr = {range_bin[8:0], doppler_bin[5:0]} * 2 + // = {range_bin[8:0], doppler_bin[5:0], 1'b0} (16 bits). + // byte_addr = bit_addr[15:3] = {range_bin[8:0], doppler_bin[5:2]} (13 bits) + // cell_idx within byte = doppler_bin[1:0] (0..3, MSB-first ordering) if (cfar_valid && frame_filling && detect_rmw_state == 2'd0 && !detect_clearing) begin - detect_rmw_addr <= {range_bin_in, doppler_bin_in[DOPPLER_BIN_BITS-1:3]}; - detect_rmw_bit <= doppler_bin_in[2:0]; - detect_rmw_value <= cfar_detection; - detect_rmw_state <= 2'd1; + detect_rmw_addr <= {range_bin_in, doppler_bin_in[DOPPLER_BIN_BITS-1:2]}; + detect_rmw_cell_idx <= doppler_bin_in[1:0]; + detect_rmw_value <= cfar_detect_class; + detect_rmw_state <= 2'd1; end // === Frame complete: latch frame, signal ft_clk domain === @@ -603,10 +609,20 @@ reg status_toggle_prev; wire frame_ready_ft = frame_ready_sync[2] ^ frame_ready_prev; wire status_req_ft = status_toggle_sync[2] ^ status_toggle_prev; -// --- Stream control CDC (6-bit, 2-stage level sync) --- +// --- Stream control CDC (6-bit wire, but only [2:0] used in PR-G v2; [5:3] reserved=0). +// 2-stage level sync (changes infrequently). --- (* ASYNC_REG = "TRUE" *) reg [5:0] stream_ctrl_sync_0; (* ASYNC_REG = "TRUE" *) reg [5:0] stream_ctrl_sync_1; +// --- PR-G: 2-tier CFAR telemetry CDC (clk → ft_clk, 2-stage level sync). +// Slow-changing per-frame values; sufficient for status readback. --- +(* ASYNC_REG = "TRUE" *) reg [7:0] alpha_soft_sync_0; +reg [7:0] alpha_soft_sync_1; +(* ASYNC_REG = "TRUE" *) reg [16:0] det_thr_soft_sync_0; +reg [16:0] det_thr_soft_sync_1; +(* ASYNC_REG = "TRUE" *) reg [15:0] det_count_cand_sync_0; +reg [15:0] det_count_cand_sync_1; + // --- AUDIT-C12: frame_drop_count CDC (slow-changing 7-bit value, 2-stage sync) --- (* ASYNC_REG = "TRUE" *) reg [6:0] frame_drop_sync_0; reg [6:0] frame_drop_sync_1; @@ -621,39 +637,42 @@ reg ddc_cic_fir_overrun_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]; -wire stream_mag_only = stream_ctrl_sync_1[3]; -wire stream_sparse_det = stream_ctrl_sync_1[4]; -// Bit 5 reserved -// NOTE: Phase 1 write FSM always sends magnitude-only range/Doppler and -// dense detection bitmap. The mag_only and sparse_det bits are included in -// the frame header for the Python parser but are not yet honored by the -// write FSM. Phase 2 will add I/Q and sparse detection paths. +// Bits [5:3] reserved=0 in PR-G v2. The legacy mag_only/sparse_det/frame_decimate +// flags were retired with v1 — there is one canonical encoding now (Manhattan-mag +// doppler + 2-bit dense detect). // --- Frame metadata snapshot (latched in clk domain, stable for ft_clk read) --- reg [15:0] frame_number_snapshot; -reg [5:0] stream_flags_snapshot; +reg [2:0] stream_flags_snapshot; // PR-G: 3 bits used (range/doppler/cfar) always @(posedge clk or negedge reset_n) begin if (!reset_n) begin frame_number_snapshot <= 16'd0; - stream_flags_snapshot <= `RP_STREAM_CTRL_DEFAULT; + stream_flags_snapshot <= 3'b111; // PR-G: all 3 streams on (range|doppler|cfar) end else if (frame_complete) begin frame_number_snapshot <= frame_number; - stream_flags_snapshot <= stream_control; + stream_flags_snapshot <= stream_control[2:0]; // PR-G: ignore reserved [5:3] end end -// --- Status snapshot (ft_clk domain) --- -reg [31:0] status_words [0:5]; +// --- Status snapshot (ft_clk domain) — PR-G: 7 words (was 6) --- +reg [31:0] status_words [0:6]; // Byte counter for write FSM (needs to be wide enough for largest section) reg [15:0] wr_byte_idx; // BRAM read address for frame transfer -reg [FRAME_ADDR_W-1:0] bram_rd_cell; // PR-F: 15-bit cell index 0..24575 -reg [RANGE_BIN_BITS-1:0] range_rd_idx; // Range bin index 0..511 -reg wr_byte_phase; // 0=MSB, 1=LSB for 16-bit values -reg [DETECT_BYTE_ADDR_W-1:0] detect_rd_idx; // PR-F: 12-bit byte index 0..3071 +reg [RANGE_BIN_BITS-1:0] range_rd_idx; // Range section: 0..511 +// PR-G: nested counters for doppler section so we emit only valid cells +// (range 0..511, doppler 0..47) and skip the BRAM padding at doppler 48..63. +reg [RANGE_BIN_BITS-1:0] dop_range_idx; // Doppler section outer: 0..511 +reg [DOPPLER_BIN_BITS-1:0] dop_doppler_idx; // Doppler section inner: 0..47 +reg wr_byte_phase; // 0=MSB, 1=LSB for 16-bit values +// PR-G: nested counters for detect section so we emit only valid bytes +// (12 per range, doppler indices 0..47) and skip the 4 padded bytes from +// doppler 48..63. detect_rd_addr is composed from these. +reg [RANGE_BIN_BITS-1:0] det_range_idx; // 0..511 +reg [3:0] det_doppler_byte_idx; // 0..11 (= NUM_DOPPLER_BINS*2/8 - 1) // ============================================================================ // CLOCK-ACTIVITY WATCHDOG (clk domain) @@ -737,17 +756,26 @@ 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; - for (si = 0; si < 6; si = si + 1) + // PR-G: 2-tier CFAR telemetry CDC reset + alpha_soft_sync_0 <= 8'd0; + alpha_soft_sync_1 <= 8'd0; + det_thr_soft_sync_0 <= 17'd0; + det_thr_soft_sync_1 <= 17'd0; + det_count_cand_sync_0 <= 16'd0; + det_count_cand_sync_1 <= 16'd0; + for (si = 0; si < 7; si = si + 1) status_words[si] <= 32'd0; - wr_state <= WR_IDLE; - wr_byte_idx <= 16'd0; - wr_byte_phase <= 1'b0; - bram_rd_cell <= {FRAME_ADDR_W{1'b0}}; - range_rd_idx <= {RANGE_BIN_BITS{1'b0}}; - range_rd_addr <= {RANGE_BIN_BITS{1'b0}}; - detect_rd_idx <= {DETECT_BYTE_ADDR_W{1'b0}}; - detect_rd_addr <= {DETECT_BYTE_ADDR_W{1'b0}}; - mag_rd_addr <= {FRAME_ADDR_W{1'b0}}; + wr_state <= WR_IDLE; + wr_byte_idx <= 16'd0; + wr_byte_phase <= 1'b0; + dop_range_idx <= {RANGE_BIN_BITS{1'b0}}; + dop_doppler_idx <= {DOPPLER_BIN_BITS{1'b0}}; + range_rd_idx <= {RANGE_BIN_BITS{1'b0}}; + range_rd_addr <= {RANGE_BIN_BITS{1'b0}}; + det_range_idx <= {RANGE_BIN_BITS{1'b0}}; + det_doppler_byte_idx <= 4'd0; + detect_rd_addr <= {DETECT_BYTE_ADDR_W{1'b0}}; + mag_rd_addr <= {FRAME_ADDR_W{1'b0}}; rd_state <= RD_IDLE; rd_byte_cnt <= 2'd0; rd_cmd_complete <= 1'b0; @@ -787,6 +815,14 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin ddc_cic_fir_overrun_sync_0 <= status_ddc_cic_fir_overrun; ddc_cic_fir_overrun_sync_1 <= ddc_cic_fir_overrun_sync_0; + // PR-G: 2-tier CFAR telemetry CDC (clk → ft_clk for status read) + alpha_soft_sync_0 <= status_cfar_alpha_soft; + alpha_soft_sync_1 <= alpha_soft_sync_0; + det_thr_soft_sync_0 <= status_detect_threshold_soft; + det_thr_soft_sync_1 <= det_thr_soft_sync_0; + det_count_cand_sync_0 <= status_detect_count_cand; + det_count_cand_sync_1 <= det_count_cand_sync_0; + // Status snapshot on request if (status_req_ft) begin // Word 0: {0xFF, mode[1:0], stream[5:0], threshold[15:0]} @@ -801,7 +837,7 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin status_agc_saturation_count, // [19:12] status_agc_enable, // [11] status_chirps_mismatch, // [10] TX-G mismatch flag - 8'd0, // [9:2] reserved + alpha_soft_sync_1, // [9:2] PR-G: host_cfar_alpha_soft echo (Q4.4) status_range_mode}; // [1:0] // Word 5: {frame_drop_count[31:25], self_test_busy[24], // reserved[23:16], self_test_detail[15:8], reserved[7], @@ -816,6 +852,15 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin ddc_cic_fir_overrun_sync_1, // [6] audit F-1.2 range_decim_watchdog_sync_1, // [5] audit F-6.4 status_self_test_flags}; // [4:0] + // PR-G word 6: {detect_count_cand[15:0], detect_threshold_soft[15:0]}. + // detect_threshold_soft is 17-bit; saturate to 16 bits for status (top + // bit set → emit 0xFFFF). alpha_soft (8-bit) does not need to be in + // the status packet — host wrote it via opcode 0x2D and tracks it + // locally; it's CDC'd here for any future readback need but is not + // emitted by the FSM today. (Pack into reserved bits if needed in v3.) + status_words[6] <= {det_count_cand_sync_1, + (det_thr_soft_sync_1[16] ? 16'hFFFF + : det_thr_soft_sync_1[15:0])}; end // ================================================================ @@ -899,15 +944,17 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin end // New frame ready for transfer else if (frame_ready_ft && ft_rxf_n) begin - wr_state <= WR_FRAME_HDR; - wr_byte_idx <= 16'd0; - bram_rd_cell <= {FRAME_ADDR_W{1'b0}}; - range_rd_idx <= {RANGE_BIN_BITS{1'b0}}; - range_rd_addr <= {RANGE_BIN_BITS{1'b0}}; // Pre-load first read addr - detect_rd_idx <= {DETECT_BYTE_ADDR_W{1'b0}}; - detect_rd_addr <= {DETECT_BYTE_ADDR_W{1'b0}}; - mag_rd_addr <= {FRAME_ADDR_W{1'b0}}; - wr_byte_phase <= 1'b0; + wr_state <= WR_FRAME_HDR; + wr_byte_idx <= 16'd0; + dop_range_idx <= {RANGE_BIN_BITS{1'b0}}; + dop_doppler_idx <= {DOPPLER_BIN_BITS{1'b0}}; + range_rd_idx <= {RANGE_BIN_BITS{1'b0}}; + range_rd_addr <= {RANGE_BIN_BITS{1'b0}}; // Pre-load first read addr + det_range_idx <= {RANGE_BIN_BITS{1'b0}}; + det_doppler_byte_idx <= 4'd0; + detect_rd_addr <= {DETECT_BYTE_ADDR_W{1'b0}}; + mag_rd_addr <= {FRAME_ADDR_W{1'b0}}; // {range=0, doppler=0} + wr_byte_phase <= 1'b0; end end @@ -917,18 +964,21 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin ft_data_oe <= 1'b1; ft_wr_n <= 1'b0; - case (wr_byte_idx[2:0]) - 3'd0: ft_data_out <= HEADER; - 3'd1: ft_data_out <= {2'b00, stream_flags_snapshot}; - 3'd2: ft_data_out <= frame_number_snapshot[15:8]; - 3'd3: ft_data_out <= frame_number_snapshot[7:0]; - 3'd4: ft_data_out <= NUM_RANGE_BINS[15:8]; // 512 >> 8 = 2 - 3'd5: ft_data_out <= NUM_RANGE_BINS[7:0]; // 512 & 0xFF = 0 - 3'd6: ft_data_out <= NUM_DOPPLER_BINS[15:8]; // 32 >> 8 = 0 - 3'd7: ft_data_out <= NUM_DOPPLER_BINS[7:0]; // 32 & 0xFF = 32 + // PR-G: 9-byte header (was 8). Byte 1 = protocol version. + case (wr_byte_idx[3:0]) + 4'd0: ft_data_out <= HEADER; + 4'd1: ft_data_out <= `RP_USB_PROTOCOL_VERSION; // 0x02 + 4'd2: ft_data_out <= {5'b00000, stream_flags_snapshot}; + 4'd3: ft_data_out <= frame_number_snapshot[15:8]; + 4'd4: ft_data_out <= frame_number_snapshot[7:0]; + 4'd5: ft_data_out <= NUM_RANGE_BINS[15:8]; // 512 >> 8 = 2 + 4'd6: ft_data_out <= NUM_RANGE_BINS[7:0]; // 512 & 0xFF = 0 + 4'd7: ft_data_out <= NUM_DOPPLER_BINS[15:8]; // 48 >> 8 = 0 + 4'd8: ft_data_out <= NUM_DOPPLER_BINS[7:0]; // 48 & 0xFF = 48 + default: ft_data_out <= 8'h00; endcase - if (wr_byte_idx[2:0] == 3'd7) begin + if (wr_byte_idx[3:0] == 4'd8) begin wr_byte_idx <= 16'd0; wr_byte_phase <= 1'b0; // Decide next section based on stream flags @@ -968,10 +1018,11 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin wr_byte_idx <= wr_byte_idx + 16'd1; if (wr_byte_idx == RANGE_SECTION_BYTES[15:0] - 16'd1) begin - wr_byte_idx <= 16'd0; - wr_byte_phase <= 1'b0; - bram_rd_cell <= {FRAME_ADDR_W{1'b0}}; - mag_rd_addr <= {FRAME_ADDR_W{1'b0}}; + wr_byte_idx <= 16'd0; + wr_byte_phase <= 1'b0; + dop_range_idx <= {RANGE_BIN_BITS{1'b0}}; + dop_doppler_idx <= {DOPPLER_BIN_BITS{1'b0}}; + mag_rd_addr <= {FRAME_ADDR_W{1'b0}}; // {range=0, doppler=0} if (stream_flags_snapshot[1]) wr_state <= WR_DOPPLER_DATA; else if (stream_flags_snapshot[2]) @@ -982,8 +1033,11 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin end end - // ---- Doppler magnitude: 16384 × 2 = 32768 bytes (mag_only mode) ---- - // Row-major: range_bin varies slowest, doppler_bin varies fastest. + // ---- Doppler magnitude: 512 × 48 × 2 = 49152 bytes ---- + // PR-G: row-major iteration over valid (range, doppler) cells + // only. Skips BRAM padding at doppler 48..63 by jumping to next + // range when doppler hits DOP_BIN_LAST. Header field + // doppler_bins=48 matches body length exactly. WR_DOPPLER_DATA: begin if (!ft_txe_n) begin ft_data_oe <= 1'b1; @@ -995,17 +1049,28 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin end else begin ft_data_out <= mag_rd_data[7:0]; wr_byte_phase <= 1'b0; - bram_rd_cell <= bram_rd_cell + {{(FRAME_ADDR_W-1){1'b0}}, 1'b1}; - mag_rd_addr <= bram_rd_cell + {{(FRAME_ADDR_W-1){1'b0}}, 1'b1}; + // Pre-load mag_rd_addr 1 cell ahead (BRAM 1-cycle + // read latency). Address layout: {range[8:0], doppler[5:0]}. + if (dop_doppler_idx == DOP_BIN_LAST) begin + dop_doppler_idx <= {DOPPLER_BIN_BITS{1'b0}}; + dop_range_idx <= dop_range_idx + {{(RANGE_BIN_BITS-1){1'b0}}, 1'b1}; + mag_rd_addr <= {dop_range_idx + {{(RANGE_BIN_BITS-1){1'b0}}, 1'b1}, + {DOPPLER_BIN_BITS{1'b0}}}; + end else begin + dop_doppler_idx <= dop_doppler_idx + {{(DOPPLER_BIN_BITS-1){1'b0}}, 1'b1}; + mag_rd_addr <= {dop_range_idx, + dop_doppler_idx + {{(DOPPLER_BIN_BITS-1){1'b0}}, 1'b1}}; + end end wr_byte_idx <= wr_byte_idx + 16'd1; if (wr_byte_idx == DOPPLER_MAG_SECTION_BYTES[15:0] - 16'd1) begin - wr_byte_idx <= 16'd0; - wr_byte_phase <= 1'b0; - detect_rd_idx <= {DETECT_BYTE_ADDR_W{1'b0}}; - detect_rd_addr <= {DETECT_BYTE_ADDR_W{1'b0}}; + wr_byte_idx <= 16'd0; + wr_byte_phase <= 1'b0; + det_range_idx <= {RANGE_BIN_BITS{1'b0}}; + det_doppler_byte_idx <= 4'd0; + detect_rd_addr <= {DETECT_BYTE_ADDR_W{1'b0}}; if (stream_flags_snapshot[2]) wr_state <= WR_DETECT_DATA; else @@ -1014,16 +1079,28 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin end end - // ---- Detection flags: 2048 bytes (dense mode) ---- + // ---- Detection flags: 512 × 12 = 6144 bytes (PR-G, 2-bit dense) ---- + // PR-G: nested advance through (range 0..511, doppler_byte 0..11). + // Skips 4 padded detect bytes per range (doppler 48..63 indices) + // so the wire body matches host's expected size of + // range_bins × doppler_bins × 2 / 8 = 6144 bytes. WR_DETECT_DATA: begin if (!ft_txe_n) begin ft_data_oe <= 1'b1; ft_wr_n <= 1'b0; // 1-byte per cycle (BRAM read latency handled by pre-loading addr) - ft_data_out <= detect_rd_data; - detect_rd_idx <= detect_rd_idx + {{(DETECT_BYTE_ADDR_W-1){1'b0}}, 1'b1}; - detect_rd_addr <= detect_rd_idx + {{(DETECT_BYTE_ADDR_W-1){1'b0}}, 1'b1}; + ft_data_out <= detect_rd_data; + if (det_doppler_byte_idx == DET_BYTE_LAST_PER_RANGE) begin + det_doppler_byte_idx <= 4'd0; + det_range_idx <= det_range_idx + {{(RANGE_BIN_BITS-1){1'b0}}, 1'b1}; + detect_rd_addr <= {det_range_idx + {{(RANGE_BIN_BITS-1){1'b0}}, 1'b1}, + 4'd0}; + end else begin + det_doppler_byte_idx <= det_doppler_byte_idx + 4'd1; + detect_rd_addr <= {det_range_idx, + det_doppler_byte_idx + 4'd1}; + end wr_byte_idx <= wr_byte_idx + 16'd1; @@ -1044,7 +1121,7 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin end end - // ---- Status packet: 26 bytes (unchanged from legacy) ---- + // ---- Status packet: 30 bytes (PR-G v2: 7 × 32-bit words) ---- WR_STATUS_SEND: begin if (!ft_txe_n) begin ft_data_oe <= 1'b1; @@ -1076,7 +1153,11 @@ always @(posedge ft_clk or negedge ft_effective_reset_n) begin 5'd22: ft_data_out <= status_words[5][23:16]; 5'd23: ft_data_out <= status_words[5][15:8]; 5'd24: ft_data_out <= status_words[5][7:0]; - 5'd25: ft_data_out <= FOOTER; + 5'd25: ft_data_out <= status_words[6][31:24]; // PR-G + 5'd26: ft_data_out <= status_words[6][23:16]; // PR-G + 5'd27: ft_data_out <= status_words[6][15:8]; // PR-G + 5'd28: ft_data_out <= status_words[6][7:0]; // PR-G + 5'd29: ft_data_out <= FOOTER; default: ft_data_out <= 8'h00; endcase @@ -1160,30 +1241,6 @@ always @(posedge ft_clk or negedge ft_reset_n) begin end `endif -// ============================================================================ -// AUDIT-C9: inert-flag checker (simulation only) -// -// stream_mag_only and stream_sparse_det are documented in the wire format -// but the write FSM does not act on them — see the "INERT FLAGS" note in -// the module header. radar_system_top.v opcode 0x04 force-clamps these -// bits when USB_MODE=1 so production firmware cannot reach an unsupported -// state. This checker is the backstop: it fires `[ASSERT FAIL]` if either -// bit ever escapes its clamped value, catching any future patch that -// bypasses the host register clamp (e.g. a different opcode that writes -// stream_control directly, or a stream_control source other than the -// host). Synthesis-inert. -// ============================================================================ -`ifdef SIMULATION -always @(posedge clk) begin - if (reset_n) begin - if (stream_mag_only !== 1'b1) - $display("[ASSERT FAIL] AUDIT-C9: stream_mag_only=0; full-I/Q write FSM not implemented"); - if (stream_sparse_det !== 1'b0) - $display("[ASSERT FAIL] AUDIT-C9: stream_sparse_det=1; sparse-list write FSM not implemented"); - end -end -`endif - // ============================================================================ // AUDIT-S22: cfar_valid-vs-RMW-busy checker (simulation only) //