Testbenches for: edge_detector (17), nco_400m (20), cic_decimator (14), fir_lowpass (13), freq_matched_filter (14), ddc_400m full-chain (7), chirp_controller (39), chirp_contract regression (20). Includes CSV output data for waveform verification. Add .gitignore to exclude VCD/VVP build artifacts.
472 lines
17 KiB
Verilog
472 lines
17 KiB
Verilog
`timescale 1ns / 1ps
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// Testbench: plfm_chirp_controller_enhanced
|
|
// Tests: A5 fix (multi-driven chirp_counter removed), FSM sequencing,
|
|
// chirp waveform output, T/R switch timing, beam scanning counters
|
|
//
|
|
// NOTE: Uses shortened timing parameters for feasible simulation.
|
|
// The real module uses T1_SAMPLES=3600, T1_RADAR_LISTENING=16440, etc.
|
|
// We override to T1=8, LISTEN=4, T2=4, GUARD=4 for fast verification.
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
module tb_chirp_controller;
|
|
|
|
// =========================================================================
|
|
// PARAMETERS — shortened for simulation
|
|
// =========================================================================
|
|
parameter T1_SAMPLES = 8; // was 3600
|
|
parameter T1_RADAR_LISTENING = 4; // was 16440
|
|
parameter T2_SAMPLES = 4; // was 60
|
|
parameter T2_RADAR_LISTENING = 4; // was 20940
|
|
parameter GUARD_SAMPLES = 4; // was 21048
|
|
parameter CHIRP_MAX = 4; // was 32 (use 4: 2 long + 2 short)
|
|
parameter ELEVATION_MAX = 2; // was 31
|
|
parameter AZIMUTH_MAX = 2; // was 50
|
|
|
|
// =========================================================================
|
|
// CLOCK GENERATION
|
|
// =========================================================================
|
|
reg clk_120m, clk_100m;
|
|
reg reset_n;
|
|
|
|
// 120 MHz: period = 8.333 ns
|
|
initial clk_120m = 0;
|
|
always #4.166 clk_120m = ~clk_120m;
|
|
|
|
// 100 MHz: period = 10 ns
|
|
initial clk_100m = 0;
|
|
always #5 clk_100m = ~clk_100m;
|
|
|
|
// =========================================================================
|
|
// DUT SIGNALS
|
|
// =========================================================================
|
|
reg new_chirp, new_elevation, new_azimuth;
|
|
reg mixers_enable;
|
|
|
|
wire [7:0] chirp_data;
|
|
wire chirp_valid;
|
|
wire new_chirp_frame;
|
|
wire chirp_done;
|
|
wire rf_switch_ctrl;
|
|
wire rx_mixer_en, tx_mixer_en;
|
|
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;
|
|
wire [5:0] chirp_counter;
|
|
wire [5:0] elevation_counter;
|
|
wire [5:0] azimuth_counter;
|
|
|
|
// =========================================================================
|
|
// DUT INSTANTIATION with overridden parameters
|
|
// =========================================================================
|
|
plfm_chirp_controller_enhanced #(
|
|
.T1_SAMPLES(T1_SAMPLES),
|
|
.T1_RADAR_LISTENING(T1_RADAR_LISTENING),
|
|
.T2_SAMPLES(T2_SAMPLES),
|
|
.T2_RADAR_LISTENING(T2_RADAR_LISTENING),
|
|
.GUARD_SAMPLES(GUARD_SAMPLES),
|
|
.CHIRP_MAX(CHIRP_MAX),
|
|
.ELEVATION_MAX(ELEVATION_MAX),
|
|
.AZIMUTH_MAX(AZIMUTH_MAX)
|
|
) dut (
|
|
.clk_120m(clk_120m),
|
|
.clk_100m(clk_100m),
|
|
.reset_n(reset_n),
|
|
.new_chirp(new_chirp),
|
|
.new_elevation(new_elevation),
|
|
.new_azimuth(new_azimuth),
|
|
.mixers_enable(mixers_enable),
|
|
.chirp_data(chirp_data),
|
|
.chirp_valid(chirp_valid),
|
|
.new_chirp_frame(new_chirp_frame),
|
|
.chirp_done(chirp_done),
|
|
.rf_switch_ctrl(rf_switch_ctrl),
|
|
.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),
|
|
.chirp_counter(chirp_counter),
|
|
.elevation_counter(elevation_counter),
|
|
.azimuth_counter(azimuth_counter)
|
|
);
|
|
|
|
// =========================================================================
|
|
// TEST INFRASTRUCTURE
|
|
// =========================================================================
|
|
integer test_num;
|
|
integer pass_count;
|
|
integer fail_count;
|
|
integer total_tests;
|
|
|
|
// State name decoder for debug
|
|
function [95:0] state_name;
|
|
input [2:0] state;
|
|
begin
|
|
case (state)
|
|
3'b000: state_name = "IDLE ";
|
|
3'b001: state_name = "LONG_CHIRP ";
|
|
3'b010: state_name = "LONG_LISTEN ";
|
|
3'b011: state_name = "GUARD_TIME ";
|
|
3'b100: state_name = "SHORT_CHIRP ";
|
|
3'b101: state_name = "SHORT_LISTEN";
|
|
3'b110: state_name = "DONE ";
|
|
default: state_name = "UNKNOWN ";
|
|
endcase
|
|
end
|
|
endfunction
|
|
|
|
task check;
|
|
input [255:0] test_name;
|
|
input condition;
|
|
begin
|
|
test_num = test_num + 1;
|
|
if (condition) begin
|
|
$display(" [PASS] Test %0d: %0s", test_num, test_name);
|
|
pass_count = pass_count + 1;
|
|
end else begin
|
|
$display(" [FAIL] Test %0d: %0s", test_num, test_name);
|
|
fail_count = fail_count + 1;
|
|
end
|
|
end
|
|
endtask
|
|
|
|
// Wait for N cycles of clk_120m
|
|
task wait_120m;
|
|
input integer n;
|
|
integer i;
|
|
begin
|
|
for (i = 0; i < n; i = i + 1)
|
|
@(posedge clk_120m);
|
|
end
|
|
endtask
|
|
|
|
// Wait until DUT enters a specific state (with timeout)
|
|
task wait_for_state;
|
|
input [2:0] target_state;
|
|
input integer timeout_cycles;
|
|
integer i;
|
|
begin
|
|
for (i = 0; i < timeout_cycles; i = i + 1) begin
|
|
@(posedge clk_120m);
|
|
if (dut.current_state == target_state) begin
|
|
i = timeout_cycles; // exit
|
|
end
|
|
end
|
|
end
|
|
endtask
|
|
|
|
// =========================================================================
|
|
// MAIN TEST SEQUENCE
|
|
// =========================================================================
|
|
initial begin
|
|
$dumpfile("tb_chirp_controller.vcd");
|
|
$dumpvars(0, tb_chirp_controller);
|
|
|
|
test_num = 0;
|
|
pass_count = 0;
|
|
fail_count = 0;
|
|
|
|
// Initialize
|
|
reset_n = 0;
|
|
new_chirp = 0;
|
|
new_elevation = 0;
|
|
new_azimuth = 0;
|
|
mixers_enable = 0;
|
|
|
|
$display("");
|
|
$display("============================================================");
|
|
$display(" CHIRP CONTROLLER TESTBENCH");
|
|
$display(" Testing A5 fix: single-driver chirp_counter on clk_120m");
|
|
$display(" Parameters: CHIRP_MAX=%0d, T1=%0d, T2=%0d", CHIRP_MAX, T1_SAMPLES, T2_SAMPLES);
|
|
$display("============================================================");
|
|
$display("");
|
|
|
|
// =====================================================================
|
|
// TEST GROUP 1: RESET BEHAVIOR
|
|
// =====================================================================
|
|
$display("--- Group 1: Reset Behavior ---");
|
|
|
|
#100;
|
|
|
|
// T1.1: After reset, should be in IDLE
|
|
check("Reset: state is IDLE", dut.current_state == 3'b000);
|
|
|
|
// T1.2: chirp_counter should be 0 after reset (was the A5 bug: Driver1 reset to 1, Driver2 to 0)
|
|
check("Reset: chirp_counter is 0", chirp_counter == 6'd0);
|
|
|
|
// T1.3: chirp_data should be 128 (midpoint) in IDLE
|
|
check("Reset: chirp_data is 128 (midpoint)", chirp_data == 8'd128);
|
|
|
|
// T1.4: rf_switch should be off
|
|
check("Reset: rf_switch_ctrl is 0", rf_switch_ctrl == 1'b0);
|
|
|
|
// T1.5: chirp_valid should be 0
|
|
check("Reset: chirp_valid is 0", chirp_valid == 1'b0);
|
|
|
|
// T1.6: chirp_done should be 0
|
|
check("Reset: chirp_done is 0", chirp_done == 1'b0);
|
|
|
|
// Release reset
|
|
@(posedge clk_120m);
|
|
reset_n = 1;
|
|
@(posedge clk_120m);
|
|
|
|
// =====================================================================
|
|
// TEST GROUP 2: IDLE STATE — no transition without mixers_enable
|
|
// =====================================================================
|
|
$display("--- Group 2: IDLE Hold ---");
|
|
|
|
// T2.1: With new_chirp but no mixers_enable, stay in IDLE
|
|
new_chirp = 1;
|
|
wait_120m(5);
|
|
check("IDLE hold: no transition without mixers_enable", dut.current_state == 3'b000);
|
|
new_chirp = 0;
|
|
|
|
// =====================================================================
|
|
// TEST GROUP 3: FULL FSM SEQUENCE
|
|
// =====================================================================
|
|
$display("--- Group 3: Full FSM Sequence ---");
|
|
|
|
// Enable mixers and trigger chirp
|
|
mixers_enable = 1;
|
|
@(posedge clk_120m);
|
|
new_chirp = 1; // chirp__toggling is just new_chirp pass-through
|
|
@(posedge clk_120m);
|
|
|
|
// T3.1: Should transition to LONG_CHIRP
|
|
wait_for_state(3'b001, 5); // LONG_CHIRP
|
|
check("FSM: enters LONG_CHIRP", dut.current_state == 3'b001);
|
|
|
|
// T3.2: RF switch should be ON during LONG_CHIRP
|
|
@(posedge clk_120m); // one cycle for output to settle
|
|
check("LONG_CHIRP: rf_switch_ctrl is 1", rf_switch_ctrl == 1'b1);
|
|
|
|
// T3.3: ADAR T/R switches should be 1 (transmit mode)
|
|
check("LONG_CHIRP: adar_tr_1 is 1", adar_tr_1 == 1'b1);
|
|
|
|
// T3.4: chirp_valid should be 1
|
|
check("LONG_CHIRP: chirp_valid is 1", chirp_valid == 1'b1);
|
|
|
|
// T3.5: chirp_data should NOT be 128 (should be reading from LUT)
|
|
// Note: with shortened params, LUT index wraps, but data shouldn't be stuck at 128
|
|
// Actually, with T1_SAMPLES=8, it reads long_chirp_lut[0..7] which has real data
|
|
check("LONG_CHIRP: chirp_data comes from LUT (not midpoint)", chirp_data != 8'd128);
|
|
|
|
// Wait for LONG_CHIRP to finish (T1_SAMPLES = 8 cycles)
|
|
wait_for_state(3'b010, T1_SAMPLES + 5); // LONG_LISTEN
|
|
|
|
// T3.6: Should reach LONG_LISTEN
|
|
check("FSM: enters LONG_LISTEN", dut.current_state == 3'b010);
|
|
|
|
// T3.7: RF switch OFF during listen
|
|
@(posedge clk_120m);
|
|
check("LONG_LISTEN: rf_switch_ctrl is 0", rf_switch_ctrl == 1'b0);
|
|
|
|
// T3.8: chirp_data should be 128 during listen
|
|
check("LONG_LISTEN: chirp_data is 128", chirp_data == 8'd128);
|
|
|
|
// T3.9: chirp_counter should have incremented to 1 after first LONG_LISTEN
|
|
// Wait for listen to finish
|
|
wait_for_state(3'b001, T1_RADAR_LISTENING + 5); // back to LONG_CHIRP
|
|
check("chirp_counter: incremented to 1 after first listen", chirp_counter == 6'd1);
|
|
|
|
// Now wait through second LONG_CHIRP + LONG_LISTEN cycle
|
|
// After CHIRP_MAX/2 = 2 long chirps, should go to GUARD_TIME
|
|
wait_for_state(3'b010, T1_SAMPLES + 5); // LONG_LISTEN again
|
|
wait_for_state(3'b011, T1_RADAR_LISTENING + 5); // GUARD_TIME
|
|
|
|
// T3.10: After CHIRP_MAX/2 long chirps, enters GUARD_TIME
|
|
check("FSM: enters GUARD_TIME after CHIRP_MAX/2 long chirps", dut.current_state == 3'b011);
|
|
|
|
// Wait through guard time
|
|
wait_for_state(3'b100, GUARD_SAMPLES + 5); // SHORT_CHIRP
|
|
|
|
// T3.11: Enters SHORT_CHIRP
|
|
check("FSM: enters SHORT_CHIRP", dut.current_state == 3'b100);
|
|
|
|
// T3.12: RF switch ON during SHORT_CHIRP
|
|
@(posedge clk_120m);
|
|
check("SHORT_CHIRP: rf_switch_ctrl is 1", rf_switch_ctrl == 1'b1);
|
|
|
|
// Wait through SHORT_CHIRP -> SHORT_LISTEN -> SHORT_CHIRP -> SHORT_LISTEN -> DONE
|
|
// That's 2 more chirps (chirp_counter goes from 2 to 3, then 3 to CHIRP_MAX-1=3)
|
|
wait_for_state(3'b101, T2_SAMPLES + 5); // SHORT_LISTEN
|
|
wait_for_state(3'b100, T2_RADAR_LISTENING + 5); // SHORT_CHIRP again
|
|
wait_for_state(3'b101, T2_SAMPLES + 5); // SHORT_LISTEN again
|
|
wait_for_state(3'b110, T2_RADAR_LISTENING + 5); // DONE
|
|
|
|
// T3.13: FSM reaches DONE state
|
|
check("FSM: reaches DONE state", dut.current_state == 3'b110);
|
|
|
|
// T3.14: chirp_done asserted — check on next clock edge
|
|
// Also deassert new_chirp NOW (during DONE state) so FSM stays in IDLE
|
|
// after DONE transitions. If we wait, FSM goes DONE→IDLE→LONG_CHIRP instantly.
|
|
new_chirp = 0;
|
|
@(posedge clk_120m);
|
|
check("DONE: chirp_done is 1", chirp_done == 1'b1);
|
|
|
|
// T3.15: Returns to IDLE
|
|
// Note: chirp_done check consumed one edge (DONE→IDLE already happened)
|
|
// With new_chirp=0, FSM should stay in IDLE
|
|
@(posedge clk_120m);
|
|
check("FSM: returns to IDLE after DONE", dut.current_state == 3'b000);
|
|
|
|
// =====================================================================
|
|
// TEST GROUP 4: SINGLE-DRIVER VERIFICATION (A5 FIX CORE TEST)
|
|
// =====================================================================
|
|
$display("--- Group 4: A5 Fix - Single Driver Verification ---");
|
|
|
|
// Reset and re-run with both clocks to verify no race condition
|
|
reset_n = 0;
|
|
mixers_enable = 0;
|
|
new_chirp = 0;
|
|
#100;
|
|
reset_n = 1;
|
|
@(posedge clk_120m);
|
|
|
|
// T4.1: After re-reset, chirp_counter is 0
|
|
check("Re-reset: chirp_counter is 0", chirp_counter == 6'd0);
|
|
|
|
// T4.2: Toggling new_chirp on clk_100m should NOT change chirp_counter
|
|
// (The old bug: clk_100m driver would increment it)
|
|
@(posedge clk_100m);
|
|
new_chirp = 1;
|
|
@(posedge clk_100m);
|
|
@(posedge clk_100m);
|
|
@(posedge clk_100m);
|
|
@(posedge clk_100m);
|
|
check("A5 fix: new_chirp pulses alone don't change chirp_counter", chirp_counter == 6'd0);
|
|
new_chirp = 0;
|
|
|
|
// T4.3: Only the FSM (clk_120m) should drive chirp_counter
|
|
// Start a chirp sequence and verify counter increments only at listen end
|
|
mixers_enable = 1;
|
|
@(posedge clk_120m);
|
|
new_chirp = 1;
|
|
@(posedge clk_120m);
|
|
|
|
// Wait for first LONG_CHIRP
|
|
wait_for_state(3'b001, 5);
|
|
check("A5 fix: chirp_counter still 0 at start of LONG_CHIRP", chirp_counter == 6'd0);
|
|
|
|
// Wait for first LONG_LISTEN completion
|
|
wait_for_state(3'b010, T1_SAMPLES + 5);
|
|
// During listen, counter hasn't incremented yet
|
|
check("A5 fix: chirp_counter still 0 during LONG_LISTEN", chirp_counter == 6'd0);
|
|
|
|
// Wait for listen to end and counter to increment
|
|
wait_for_state(3'b001, T1_RADAR_LISTENING + 5); // back to LONG_CHIRP
|
|
check("A5 fix: chirp_counter is 1 after first listen completes", chirp_counter == 6'd1);
|
|
|
|
// =====================================================================
|
|
// TEST GROUP 5: MIXER DISABLE
|
|
// =====================================================================
|
|
$display("--- Group 5: Mixer Disable ---");
|
|
|
|
// T5.1: Disabling mixers should reset outputs
|
|
mixers_enable = 0;
|
|
wait_120m(3);
|
|
check("Mixer disable: chirp_data returns to 128", chirp_data == 8'd128);
|
|
check("Mixer disable: chirp_valid is 0", chirp_valid == 1'b0);
|
|
check("Mixer disable: rf_switch_ctrl is 0", rf_switch_ctrl == 1'b0);
|
|
|
|
// =====================================================================
|
|
// TEST GROUP 6: ELEVATION/AZIMUTH COUNTERS (clk_100m domain, separate)
|
|
// =====================================================================
|
|
$display("--- Group 6: Beam Steering Counters ---");
|
|
|
|
// Reset
|
|
reset_n = 0;
|
|
mixers_enable = 0;
|
|
new_chirp = 0;
|
|
new_elevation = 0;
|
|
new_azimuth = 0;
|
|
#100;
|
|
reset_n = 1;
|
|
@(posedge clk_100m);
|
|
|
|
// T6.1: Elevation counter resets to 1
|
|
check("Reset: elevation_counter is 1", elevation_counter == 6'd1);
|
|
|
|
// T6.2: Azimuth counter resets to 1
|
|
check("Reset: azimuth_counter is 1", azimuth_counter == 6'd1);
|
|
|
|
// T6.3: Elevation counter increments on new_elevation
|
|
// Note: elevation__toggling = new_elevation (level-sensitive pass-through)
|
|
// With ELEVATION_MAX=2, holding high oscillates 1->2->1->...
|
|
new_elevation = 1;
|
|
@(posedge clk_100m);
|
|
@(posedge clk_100m);
|
|
check("Elevation: increments on new_elevation", elevation_counter == 6'd2 || elevation_counter == 6'd1);
|
|
|
|
// T6.4: Elevation counter wraps at ELEVATION_MAX
|
|
// Counter toggles between 1 and 2 each cycle when held high
|
|
@(posedge clk_100m);
|
|
check("Elevation: wraps at ELEVATION_MAX",
|
|
(elevation_counter == 6'd1) || (elevation_counter == 6'd2));
|
|
new_elevation = 0;
|
|
@(posedge clk_100m);
|
|
|
|
// T6.5: Azimuth counter increments on new_azimuth
|
|
new_azimuth = 1;
|
|
@(posedge clk_100m);
|
|
@(posedge clk_100m);
|
|
check("Azimuth: increments on new_azimuth", azimuth_counter == 6'd2 || azimuth_counter == 6'd1);
|
|
new_azimuth = 0;
|
|
|
|
// =====================================================================
|
|
// TEST GROUP 7: MIXER ENABLE SIGNALS
|
|
// =====================================================================
|
|
$display("--- Group 7: Mixer Control Outputs ---");
|
|
|
|
// T7.1: rx_mixer_en follows mixers_enable
|
|
mixers_enable = 1;
|
|
#1;
|
|
check("rx_mixer_en follows mixers_enable", rx_mixer_en == 1'b1);
|
|
|
|
// T7.2: tx_mixer_en follows mixers_enable
|
|
check("tx_mixer_en follows mixers_enable", tx_mixer_en == 1'b1);
|
|
|
|
// T7.3: ADAR load pins tied low
|
|
check("ADAR load pins: adar_tx_load_1 is 0", adar_tx_load_1 == 1'b0);
|
|
check("ADAR load pins: adar_rx_load_1 is 0", adar_rx_load_1 == 1'b0);
|
|
|
|
// =====================================================================
|
|
// SUMMARY
|
|
// =====================================================================
|
|
$display("");
|
|
$display("============================================================");
|
|
total_tests = pass_count + fail_count;
|
|
$display(" RESULTS: %0d/%0d tests passed", pass_count, total_tests);
|
|
if (fail_count == 0)
|
|
$display(" STATUS: ALL TESTS PASSED");
|
|
else
|
|
$display(" STATUS: %0d TESTS FAILED", fail_count);
|
|
$display("============================================================");
|
|
$display("");
|
|
|
|
#100;
|
|
$finish;
|
|
end
|
|
|
|
// Timeout watchdog
|
|
initial begin
|
|
#500000; // 500 us max
|
|
$display("TIMEOUT: Simulation took too long!");
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|