test(fpga): PR-AD AD.3 — FT601 system-opcode TB + SUPPORT_LONG_RANGE compile guard

Guards against future drift between the two USB driver command paths
(USB_MODE=0 vs USB_MODE=1) and against 200T+long-range build breakage
in usb_data_interface.v.

tb/tb_system_opcodes_ft601.v — new (~397 LOC):
  - Sibling of tb_system_opcodes.v with radar_system_top
    #(.USB_MODE(0))
  - FT601 RX BFM: 32-bit single-word read with 5-cycle word holds
    (IDLE -> OE_ASSERT -> READING -> DEASSERT -> PROCESS)
  - 28/0 PASS, same opcode coverage as the FT2232H sibling

run_regression.sh — adds 2 entries:
  - "FT601 Driver Long-Range Compile (PR-AD AD.3)" in Phase 1:
    inline iverilog -DSUPPORT_LONG_RANGE on usb_data_interface.v;
    suppresses informational warnings (timescale / dangling / @*
    sensitive) and fails on real warnings. Catches macro creep
    that would break the 200T 20-km build before Vivado synthesis.
  - "System Opcodes FT601 (tb_system_opcodes_ft601)" in Phase 2
    (non-quick block).

radar_system_top.v — header doc block documenting the
USB_MODE x SUPPORT_LONG_RANGE 4-cell build matrix and clarifying
that the flags are orthogonal (200T+FT601 with 3-km-only DSP is a
valid bring-up baseline; 50T+FT2232H+LR is rare but must still
compile). Notes that both drivers emit the identical v2 bulk frame
and that byte-equality is enforced by tb_usb_drivers_parity.v.

Regression: 46/0/0 (44 + 1 system TB + 1 LR compile guard).
Lint: 0 errors, 2 advisory (pre-existing, unrelated).
This commit is contained in:
Jason
2026-05-18 13:29:11 +05:45
parent e1abeecaa9
commit 4c9032187a
3 changed files with 454 additions and 2 deletions

View File

@@ -4,18 +4,40 @@
/**
* radar_system_top.v
*
*
* Complete Radar System Top Module
* Integrates:
* - Radar Transmitter (PLFM chirp generation)
* - Radar Receiver (ADC interface, DDC, matched filtering, Doppler processing)
* - USB Data Interface (FT601 USB 3.0 or FT2232H USB 2.0, selected by USB_MODE)
*
*
* Clock domains:
* - clk_100m: System clock (100MHz)
* - clk_120m_dac: DAC clock (120MHz)
* - ft601_clk: USB interface clock (100MHz FT601 or 60MHz FT2232H)
*
* ========================================================================
* BUILD MATRIX USB_MODE × SUPPORT_LONG_RANGE
* ========================================================================
* USB_MODE | SUPPORT_LONG_RANGE | Board / Variant
* ---------+--------------------+----------------------------------------
* 1 | undefined | 50T + FT2232H 3 km production
* 0 | defined | 200T + FT601 3 km + 20 km
* 0 | undefined | 200T + FT601 3 km only (compile-tested)
* 1 | defined | 50T + FT2232H + LR (rare; allowed)
*
* Both USB drivers (usb_data_interface.v / usb_data_interface_ft2232h.v)
* emit the identical v2 bulk frame protocol (PR-G, 56330 B/frame at the
* default 512 range × 48 doppler × 3 sub-frames). Byte-equality is asserted
* end-to-end by tb/tb_usb_drivers_parity.v (PR-AD AD.2).
*
* SUPPORT_LONG_RANGE only widens the upstream pipeline (range_bin_decimator,
* doppler_processor BRAM, cfar magnitude memory) from 512 4096 range bins;
* the USB wire protocol stays at 512 range bins regardless. The driver
* builds cleanly under either flag (verified by the FT601 Long-Range
* compile-only check in run_regression.sh).
* ========================================================================
*
* USB_MODE parameter:
* 0 = FT601 (32-bit, USB 3.0) 200T premium board
* 1 = FT2232H (8-bit, USB 2.0) 50T production board

View File

@@ -562,6 +562,33 @@ 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
# PR-AD AD.3 compile-only: prove FT601 driver builds clean under the 200T
# long-range define. Driver wire protocol (NUM_RANGE_BINS=512) is unaffected
# by SUPPORT_LONG_RANGE, but RP_RANGE_BIN_WIDTH_MAX widens 9→12; this guards
# any future macro creep from breaking the long-range build path.
printf " %-45s " "FT601 Driver Long-Range Compile (PR-AD AD.3)"
lr_log=/tmp/ft601_lr_compile_$$.log
if iverilog -g2001 -DSIMULATION -DSUPPORT_LONG_RANGE -Wall \
-o /tmp/ft601_lr_compile_$$.vvp usb_data_interface.v 2>"$lr_log"; then
# Suppress informational warnings (timescale / dangling / array @* sensitivity)
lr_err=$(grep -vE 'sensitive to all|timescale|dangling' "$lr_log" | grep -c . || true)
if [[ "$lr_err" -eq 0 ]]; then
echo -e "${GREEN}PASS${NC} (compile clean, SUPPORT_LONG_RANGE)"
PASS=$((PASS + 1))
else
echo -e "${RED}FAIL${NC} ($lr_err warning(s))"
grep -vE 'sensitive to all|timescale|dangling' "$lr_log" | sed 's/^/ /'
ERRORS="$ERRORS\n FT601 Long-Range Compile: $lr_err warning(s)"
FAIL=$((FAIL + 1))
fi
else
echo -e "${RED}COMPILE FAIL${NC}"
sed 's/^/ /' "$lr_log"
ERRORS="$ERRORS\n FT601 Long-Range Compile: iverilog compile error"
FAIL=$((FAIL + 1))
fi
rm -f /tmp/ft601_lr_compile_$$.vvp "$lr_log"
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
@@ -682,6 +709,12 @@ if [[ "$QUICK" -eq 0 ]]; then
tb/tb_system_opcodes_reg.vvp \
tb/tb_system_opcodes.v "${SYSTEM_RTL[@]}"
# PR-AD AD.3: FT601 path opcode dispatch — guards against future drift
# between the two USB driver command paths (USB_MODE=0 vs USB_MODE=1).
run_test "System Opcodes FT601 (tb_system_opcodes_ft601)" \
tb/tb_system_opcodes_ft601_reg.vvp \
tb/tb_system_opcodes_ft601.v "${SYSTEM_RTL[@]}"
run_test "System Mechanics (tb_system_mechanics)" \
tb/tb_system_mechanics_reg.vvp \
tb/tb_system_mechanics.v "${SYSTEM_RTL[@]}"

View File

@@ -0,0 +1,397 @@
`timescale 1ns / 1ps
`include "radar_params.vh"
// ============================================================================
// tb_system_opcodes_ft601.v (PR-AD AD.3 FT601 sibling of tb_system_opcodes)
//
// Verifies host opcode dispatch through the FT601 USB 3.0 path.
// radar_system_top is instantiated with USB_MODE=0; ft601_data / ft601_rxf
// are driven by a BFM. The FT601 RX FSM reads one 32-bit word per command
// transaction (vs FT2232H's 4-byte shift), so the BFM is simpler than its
// FT2232H sibling.
//
// Each test sends a command word {opcode[31:24], addr[23:16], value[15:0]}
// and verifies the corresponding dut.host_* register updates after CDC
// propagation.
//
// Mirrors the same test groups as tb_system_opcodes.v (G6 / G7 / G13 / G14
// / G17) so any opcode-dispatch regression caught on the FT2232H path is
// also caught on the FT601 path. This guards against future drift between
// the two USB drivers' command paths.
// ============================================================================
module tb_system_opcodes_ft601;
// ----------------------------------------------------------------------------
// Clocks
// ----------------------------------------------------------------------------
localparam CLK_100M_PERIOD = 10.0; // 100 MHz radar clock
localparam CLK_120M_PERIOD = 8.333; // 120 MHz DAC clock
localparam FT601_CLK_PERIOD = 10.0; // 100 MHz FT601 clock (asynchronous to clk_100m)
localparam ADC_DCO_PERIOD = 2.5;
reg clk_100m = 1'b0;
reg clk_120m_dac = 1'b0;
reg ft601_clk_in = 1'b0;
reg adc_dco_p = 1'b0;
reg adc_dco_n = 1'b1;
always #(CLK_100M_PERIOD/2) clk_100m = ~clk_100m;
always #(CLK_120M_PERIOD/2) clk_120m_dac = ~clk_120m_dac;
always #(FT601_CLK_PERIOD/2) ft601_clk_in = ~ft601_clk_in;
always #(ADC_DCO_PERIOD/2) begin adc_dco_p = ~adc_dco_p; adc_dco_n = ~adc_dco_n; end
// ----------------------------------------------------------------------------
// DUT signals
// ----------------------------------------------------------------------------
reg reset_n = 1'b0;
reg [7:0] adc_d_p = 8'h80;
reg [7:0] adc_d_n = 8'h7F;
reg stm32_mixers_enable = 1'b0;
reg stm32_sclk_3v3 = 1'b0;
reg stm32_mosi_3v3 = 1'b0;
wire stm32_miso_3v3;
reg stm32_cs_adar1_3v3 = 1'b1, stm32_cs_adar2_3v3 = 1'b1;
reg stm32_cs_adar3_3v3 = 1'b1, stm32_cs_adar4_3v3 = 1'b1;
wire stm32_sclk_1v8, stm32_mosi_1v8;
reg stm32_miso_1v8 = 1'b0;
wire stm32_cs_adar1_1v8, stm32_cs_adar2_1v8;
wire stm32_cs_adar3_1v8, stm32_cs_adar4_1v8;
wire [7:0] dac_data;
wire dac_clk;
wire dac_sleep;
wire fpga_rf_switch;
wire rx_mixer_en, tx_mixer_en;
wire adc_pwdn;
wire adar_tx_load_1, adar_rx_load_1;
wire adar_tx_load_2, adar_rx_load_2;
wire adar_tx_load_3, adar_rx_load_3;
wire adar_tx_load_4, adar_rx_load_4;
wire adar_tr_1, adar_tr_2, adar_tr_3, adar_tr_4;
// FT601 ports DRIVEN BY THIS TB
wire [31:0] ft601_data;
wire [3:0] ft601_be;
wire ft601_txe_n;
wire ft601_rxf_n;
reg ft601_txe = 1'b0;
reg ft601_rxf = 1'b1;
wire ft601_wr_n;
wire ft601_rd_n;
wire ft601_oe_n;
wire ft601_siwu_n;
reg [1:0] ft601_srb = 2'b00;
reg [1:0] ft601_swb = 2'b00;
wire ft601_clk_out;
// TB-side bus driver: drive ft601_data on host->FPGA, release otherwise.
reg [31:0] ft601_data_drive = 32'd0;
reg ft601_data_drive_en = 1'b0;
assign ft601_data = ft601_data_drive_en ? ft601_data_drive : 32'hzzzz_zzzz;
pulldown pd[31:0] (ft601_data);
// FT2232H ports unused in USB_MODE=0; tie inputs, ignore outputs
wire [7:0] ft_data;
reg ft_rxf_n = 1'b1;
reg ft_txe_n = 1'b1;
wire ft_rd_n;
wire ft_wr_n;
wire ft_oe_n;
wire ft_siwu;
wire [5:0] current_chirp;
wire new_chirp_frame;
wire [31:0] dbg_doppler_data;
wire dbg_doppler_valid;
wire [`RP_DOPPLER_BIN_WIDTH-1:0] dbg_doppler_bin;
wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] dbg_range_bin;
wire [3:0] system_status;
wire gpio_dig5, gpio_dig6, gpio_dig7;
// ----------------------------------------------------------------------------
// DUT radar_system_top with USB_MODE=0 (FT601 path, 200T premium board)
// ----------------------------------------------------------------------------
radar_system_top #(
.USB_MODE(0)
) dut (
.clk_100m(clk_100m),
.clk_120m_dac(clk_120m_dac),
.ft601_clk_in(ft601_clk_in),
.reset_n(reset_n),
.dac_data(dac_data), .dac_clk(dac_clk), .dac_sleep(dac_sleep),
.fpga_rf_switch(fpga_rf_switch),
.rx_mixer_en(rx_mixer_en), .tx_mixer_en(tx_mixer_en),
.adar_tx_load_1(adar_tx_load_1), .adar_rx_load_1(adar_rx_load_1),
.adar_tx_load_2(adar_tx_load_2), .adar_rx_load_2(adar_rx_load_2),
.adar_tx_load_3(adar_tx_load_3), .adar_rx_load_3(adar_rx_load_3),
.adar_tx_load_4(adar_tx_load_4), .adar_rx_load_4(adar_rx_load_4),
.adar_tr_1(adar_tr_1), .adar_tr_2(adar_tr_2),
.adar_tr_3(adar_tr_3), .adar_tr_4(adar_tr_4),
.stm32_sclk_3v3(stm32_sclk_3v3),
.stm32_mosi_3v3(stm32_mosi_3v3),
.stm32_miso_3v3(stm32_miso_3v3),
.stm32_cs_adar1_3v3(stm32_cs_adar1_3v3),
.stm32_cs_adar2_3v3(stm32_cs_adar2_3v3),
.stm32_cs_adar3_3v3(stm32_cs_adar3_3v3),
.stm32_cs_adar4_3v3(stm32_cs_adar4_3v3),
.stm32_sclk_1v8(stm32_sclk_1v8),
.stm32_mosi_1v8(stm32_mosi_1v8),
.stm32_miso_1v8(stm32_miso_1v8),
.stm32_cs_adar1_1v8(stm32_cs_adar1_1v8),
.stm32_cs_adar2_1v8(stm32_cs_adar2_1v8),
.stm32_cs_adar3_1v8(stm32_cs_adar3_1v8),
.stm32_cs_adar4_1v8(stm32_cs_adar4_1v8),
.adc_d_p(adc_d_p), .adc_d_n(adc_d_n),
.adc_dco_p(adc_dco_p), .adc_dco_n(adc_dco_n),
.adc_or_p(1'b0), .adc_or_n(1'b1),
.adc_pwdn(adc_pwdn),
.stm32_beam_ready(1'b0),
.stm32_mixers_enable(stm32_mixers_enable),
// FT601 ports driven by this TB
.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),
// FT2232H ports tied off in USB_MODE=0
.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),
.current_chirp(current_chirp),
.new_chirp_frame(new_chirp_frame),
.dbg_doppler_data(dbg_doppler_data),
.dbg_doppler_valid(dbg_doppler_valid),
.dbg_doppler_bin(dbg_doppler_bin),
.dbg_range_bin(dbg_range_bin),
.system_status(system_status),
.gpio_dig5(gpio_dig5),
.gpio_dig6(gpio_dig6),
.gpio_dig7(gpio_dig7)
);
// ----------------------------------------------------------------------------
// BFM FT601 RX FSM: 32-bit single-word read
// FSM advances RD_IDLE -> RD_OE_ASSERT -> RD_READING -> RD_DEASSERT ->
// RD_PROCESS over 4 ft601_clk cycles, sampling ft601_data once at RD_READING.
// Command word format: {opcode[31:24], addr[23:16], value[15:0]}.
// ----------------------------------------------------------------------------
task wait_clk;
input integer n;
integer i;
begin
for (i = 0; i < n; i = i + 1) @(posedge clk_100m);
end
endtask
task send_cmd;
input [7:0] op;
input [7:0] addr;
input [15:0] val;
begin
@(posedge ft601_clk_in); #1;
ft601_rxf = 1'b0;
ft601_data_drive = {op, addr, val};
ft601_data_drive_en = 1'b1;
@(posedge ft601_clk_in); #1; // RD_IDLE -> RD_OE_ASSERT
@(posedge ft601_clk_in); #1; // RD_OE_ASSERT -> RD_READING
@(posedge ft601_clk_in); #1; // RD_READING samples ft601_data
@(posedge ft601_clk_in); #1; // RD_DEASSERT
@(posedge ft601_clk_in); #1; // RD_PROCESS pulses cmd_valid
ft601_rxf = 1'b1;
ft601_data_drive_en = 1'b0;
wait_clk(40); // CDC ft601_clk_in -> clk_100m
end
endtask
// ----------------------------------------------------------------------------
// Test infrastructure
// ----------------------------------------------------------------------------
integer pass_count = 0;
integer fail_count = 0;
integer test_num = 0;
task check;
input cond;
input [80*8-1:0] msg;
begin
test_num = test_num + 1;
if (cond) begin
$display(" [PASS] %0d: %0s", test_num, msg);
pass_count = pass_count + 1;
end else begin
$display(" [FAIL] %0d: %0s", test_num, msg);
fail_count = fail_count + 1;
end
end
endtask
// ----------------------------------------------------------------------------
// Main test sequence mirrors tb_system_opcodes.v groups
// ----------------------------------------------------------------------------
initial begin
$display("============================================================");
$display(" tb_system_opcodes_ft601 — opcode dispatch via FT601");
$display("============================================================");
reset_n = 1'b0;
wait_clk(20);
reset_n = 1'b1;
wait_clk(50);
// ====================================================================
// Group 6: USB Command Decode
// ====================================================================
$display("\n--- Group 6: USB Command Decode (FT601) ---");
send_cmd(8'h03, 8'h00, 16'h1234);
check(dut.host_detect_threshold == 16'h1234,
"G6.2: 0x03 -> host_detect_threshold = 0x1234");
send_cmd(8'h04, 8'h00, 16'h0005);
check(dut.host_stream_control == 6'b000_101,
"G6.3: 0x04 -> host_stream_control[2:0] = 3'b101");
send_cmd(8'h10, 8'h00, 16'd2000);
check(dut.host_long_chirp_cycles == 16'd2000,
"G6.4: 0x10 -> host_long_chirp_cycles = 2000");
send_cmd(8'h15, 8'h00, 16'd48);
check(dut.host_chirps_per_elev == 6'd48,
"G6.5: 0x15 -> host_chirps_per_elev = 48");
check(dut.chirps_mismatch_error == 1'b0,
"G6.5b: chirps_mismatch_error clear when chirps==48");
// ====================================================================
// Group 7: CDC Integrity
// ====================================================================
$display("\n--- Group 7: USB Command CDC Integrity ---");
send_cmd(8'h03, 8'h00, 16'hAAAA);
send_cmd(8'h03, 8'h00, 16'hBBBB);
send_cmd(8'h03, 8'h00, 16'hCCCC);
check(dut.host_detect_threshold == 16'hCCCC,
"G7.2: Last of 3 rapid commands applied (0xCCCC)");
check(dut.host_detect_threshold == 16'hCCCC,
"G7.4: CDC-transferred threshold bit-exact");
// ====================================================================
// Group 13: Chirps/Doppler Mismatch Protection
// ====================================================================
$display("\n--- Group 13: Chirps/Doppler Mismatch Protection ---");
send_cmd(8'h15, 8'h00, 16'd48);
check(dut.host_chirps_per_elev == 6'd48,
"G13.1: chirps_per_elev=48 accepted");
check(dut.chirps_mismatch_error == 1'b0,
"G13.2: Mismatch clear when chirps==48");
send_cmd(8'h15, 8'h00, 16'd56);
check(dut.host_chirps_per_elev == 6'd48,
"G13.3: chirps=56 clamped to 48");
check(dut.chirps_mismatch_error == 1'b1,
"G13.4: Mismatch set when chirps>48");
send_cmd(8'h15, 8'h00, 16'd0);
check(dut.host_chirps_per_elev == 6'd48,
"G13.5: chirps=0 clamped to 48");
send_cmd(8'h15, 8'h00, 16'd16);
check(dut.host_chirps_per_elev == 6'd16,
"G13.6: chirps_per_elev=16 accepted (not clamped)");
check(dut.chirps_mismatch_error == 1'b1,
"G13.7: Mismatch set when chirps<48");
send_cmd(8'h15, 8'h00, 16'd48);
check(dut.chirps_mismatch_error == 1'b0,
"G13.8: Mismatch clears when restored to 48");
// ====================================================================
// Group 14: CFAR Opcodes
// ====================================================================
$display("\n--- Group 14: CFAR Opcodes ---");
send_cmd(8'h21, 8'h00, 16'h0004);
check(dut.host_cfar_guard == 4'd4, "G14.4: 0x21 -> host_cfar_guard = 4");
send_cmd(8'h21, 8'h00, 16'h0000);
check(dut.host_cfar_guard == 4'd0, "G14.5: 0x21 -> host_cfar_guard = 0");
send_cmd(8'h22, 8'h00, 16'h0010);
check(dut.host_cfar_train == 5'd16, "G14.6: 0x22 -> host_cfar_train = 16");
send_cmd(8'h22, 8'h00, 16'h0001);
check(dut.host_cfar_train == 5'd1, "G14.7: 0x22 -> host_cfar_train = 1");
send_cmd(8'h23, 8'h00, 16'h0048);
check(dut.host_cfar_alpha == 8'h48, "G14.8: 0x23 -> host_cfar_alpha = 0x48");
send_cmd(8'h23, 8'h00, 16'h0010);
check(dut.host_cfar_alpha == 8'h10, "G14.9: 0x23 -> host_cfar_alpha = 0x10");
send_cmd(8'h24, 8'h00, 16'h0001);
check(dut.host_cfar_mode == 2'b01, "G14.10: 0x24 -> host_cfar_mode = GO-CFAR");
send_cmd(8'h24, 8'h00, 16'h0002);
check(dut.host_cfar_mode == 2'b10, "G14.11: 0x24 -> host_cfar_mode = SO-CFAR");
send_cmd(8'h25, 8'h00, 16'h0001);
check(dut.host_cfar_enable == 1'b1, "G14.12: 0x25 -> host_cfar_enable = 1");
send_cmd(8'h25, 8'h00, 16'h0000);
check(dut.host_cfar_enable == 1'b0, "G14.13: 0x25 -> host_cfar_enable = 0");
// ====================================================================
// Group 17: PR-G additions (MEDIUM ladder + alpha_soft)
// ====================================================================
$display("\n--- Group 17: PR-G MEDIUM ladder + alpha_soft ---");
send_cmd(`RP_OP_MEDIUM_CHIRP_CYCLES, 8'h00, 16'd750);
check(dut.host_medium_chirp_cycles == 16'd750,
"G17.1: 0x17 -> host_medium_chirp_cycles = 750");
send_cmd(`RP_OP_MEDIUM_LISTEN_CYCLES, 8'h00, 16'd16500);
check(dut.host_medium_listen_cycles == 16'd16500,
"G17.2: 0x18 -> host_medium_listen_cycles = 16500");
send_cmd(`RP_OP_CFAR_ALPHA_SOFT, 8'h00, 16'h0024);
check(dut.host_cfar_alpha_soft == 8'h24,
"G17.3: 0x2D -> host_cfar_alpha_soft = 0x24");
$display("\n============================================================");
$display(" RESULTS: %0d passed, %0d failed / %0d total",
pass_count, fail_count, test_num);
$display("============================================================");
if (fail_count == 0) $display(" *** ALL TESTS PASSED ***");
else $display(" *** %0d TEST(S) FAILED ***", fail_count);
$finish;
end
initial begin
#2_000_000;
$display("[WATCHDOG] tb_system_opcodes_ft601 timeout");
$display(" Tests: %0d, Pass: %0d, Fail: %0d",
test_num, pass_count, fail_count);
$finish;
end
endmodule