test(opcodes): PR-I.1 — tb_system_opcodes via production FT2232H path

New TB instantiates radar_system_top with USB_MODE=1 and wires the
FT2232H ports correctly (which tb_system_e2e never did — its BFM was
FT601-only, so USB_MODE=1 opcode-dispatch tests were stimulating
dangling ports). Uses the proven send_cmd pattern from
tb_usb_protocol_v2.

Coverage migrated from tb_system_e2e:
- G6.1-6.6 — opcode 0x01/0x02/0x03/0x04/0x10/0x15 dispatch
- G7.2/G7.4 — rapid USB cmd CDC integrity
- G13.1-8 — chirps_per_elev clamp at DOPPLER_FRAME_CHIRPS=48 (PR-F-aware;
  was hardcoded to 32 in tb_system_e2e G13)
- G14.1-13 — range_mode + CFAR opcode dispatch (0x20-0x25)

Plus new PR-G coverage:
- 0x17/0x18 MEDIUM ladder timing
- 0x2D cfar_alpha_soft

Result: 33/33 PASS in 15.7 ms sim. Resolves 10 of the 26 USB_MODE=1
failures from T-3 (the FT2232H-specific cluster). Remaining 16 in
USB_MODE=1 are T-2 pipeline-timing failures, addressed in PR-I.3
(tb_system_dataflow). tb_system_e2e is not yet retired — see PR-I.4.
This commit is contained in:
Jason
2026-05-01 12:07:31 +05:45
parent b7a841a32c
commit 413a01e2fa

View File

@@ -0,0 +1,481 @@
`timescale 1ns / 1ps
`include "radar_params.vh"
// ============================================================================
// tb_system_opcodes.v (PR-I, replaces tb_system_e2e G6/G7.2/G7.4/G13/G14)
//
// Verifies host opcode dispatch through the production FT2232H USB path.
// radar_system_top is instantiated with USB_MODE=1; ft_data / ft_rxf_n / etc.
// are driven by a BFM modeled on tb_usb_protocol_v2's send_cmd task (the only
// stimulus pattern proven to align with the FT2232H 4-cycle read FSM).
//
// Each test sends a 4-byte command (op, addr, val_hi, val_lo) and verifies
// the corresponding dut.host_* register updates after CDC propagation.
//
// Sim budget: ~1 ms opcode dispatch only, no chirp pipeline.
// ============================================================================
module tb_system_opcodes;
// ----------------------------------------------------------------------------
// Clocks
// ----------------------------------------------------------------------------
localparam CLK_100M_PERIOD = 10.0; // 100 MHz radar clock
localparam CLK_120M_PERIOD = 8.333; // 120 MHz DAC clock
localparam FT_CLK_PERIOD = 16.667; // 60 MHz FT2232H clock
localparam ADC_DCO_PERIOD = 2.5; // 400 MHz ADC DCO
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 #(FT_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 (FT2232H production path)
// ----------------------------------------------------------------------------
reg reset_n = 1'b0;
// ADC quiescent (no chirp data needed for opcode tests)
reg [7:0] adc_d_p = 8'h80;
reg [7:0] adc_d_n = 8'h7F;
// STM32 control tied off
reg stm32_new_chirp = 1'b0;
reg stm32_new_elevation = 1'b0;
reg stm32_new_azimuth = 1'b0;
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;
// DAC outputs (ignored)
wire [7:0] dac_data;
wire dac_clk;
wire dac_sleep;
// RF control (ignored)
wire fpga_rf_switch;
wire rx_mixer_en, tx_mixer_en;
wire adc_pwdn;
// ADAR (ignored)
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 unused in USB_MODE=1; tie inputs and ignore outputs
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;
// FT2232H ports DRIVEN BY THIS TB
wire [7:0] ft_data; // bidirectional
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;
// TB-side bus driver: drive ft_data while sending command bytes,
// release to high-Z otherwise (DUT may drive on writes, but opcode
// dispatch tests don't trigger writes).
reg [7:0] ft_data_drive = 8'h00;
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);
// Status / debug outputs (mostly ignored)
wire [5:0] current_elevation, current_azimuth, 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=1 (production FT2232H path)
// ----------------------------------------------------------------------------
radar_system_top #(
.USB_MODE(1)
) 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_new_chirp(stm32_new_chirp),
.stm32_new_elevation(stm32_new_elevation),
.stm32_new_azimuth(stm32_new_azimuth),
.stm32_mixers_enable(stm32_mixers_enable),
// FT601 ports tied off / unused in USB_MODE=1
.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 driven by this TB
.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_elevation(current_elevation),
.current_azimuth(current_azimuth),
.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 proven send_cmd from tb_usb_protocol_v2 (4-cycle FT2232H read FSM)
// ----------------------------------------------------------------------------
task wait_clk;
input integer n;
integer i;
begin
for (i = 0; i < n; i = i + 1) @(posedge clk_100m);
end
endtask
task wait_ft;
input integer n;
integer i;
begin
for (i = 0; i < n; i = i + 1) @(posedge ft601_clk_in);
end
endtask
task send_cmd;
input [7:0] op;
input [7:0] addr;
input [15:0] val;
begin
@(posedge ft601_clk_in); #1;
ft_rxf_n = 1'b0;
ft_data_drive = op;
ft_data_drive_en = 1'b1;
@(posedge ft601_clk_in); #1; // RD_IDLE -> RD_OE_ASSERT (NBA)
@(posedge ft601_clk_in); #1; // RD_OE_ASSERT -> RD_READING
@(posedge ft601_clk_in); #1; // RD_READING samples op
ft_data_drive = addr;
@(posedge ft601_clk_in); #1; // samples addr
ft_data_drive = val[15:8];
@(posedge ft601_clk_in); #1; // samples val_hi
ft_data_drive = val[7:0];
@(posedge ft601_clk_in); #1; // samples val_lo
ft_rxf_n = 1'b1;
ft_data_drive_en = 1'b0;
wait_clk(40); // CDC ft_clk -> clk_100m + dispatch register
end
endtask
// ----------------------------------------------------------------------------
// Test infrastructure
// ----------------------------------------------------------------------------
integer pass_count = 0;
integer fail_count = 0;
integer test_num = 0;
// Sticky flag for G6.6 (host_trigger_pulse is a 1-cycle self-clearing pulse).
reg trigger_pulse_seen = 1'b0;
always @(posedge clk_100m) begin
if (!reset_n) trigger_pulse_seen <= 1'b0;
else if (dut.host_trigger_pulse) trigger_pulse_seen <= 1'b1;
end
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
// ----------------------------------------------------------------------------
initial begin
$display("============================================================");
$display(" tb_system_opcodes — opcode dispatch via FT2232H");
$display("============================================================");
// Reset
reset_n = 1'b0;
wait_clk(20);
reset_n = 1'b1;
wait_clk(50);
// ====================================================================
// GROUP 6: USB COMMAND DECODE (was tb_system_e2e G6)
// ====================================================================
$display("\n--- Group 6: USB Command Decode ---");
// G6.1: Set radar mode (opcode 0x01) -> host_radar_mode[1:0]
send_cmd(8'h01, 8'h00, 16'h0002);
check(dut.host_radar_mode == 2'b10,
"G6.1: 0x01 -> host_radar_mode = 2'b10 (single chirp)");
// G6.2: Set detection threshold (0x03) -> host_detect_threshold
send_cmd(8'h03, 8'h00, 16'h1234);
check(dut.host_detect_threshold == 16'h1234,
"G6.2: 0x03 -> host_detect_threshold = 0x1234");
// G6.3: Set stream control (0x04) -> host_stream_control[2:0]
// Bits [5:3] are reserved (forced to 000 by dispatch logic)
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");
// G6.4: Long chirp cycles (0x10) -> host_long_chirp_cycles
send_cmd(8'h10, 8'h00, 16'd2000);
check(dut.host_long_chirp_cycles == 16'd2000,
"G6.4: 0x10 -> host_long_chirp_cycles = 2000");
// G6.5: chirps_per_elev (0x15). Production frame is 48 chirps; value
// matching DOPPLER_FRAME_CHIRPS clears the mismatch flag.
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");
// G6.6: Trigger pulse (0x02) self-clearing, latches host_trigger_pulse
// for one clk_100m cycle. Capture via a flag set on rising edge.
@(posedge clk_100m);
send_cmd(8'h02, 8'h00, 16'h0000);
// host_trigger_pulse self-clears the cycle after; we observed it via
// a sticky flag (see below).
check(trigger_pulse_seen == 1'b1,
"G6.6: 0x02 trigger pulse observed");
// ====================================================================
// GROUP 7: USB COMMAND CDC INTEGRITY (was G7.2 / G7.4)
// ====================================================================
$display("\n--- Group 7: USB Command CDC Integrity ---");
// G7.2: Three rapid USB commands; verify the last one is applied.
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 USB commands applied (0xCCCC)");
// G7.4: CDC carries value bit-exact (no corruption).
check(dut.host_detect_threshold == 16'hCCCC,
"G7.4: CDC-transferred detect threshold bit-exact");
// ====================================================================
// GROUP 13: DOPPLER/CHIRPS MISMATCH PROTECTION (PR-F: 48-chirp aware)
// ====================================================================
$display("\n--- Group 13: Chirps/Doppler Mismatch Protection ---");
// G13.1: chirps==48 (matches DOPPLER_FRAME_CHIRPS) -> mismatch clear
send_cmd(8'h15, 8'h00, 16'd48);
check(dut.host_chirps_per_elev == 6'd48,
"G13.1: chirps_per_elev=48 accepted (matches frame size)");
check(dut.chirps_mismatch_error == 1'b0,
"G13.2: Mismatch clear when chirps==48");
// G13.3: chirps>48 clamped to 48, error set
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 (was 56)");
// G13.5: chirps==0 clamped to 48
send_cmd(8'h15, 8'h00, 16'd0);
check(dut.host_chirps_per_elev == 6'd48,
"G13.5: chirps=0 clamped to 48");
// G13.6: chirps<48 accepted but flagged
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 (was 16)");
// G13.8: Restore chirps=48, error clears
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 + RANGE-MODE OPCODES
// ====================================================================
$display("\n--- Group 14: CFAR / Range-Mode Opcodes ---");
// G14.1: range_mode=0x01 (long-range)
send_cmd(8'h20, 8'h00, 16'h0001);
check(dut.host_range_mode == 2'b01,
"G14.1: 0x20 -> host_range_mode = 2'b01 (long-range)");
// G14.2: range_mode=0x02 (reserved, stored as-is)
send_cmd(8'h20, 8'h00, 16'h0002);
check(dut.host_range_mode == 2'b10,
"G14.2: 0x20 -> host_range_mode = 2'b10 (reserved)");
// G14.3: range_mode=0x00 (3 km)
send_cmd(8'h20, 8'h00, 16'h0000);
check(dut.host_range_mode == 2'b00,
"G14.3: 0x20 -> host_range_mode = 2'b00 (3 km)");
// G14.4-5: CFAR guard cells (0x21)
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");
// G14.6-7: CFAR training cells (0x22)
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");
// G14.8-9: CFAR alpha (0x23, Q4.4)
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");
// G14.10-11: CFAR mode (0x24)
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");
// G14.12-13: CFAR enable (0x25)
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 (0x17 / 0x18 MEDIUM ladder, 0x2D alpha_soft)
// ====================================================================
$display("\n--- Group 17: PR-G MEDIUM ladder + alpha_soft ---");
// 0x17 / 0x18 MEDIUM ladder (PR-G G2)
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");
// 0x2D cfar_alpha_soft (PR-G G1)
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");
// ====================================================================
// SUMMARY
// ====================================================================
$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
// ----------------------------------------------------------------------------
// Watchdog
// ----------------------------------------------------------------------------
initial begin
#2_000_000; // 2 ms plenty for ~30 send_cmd calls
$display("[WATCHDOG] tb_system_opcodes timeout");
$display(" Tests: %0d, Pass: %0d, Fail: %0d",
test_num, pass_count, fail_count);
$finish;
end
endmodule