From e1abeecaa90d8c80e50594dc76690029af1c7e7e Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Mon, 18 May 2026 13:28:40 +0545 Subject: [PATCH] =?UTF-8?q?test(fpga):=20PR-AD=20AD.2=20=E2=80=94=20USB=20?= =?UTF-8?q?driver=20TB=20rewrite=20+=20cross-comparison=20parity=20TB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tb/tb_usb_data_interface.v was on the obsolete pre-PR-G streaming protocol (1-bit cfar_detection, 4-state FSM) — green CI gave false confidence because TB and DUT were equally out of date. This rewrite exercises the new v2 bulk FSM and adds a cross-comparison TB that asserts byte-for-byte equality between the FT601 and FT2232H drivers fed identical stimulus. tb/tb_usb_data_interface.v — full rewrite (~370 LOC): - Mirrors tb_usb_protocol_v2.v test groups for the FT601 path - BE-aware byte capture using a single capture_idx integer with blocking arithmetic + one non-blocking egress_count update (four separate non-blocking assigns dropped 3 of 4 lanes — race caught during development) - 31/0 PASS, egress_count = 56330 bytes (matches FT2232H exactly) tb/tb_usb_drivers_parity.v — new (~376 LOC): - Instantiates both drivers in one TB, fed identical clk/reset/ streaming inputs, drained concurrently - Captures each driver's byte stream into a 65536-byte ring buffer - assert_parity task finds first byte mismatch index - 3 scenarios: header-only (10 B), status packet (34 B), full frame (56330 B) - 9/0 PASS — drivers are now byte-equal across all scenarios. Any future v2 protocol change on FT2232H must land on FT601 in the same PR or this guard fires. run_regression.sh — adds "USB Drivers Parity (PR-AD AD.2 cross-comparison)" entry in Phase 1 (changed-modules block). Regression: 44/0/0 (43 baseline + 1 parity test). --- 9_Firmware/9_2_FPGA/run_regression.sh | 4 + .../9_2_FPGA/tb/tb_usb_data_interface.v | 1325 ++++------------- .../9_2_FPGA/tb/tb_usb_drivers_parity.v | 376 +++++ 3 files changed, 683 insertions(+), 1022 deletions(-) create mode 100644 9_Firmware/9_2_FPGA/tb/tb_usb_drivers_parity.v diff --git a/9_Firmware/9_2_FPGA/run_regression.sh b/9_Firmware/9_2_FPGA/run_regression.sh index 96aa4f9..f917dee 100755 --- a/9_Firmware/9_2_FPGA/run_regression.sh +++ b/9_Firmware/9_2_FPGA/run_regression.sh @@ -558,6 +558,10 @@ run_test "FT2232H Frame Drop Counter (AUDIT-C12)" \ tb/tb_ft2232h_frame_drop.vvp \ tb/tb_ft2232h_frame_drop.v usb_data_interface_ft2232h.v +run_test "USB Drivers Parity (PR-AD AD.2 cross-comparison)" \ + tb/tb_usb_drivers_parity.vvp \ + tb/tb_usb_drivers_parity.v usb_data_interface.v usb_data_interface_ft2232h.v + run_test "Doppler Frame-Start Gate (AUDIT-S3)" \ tb/tb_doppler_frame_start_gate.vvp \ tb/tb_doppler_frame_start_gate.v doppler_processor.v xfft_16.v fft_engine.v diff --git a/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v b/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v index 55d8f54..00582eb 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v +++ b/9_Firmware/9_2_FPGA/tb/tb_usb_data_interface.v @@ -1,1092 +1,373 @@ `timescale 1ns / 1ps `include "radar_params.vh" +// ============================================================================ +// tb_usb_data_interface.v +// +// PR-AD (AD.2) v2 bulk protocol unit TB for usb_data_interface.v (FT601). +// Mirrors tb_usb_protocol_v2.v's structure but adapted for the FT601 32-bit +// data bus + BE byte-enable. Byte stream reconstructed from BE lanes is +// asserted byte-equal to what the FT2232H driver would emit, by design. +// +// 1. Opcode 0x2D (host_cfar_alpha_soft) round-trip on the RX path. +// 2. Bulk frame header v2 — byte0=0xAA, byte1=0x02 (version), byte2 flags, +// bytes3-8 = frame_num/range_bins/doppler_bins. +// 3. Status packet length 34 bytes (M-5), word[6] CFAR telemetry, word[7] +// medium_chirp/medium_listen. +// 4. Full-frame length consistency with all 3 streams enabled (PR-G trim). +// 5. MEDIUM ladder opcodes 0x17 / 0x18 round-trip. +// ============================================================================ + module tb_usb_data_interface; + localparam CLK_PER = 10.0; // 100 MHz radar clk + localparam FT_CLK_PER = 10.0; // 100 MHz ft601_clk_in (asynchronous) - // ── Parameters ───────────────────────────────────────────── - localparam CLK_PERIOD = 10.0; // 100 MHz main clock - localparam FT_CLK_PERIOD = 10.0; // 100 MHz FT601 clock (asynchronous) + reg clk = 1'b0; + reg ft601_clk_in = 1'b0; + reg reset_n = 1'b0; + reg ft601_reset_n = 1'b0; - // State definitions (mirror the DUT — 4-state packed-word FSM) - localparam [3:0] S_IDLE = 4'd0, - S_SEND_DATA_WORD = 4'd1, - S_SEND_STATUS = 4'd2, - S_WAIT_ACK = 4'd3; + // 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; - // ── Signals ──────────────────────────────────────────────── - reg clk; - reg reset_n; + 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; - // Radar data inputs - reg [31:0] range_profile; - reg range_valid; - reg [15:0] doppler_real; - reg [15:0] doppler_imag; - reg doppler_valid; - reg cfar_detection; - reg cfar_valid; - - // FT601 interface + // FT601 interface signals wire [31:0] ft601_data; wire [3:0] ft601_be; - wire ft601_txe_n; - wire ft601_rxf_n; - reg ft601_txe; - reg ft601_rxf; + wire ft601_txe_n; // VESTIGIAL output (tied to 1) + wire ft601_rxf_n; // VESTIGIAL output (tied to 1) + reg ft601_txe = 1'b0; // active-low: 0 = FIFO has space + reg ft601_rxf = 1'b1; // active-low: 0 = host data available wire ft601_wr_n; wire ft601_rd_n; wire ft601_oe_n; wire ft601_siwu_n; - reg [1:0] ft601_srb; - reg [1:0] ft601_swb; + reg [1:0] ft601_srb = 2'd0; + reg [1:0] ft601_swb = 2'd0; wire ft601_clk_out; - reg ft601_clk_in; - // Pulldown: when nobody drives, data reads as 0 (not X) pulldown pd[31:0] (ft601_data); - // Host-to-FPGA data bus driver (for read path testing) - reg [31:0] host_data_drive; - reg host_data_drive_en; + // Host-to-FPGA bus driver for the RD path + reg [31:0] host_data_drive = 32'd0; + reg host_data_drive_en = 1'b0; assign ft601_data = host_data_drive_en ? host_data_drive : 32'hzzzz_zzzz; - // DUT command outputs (Gap 4: USB Read Path) wire [31:0] cmd_data; wire cmd_valid; wire [7:0] cmd_opcode; wire [7:0] cmd_addr; wire [15:0] cmd_value; - // Gap 2: Stream control + status readback inputs - reg [2:0] stream_control; - reg status_request; - reg [15:0] status_cfar_threshold; - reg [2:0] status_stream_ctrl; - reg [15:0] status_long_chirp; - reg [15:0] status_long_listen; - reg [15:0] status_guard; - reg [15:0] status_short_chirp; - reg [15:0] status_short_listen; - reg [5:0] status_chirps_per_elev; - reg status_chirps_mismatch; + // PR-G v2 stream control — enable all 3 streams (range|doppler|cfar). + reg [5:0] stream_control = 6'b000_111; + reg [5:0] status_stream_ctrl = 6'b000_111; + // PR-U / M-8: production 3-PRI ladder. + reg [2:0] subframe_enable = 3'b111; - // Self-test status readback inputs - reg [4:0] status_self_test_flags; - reg [7:0] status_self_test_detail; - reg status_self_test_busy; + reg status_request = 1'b0; + reg [15:0] status_cfar_threshold = 16'h1234; + 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 [15:0] status_medium_chirp = 16'd`RP_DEF_MEDIUM_CHIRP_CYCLES; + reg [15:0] status_medium_listen = 16'd`RP_DEF_MEDIUM_LISTEN_CYCLES; + reg [5:0] status_chirps_per_elev = 6'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; + reg status_beam_handshake_watchdog = 1'b0; + 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; - // AGC status readback inputs - reg [3:0] status_agc_current_gain; - reg [7:0] status_agc_peak_magnitude; - reg [7:0] status_agc_saturation_count; - reg status_agc_enable; + integer pass = 0; + integer fail = 0; - // ── Clock generators (asynchronous) ──────────────────────── - always #(CLK_PERIOD / 2) clk = ~clk; - always #(FT_CLK_PERIOD / 2) ft601_clk_in = ~ft601_clk_in; + always #(CLK_PER/2) clk = ~clk; + always #(FT_CLK_PER/2) ft601_clk_in = ~ft601_clk_in; - // ── DUT ──────────────────────────────────────────────────── - usb_data_interface uut ( - .clk (clk), - .reset_n (reset_n), - .ft601_reset_n (reset_n), // In TB, share same reset for both domains - .range_profile (range_profile), - .range_valid (range_valid), - .doppler_real (doppler_real), - .doppler_imag (doppler_imag), - .doppler_valid (doppler_valid), - .cfar_detection (cfar_detection), - .cfar_valid (cfar_valid), - .ft601_data (ft601_data), - .ft601_be (ft601_be), - .ft601_txe_n (ft601_txe_n), - .ft601_rxf_n (ft601_rxf_n), - .ft601_txe (ft601_txe), - .ft601_rxf (ft601_rxf), - .ft601_wr_n (ft601_wr_n), - .ft601_rd_n (ft601_rd_n), - .ft601_oe_n (ft601_oe_n), - .ft601_siwu_n (ft601_siwu_n), - .ft601_srb (ft601_srb), - .ft601_swb (ft601_swb), - .ft601_clk_out (ft601_clk_out), - .ft601_clk_in (ft601_clk_in), - - // Host command outputs (Gap 4: USB Read Path) - .cmd_data (cmd_data), - .cmd_valid (cmd_valid), - .cmd_opcode (cmd_opcode), - .cmd_addr (cmd_addr), - .cmd_value (cmd_value), - - // Gap 2: Stream control + status readback - .stream_control (stream_control), - .status_request (status_request), - .status_cfar_threshold (status_cfar_threshold), - .status_stream_ctrl (status_stream_ctrl), - .status_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), + usb_data_interface u_dut ( + .clk(clk), + .reset_n(reset_n), + .ft601_reset_n(ft601_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), + .ft601_data(ft601_data), + .ft601_be(ft601_be), + .ft601_txe_n(ft601_txe_n), + .ft601_rxf_n(ft601_rxf_n), + .ft601_txe(ft601_txe), + .ft601_rxf(ft601_rxf), + .ft601_wr_n(ft601_wr_n), + .ft601_rd_n(ft601_rd_n), + .ft601_oe_n(ft601_oe_n), + .ft601_siwu_n(ft601_siwu_n), + .ft601_srb(ft601_srb), + .ft601_swb(ft601_swb), + .ft601_clk_out(ft601_clk_out), + .ft601_clk_in(ft601_clk_in), + .cmd_data(cmd_data), + .cmd_valid(cmd_valid), + .cmd_opcode(cmd_opcode), + .cmd_addr(cmd_addr), + .cmd_value(cmd_value), + .stream_control(stream_control), + .subframe_enable(subframe_enable), + .status_request(status_request), + .status_cfar_threshold(status_cfar_threshold), + .status_stream_ctrl(status_stream_ctrl), + .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_medium_chirp(status_medium_chirp), + .status_medium_listen(status_medium_listen), .status_chirps_per_elev(status_chirps_per_elev), .status_chirps_mismatch(status_chirps_mismatch), - - // Self-test status readback - .status_self_test_flags (status_self_test_flags), + .status_self_test_flags(status_self_test_flags), .status_self_test_detail(status_self_test_detail), - .status_self_test_busy (status_self_test_busy), - - // AGC status readback - .status_agc_current_gain (status_agc_current_gain), - .status_agc_peak_magnitude (status_agc_peak_magnitude), + .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), - // AUDIT-S10: control-fault flags tied off (pre-existing TB scope) - .status_range_decim_watchdog(1'b0), - .status_ddc_cic_fir_overrun (1'b0), - // PR-AB.b expanded commit 5: beam-handshake watchdog tied off - .status_beam_handshake_watchdog(1'b0) + .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_beam_handshake_watchdog(status_beam_handshake_watchdog), + .status_cfar_alpha_soft(status_cfar_alpha_soft), + .status_detect_threshold_soft(status_detect_threshold_soft), + .status_detect_count_cand(status_detect_count_cand) ); - // ── Test bookkeeping ─────────────────────────────────────── - integer pass_count; - integer fail_count; - integer test_num; - integer csv_file; + // ============================================================================ + // BE-aware byte capture + // + // FT601 emits a 32-bit word + 4-bit BE per ft601_clk cycle when + // (!ft601_wr_n && !ft601_txe). Each enabled BE lane carries one stream + // byte. Convention: byte 0 of stream -> ft601_data[7:0] (BE[0]), + // byte 1 -> [15:8] (BE[1]), byte 2 -> [23:16] (BE[2]), byte 3 -> [31:24] + // (BE[3]). Reconstructed byte stream must match what FT2232H emits + // byte-for-byte on the same stimulus (cross-comparison TB asserts this). + // ============================================================================ + reg [7:0] egress_bytes [0:35]; + integer egress_count = 0; + integer capture_idx; + always @(posedge ft601_clk_in) begin + if (!ft601_wr_n && !ft601_txe) begin + capture_idx = egress_count; + if (ft601_be[0]) begin + if (capture_idx < 36) egress_bytes[capture_idx] <= ft601_data[7:0]; + capture_idx = capture_idx + 1; + end + if (ft601_be[1]) begin + if (capture_idx < 36) egress_bytes[capture_idx] <= ft601_data[15:8]; + capture_idx = capture_idx + 1; + end + if (ft601_be[2]) begin + if (capture_idx < 36) egress_bytes[capture_idx] <= ft601_data[23:16]; + capture_idx = capture_idx + 1; + end + if (ft601_be[3]) begin + if (capture_idx < 36) egress_bytes[capture_idx] <= ft601_data[31:24]; + capture_idx = capture_idx + 1; + end + egress_count <= capture_idx; + end + end - // ── Check task (512-bit label) ───────────────────────────── - task check; - input cond; - input [511:0] label; + task check_b; + input [127:0] tag; + input cond; begin - test_num = test_num + 1; if (cond) begin - $display("[PASS] Test %0d: %0s", test_num, label); - pass_count = pass_count + 1; - $fwrite(csv_file, "%0d,PASS,%0s\n", test_num, label); + $display("[PASS] %0s", tag); + pass = pass + 1; end else begin - $display("[FAIL] Test %0d: %0s", test_num, label); - fail_count = fail_count + 1; - $fwrite(csv_file, "%0d,FAIL,%0s\n", test_num, label); + $display("[FAIL] %0s", tag); + fail = fail + 1; end end endtask - // ── Helper: apply reset ──────────────────────────────────── - task apply_reset; + task wait_clk; + input integer n; + integer i; begin - reset_n = 0; - range_profile = 32'h0; - range_valid = 0; - doppler_real = 16'h0; - doppler_imag = 16'h0; - doppler_valid = 0; - cfar_detection = 0; - cfar_valid = 0; - ft601_txe = 0; // TX FIFO ready (active low) - ft601_rxf = 1; - ft601_srb = 2'b00; - ft601_swb = 2'b00; - host_data_drive = 32'h0; - host_data_drive_en = 0; - // Gap 2: Stream control defaults (all streams enabled) - stream_control = 3'b111; - status_request = 0; - status_cfar_threshold = 16'd10000; - status_stream_ctrl = 3'b111; - status_long_chirp = 16'd3000; - status_long_listen = 16'd13700; - status_guard = 16'd17540; - status_short_chirp = 16'd50; - status_short_listen = 16'd17450; - status_chirps_per_elev = 6'd32; - status_chirps_mismatch = 1'b0; - status_self_test_flags = 5'b00000; - status_self_test_detail = 8'd0; - status_self_test_busy = 1'b0; - status_agc_current_gain = 4'd0; - status_agc_peak_magnitude = 8'd0; - status_agc_saturation_count = 8'd0; - status_agc_enable = 1'b0; - repeat (6) @(posedge ft601_clk_in); - reset_n = 1; - // Wait enough cycles for stream_control CDC to propagate - // (DUT resets stream_ctrl_sync to 3'b001; TB sets stream_control=3'b111 - // which needs 2-stage sync + 1 cycle = 4+ ft601_clk cycles) - repeat (6) @(posedge ft601_clk_in); + for (i = 0; i < n; i = i + 1) @(posedge clk); end endtask - // ── Helper: wait for DUT to reach a specific write FSM state ── - task wait_for_state; - input [3:0] target; - input integer max_cyc; - integer cnt; + // 4-byte command bus driver (host -> FPGA, ft601_clk domain). + // FT601 RD FSM reads one 32-bit word per transaction; cmd word is + // packed per the FT601 RX layout: {opcode[31:24], addr[23:16], value[15:0]}. + task send_cmd; + input [7:0] op; + input [7:0] addr; + input [15:0] val; begin - cnt = 0; - while (uut.current_state !== target && cnt < max_cyc) begin - @(posedge ft601_clk_in); - cnt = cnt + 1; - end - end - endtask - - // ── Helper: assert range_valid in clk domain, wait for CDC ── - task assert_range_valid; - input [31:0] data; - begin - @(posedge clk); - range_profile = data; - range_valid = 1; - repeat (3) @(posedge ft601_clk_in); - @(posedge clk); - range_valid = 0; - repeat (3) @(posedge ft601_clk_in); - end - endtask - - // Pulse doppler_valid once (produces ONE rising-edge in ft601 domain) - task pulse_doppler_once; - input [15:0] dr; - input [15:0] di; - begin - @(posedge clk); - doppler_real = dr; - doppler_imag = di; - doppler_valid = 1; - repeat (3) @(posedge ft601_clk_in); - @(posedge clk); - doppler_valid = 0; - repeat (3) @(posedge ft601_clk_in); - end - endtask - - // Pulse cfar_valid once - task pulse_cfar_once; - input det; - begin - @(posedge clk); - cfar_detection = det; - cfar_valid = 1; - repeat (3) @(posedge ft601_clk_in); - @(posedge clk); - cfar_valid = 0; - repeat (3) @(posedge ft601_clk_in); - end - endtask - - // Set data_pending flags directly via hierarchical access. - // This is the standard TB technique for internal state setup — - // bypasses the CDC path for immediate, reliable flag setting. - // Call BEFORE assert_range_valid in tests that need doppler/cfar data. - task preload_pending_data; - begin - @(posedge ft601_clk_in); - uut.doppler_data_pending = 1'b1; - uut.cfar_data_pending = 1'b1; - @(posedge ft601_clk_in); - end - endtask - - // Set only doppler pending (no cfar) - task preload_doppler_pending; - begin - @(posedge ft601_clk_in); - uut.doppler_data_pending = 1'b1; - @(posedge ft601_clk_in); - end - endtask - - // Set only cfar pending (no doppler) - task preload_cfar_pending; - begin - @(posedge ft601_clk_in); - uut.cfar_data_pending = 1'b1; - @(posedge ft601_clk_in); - end - endtask - - // ── Helper: wait for read FSM to reach a specific state ─── - task wait_for_read_state; - input [2:0] target; - input integer max_cyc; - integer cnt; - begin - cnt = 0; - while (uut.read_state !== target && cnt < max_cyc) begin - @(posedge ft601_clk_in); - cnt = cnt + 1; - end - end - endtask - - // ── Helper: send a single host command word via the read path ── - // Simulates the FT601 host presenting a 32-bit command word. - // Protocol: Assert RXF=0 (data available), wait for OE_N=0, - // drive data bus, wait for RD_N=0, then release. - task send_host_command; - input [31:0] cmd_word; - begin - // Signal host has data - ft601_rxf = 0; - // Wait for FPGA to assert OE_N (bus turnaround) - wait_for_read_state(3'd1, 20); // RD_OE_ASSERT = 3'd1 @(posedge ft601_clk_in); #1; - // Drive data bus (FT601 drives in real hardware) - host_data_drive = cmd_word; - host_data_drive_en = 1; - // Wait for FPGA to assert RD_N=0 (RD_READING state) - wait_for_read_state(3'd2, 20); // RD_READING = 3'd2 + ft601_rxf = 1'b0; + host_data_drive = {op, addr, val}; + host_data_drive_en = 1'b1; @(posedge ft601_clk_in); #1; - // Data has been sampled. FPGA deasserts RD then OE. - // Wait for RD_PROCESS or back to RD_IDLE - wait_for_read_state(3'd4, 20); // RD_PROCESS = 3'd4 @(posedge ft601_clk_in); #1; - // Release bus and deassert RXF - host_data_drive_en = 0; - host_data_drive = 32'h0; - ft601_rxf = 1; - // Wait for read FSM to return to idle - wait_for_read_state(3'd0, 20); // RD_IDLE = 3'd0 @(posedge ft601_clk_in); #1; + @(posedge ft601_clk_in); #1; + ft601_rxf = 1'b1; + host_data_drive_en = 1'b0; + wait_clk(20); end endtask - // Drive a complete data packet through the new 3-word packed FSM. - // Pre-loads pending flags, triggers range_valid, and waits for IDLE. - // With the new FSM, all data is pre-packed in IDLE then sent as 3 words. - task drive_full_packet; - input [31:0] rng; - input [15:0] dr; - input [15:0] di; - input det; - begin - // Set doppler/cfar captured values via CDC inputs - @(posedge clk); - doppler_real = dr; - doppler_imag = di; - cfar_detection = det; - @(posedge clk); - // Pre-load pending flags so FSM includes doppler/cfar in packet - preload_pending_data; - // Trigger the packet - assert_range_valid(rng); - // Wait for complete packet cycle: IDLE → SEND_DATA_WORD(×3) → WAIT_ACK → IDLE - wait_for_state(S_IDLE, 100); - end - endtask - - // ── Stimulus ─────────────────────────────────────────────── initial begin - $dumpfile("tb_usb_data_interface.vcd"); - $dumpvars(0, tb_usb_data_interface); + $display("\n========== tb_usb_data_interface (FT601 v2 bulk) =========="); + // Reset + reset_n = 1'b0; + ft601_reset_n = 1'b0; + wait_clk(10); + reset_n = 1'b1; + ft601_reset_n = 1'b1; + wait_clk(20); - clk = 0; - ft601_clk_in = 0; - pass_count = 0; - fail_count = 0; - test_num = 0; - host_data_drive = 32'h0; - host_data_drive_en = 0; + // ------------------------------------------------------------- + // 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); + 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); - csv_file = $fopen("tb_usb_data_interface.csv", "w"); - $fwrite(csv_file, "test_num,pass_fail,label\n"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 1: Reset behaviour - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 1: Reset Behaviour ---"); - apply_reset; - reset_n = 0; - repeat (4) @(posedge ft601_clk_in); #1; - - check(uut.current_state === S_IDLE, - "State is IDLE after reset"); - check(ft601_wr_n === 1'b1, - "ft601_wr_n=1 after reset"); - check(uut.ft601_data_oe === 1'b0, - "ft601_data_oe=0 after reset"); - check(ft601_rd_n === 1'b1, - "ft601_rd_n=1 after reset"); - check(ft601_oe_n === 1'b1, - "ft601_oe_n=1 after reset"); - check(ft601_siwu_n === 1'b1, - "ft601_siwu_n=1 after reset"); - - // ──────────────────────────────────────────────────────── - // Frame-sync regression (NUM_CELLS bug — historical 12-bit, AUDIT-C16) - // - // History: sample_counter was 12 bits with NUM_CELLS=2048 before the - // 2048-pt FFT architecture; under-width counter wrapped 8x per real - // 16384-cell frame, host saw 8 false markers. - // - // AUDIT-C16: same failure mode would re-emerge on 200T builds where - // RP_MAX_OUTPUT_BINS=4096 -> NUM_CELLS=131072. Fix derives NUM_CELLS - // and counter width from radar_params.vh so both scale with the build - // (50T: 16384 cells / 14-bit counter; 200T: 131072 / 17-bit). - // - // These checks pin the parameterization so a future regression that - // reverts to a hardcoded value fails loudly. - // ──────────────────────────────────────────────────────── - check($bits(uut.sample_counter) == `RP_DOPPLER_MEM_ADDR_W, - "Frame-sync: sample_counter width == RP_DOPPLER_MEM_ADDR_W"); - check(uut.NUM_CELLS == - (`RP_MAX_OUTPUT_BINS * `RP_NUM_DOPPLER_BINS), - "Frame-sync: NUM_CELLS == RP_MAX_OUTPUT_BINS*RP_NUM_DOPPLER_BINS"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 2: Data packet word packing - // - // New FSM packs 11-byte data into 3 × 32-bit words: - // Word 0: {HEADER, range[31:24], range[23:16], range[15:8]} - // Word 1: {range[7:0], dop_re_hi, dop_re_lo, dop_im_hi} - // Word 2: {dop_im_lo, detection, FOOTER, 0x00} BE=1110 - // - // The DUT uses range_data_ready (1-cycle delayed range_valid_ft) - // to trigger packing. Doppler/CFAR _cap registers must be - // pre-loaded via hierarchical access because no valid pulse is - // given in this test (we only want to verify packing, not CDC). - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 2: Data Packet Word Packing ---"); - apply_reset; - ft601_txe = 1; // Stall so we can inspect packed words - - // Set known doppler/cfar values on clk-domain inputs + // ------------------------------------------------------------- + // TEST 2: Frame header v2 — 9 bytes, byte1=0x02 + // ------------------------------------------------------------- + $display("\n[TEST 2] Frame header v2 emission"); + stream_control = 6'b000_000; // skip data sections + wait_clk(50); + egress_count = 0; @(posedge clk); - doppler_real = 16'hABCD; - doppler_imag = 16'hEF01; - cfar_detection = 1'b1; + frame_complete = 1'b1; @(posedge clk); + frame_complete = 1'b0; + wait_clk(200); // drain + 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 = {00, sf=111, stream=0} = 0x38", + egress_bytes[2] == 8'h38); + 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); - // Pre-load pending flags AND captured-data registers directly. - // No doppler/cfar valid pulses are given, so the CDC capture path - // never fires — we must set the _cap registers via hierarchical - // access for the word-packing checks to be meaningful. - preload_pending_data; - @(posedge ft601_clk_in); - uut.doppler_real_cap = 16'hABCD; - uut.doppler_imag_cap = 16'hEF01; - uut.cfar_detection_cap = 1'b1; - @(posedge ft601_clk_in); - - assert_range_valid(32'hDEAD_BEEF); - - // FSM should be in SEND_DATA_WORD, stalled on ft601_txe=1 - wait_for_state(S_SEND_DATA_WORD, 50); - repeat (2) @(posedge ft601_clk_in); #1; - - check(uut.current_state === S_SEND_DATA_WORD, - "Stalled in SEND_DATA_WORD (backpressure)"); - - // Verify pre-packed words - // range_profile = 0xDEAD_BEEF → range[31:24]=0xDE, [23:16]=0xAD, [15:8]=0xBE, [7:0]=0xEF - // Word 0: {0xAA, 0xDE, 0xAD, 0xBE} - check(uut.data_pkt_word0 === {8'hAA, 8'hDE, 8'hAD, 8'hBE}, - "Word 0: {HEADER=AA, range[31:8]}"); - // Word 1: {0xEF, 0xAB, 0xCD, 0xEF} - check(uut.data_pkt_word1 === {8'hEF, 8'hAB, 8'hCD, 8'hEF}, - "Word 1: {range[7:0], dop_re, dop_im_hi}"); - // Word 2: {0x01, detection_byte, 0x55, 0x00} - // detection_byte bit 7 = frame_start (sample_counter==0 → 1), bit 0 = cfar=1 - // so detection_byte = 8'b1000_0001 = 8'h81 - check(uut.data_pkt_word2 === {8'h01, 8'h81, 8'h55, 8'h00}, - "Word 2: {dop_im_lo, det=81, FOOTER=55, pad=00}"); - check(uut.data_pkt_be2 === 4'b1110, - "Word 2 BE=1110 (3 valid bytes + 1 pad)"); - - // Release backpressure and verify word 0 appears on bus. - // On the first posedge with !ft601_txe the FSM drives word 0 and - // advances data_word_idx 0→1 via NBA. After #1 the NBA has - // resolved, so we see idx=1 and ft601_data_out=word0. - ft601_txe = 0; - @(posedge ft601_clk_in); #1; - - check(uut.ft601_data_out === {8'hAA, 8'hDE, 8'hAD, 8'hBE}, - "Word 0 driven on data bus after backpressure release"); - check(ft601_wr_n === 1'b0, - "Write strobe active during SEND_DATA_WORD"); - check(ft601_be === 4'b1111, - "Byte enable=1111 for word 0"); - check(uut.ft601_data_oe === 1'b1, - "Data bus output enabled during SEND_DATA_WORD"); - - // Next posedge: FSM drives word 1, advances idx 1→2. - // After NBA: idx=2, ft601_data_out=word1. - @(posedge ft601_clk_in); #1; - check(uut.data_word_idx === 2'd2, - "data_word_idx advanced past word 1 (now 2)"); - check(uut.ft601_data_out === {8'hEF, 8'hAB, 8'hCD, 8'hEF}, - "Word 1 driven on data bus"); - check(ft601_be === 4'b1111, - "Byte enable=1111 for word 1"); - - // Next posedge: FSM drives word 2, idx resets 2→0, - // and current_state transitions to WAIT_ACK. - @(posedge ft601_clk_in); #1; - check(uut.current_state === S_WAIT_ACK, - "Transitioned to WAIT_ACK after 3 data words"); - check(uut.ft601_data_out === {8'h01, 8'h81, 8'h55, 8'h00}, - "Word 2 driven on data bus"); - check(ft601_be === 4'b1110, - "Byte enable=1110 for word 2 (last byte is pad)"); - - // Then back to IDLE - @(posedge ft601_clk_in); #1; - check(uut.current_state === S_IDLE, - "Returned to IDLE after WAIT_ACK"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 3: Header and footer verification - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 3: Header and Footer Verification ---"); - apply_reset; - ft601_txe = 1; // Stall to inspect - + // ------------------------------------------------------------- + // TEST 3: Status packet length = 34 bytes (M-5) + // ------------------------------------------------------------- + $display("\n[TEST 3] Status packet length 34B + word[6]/word[7]"); + egress_count = 0; @(posedge clk); - doppler_real = 16'h0000; - doppler_imag = 16'h0000; - cfar_detection = 1'b0; + status_request = 1'b1; @(posedge clk); - preload_pending_data; - assert_range_valid(32'hCAFE_BABE); + status_request = 1'b0; + wait_clk(400); + check_b("T3.1: byte0 = 0xBB (status header)", egress_bytes[0] == 8'hBB); + check_b("T3.2: byte33 = 0x55 (footer)", egress_bytes[33] == 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 20. + check_b("T3.7: status_words[4][7:0] = alpha_soft<<2 = 0x60 (alpha=0x18)", + egress_bytes[20] == 8'h60); + // M-5: status_words[7] = {medium_chirp (0x01F4), medium_listen (0x3CF0)}. + check_b("T3.8: status_words[7] medium_chirp[15:8]=0x01", egress_bytes[29] == 8'h01); + check_b("T3.9: status_words[7] medium_chirp[7:0]=0xF4", egress_bytes[30] == 8'hF4); + check_b("T3.10: status_words[7] medium_listen[15:8]=0x3C", egress_bytes[31] == 8'h3C); + check_b("T3.11: status_words[7] medium_listen[7:0]=0xF0", egress_bytes[32] == 8'hF0); - wait_for_state(S_SEND_DATA_WORD, 50); - repeat (2) @(posedge ft601_clk_in); #1; - - // Header is in byte 3 (MSB) of word 0 - check(uut.data_pkt_word0[31:24] === 8'hAA, - "Header byte 0xAA in word 0 MSB"); - // Footer is in byte 1 (bits [15:8]) of word 2 - check(uut.data_pkt_word2[15:8] === 8'h55, - "Footer byte 0x55 in word 2"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 4: Doppler data capture verification - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 4: Doppler Data Capture ---"); - apply_reset; - ft601_txe = 0; - - // Provide doppler data via valid pulse (updates captured values) + // ------------------------------------------------------------- + // TEST 4: full-frame length consistency (PR-G trim) + // ------------------------------------------------------------- + $display("\n[TEST 4] Full-frame header/body length consistency"); + stream_control = 6'b000_111; + wait_clk(50); + egress_count = 0; @(posedge clk); - doppler_real = 16'hAAAA; - doppler_imag = 16'h5555; - doppler_valid = 1; - repeat (3) @(posedge ft601_clk_in); + frame_complete = 1'b1; @(posedge clk); - doppler_valid = 0; - repeat (4) @(posedge ft601_clk_in); #1; - - check(uut.doppler_real_cap === 16'hAAAA, - "doppler_real captured correctly"); - check(uut.doppler_imag_cap === 16'h5555, - "doppler_imag captured correctly"); - - // Drive a packet with pending doppler + cfar (both needed for gating - // since all streams are enabled after reset/apply_reset). - preload_pending_data; - assert_range_valid(32'h0000_0001); - wait_for_state(S_IDLE, 100); - #1; - check(uut.current_state === S_IDLE, - "Packet completed with doppler data"); - check(uut.doppler_data_pending === 1'b0, - "doppler_data_pending cleared after packet"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 5: CFAR detection data - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 5: CFAR Detection Data ---"); - apply_reset; - ft601_txe = 0; - preload_pending_data; - assert_range_valid(32'h0000_0002); - wait_for_state(S_IDLE, 200); - #1; - check(uut.cfar_data_pending === 1'b0, - "cfar_data_pending cleared after packet"); - check(uut.current_state === S_IDLE && - uut.doppler_data_pending === 1'b0 && - uut.cfar_data_pending === 1'b0, - "CFAR detection sent, all pending flags cleared"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 6: Footer retained after packet - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 6: Footer Retention ---"); - apply_reset; - ft601_txe = 0; - - @(posedge clk); - cfar_detection = 1'b1; - @(posedge clk); - preload_pending_data; - assert_range_valid(32'hFACE_FEED); - wait_for_state(S_IDLE, 100); - #1; - check(uut.current_state === S_IDLE, - "Full packet incl. footer completed, back in IDLE"); - - // The last word driven was word 2 which contains footer 0x55. - // WAIT_ACK and IDLE don't overwrite ft601_data_out, so it retains - // the last driven value. - check(uut.ft601_data_out[15:8] === 8'h55, - "ft601_data_out retains footer 0x55 in word 2 position"); - - // Verify WAIT_ACK → IDLE transition - apply_reset; - ft601_txe = 0; - preload_pending_data; - assert_range_valid(32'h1234_5678); - wait_for_state(S_IDLE, 100); - #1; - check(uut.current_state === S_IDLE, - "Returned to IDLE after WAIT_ACK"); - check(ft601_wr_n === 1'b1, - "ft601_wr_n deasserted in IDLE"); - check(uut.ft601_data_oe === 1'b0, - "Data bus released in IDLE"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 7: Full packet sequence (end-to-end) - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 7: Full Packet Sequence ---"); - apply_reset; - ft601_txe = 0; - - drive_full_packet(32'hCAFE_BABE, 16'h1234, 16'h5678, 1'b1); - - check(uut.current_state === S_IDLE, - "Full packet completed, back in IDLE"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 8: FIFO backpressure - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 8: FIFO Backpressure ---"); - apply_reset; - ft601_txe = 1; // FIFO full — stall - - preload_pending_data; - assert_range_valid(32'hBBBB_CCCC); - - wait_for_state(S_SEND_DATA_WORD, 50); - repeat (10) @(posedge ft601_clk_in); #1; - - check(uut.current_state === S_SEND_DATA_WORD, - "Stalled in SEND_DATA_WORD when ft601_txe=1 (FIFO full)"); - check(ft601_wr_n === 1'b1, - "ft601_wr_n not asserted during backpressure stall"); - - ft601_txe = 0; - repeat (6) @(posedge ft601_clk_in); #1; - - check(uut.current_state === S_IDLE || uut.current_state === S_WAIT_ACK, - "Resumed and completed after backpressure released"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 9: Clock divider - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 9: Clock Forwarding ---"); - apply_reset; - // Let the system run for a few clocks to stabilize after reset - repeat (2) @(posedge ft601_clk_in); - - // After ODDR change, ft601_clk_out is a forwarded copy of - // ft601_clk_in (in simulation: direct assign passthrough). - // Verify that ft601_clk_out tracks ft601_clk_in over 20 edges. - begin : clk_fwd_block - integer match_count; - match_count = 0; - - repeat (20) begin - @(posedge ft601_clk_in); #1; - if (ft601_clk_out === 1'b1) - match_count = match_count + 1; - end - - check(match_count === 20, - "ft601_clk_out follows ft601_clk_in (forwarded clock)"); - end - - // ════════════════════════════════════════════════════════ - // TEST GROUP 10: Bus release in IDLE and WAIT_ACK - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 10: Bus Release ---"); - apply_reset; - #1; - - check(uut.ft601_data_oe === 1'b0, - "ft601_data_oe=0 in IDLE (bus released)"); - check(ft601_data === 32'h0000_0000, - "ft601_data reads 0 in IDLE (pulldown active)"); - - // Drive a full packet and check WAIT_ACK - ft601_txe = 0; - preload_pending_data; - assert_range_valid(32'h1111_2222); - wait_for_state(S_WAIT_ACK, 50); - #1; - - check(uut.ft601_data_oe === 1'b0, - "ft601_data_oe=0 in WAIT_ACK (bus released)"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 11: Multiple consecutive packets - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 11: Multiple Consecutive Packets ---"); - apply_reset; - ft601_txe = 0; - - drive_full_packet(32'hAAAA_BBBB, 16'h1111, 16'h2222, 1'b1); - check(uut.current_state === S_IDLE, - "Packet 1 complete, back in IDLE"); - - repeat (4) @(posedge ft601_clk_in); - - drive_full_packet(32'hCCCC_DDDD, 16'h5555, 16'h6666, 1'b0); - check(uut.current_state === S_IDLE, - "Packet 2 complete, back in IDLE"); - - check(uut.range_profile_cap === 32'hCCCC_DDDD, - "Packet 2 range data captured correctly"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 12: Read Path - Single Command (Gap 4) - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 12: Read Path - Single Command ---"); - apply_reset; - // Write FSM is IDLE, so read FSM can activate - - // Send "Set radar mode" command: opcode=0x01, addr=0x00, value=0x0002 - send_host_command({8'h01, 8'h00, 16'h0002}); - - check(cmd_opcode === 8'h01, - "Read path: cmd_opcode=0x01 (set mode)"); - check(cmd_addr === 8'h00, - "Read path: cmd_addr=0x00"); - check(cmd_value === 16'h0002, - "Read path: cmd_value=0x0002 (single-chirp mode)"); - check(cmd_data === {8'h01, 8'h00, 16'h0002}, - "Read path: cmd_data matches full command word"); - - // Verify read FSM returned to idle - check(uut.read_state === 3'd0, - "Read FSM returned to RD_IDLE after command"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 13: Read Path - Multiple Commands (Gap 4) - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 13: Read Path - Multiple Commands ---"); - apply_reset; - - // Command 1: Set radar mode to auto-scan (0x01) - send_host_command({8'h01, 8'h00, 16'h0001}); - check(cmd_opcode === 8'h01, - "Multi-cmd 1: opcode=0x01 (set mode)"); - check(cmd_value === 16'h0001, - "Multi-cmd 1: value=0x0001 (auto-scan)"); - - // Command 2: Single chirp trigger (0x02) - send_host_command({8'h02, 8'h00, 16'h0000}); - check(cmd_opcode === 8'h02, - "Multi-cmd 2: opcode=0x02 (trigger)"); - - // Command 3: Set CFAR threshold (0x03) - send_host_command({8'h03, 8'h00, 16'h1234}); - check(cmd_opcode === 8'h03, - "Multi-cmd 3: opcode=0x03 (CFAR threshold)"); - check(cmd_value === 16'h1234, - "Multi-cmd 3: value=0x1234"); - - // Command 4: Set stream control (0x04) - send_host_command({8'h04, 8'h00, 16'h0005}); - check(cmd_opcode === 8'h04, - "Multi-cmd 4: opcode=0x04 (stream control)"); - check(cmd_value === 16'h0005, - "Multi-cmd 4: value=0x0005 (range+cfar)"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 14: Read/Write Interleave (Gap 4) - // Verifies no bus contention: read FSM only operates when - // write FSM is IDLE. - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 14: Read/Write Interleave ---"); - apply_reset; - ft601_txe = 0; - - // Start a write packet - preload_pending_data; - assert_range_valid(32'hFACE_FEED); - wait_for_state(S_SEND_DATA_WORD, 50); - @(posedge ft601_clk_in); #1; - - // While write FSM is active, assert RXF=0 (host has data) - // Read FSM should NOT activate (read_state stays RD_IDLE) - ft601_rxf = 0; - repeat (5) @(posedge ft601_clk_in); #1; - - check(uut.read_state === 3'd0, - "Read FSM stays in RD_IDLE while write FSM active"); - - // Deassert RXF, complete the write packet - ft601_rxf = 1; - wait_for_state(S_IDLE, 100); - @(posedge ft601_clk_in); #1; - - check(uut.current_state === S_IDLE, - "Write packet completed, FSM in IDLE"); - - // Now send a read command — should work fine after write completes - send_host_command({8'h01, 8'h00, 16'h0002}); - check(cmd_opcode === 8'h01, - "Read after write: cmd_opcode=0x01"); - check(cmd_value === 16'h0002, - "Read after write: cmd_value=0x0002"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 15: Stream Control Gating (Gap 2) - // Verify that disabling individual streams causes the write - // FSM to zero those fields in the packed words. - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 15: Stream Control Gating (Gap 2) ---"); - - // 15a: Disable doppler stream (stream_control = 3'b101 = range + cfar only) - apply_reset; - ft601_txe = 1; // Stall to inspect packed words - stream_control = 3'b101; // range + cfar, no doppler - // Wait for CDC propagation (2-stage sync) - repeat (6) @(posedge ft601_clk_in); - - @(posedge clk); - doppler_real = 16'hAAAA; - doppler_imag = 16'hBBBB; - cfar_detection = 1'b1; - @(posedge clk); - - preload_cfar_pending; - assert_range_valid(32'hAA11_BB22); - - wait_for_state(S_SEND_DATA_WORD, 200); - repeat (2) @(posedge ft601_clk_in); #1; - - // With doppler disabled, doppler fields in words 1 and 2 should be zero - // Word 1: {range[7:0], 0x00, 0x00, 0x00} (doppler zeroed) - check(uut.data_pkt_word1[23:0] === 24'h000000, - "Stream gate: doppler bytes zeroed in word 1 when disabled"); - - // Word 2 byte 3 (dop_im_lo) should also be zero - check(uut.data_pkt_word2[31:24] === 8'h00, - "Stream gate: dop_im_lo zeroed in word 2 when disabled"); - - // Let it complete - ft601_txe = 0; - wait_for_state(S_IDLE, 100); - #1; - check(uut.current_state === S_IDLE, - "Stream gate: packet completed without doppler"); - - // 15b: Disable all streams (stream_control = 3'b000) - // With no streams enabled, a range_valid pulse should NOT trigger the write FSM. - apply_reset; - ft601_txe = 0; - stream_control = 3'b000; - repeat (6) @(posedge ft601_clk_in); - - // Assert range_valid — FSM should stay in IDLE - @(posedge clk); - range_profile = 32'hDEAD_DEAD; - range_valid = 1; - repeat (3) @(posedge ft601_clk_in); - @(posedge clk); - range_valid = 0; - // Wait a few more cycles for any CDC propagation - repeat (10) @(posedge ft601_clk_in); #1; - - check(uut.current_state === S_IDLE, - "Stream gate: FSM stays IDLE when all streams disabled"); - - // 15c: Restore all streams - stream_control = 3'b111; - repeat (6) @(posedge ft601_clk_in); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 16: Status Readback (Gap 2) - // Verify that pulsing status_request triggers an 8-word - // status response via the SEND_STATUS state. - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 16: Status Readback (Gap 2) ---"); - apply_reset; - ft601_txe = 0; - - // Set known status input values - status_cfar_threshold = 16'hABCD; - status_stream_ctrl = 3'b101; - status_long_chirp = 16'd3000; - status_long_listen = 16'd13700; - status_guard = 16'd17540; - status_short_chirp = 16'd50; - status_short_listen = 16'd17450; - status_chirps_per_elev = 6'd32; - status_chirps_mismatch = 1'b1; // TX-G: exercise the new bit too - // Self-test status: all 5 tests passed, detail=0xA5, not busy - status_self_test_flags = 5'b11111; - status_self_test_detail = 8'hA5; - status_self_test_busy = 1'b0; - // AGC status: gain=5, peak=180, sat_count=12, enabled - status_agc_current_gain = 4'd5; - status_agc_peak_magnitude = 8'd180; - status_agc_saturation_count = 8'd12; - status_agc_enable = 1'b1; - - // Pulse status_request (1 cycle in clk domain — toggles status_req_toggle_100m) - @(posedge clk); - status_request = 1; - @(posedge clk); - status_request = 0; - - // Wait for toggle CDC propagation to ft601_clk domain - // (2-stage sync + edge detect = ~3-4 ft601_clk cycles) - repeat (8) @(posedge ft601_clk_in); #1; - - // The write FSM should enter SEND_STATUS - // Give it time to start (IDLE sees status_req_ft601) - wait_for_state(S_SEND_STATUS, 20); - #1; - check(uut.current_state === S_SEND_STATUS, - "Status readback: FSM entered SEND_STATUS"); - - // The SEND_STATUS state sends 8 words (idx 0-7): - // idx 0: 0xBB header, idx 1-6: status_words[0-5], idx 7: 0x55 footer - // After idx 7 it transitions to WAIT_ACK -> IDLE. - // Since ft601_txe=0, all 8 words should stream without stall. - wait_for_state(S_IDLE, 100); - #1; - check(uut.current_state === S_IDLE, - "Status readback: returned to IDLE after 8-word response"); - - // Verify the status snapshot was captured correctly. - check(uut.status_words[1] === {16'd3000, 16'd13700}, - "Status readback: word 1 = {long_chirp, long_listen}"); - check(uut.status_words[2] === {16'd17540, 16'd50}, - "Status readback: word 2 = {guard, short_chirp}"); - check(uut.status_words[3] === {16'd17450, 10'd0, 6'd32}, - "Status readback: word 3 = {short_listen, 0, chirps_per_elev}"); - // PR-AB.b expanded: status_words[4][1:0] formerly range_mode, now reserved 2'd0. - check(uut.status_words[4] === {4'd5, 8'd180, 8'd12, 1'b1, 1'b1, 8'd0, 2'b00}, - "Status readback: word 4 = {agc_gain=5, peak=180, sat=12, en=1, mismatch=1, reserved=0}"); - // status_words[5] = {7'd0, busy, 8'd0, detail[7:0], 3'd0, flags[4:0]} - // = {7'd0, 1'b0, 8'd0, 8'hA5, 3'd0, 5'b11111} - check(uut.status_words[5] === {7'd0, 1'b0, 8'd0, 8'hA5, 3'd0, 5'b11111}, - "Status readback: word 5 = self-test {busy=0, detail=A5, flags=1F}"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 17: New Chirp Timing Opcodes (Gap 2) - // Verify opcodes 0x10-0x15 are properly decoded by the - // read path. - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 17: Chirp Timing Opcodes (Gap 2) ---"); - apply_reset; - - // 0x10: Long chirp cycles - send_host_command({8'h10, 8'h00, 16'd2500}); - check(cmd_opcode === 8'h10, - "Chirp opcode: 0x10 (long chirp cycles)"); - check(cmd_value === 16'd2500, - "Chirp opcode: value=2500"); - - // 0x11: Long listen cycles - send_host_command({8'h11, 8'h00, 16'd12000}); - check(cmd_opcode === 8'h11, - "Chirp opcode: 0x11 (long listen cycles)"); - check(cmd_value === 16'd12000, - "Chirp opcode: value=12000"); - - // 0x12: Guard cycles - send_host_command({8'h12, 8'h00, 16'd15000}); - check(cmd_opcode === 8'h12, - "Chirp opcode: 0x12 (guard cycles)"); - check(cmd_value === 16'd15000, - "Chirp opcode: value=15000"); - - // 0x13: Short chirp cycles - send_host_command({8'h13, 8'h00, 16'd40}); - check(cmd_opcode === 8'h13, - "Chirp opcode: 0x13 (short chirp cycles)"); - check(cmd_value === 16'd40, - "Chirp opcode: value=40"); - - // 0x14: Short listen cycles - send_host_command({8'h14, 8'h00, 16'd16000}); - check(cmd_opcode === 8'h14, - "Chirp opcode: 0x14 (short listen cycles)"); - check(cmd_value === 16'd16000, - "Chirp opcode: value=16000"); - - // 0x15: Chirps per elevation - send_host_command({8'h15, 8'h00, 16'd16}); - check(cmd_opcode === 8'h15, - "Chirp opcode: 0x15 (chirps per elevation)"); - check(cmd_value === 16'd16, - "Chirp opcode: value=16"); - - // 0xFF: Status request (opcode decode check — actual readback tested above) - send_host_command({8'hFF, 8'h00, 16'h0000}); - check(cmd_opcode === 8'hFF, - "Chirp opcode: 0xFF (status request)"); - - // ════════════════════════════════════════════════════════ - // TEST GROUP 18: Self-Test Readback Variants - // Verify self-test busy flag, partial failures, and - // alternate status word 5 values. - // ════════════════════════════════════════════════════════ - $display("\n--- Test Group 18: Self-Test Readback Variants ---"); - apply_reset; - ft601_txe = 0; - - // Scenario A: Self-test busy, partial failure, different detail - status_self_test_flags = 5'b10110; // T0 fail, T3 fail - status_self_test_detail = 8'h42; - status_self_test_busy = 1'b1; - - // Trigger status readback - @(posedge clk); - status_request = 1; - @(posedge clk); - status_request = 0; - - repeat (8) @(posedge ft601_clk_in); #1; - wait_for_state(S_SEND_STATUS, 20); - #1; - check(uut.current_state === S_SEND_STATUS, - "Self-test readback A: FSM entered SEND_STATUS"); - - wait_for_state(S_IDLE, 100); - #1; - check(uut.current_state === S_IDLE, - "Self-test readback A: returned to IDLE"); - - // Verify word 5: {7'd0, busy=1, 8'd0, detail=0x42, 3'd0, flags=5'b10110} - check(uut.status_words[5] === {7'd0, 1'b1, 8'd0, 8'h42, 3'd0, 5'b10110}, - "Self-test readback A: word 5 = {busy=1, detail=42, flags=16}"); - - // ════════════════════════════════════════════════════════ - // Summary - // ════════════════════════════════════════════════════════ - $display(""); - $display("========================================"); - $display(" USB DATA INTERFACE TESTBENCH RESULTS"); - $display(" PASSED: %0d / %0d", pass_count, test_num); - $display(" FAILED: %0d / %0d", fail_count, test_num); - if (fail_count == 0) - $display(" ** ALL TESTS PASSED **"); - else - $display(" ** SOME TESTS FAILED **"); - $display("========================================"); - $display(""); - - $fclose(csv_file); - #100; + frame_complete = 1'b0; + // Wait for full drain: 9 + 1024 + 49152 + 6144 + 1 = 56330 bytes. + // FT601 at 100 MHz produces 1 byte/cycle; budget ~70k ft601_clk + slack. + wait_clk(100_000); + 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", + 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); + 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 round-trip + // ------------------------------------------------------------- + $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); + + $display("\n-----------------------------------------------------------"); + $display("RESULTS: %0d PASS, %0d FAIL", pass, fail); + $display("-----------------------------------------------------------"); + if (fail == 0) $display("[OVERALL PASS]"); else $display("[OVERALL FAIL]"); + $finish; + end + + initial begin + #20_000_000; + $display("[TIMEOUT] tb_usb_data_interface watchdog"); $finish; end diff --git a/9_Firmware/9_2_FPGA/tb/tb_usb_drivers_parity.v b/9_Firmware/9_2_FPGA/tb/tb_usb_drivers_parity.v new file mode 100644 index 0000000..dc2c4fd --- /dev/null +++ b/9_Firmware/9_2_FPGA/tb/tb_usb_drivers_parity.v @@ -0,0 +1,376 @@ +`timescale 1ns / 1ps +`include "radar_params.vh" + +// ============================================================================ +// tb_usb_drivers_parity.v +// +// PR-AD (AD.2) cross-comparison parity TB. Instantiates BOTH +// usb_data_interface_ft2232h.v (8-bit, 60 MHz) and usb_data_interface.v +// (FT601, 32-bit+BE, 100 MHz) and feeds them identical stimulus. +// +// Each driver's egress byte stream is captured into a ring buffer (the +// FT601 stream is BE-reconstructed in lane order). The TB asserts: +// - byte counts are equal +// - per-index bytes are equal +// +// This is the byte-equality contract that makes the FT601 driver canonical +// with the FT2232H driver. Any future change to the FT2232H WR FSM that +// isn't mirrored in the FT601 driver will fail this TB at the diverging +// byte index. +// +// Scenarios: +// A. Frame header only (stream_control = 0) — 10 bytes +// B. Status packet — 34 bytes +// C. Full frame with all 3 streams — 56330 bytes +// ============================================================================ + +module tb_usb_drivers_parity; + // Common system clock (100 MHz radar domain) + localparam CLK_PER = 10.0; + // Each driver has its own USB-side clock. + localparam FT_CLK_PER = 16.667; // 60 MHz FT2232H ft_clk + localparam FT601_CLK_PER = 10.0; // 100 MHz FT601 ft601_clk_in + + reg clk = 1'b0; + reg ft_clk = 1'b0; + reg ft601_clk = 1'b0; + reg reset_n = 1'b0; + reg ft_reset_n = 1'b0; + reg ft601_reset_n = 1'b0; + + always #(CLK_PER/2) clk = ~clk; + always #(FT_CLK_PER/2) ft_clk = ~ft_clk; + always #(FT601_CLK_PER/2) ft601_clk = ~ft601_clk; + + // Shared radar stimulus + 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; + + // Shared control / status + reg [5:0] stream_control = 6'b000_111; + reg [2:0] subframe_enable = 3'b111; + reg status_request = 1'b0; + reg [15:0] status_cfar_threshold = 16'h1234; + reg [5:0] status_stream_ctrl = 6'b000_111; + 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 [15:0] status_medium_chirp = 16'd`RP_DEF_MEDIUM_CHIRP_CYCLES; + reg [15:0] status_medium_listen = 16'd`RP_DEF_MEDIUM_LISTEN_CYCLES; + reg [5:0] status_chirps_per_elev = 6'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; + reg status_beam_handshake_watchdog = 1'b0; + reg [7:0] status_cfar_alpha_soft = `RP_DEF_CFAR_ALPHA_SOFT; + reg [16:0] status_detect_threshold_soft = 17'h00ABC; + reg [15:0] status_detect_count_cand = 16'd42; + + // ---- FT2232H driver instance ---- + wire [7:0] ft_data; + reg ft_rxf_n = 1'b1; + reg ft_txe_n = 1'b0; + wire ft_rd_n, ft_wr_n, ft_oe_n, ft_siwu; + pulldown pd_ft[7:0] (ft_data); + + usb_data_interface_ft2232h u_ft2232h ( + .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_valid(), .cmd_opcode(), .cmd_addr(), .cmd_value(), + .stream_control(stream_control), + .subframe_enable(subframe_enable), + .status_request(status_request), + .status_cfar_threshold(status_cfar_threshold), + .status_stream_ctrl(status_stream_ctrl), + .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_medium_chirp(status_medium_chirp), + .status_medium_listen(status_medium_listen), + .status_chirps_per_elev(status_chirps_per_elev), + .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_beam_handshake_watchdog(status_beam_handshake_watchdog), + .status_cfar_alpha_soft(status_cfar_alpha_soft), + .status_detect_threshold_soft(status_detect_threshold_soft), + .status_detect_count_cand(status_detect_count_cand) + ); + + // ---- FT601 driver instance ---- + wire [31:0] ft601_data; + wire [3:0] ft601_be; + wire ft601_txe_n_unused, ft601_rxf_n_unused; + reg ft601_txe = 1'b0; + reg ft601_rxf = 1'b1; + wire ft601_wr_n, ft601_rd_n, ft601_oe_n, ft601_siwu_n; + wire ft601_clk_out_unused; + pulldown pd_ft601[31:0] (ft601_data); + + usb_data_interface u_ft601 ( + .clk(clk), .reset_n(reset_n), .ft601_reset_n(ft601_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), + .ft601_data(ft601_data), .ft601_be(ft601_be), + .ft601_txe_n(ft601_txe_n_unused), .ft601_rxf_n(ft601_rxf_n_unused), + .ft601_txe(ft601_txe), .ft601_rxf(ft601_rxf), + .ft601_wr_n(ft601_wr_n), .ft601_rd_n(ft601_rd_n), + .ft601_oe_n(ft601_oe_n), .ft601_siwu_n(ft601_siwu_n), + .ft601_srb(2'd0), .ft601_swb(2'd0), + .ft601_clk_out(ft601_clk_out_unused), .ft601_clk_in(ft601_clk), + .cmd_data(), .cmd_valid(), .cmd_opcode(), .cmd_addr(), .cmd_value(), + .stream_control(stream_control), + .subframe_enable(subframe_enable), + .status_request(status_request), + .status_cfar_threshold(status_cfar_threshold), + .status_stream_ctrl(status_stream_ctrl), + .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_medium_chirp(status_medium_chirp), + .status_medium_listen(status_medium_listen), + .status_chirps_per_elev(status_chirps_per_elev), + .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_beam_handshake_watchdog(status_beam_handshake_watchdog), + .status_cfar_alpha_soft(status_cfar_alpha_soft), + .status_detect_threshold_soft(status_detect_threshold_soft), + .status_detect_count_cand(status_detect_count_cand) + ); + + // ============================================================================ + // Byte capture rings — sized to fit a full frame (56330 bytes) + // ============================================================================ + localparam integer RING_LEN = 65536; + reg [7:0] a_bytes [0:RING_LEN-1]; + reg [7:0] b_bytes [0:RING_LEN-1]; + integer a_count = 0; + integer b_count = 0; + + // FT2232H byte capture: 1 byte per ft_clk cycle when (!ft_wr_n && !ft_txe_n). + always @(posedge ft_clk) begin + if (!ft_wr_n && !ft_txe_n) begin + if (a_count < RING_LEN) + a_bytes[a_count] <= ft_data; + a_count <= a_count + 1; + end + end + + // FT601 byte capture: up to 4 bytes per ft601_clk cycle (BE-masked). + // Lane mapping: byte0 -> data[7:0] (BE[0]), byte1 -> data[15:8] (BE[1]), + // byte2 -> data[23:16] (BE[2]), byte3 -> data[31:24] (BE[3]). + integer b_idx; + always @(posedge ft601_clk) begin + if (!ft601_wr_n && !ft601_txe) begin + b_idx = b_count; + if (ft601_be[0]) begin + if (b_idx < RING_LEN) b_bytes[b_idx] <= ft601_data[7:0]; + b_idx = b_idx + 1; + end + if (ft601_be[1]) begin + if (b_idx < RING_LEN) b_bytes[b_idx] <= ft601_data[15:8]; + b_idx = b_idx + 1; + end + if (ft601_be[2]) begin + if (b_idx < RING_LEN) b_bytes[b_idx] <= ft601_data[23:16]; + b_idx = b_idx + 1; + end + if (ft601_be[3]) begin + if (b_idx < RING_LEN) b_bytes[b_idx] <= ft601_data[31:24]; + b_idx = b_idx + 1; + end + b_count <= b_idx; + end + end + + // ============================================================================ + // Bookkeeping + // ============================================================================ + integer pass = 0; + integer fail = 0; + integer first_diff_idx; + integer i; + + 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 assert_parity; + input [127:0] scenario; + input integer expected_count; + begin + $display("--- Parity check: %0s ---", scenario); + $display(" FT2232H count = %0d", a_count); + $display(" FT601 count = %0d", b_count); + check_b("count equal across drivers", a_count == b_count); + check_b("count matches expected", a_count == expected_count); + + // Find first byte difference (if any) + first_diff_idx = -1; + for (i = 0; i < a_count && i < b_count; i = i + 1) begin + if (a_bytes[i] !== b_bytes[i] && first_diff_idx == -1) + first_diff_idx = i; + end + if (first_diff_idx != -1) begin + $display(" [FAIL] first byte mismatch at index %0d: ft2232h=0x%02h ft601=0x%02h", + first_diff_idx, a_bytes[first_diff_idx], b_bytes[first_diff_idx]); + fail = fail + 1; + end else begin + $display(" [PASS] byte streams identical"); + pass = pass + 1; + end + end + endtask + + task wait_clk; + input integer n; + integer j; + begin + for (j = 0; j < n; j = j + 1) @(posedge clk); + end + endtask + + task reset_capture; + begin + a_count = 0; + b_count = 0; + end + endtask + + initial begin + $display("\n========== tb_usb_drivers_parity =========="); + reset_n = 1'b0; + ft_reset_n = 1'b0; + ft601_reset_n = 1'b0; + wait_clk(15); + reset_n = 1'b1; + ft_reset_n = 1'b1; + ft601_reset_n = 1'b1; + wait_clk(40); + + // -------------------------------------------------------------- + // SCENARIO A — frame header only (no stream bodies) + // Expected total = 9 (header) + 1 (footer) = 10 bytes per driver. + // -------------------------------------------------------------- + $display("\n[SCENARIO A] Frame header only"); + stream_control = 6'b000_000; + wait_clk(50); + reset_capture; + @(posedge clk); + frame_complete = 1'b1; + @(posedge clk); + frame_complete = 1'b0; + wait_clk(500); // both drivers drain + assert_parity("scenario A (header+footer)", 10); + + // -------------------------------------------------------------- + // SCENARIO B — status packet + // Expected total = 34 bytes per driver. + // -------------------------------------------------------------- + $display("\n[SCENARIO B] Status packet"); + wait_clk(50); + reset_capture; + @(posedge clk); + status_request = 1'b1; + @(posedge clk); + status_request = 1'b0; + wait_clk(800); + assert_parity("scenario B (status pkt)", 34); + + // -------------------------------------------------------------- + // SCENARIO C — full frame with all 3 streams enabled + // Expected total = 9 + 1024 + 49152 + 6144 + 1 = 56330 bytes. + // BRAMs zero-init in SIMULATION mode so content matches across + // drivers (both emit 0x00 for every cell). + // -------------------------------------------------------------- + $display("\n[SCENARIO C] Full frame (all 3 streams)"); + stream_control = 6'b000_111; + wait_clk(50); + reset_capture; + @(posedge clk); + frame_complete = 1'b1; + @(posedge clk); + frame_complete = 1'b0; + // FT2232H @ 60 MHz needs ~56330 ft_clk cycles ≈ 940 µs. + // FT601 @ 100 MHz needs ~56330 ft601_clk ≈ 564 µs. + // Use clk-domain wait covering the slower driver: ~94000 clk + slack. + wait_clk(150_000); + assert_parity("scenario C (full frame)", + `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); + + $display("\n-----------------------------------------------------------"); + $display("RESULTS: %0d PASS, %0d FAIL", pass, fail); + $display("-----------------------------------------------------------"); + if (fail == 0) $display("[OVERALL PASS]"); else $display("[OVERALL FAIL]"); + $finish; + end + + initial begin + #50_000_000; + $display("[TIMEOUT] tb_usb_drivers_parity watchdog"); + $finish; + end + +endmodule