chirp-v2 PR-G: header/body consistency + runtime MEDIUM ladder

G1.5 (FSM trim): doppler section emits NUM_RANGE_BINS*NUM_DOPPLER_BINS
cells (49152 B) and detect emits packed valid bytes (6144 B), matching
the 9-byte header advertisement. Replaces flat counters with nested
range x doppler indices in usb_data_interface_ft2232h.v. Saves ~18.4 kB
per frame on the wire.

G2 (runtime MEDIUM ladder): adds opcodes 0x17/0x18 for medium chirp/
listen cycles with RP_DEF_MEDIUM_* defaults. Plumbed through
radar_system_top -> radar_receiver_final -> chirp_scheduler. SHORT/LONG
were already runtime-tunable; MEDIUM was hardcoded.

TBs: tb_usb_protocol_v2 adds TEST 4 (full-frame egress byte count =
56330) and TEST 5 (MEDIUM opcode round-trip) - 27/27 PASS.
tb_ft2232h_frame_drop updated for new section sizes - 10/10 PASS.

Full regression: 37/41 with 4 pre-existing failures (T-2..T-5,
tracked in PR-Tests-1 / PR-I). Stash test confirmed pre-PR-G HEAD has
identical failures - PR-G introduces zero new test regressions.
This commit is contained in:
Jason
2026-05-01 11:10:06 +05:45
parent 65f1e02766
commit 58792d0e7d
6 changed files with 770 additions and 273 deletions

View File

@@ -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 34: Frame number (uint16, MSB first)
// Bytes 56: Range bin count (uint16, MSB first) = `RP_NUM_RANGE_BINS`
// Bytes 78: 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
// 0x190x1F 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)
// 0x2E0x2F 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

View File

@@ -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),

View File

@@ -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)

View File

@@ -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);

View File

@@ -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

View File

@@ -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 (FPGAHost): variable length, up to ~35 KB
* Frame packet (FPGAHost): 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 (FPGAHost): 26 bytes (unchanged from legacy)
* Status packet (FPGAHost): 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 (HostFPGA): 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 3040 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<<DOPPLER_BIN_BITS) = 32768 (full 15-bit space)
// PR-G: byte-counter (was 15-bit bit-counter in PR-F). 8192 bytes = 13 bits.
reg [DETECT_BYTE_ADDR_W-1:0] detect_clear_addr;
reg detect_clearing; // 1 = bulk clear in progress
// Range bin counter for range profile writes
@@ -427,11 +431,11 @@ always @(posedge clk or negedge reset_n) begin
detect_wr_addr <= {DETECT_BYTE_ADDR_W{1'b0}};
detect_wr_data <= 8'd0;
detect_clearing <= 1'b0;
detect_clear_addr <= {FRAME_ADDR_W{1'b0}};
detect_clear_addr <= {DETECT_BYTE_ADDR_W{1'b0}};
detect_rmw_state <= 2'd0;
detect_rmw_addr <= {DETECT_BYTE_ADDR_W{1'b0}};
detect_rmw_bit <= 3'd0;
detect_rmw_value <= 1'b0;
detect_rmw_cell_idx <= 2'd0;
detect_rmw_value <= `RP_DETECT_NONE;
range_write_counter <= {RANGE_BIN_BITS{1'b0}};
end else begin
// Default: deassert write enables
@@ -439,22 +443,22 @@ always @(posedge clk or negedge reset_n) begin
range_wr_en <= 1'b0;
detect_wr_en <= 1'b0;
// === Detection BRAM bulk clear (runs after frame_complete) ===
// PR-F: bit-counter is FRAME_ADDR_W (15-bit), byte addr is the upper
// (FRAME_ADDR_W-3) bits.
// === Detect-class BRAM bulk clear (runs after frame_complete) ===
// PR-G: 1 byte/cycle byte-counter (was 8-bits-per-cycle bit-counter).
if (detect_clearing) begin
detect_wr_en <= 1'b1;
detect_wr_addr <= detect_clear_addr[FRAME_ADDR_W-1:3];
detect_wr_addr <= detect_clear_addr;
detect_wr_data <= 8'd0;
if (detect_clear_addr[FRAME_ADDR_W-1:3] == DETECT_BYTE_LAST[DETECT_BYTE_ADDR_W-1:0]) begin
if (detect_clear_addr == DETECT_BYTE_LAST[DETECT_BYTE_ADDR_W-1:0]) begin
detect_clearing <= 1'b0;
detect_clear_addr <= {FRAME_ADDR_W{1'b0}};
detect_clear_addr <= {DETECT_BYTE_ADDR_W{1'b0}};
end else begin
detect_clear_addr <= detect_clear_addr + 15'd8; // Step by 8 bits = 1 byte
detect_clear_addr <= detect_clear_addr + {{(DETECT_BYTE_ADDR_W-1){1'b0}}, 1'b1};
end
end
// === Detection RMW state machine ===
// === Detect-class RMW state machine (PR-G: 2-bit pack) ===
// Cell N within byte MSB-first: shift = (3 - N) * 2 = {!N[1], !N[0], 1'b0}
case (detect_rmw_state)
2'd0: begin /* idle */ end
2'd1: begin
@@ -462,13 +466,13 @@ always @(posedge clk or negedge reset_n) begin
detect_rmw_state <= 2'd2;
end
2'd2: begin
// Write back with bit set/cleared
// Write back with the 2-bit class field updated.
detect_wr_en <= 1'b1;
detect_wr_addr <= detect_rmw_addr;
if (detect_rmw_value)
detect_wr_data <= detect_rmw_rddata | (8'd1 << detect_rmw_bit);
else
detect_wr_data <= detect_rmw_rddata & ~(8'd1 << detect_rmw_bit);
// Mask out the 2 bits for this cell, OR in the new class.
// shift_amt = (3 - cell_idx) * 2 {6, 4, 2, 0}
detect_wr_data <= (detect_rmw_rddata & ~(8'b11000000 >> ({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)
//