chirp-v2 PR-E: plfm_chirp_controller_v2 + scheduler-driven TX via async-FIFO

Replaces plfm_chirp_controller_enhanced (5-state FSM with hardcoded
LONG/SHORT timings + 60-entry inline short LUT) with plfm_chirp_controller_v2,
a pure DAC playback driver: IDLE -> CHIRP -> IDLE keyed off a 1-cycle
dst_chirp_valid pulse, with sample count selected by dst_wave_sel
(SHORT=120 / MEDIUM=600 / LONG=3600). Inter-chirp timing (LISTEN, GUARD,
frame boundaries) is now owned exclusively by chirp_scheduler.

Scheduler -> TX bridge: cdc_async_fifo (Cummings style #2, WIDTH=2 DEPTH=4)
crosses {wave_sel} from clk_100m to clk_120m_dac, with chirp_pulse as
src_valid. frame_pulse rides a separate toggle CDC for chirp_counter
clear and the new_chirp_frame status output. mixers_enable now also gates
the scheduler so it stays in S_IDLE while the radar is "off" — without
this gate the first chirp_pulse fires at reset and gets dropped before
mixers come up.

Files:
- NEW  plfm_chirp_controller_v2.v      DAC playback driver (3 LUTs, FSM)
- DEL  plfm_chirp_controller.v         legacy controller (382 lines)
- DEL  long_chirp_lut.mem              legacy LUT (3600 lines), replaced
                                       by tx_long_lut.mem from PR-B
- chirp_scheduler.v       + mixers_enable input (master quiesce)
- radar_receiver_final.v  + sched_*_out output ports + mixers_enable_100m
- radar_system_top.v      wire sched_*_out -> tx_inst.sched_*; pass
                          stm32_mixers_enable_100m to rx_inst
- radar_transmitter.v     full rewrite: drop new_chirp edge detector +
                          toggle CDC, instantiate cdc_async_fifo for
                          {wave_sel}, toggle CDC for frame_pulse,
                          plfm_chirp_controller_v2 in place of _enhanced
- tb/tb_chirp_controller.v  + tb/tb_chirp_contract.v  rewritten for v2
                          contract (43/43 unit + 10/10 contract green)
- tb/tb_radar_receiver_final.v  + .mixers_enable_100m(1'b1) pin
- run_regression.sh, scripts/200t/build_200t.tcl  file-list bumped

Test summary:
- tb_chirp_controller_v2:   43/43 PASS
- tb_chirp_contract:        10/10 contracts upheld
- tb_rxb_fullchain:         peak 24033 ~80x (parity with PR-D)
- tb_mti_canceller:         43/43 PASS
- tb_system_e2e:            33/49 (1 new vs 34/49 PR-D baseline: G2.2
                            new_chirp_frame, intentional v2 frame-pulse
                            semantics — fires once per Doppler frame
                            instead of once per stm32 chirp toggle.
                            TB needs widening in PR-H to wait the full
                            frame.)
This commit is contained in:
Jason
2026-04-30 21:51:46 +05:45
parent 8e8f3e60c4
commit a1a8fa7107
12 changed files with 971 additions and 5018 deletions
+195 -457
View File
@@ -1,76 +1,80 @@
`timescale 1ns / 1ps
// ============================================================================
// tb_chirp_contract.v Architectural Contract Regression Test
// tb_chirp_contract.v Architectural Contract Regression for plfm_chirp_controller_v2
// ============================================================================
// Purpose: Encode the invariants of the chirp_counter signal path as hard
// assertions. If the original author (or anyone) modifies the RTL in a way
// that violates these contracts, this testbench will FAIL immediately.
// Encodes the chirp-v2 (PR-E) invariants of the chirp_counter signal path as
// hard assertions. If the RTL is modified in a way that violates one of these
// contracts, this testbench fails immediately.
//
// Contracts verified:
// C1. chirp_counter is 0-indexed, range [0, CHIRP_MAX-1]
// C2. chirp_counter resets to 0 (not 1)
// C3. chirp_counter increments only on clk_120m (never on clk_100m alone)
// C4. chirp_counter increments monotonically (no skips > 1)
// C5. chirp_counter increments only at end of listen states
// C6. new_chirp input does NOT directly drive chirp_counter
// C7. chirp_counter wraps correctly: 0 CHIRP_MAX-1 0
// C8. Frame sync compatibility: chirp_counter hits 0 at frame start
// C9. GUI mask compatibility: chirp_counter stays within [0, 31] (5-bit safe)
// C10. Receiver port connectivity: chirp_counter output matches input expectation
// C1. chirp_counter is 0-indexed, wraps via frame_pulse_120m
// C2. chirp_counter resets to 0 on frame_pulse_120m (not at chirp_done)
// C3. chirp_counter increments only on clk_120m edges (never clk_100m alone)
// C4. chirp_counter increments monotonically (no skips > 1)
// C5. chirp_counter increments exactly when the FSM leaves ST_CHIRP
// C6. dst_chirp_valid pulses (not stm32 toggles) drive chirp_counter
// C7. chirp_counter wraps cleanly via frame_pulse: N 0
// C8. chirp_counter stays in [0, 31] when frame 32 chirps (5-bit safe)
// C9. Receiver port-connectivity: TX-side chirp_counter still surfaces on
// radar_transmitter.current_chirp (for status_reg compatibility)
//
// Related bugs: A5 (multi-driven fix), NEW-1 (receiver port fix)
// Related history: chirp-v1 had a multi-driven chirp_counter bug (A5).
// In chirp-v2 the counter has only ONE driver (the FSM in clk_120m), so
// the original A5 race is structurally unreachable but C3 / C5 still
// guard against any future regression that re-introduces a clk_100m driver.
// ============================================================================
`include "radar_params.vh"
module tb_chirp_contract;
// ---- Parameters (must match RTL) ----
localparam CHIRP_MAX = 32;
localparam T1_SAMPLES = 3600;
localparam T1_RADAR_LISTENING = 16440;
localparam T2_SAMPLES = 60;
localparam T2_RADAR_LISTENING = 20940;
localparam GUARD_SAMPLES = 21048;
// For fast simulation, use a reduced version
// Set USE_FAST_SIM=1 to use CHIRP_MAX=4 (completes in ~1ms sim time)
// Set USE_FAST_SIM=0 to use real parameters (very long sim time)
localparam USE_FAST_SIM = 1;
localparam SIM_CHIRP_MAX = USE_FAST_SIM ? 4 : CHIRP_MAX;
// ---- Sample-count constants ----
localparam integer SHORT_SAMPLES = 120;
localparam integer MEDIUM_SAMPLES = 600;
// ---- Clock generation ----
reg clk_120m, clk_100m;
reg reset_n;
reg new_chirp, new_elevation, new_azimuth, mixers_enable;
reg reset_n, reset_100m_n;
reg mixers_enable;
reg dst_chirp_valid;
reg [1:0] dst_wave_sel;
reg frame_pulse_120m;
reg new_elevation, new_azimuth;
// DUT outputs
// DUT outputs (subset — only those used in the contract checks)
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 tx_mixer_en, rx_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 new_chirp_frame;
wire [5:0] chirp_counter;
wire [5:0] elevation_counter;
wire [5:0] azimuth_counter;
wire [5:0] elevation_counter, azimuth_counter;
// ---- DUT instantiation ----
plfm_chirp_controller_enhanced #(
.CHIRP_MAX(SIM_CHIRP_MAX),
.ELEVATION_MAX(31),
.AZIMUTH_MAX(50)
) dut (
// 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 ----
plfm_chirp_controller_v2 dut (
.clk_120m(clk_120m),
.clk_100m(clk_100m),
.reset_n(reset_n),
.new_chirp(new_chirp),
.reset_100m_n(reset_100m_n),
.mixers_enable(mixers_enable),
.dst_chirp_valid(dst_chirp_valid),
.dst_wave_sel(dst_wave_sel),
.frame_pulse_120m(frame_pulse_120m),
.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),
@@ -95,456 +99,190 @@ plfm_chirp_controller_enhanced #(
.azimuth_counter(azimuth_counter)
);
// ---- Clock generation ----
// 120 MHz: period = 8.333ns
initial clk_120m = 0;
always #4.167 clk_120m = ~clk_120m;
// 100 MHz: period = 10ns
initial clk_100m = 0;
always #5 clk_100m = ~clk_100m;
// ---- Test infrastructure ----
integer pass_count = 0;
integer fail_count = 0;
integer total_tests = 0;
integer test_num;
integer pass_count;
integer fail_count;
integer total_tests;
// C4 monitor: chirp_counter must change by ±1 or 0 per clk_120m edge.
// Wraps via frame_pulse_120m: the FSM samples the pulse at edge T and
// schedules chirp_counter <= 0; the wrap (K → 0) is observable on the
// next monitor sample (edge T+1). frame_pulse_seen carries the pulse
// forward one cycle so the (pre, post) = (K, 0) transition is allowed.
reg [5:0] prev_counter;
reg frame_pulse_seen;
reg c4_violated;
always @(posedge clk_120m or negedge reset_n) begin
if (!reset_n) begin
prev_counter <= 6'd0;
frame_pulse_seen <= 1'b0;
c4_violated <= 1'b0;
end else begin
if (chirp_counter != prev_counter &&
chirp_counter != prev_counter + 6'd1 &&
!(chirp_counter == 6'd0 && (frame_pulse_120m || frame_pulse_seen)))
begin
c4_violated <= 1'b1;
end
frame_pulse_seen <= frame_pulse_120m;
prev_counter <= chirp_counter;
end
end
task check;
input [255:0] name; // Reduced from 512 for Icarus compat
input [255:0] test_name;
input condition;
begin
total_tests = total_tests + 1;
test_num = test_num + 1;
if (condition) begin
$display(" [PASS] Contract %0d: %0s", test_num, test_name);
pass_count = pass_count + 1;
$display(" [PASS] %0s", name);
end else begin
$display(" [FAIL] Contract %0d: %0s", test_num, test_name);
fail_count = fail_count + 1;
$display(" [FAIL] %0s", name);
end
end
endtask
// ---- Continuous monitors for contract violations ----
// Contract C1: Range check chirp_counter must always be in [0, SIM_CHIRP_MAX]
// KNOWN BEHAVIOR: chirp_counter reaches CHIRP_MAX for exactly 1 cycle during DONE state.
// This is because the combinational next_state logic checks chirp_counter == CHIRP_MAX-1
// at the same clock edge that the registered block increments chirp_counter.
// The value CHIRP_MAX only appears in DONE (state 6) and IDLE (state 0, briefly).
// This is benign: no chirp is transmitting during DONE, and the receiver doesn't use
// chirp_counter during that state. The counter resets to 0 on the next reset.
// We flag as a violation ONLY if chirp_counter exceeds CHIRP_MAX (should never happen).
reg reset_done;
initial reset_done = 0;
always @(posedge clk_120m) begin
if (reset_done && chirp_counter > SIM_CHIRP_MAX) begin
$display(" [FAIL] CONTRACT C1 VIOLATION: chirp_counter=%0d > CHIRP_MAX=%0d at time %0t",
chirp_counter, SIM_CHIRP_MAX, $time);
fail_count = fail_count + 1;
end
end
// Contract C4: Monotonicity chirp_counter must not skip values
// It can increment by 0 (hold) or 1 (increment), or reset to 0 (via reset or new sequence)
reg [5:0] prev_chirp_counter;
reg prev_valid;
initial prev_valid = 0;
always @(posedge clk_120m) begin
if (reset_done && prev_valid) begin
// Allowed transitions:
// same value (hold)
// +1 (increment, including CHIRP_MAX-1 CHIRP_MAX overshoot)
// reset to 0 (from DONE/IDLE or hardware reset)
if (chirp_counter != prev_chirp_counter &&
chirp_counter != prev_chirp_counter + 1 &&
chirp_counter != 0) begin
$display(" [FAIL] CONTRACT C4 VIOLATION: chirp_counter jumped %0d -> %0d at time %0t",
prev_chirp_counter, chirp_counter, $time);
fail_count = fail_count + 1;
end
end
prev_chirp_counter <= chirp_counter;
if (reset_done) prev_valid <= 1;
end
// ---- Helper: wait for N clk_120m rising edges ----
task wait_120m_cycles;
input integer n;
integer i;
task issue_chirp;
input [1:0] wsel;
begin
@(posedge clk_120m);
dst_wave_sel <= wsel;
dst_chirp_valid <= 1'b1;
@(posedge clk_120m);
dst_chirp_valid <= 1'b0;
end
endtask
task wait_for_idle;
input integer timeout_cycles;
integer i;
begin
for (i = 0; i < timeout_cycles; i = i + 1) begin
@(posedge clk_120m);
if (dut.state == 1'b0) i = timeout_cycles;
end
end
endtask
task pulse_frame;
begin
for (i = 0; i < n; i = i + 1)
@(posedge clk_120m);
end
endtask
// ---- Helper: wait for N clk_100m rising edges ----
task wait_100m_cycles;
input integer n;
integer i;
begin
for (i = 0; i < n; i = i + 1)
@(posedge clk_100m);
end
endtask
// ---- Helper: run one full chirp sequence (IDLE DONE) ----
// Returns the final chirp_counter value
reg [5:0] final_chirp_value;
reg sequence_completed;
task run_full_sequence;
begin
// Trigger: assert new_chirp and mixers_enable
mixers_enable = 1;
new_chirp = 1;
wait_100m_cycles(5);
// Wait for FSM to leave IDLE
@(posedge clk_120m);
while (dut.current_state == 3'd0) // IDLE = 0
@(posedge clk_120m);
// Now wait for DONE state (state 6)
while (dut.current_state != 3'd6) // DONE = 6
@(posedge clk_120m);
final_chirp_value = chirp_counter;
sequence_completed = 1;
// Wait for return to IDLE
frame_pulse_120m <= 1'b1;
@(posedge clk_120m);
while (dut.current_state != 3'd0)
@(posedge clk_120m);
// Deassert
new_chirp = 0;
mixers_enable = 0;
wait_120m_cycles(5);
frame_pulse_120m <= 1'b0;
end
endtask
// ---- Main test sequence ----
// =========================================================================
// MAIN
// =========================================================================
initial begin
$dumpfile("tb_chirp_contract.vcd");
$dumpvars(0, tb_chirp_contract);
// Initialize
reset_n = 0;
new_chirp = 0;
new_elevation = 0;
new_azimuth = 0;
mixers_enable = 0;
sequence_completed = 0;
$display("============================================================");
$display("ARCHITECTURAL CONTRACT REGRESSION TEST chirp_counter");
$display("CHIRP_MAX (sim) = %0d", SIM_CHIRP_MAX);
$display("============================================================");
// ================================================================
// TEST GROUP 1: Reset Contracts
// ================================================================
test_num = 0;
pass_count = 0;
fail_count = 0;
reset_n = 0;
reset_100m_n = 0;
mixers_enable = 0;
dst_chirp_valid = 0;
dst_wave_sel = `RP_WAVE_SHORT;
frame_pulse_120m = 0;
new_elevation = 0;
new_azimuth = 0;
$display("");
$display("--- GROUP 1: Reset Contracts ---");
// Apply reset
$display("============================================================");
$display(" CHIRP CONTRACT REGRESSION (chirp-v2 PR-E)");
$display("============================================================");
$display("");
#100;
reset_n = 1;
wait_120m_cycles(3);
reset_done = 1;
// C2: Reset value is 0
check("C2: chirp_counter resets to 0 (not 1)", chirp_counter == 6'd0);
// ================================================================
// TEST GROUP 2: clk_100m Isolation (Contract C3)
// ================================================================
$display("");
$display("--- GROUP 2: clk_100m Isolation (Contract C3) ---");
// C3a: Toggling new_chirp on clk_100m with mixers OFF should not change chirp_counter
new_chirp = 1;
wait_100m_cycles(20);
new_chirp = 0;
wait_100m_cycles(20);
new_chirp = 1;
wait_100m_cycles(20);
new_chirp = 0;
wait_100m_cycles(10);
check("C3a: new_chirp pulses (mixers off) don't change chirp_counter", chirp_counter == 6'd0);
// C3b: Toggling new_chirp on clk_100m with mixers ON but before FSM starts
// chirp_counter should still be 0 until FSM actually enters a listen state
mixers_enable = 1;
wait_100m_cycles(5);
// FSM should transition out of IDLE now (chirp__toggling is high and mixers on)
// But chirp_counter should only change at end of listen, not from clk_100m
// Record value immediately
begin : c3b_block
reg [5:0] val_before;
val_before = chirp_counter;
// Now toggle new_chirp rapidly on clk_100m only
new_chirp = 0;
wait_100m_cycles(3);
new_chirp = 1;
wait_100m_cycles(3);
new_chirp = 0;
wait_100m_cycles(3);
// If there was a clk_100m driver, chirp_counter would have changed
// But the clk_100m toggling alone should have no effect on chirp_counter
// (FSM may increment it on clk_120m that's OK, we just check no EXTRA increments)
check("C3b: clk_100m toggling alone doesn't add extra increments",
chirp_counter >= val_before); // Must be >= (FSM may have started)
end
// Reset for next test group
reset_n = 0;
reset_done = 0;
prev_valid = 0;
new_chirp = 0;
mixers_enable = 0;
wait_120m_cycles(5);
reset_n = 1;
wait_120m_cycles(3);
reset_done = 1;
// ================================================================
// TEST GROUP 3: Full Sequence Contracts (C1, C5, C7, C8, C9)
// ================================================================
$display("");
$display("--- GROUP 3: Full Sequence Contracts ---");
// Run a complete chirp sequence
run_full_sequence;
// C1: Final value in DONE state is CHIRP_MAX (1-cycle overshoot see C1 comment)
// The combinational FSM correctly sees CHIRP_MAX-1 for the state transition,
// but the registered increment on the same edge pushes it to CHIRP_MAX.
check("C1: Final chirp_counter = CHIRP_MAX (known DONE overshoot)",
final_chirp_value == SIM_CHIRP_MAX);
// C7: After DONE IDLE, chirp_counter should still be CHIRP_MAX
// (it resets to 0 on the next reset, not automatically)
check("C7a: chirp_counter holds at CHIRP_MAX after DONE",
chirp_counter == SIM_CHIRP_MAX);
// C8: Verify that chirp_counter was 0 at the start of the sequence
// (we tested this via C2 it starts at 0 after reset)
check("C8: Frame start aligns with chirp_counter=0 (from reset)",
1'b1); // Verified by C2 above
// C9: GUI mask compatibility all OPERATIONAL values must be <= 31 (5-bit safe)
// The DONE-state overshoot to CHIRP_MAX is OK because no USB data is packed in DONE.
// With real CHIRP_MAX=32, the overshoot value (32) exceeds 5 bits, but it's never sent.
// For this test with SIM_CHIRP_MAX=4, the value is 4 which fits in 5 bits anyway.
check("C9: Overshoot value fits in 6 bits (port width safe)",
final_chirp_value <= 6'd63);
// ================================================================
// TEST GROUP 4: Contract C6 new_chirp doesn't drive chirp_counter
// ================================================================
$display("");
$display("--- GROUP 4: new_chirp Independence (Contract C6) ---");
// Reset
reset_n = 0;
reset_done = 0;
prev_valid = 0;
new_chirp = 0;
mixers_enable = 0;
wait_120m_cycles(5);
reset_n = 1;
wait_120m_cycles(3);
reset_done = 1;
// C6a: With mixers OFF, new_chirp pulses should not increment chirp_counter
new_chirp = 1;
wait_100m_cycles(10);
new_chirp = 0;
wait_100m_cycles(10);
check("C6a: new_chirp pulse (mixers off) -> chirp_counter stays 0",
chirp_counter == 6'd0);
// C6b: 100 rapid new_chirp toggles should not cause any chirp_counter change
begin : c6b_block
integer k;
for (k = 0; k < 100; k = k + 1) begin
new_chirp = ~new_chirp;
#10; // 10ns per toggle = 100MHz-ish
end
new_chirp = 0;
wait_100m_cycles(5);
check("C6b: 100 rapid new_chirp toggles -> chirp_counter still 0",
chirp_counter == 6'd0);
end
// C6c: Even with mixers ON, new_chirp should only START the FSM,
// not directly increment chirp_counter
mixers_enable = 1;
new_chirp = 1;
wait_100m_cycles(3);
// FSM should be transitioning, but chirp_counter should still be 0
// (it only increments at end of first listen state)
check("C6c: FSM started but chirp_counter still 0 (no direct drive)",
chirp_counter == 6'd0);
new_chirp = 0;
mixers_enable = 0;
// ================================================================
// TEST GROUP 5: Contract C5 Increment only at listen state end
// ================================================================
$display("");
$display("--- GROUP 5: Increment Timing (Contract C5) ---");
// Reset
reset_n = 0;
reset_done = 0;
prev_valid = 0;
new_chirp = 0;
mixers_enable = 0;
wait_120m_cycles(5);
reset_n = 1;
wait_120m_cycles(3);
reset_done = 1;
// Start sequence
mixers_enable = 1;
new_chirp = 1;
wait_100m_cycles(5);
// Wait for LONG_CHIRP state (state 1)
@(posedge clk_120m);
while (dut.current_state == 3'd0)
@(posedge clk_120m);
// C5a: During LONG_CHIRP, chirp_counter should remain 0
check("C5a: chirp_counter=0 during first LONG_CHIRP", chirp_counter == 6'd0);
// Wait through LONG_CHIRP into LONG_LISTEN
while (dut.current_state == 3'd1) // LONG_CHIRP
@(posedge clk_120m);
// Now in LONG_LISTEN (state 2)
// C5b: At start of LONG_LISTEN, chirp_counter should still be 0
check("C5b: chirp_counter=0 at start of LONG_LISTEN", chirp_counter == 6'd0);
// Wait for LONG_LISTEN to finish
while (dut.current_state == 3'd2) // LONG_LISTEN
@(posedge clk_120m);
// C5c: After first LONG_LISTEN completes, chirp_counter should be 1
check("C5c: chirp_counter=1 after first LONG_LISTEN", chirp_counter == 6'd1);
// ================================================================
// TEST GROUP 6: Multi-Reset Stability (C2 regression)
// ================================================================
$display("");
$display("--- GROUP 6: Multi-Reset Stability ---");
// Reset mid-sequence
reset_n = 0;
reset_done = 0;
prev_valid = 0;
wait_120m_cycles(3);
reset_n = 1;
wait_120m_cycles(3);
reset_done = 1;
check("C2-repeat: chirp_counter=0 after mid-sequence reset", chirp_counter == 6'd0);
// Another reset
reset_n = 0;
reset_done = 0;
prev_valid = 0;
wait_120m_cycles(10);
reset_n = 1;
wait_120m_cycles(3);
reset_done = 1;
check("C2-long: chirp_counter=0 after long reset", chirp_counter == 6'd0);
// ================================================================
// TEST GROUP 7: Back-to-Back Sequences (C7 wrap behavior)
// ================================================================
$display("");
$display("--- GROUP 7: Back-to-Back Sequences (Wrap Behavior) ---");
// Run first sequence
run_full_sequence;
begin : c7b_check
reg [5:0] val_after_first;
val_after_first = chirp_counter;
check("C7b: First sequence ends at CHIRP_MAX (DONE overshoot)",
val_after_first == SIM_CHIRP_MAX);
reset_n <= 1;
@(posedge clk_100m);
reset_100m_n <= 1;
@(posedge clk_120m);
mixers_enable = 1;
@(posedge clk_120m);
// ---------- C2: counter is 0 after reset (before any chirp) ----------
check("C2: chirp_counter == 0 after reset", chirp_counter == 6'd0);
// ---------- C5/C6: dst_chirp_valid drives the counter ----------
issue_chirp(`RP_WAVE_SHORT);
wait_for_idle(SHORT_SAMPLES + 20);
check("C5/C6: chirp_counter == 1 after first SHORT chirp", chirp_counter == 6'd1);
issue_chirp(`RP_WAVE_SHORT);
wait_for_idle(SHORT_SAMPLES + 20);
check("C5/C6: chirp_counter == 2 after second SHORT chirp", chirp_counter == 6'd2);
// ---------- C3: stm32 toggles do NOT drive chirp_counter ----------
repeat (8) begin
new_elevation = ~new_elevation;
new_azimuth = ~new_azimuth;
@(posedge clk_100m);
end
// Reset and run second sequence
reset_n = 0;
reset_done = 0;
prev_valid = 0;
new_chirp = 0;
mixers_enable = 0;
wait_120m_cycles(5);
reset_n = 1;
wait_120m_cycles(3);
reset_done = 1;
check("C7c: chirp_counter wraps to 0 after reset between sequences",
chirp_counter == 6'd0);
// Run second sequence
run_full_sequence;
check("C7d: Second sequence also ends at CHIRP_MAX",
chirp_counter == SIM_CHIRP_MAX);
// ================================================================
// TEST GROUP 8: Contract C10 Receiver Port Compatibility
// ================================================================
$display("");
$display("--- GROUP 8: Receiver Port Compatibility (C10) ---");
// Verify the output port width is 6 bits (compile-time check via the wire declaration)
// If someone changes it to 5 bits, the connection will produce warnings/errors
check("C10a: chirp_counter output is 6 bits wide",
$bits(chirp_counter) == 6);
// Verify value range is compatible with receiver frame sync
// Receiver checks: chirp_counter == 0 || chirp_counter == 32
// With CHIRP_MAX=32, value 32 is never reached (range is 0-31)
// So only chirp_counter==0 triggers frame sync this is correct
check("C10b: CHIRP_MAX-1 < 32, so chirp_counter==32 never occurs (expected)",
SIM_CHIRP_MAX - 1 < 32 || SIM_CHIRP_MAX > 32);
// ================================================================
new_elevation = 0;
new_azimuth = 0;
check("C3: stm32 toggles do not change chirp_counter", chirp_counter == 6'd2);
// ---------- C7: frame_pulse wraps to 0 ----------
pulse_frame();
@(posedge clk_120m);
check("C7: chirp_counter wraps to 0 on frame_pulse", chirp_counter == 6'd0);
// ---------- C5/C6: incremental sequence after wrap ----------
issue_chirp(`RP_WAVE_MEDIUM);
wait_for_idle(MEDIUM_SAMPLES + 20);
check("C5/C6: chirp_counter == 1 after MEDIUM post-wrap", chirp_counter == 6'd1);
issue_chirp(`RP_WAVE_SHORT);
wait_for_idle(SHORT_SAMPLES + 20);
check("C5/C6: chirp_counter == 2 after SHORT post-wrap", chirp_counter == 6'd2);
// ---------- C4: monotonic confirmed by the running monitor ----------
check("C4: monotonic ±1 increments only (monitor flag)", c4_violated == 1'b0);
// ---------- C8: 5-bit safe over a 4-chirp run ----------
issue_chirp(`RP_WAVE_SHORT); wait_for_idle(SHORT_SAMPLES + 20);
issue_chirp(`RP_WAVE_SHORT); wait_for_idle(SHORT_SAMPLES + 20);
check("C8: chirp_counter 31 during a normal frame", chirp_counter <= 6'd31);
// ---------- C1: full sequence then frame wrap to 0 ----------
pulse_frame();
@(posedge clk_120m);
check("C1/C7: chirp_counter wraps cleanly back to 0", chirp_counter == 6'd0);
// =====================================================================
// SUMMARY
// ================================================================
// =====================================================================
$display("");
$display("============================================================");
$display("ARCHITECTURAL CONTRACT TEST SUMMARY");
$display("============================================================");
$display(" Total : %0d", total_tests);
$display(" Passed: %0d", pass_count);
$display(" Failed: %0d", fail_count);
$display("============================================================");
total_tests = pass_count + fail_count;
$display(" CONTRACT RESULTS: %0d/%0d contracts upheld", pass_count, total_tests);
if (fail_count == 0)
$display("ALL CONTRACTS VERIFIED chirp_counter architecture is safe.");
$display(" STATUS: ALL CONTRACTS UPHELD");
else
$display("CONTRACT VIOLATIONS DETECTED review changes to chirp_counter!");
$display(" STATUS: %0d CONTRACT VIOLATIONS", fail_count);
$display("============================================================");
$display("");
#100;
$finish;
end
// ---- Timeout watchdog ----
initial begin
#500_000_000; // 500ms sim time
$display("[TIMEOUT] Simulation exceeded 500ms aborting");
$display(" Tests run so far: %0d passed, %0d failed", pass_count, fail_count);
#500000; // 500 µs
$display("TIMEOUT: Simulation took too long!");
$finish;
end
+174 -357
View File
@@ -1,33 +1,36 @@
`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
// Testbench: plfm_chirp_controller_v2 (chirp-v2 PR-E)
//
// 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.
// The v2 module is a pure DAC playback driver it no longer owns its own
// LISTEN/GUARD/DONE FSM (that moved into chirp_scheduler on the RX side).
// Tests here verify:
// - Reset behavior (IDLE, idle-code 128, all flags low)
// - IDLE hold while mixers_enable=0
// - SHORT/MEDIUM/LONG chirp playback durations match LUT lengths
// - chirp_data exits idle code and rf_switch / adar_tr / chirp_valid go
// active during CHIRP, deassert after
// - chirp_counter increments per chirp and clears on frame_pulse_120m
// - mixer enables: tx_mixer_en active during CHIRP, rx_mixer_en otherwise
// - elevation_counter / azimuth_counter still bump on STM32 toggles
//
// Sample counts (must mirror plfm_chirp_controller_v2.v localparams):
// SHORT = 120, MEDIUM = 600, LONG = 3600
//////////////////////////////////////////////////////////////////////////////
`include "radar_params.vh"
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
// ---- Sample-count constants (match the RTL) ----
localparam integer SHORT_SAMPLES = 120;
localparam integer MEDIUM_SAMPLES = 600;
localparam integer LONG_SAMPLES = 3600;
// =========================================================================
// CLOCK GENERATION
// =========================================================================
reg clk_120m, clk_100m;
reg reset_n;
reg reset_n, reset_100m_n;
// 120 MHz: period = 8.333 ns
initial clk_120m = 0;
@@ -40,8 +43,12 @@ always #5 clk_100m = ~clk_100m;
// =========================================================================
// DUT SIGNALS
// =========================================================================
reg new_chirp, new_elevation, new_azimuth;
reg mixers_enable;
reg mixers_enable;
reg dst_chirp_valid;
reg [1:0] dst_wave_sel;
reg frame_pulse_120m;
reg new_elevation;
reg new_azimuth;
wire [7:0] chirp_data;
wire chirp_valid;
@@ -59,25 +66,19 @@ wire [5:0] elevation_counter;
wire [5:0] azimuth_counter;
// =========================================================================
// DUT INSTANTIATION with overridden parameters
// DUT
// =========================================================================
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 (
plfm_chirp_controller_v2 dut (
.clk_120m(clk_120m),
.clk_100m(clk_100m),
.reset_n(reset_n),
.new_chirp(new_chirp),
.reset_100m_n(reset_100m_n),
.mixers_enable(mixers_enable),
.dst_chirp_valid(dst_chirp_valid),
.dst_wave_sel(dst_wave_sel),
.frame_pulse_120m(frame_pulse_120m),
.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),
@@ -110,23 +111,6 @@ 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;
@@ -142,357 +126,190 @@ task check;
end
endtask
// Wait for N cycles of clk_120m
task wait_120m;
input integer n;
integer i;
// Pulse dst_chirp_valid for 1 cycle on clk_120m with the requested wave_sel
task issue_chirp;
input [1:0] wsel;
begin
for (i = 0; i < n; i = i + 1)
@(posedge clk_120m);
@(posedge clk_120m);
dst_wave_sel <= wsel;
dst_chirp_valid <= 1'b1;
@(posedge clk_120m);
dst_chirp_valid <= 1'b0;
end
endtask
// Wait until DUT enters a specific state (with timeout)
task wait_for_state;
input [2:0] target_state;
// Wait until DUT enters ST_IDLE again (chirp finished), with timeout
task wait_for_idle;
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
if (dut.state == 1'b0) begin
i = timeout_cycles;
end
end
end
endtask
// Pulse frame_pulse_120m for 1 cycle on clk_120m
task pulse_frame;
begin
@(posedge clk_120m);
frame_pulse_120m <= 1'b1;
@(posedge clk_120m);
frame_pulse_120m <= 1'b0;
end
endtask
// =========================================================================
// MAIN TEST SEQUENCE
// MAIN
// =========================================================================
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;
reset_n = 0;
reset_100m_n = 0;
mixers_enable = 0;
dst_chirp_valid = 0;
dst_wave_sel = `RP_WAVE_SHORT;
frame_pulse_120m = 0;
new_elevation = 0;
new_azimuth = 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(" PLFM CHIRP CONTROLLER V2 TESTBENCH (chirp-v2 PR-E)");
$display(" SHORT=%0d, MEDIUM=%0d, LONG=%0d samples",
SHORT_SAMPLES, MEDIUM_SAMPLES, LONG_SAMPLES);
$display("============================================================");
$display("");
// =====================================================================
// TEST GROUP 1: RESET BEHAVIOR
// =====================================================================
$display("--- Group 1: Reset Behavior ---");
// ---------- Reset ----------
$display("--- Group 1: Reset ---");
#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
check("Reset: state == IDLE", dut.state == 1'b0);
check("Reset: chirp_data == 128", chirp_data == 8'd128);
check("Reset: chirp_valid low", chirp_valid == 1'b0);
check("Reset: rf_switch_ctrl low", rf_switch_ctrl == 1'b0);
check("Reset: chirp_done low", chirp_done == 1'b0);
check("Reset: chirp_counter == 0", chirp_counter == 6'd0);
check("Reset: elevation_counter==1", elevation_counter == 6'd1);
check("Reset: azimuth_counter==1", azimuth_counter == 6'd1);
@(posedge clk_120m);
reset_n = 1;
reset_n <= 1;
@(posedge clk_100m);
reset_100m_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
// ---------- IDLE hold without mixers_enable ----------
$display("--- Group 2: IDLE Hold (mixers_enable=0) ---");
issue_chirp(`RP_WAVE_SHORT);
repeat (4) @(posedge clk_120m);
check("Without mixers_enable, no transition into CHIRP", dut.state == 1'b0);
check("Without mixers_enable, chirp_data stays 128", chirp_data == 8'd128);
check("Without mixers_enable, chirp_valid stays 0", chirp_valid == 1'b0);
// ---------- SHORT chirp playback ----------
$display("--- Group 3: SHORT chirp playback (120 samples) ---");
mixers_enable = 1;
@(posedge clk_120m);
new_chirp = 1; // chirp__toggling is just new_chirp pass-through
issue_chirp(`RP_WAVE_SHORT);
// 1 dst_clk for IDLECHIRP transition, 1 more for CHIRP-branch output
// registers (rf_switch / adar_tr / chirp_valid) to assert.
@(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); #1;
check("SHORT: enters CHIRP", dut.state == 1'b1);
check("SHORT: rf_switch_ctrl asserted", rf_switch_ctrl == 1'b1);
check("SHORT: adar_tr_1 asserted", adar_tr_1 == 1'b1);
check("SHORT: chirp_valid asserted", chirp_valid == 1'b1);
check("SHORT: tx_mixer_en asserted", tx_mixer_en == 1'b1);
check("SHORT: rx_mixer_en deasserted", rx_mixer_en == 1'b0);
// Drain the chirp window and confirm we land back in IDLE within bound.
wait_for_idle(SHORT_SAMPLES + 20);
check("SHORT: returns to IDLE within 120+20 cycles", dut.state == 1'b0);
check("SHORT: rf_switch_ctrl deasserted in IDLE", rf_switch_ctrl == 1'b0);
check("SHORT: chirp_data idle code 128 in IDLE", chirp_data == 8'd128);
check("SHORT: chirp_counter incremented to 1", chirp_counter == 6'd1);
// ---------- MEDIUM chirp playback ----------
$display("--- Group 4: MEDIUM chirp playback (600 samples) ---");
issue_chirp(`RP_WAVE_MEDIUM);
@(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
check("MEDIUM: enters CHIRP", dut.state == 1'b1);
check("MEDIUM: active_max_samples==600", dut.active_max_samples == 12'd600);
wait_for_idle(MEDIUM_SAMPLES + 20);
check("MEDIUM: returns to IDLE", dut.state == 1'b0);
check("MEDIUM: chirp_counter == 2", chirp_counter == 6'd2);
// ---------- LONG chirp playback ----------
$display("--- Group 5: LONG chirp playback (3600 samples) ---");
issue_chirp(`RP_WAVE_LONG);
@(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 DONEIDLELONG_CHIRP instantly.
new_chirp = 0;
check("LONG: enters CHIRP", dut.state == 1'b1);
check("LONG: active_max_samples==3600", dut.active_max_samples == 12'd3600);
wait_for_idle(LONG_SAMPLES + 20);
check("LONG: returns to IDLE", dut.state == 1'b0);
check("LONG: chirp_counter == 3", chirp_counter == 6'd3);
// ---------- frame_pulse clears chirp_counter ----------
$display("--- Group 6: frame_pulse clears chirp_counter ---");
pulse_frame();
@(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 (DONEIDLE 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);
check("frame_pulse: chirp_counter back to 0", chirp_counter == 6'd0);
// =====================================================================
// TEST GROUP 3b: MULTI-FRAME REGRESSION (C-3)
//
// Bug: plfm_chirp_controller_enhanced never reset chirp_counter when the
// frame completed. After frame 1 the counter sat at CHIRP_MAX, so the
// LONG_LISTEN -> GUARD transition guard (== CHIRP_MAX/2-1) never matched
// on subsequent frames and frame 2+ ran extra chirps until the 6-bit
// counter wrapped.
//
// These checks prove the counter is cleared at DONE and frame 2 matches
// frame 1 exactly.
// =====================================================================
$display("--- Group 3b: Multi-Frame Regression (C-3) ---");
// ---------- LUT data (chirp_data leaves idle during CHIRP) ----------
$display("--- Group 7: LUT-driven chirp_data ---");
issue_chirp(`RP_WAVE_SHORT);
repeat (4) @(posedge clk_120m);
check("SHORT mid-chirp: chirp_data != 128 (LUT-driven)", chirp_data != 8'd128);
wait_for_idle(SHORT_SAMPLES + 20);
// T3b.1: Immediately after frame 1 DONE -> IDLE, counter is back to 0.
check("C-3: chirp_counter reset to 0 after 1st DONE", chirp_counter == 6'd0);
// Kick off frame 2 from the same IDLE state (no reset between frames).
@(posedge clk_120m);
new_chirp = 1;
@(posedge clk_120m);
// T3b.2: Frame 2 enters LONG_CHIRP.
wait_for_state(3'b001, 10);
check("Frame 2: enters LONG_CHIRP", dut.current_state == 3'b001);
// T3b.3: Frame 2 reaches GUARD_TIME after exactly CHIRP_MAX/2 long chirps.
// (If the counter were not reset, the FSM would stall in
// LONG_CHIRP/LONG_LISTEN until the 6-bit counter wrapped.)
wait_for_state(3'b011,
(T1_SAMPLES + T1_RADAR_LISTENING) * (CHIRP_MAX/2) + 20);
check("Frame 2: reaches GUARD_TIME after CHIRP_MAX/2 long chirps",
dut.current_state == 3'b011);
check("Frame 2: chirp_counter == CHIRP_MAX/2 at GUARD_TIME",
chirp_counter == CHIRP_MAX/2);
// T3b.4: Frame 2 reaches DONE.
wait_for_state(3'b110,
GUARD_SAMPLES +
(T2_SAMPLES + T2_RADAR_LISTENING) * (CHIRP_MAX/2) + 20);
check("Frame 2: reaches DONE", dut.current_state == 3'b110);
// Deassert new_chirp so FSM stays in IDLE after DONE.
new_chirp = 0;
@(posedge clk_120m);
// T3b.5: Counter cleared again after frame 2 completes.
check("C-3: chirp_counter reset to 0 after 2nd DONE", chirp_counter == 6'd0);
// =====================================================================
// 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;
// ---------- Mixer disable resets state ----------
$display("--- Group 8: Mixer disable ---");
issue_chirp(`RP_WAVE_MEDIUM);
repeat (10) @(posedge clk_120m);
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->...
repeat (3) @(posedge clk_120m);
check("Mixer disable: chirp_data idle 128", chirp_data == 8'd128);
check("Mixer disable: chirp_valid 0", chirp_valid == 1'b0);
check("Mixer disable: rf_switch_ctrl 0", rf_switch_ctrl == 1'b0);
check("Mixer disable: tx_mixer_en 0", tx_mixer_en == 1'b0);
check("Mixer disable: rx_mixer_en 0", rx_mixer_en == 1'b0);
check("Mixer disable: state forced IDLE", dut.state == 1'b0);
// ---------- Beam-step counters ----------
$display("--- Group 9: Beam steering counters ---");
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));
check("Elevation: increments on toggle",
elevation_counter == 6'd2 || elevation_counter == 6'd3);
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);
check("Azimuth: increments on toggle",
azimuth_counter == 6'd2 || azimuth_counter == 6'd3);
new_azimuth = 0;
// =====================================================================
// TEST GROUP 7: MIXER ENABLE SIGNALS
// =====================================================================
$display("--- Group 7: Mixer Control Outputs ---");
// T7.1: In IDLE state, both mixers are off even with mixers_enable=1
// (Fix #4: mixers are state-dependent, not tied to mixers_enable directly)
mixers_enable = 1;
#1;
check("rx_mixer_en off in IDLE (state-dependent)", rx_mixer_en == 1'b0);
// T7.2: tx_mixer_en also off in IDLE
check("tx_mixer_en off in IDLE (state-dependent)", tx_mixer_en == 1'b0);
// 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);
// ---------- ADAR load pins tied low ----------
$display("--- Group 10: ADAR load pins ---");
check("adar_tx_load_1 tied low", adar_tx_load_1 == 1'b0);
check("adar_rx_load_1 tied low", adar_rx_load_1 == 1'b0);
check("adar_tx_load_4 tied low", adar_tx_load_4 == 1'b0);
check("adar_rx_load_4 tied low", adar_rx_load_4 == 1'b0);
// =====================================================================
// SUMMARY
// =====================================================================
@@ -506,14 +323,14 @@ initial begin
$display(" STATUS: %0d TESTS FAILED", fail_count);
$display("============================================================");
$display("");
#100;
$finish;
end
// Timeout watchdog
initial begin
#500000; // 500 us max
#500000; // 500 µs covers LONG playback (~30 µs) + headroom
$display("TIMEOUT: Simulation took too long!");
$finish;
end
@@ -181,7 +181,10 @@ radar_receiver_final dut (
// AUDIT-C3: ADC format select — offset-binary baseline
.host_adc_format(2'b00),
// CFAR: frame-complete output (not used in this TB)
.doppler_frame_done_out()
.doppler_frame_done_out(),
// PR-E: pin mixers_enable HIGH so the scheduler runs in this TB
.mixers_enable_100m(1'b1)
);
// ============================================================================