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
+21 -16
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);
+357
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