chirp-v2 PR-D: chirp_scheduler replaces radar_mode_controller; MF/MTI wave_sel-native

Single 100 MHz scheduler emits wave_sel[1:0] and chirp_pulse natively. Modes
00 (STM32 pass-through), 01 (auto-scan over SHORT/MEDIUM/LONG sub-frames),
10 (single-chirp debug), 11 (track dwell with watchdog scan-fallback after
RP_DEF_TRACK_WATCHDOG_FRAMES=5 idle frames). Sub-frame mask lets ops drop a
waveform without recompiling.

Drops the receiver_final wave_sel shim added in PR-C: wave_sel comes
straight from the scheduler; chirp_pulse replaces the old mc_new_chirp
toggle + XOR edge converter. matched_filter_multi_segment and mti_canceller
take wave_sel[1:0] and chirp_pulse directly — no parallel paths.

multi_segment also bumped: SHORT_CHIRP_SAMPLES 50 -> 100 (V2 1 us SHORT)
and MEDIUM_CHIRP_SAMPLES = 500 (5 us). LONG path unchanged. Dead
mc_new_elevation/azimuth XOR converters removed.

Deletes radar_mode_controller.v, formal/fv_radar_mode_controller.v, and
tb/tb_radar_mode_controller.v. Build manifests (run_regression.sh,
scripts/200t/build_200t.tcl) updated. Receiver_final pins medium/track/
subframe_enable inputs to RP_DEF_* defaults until PR-G plumbs USB opcodes.

Verification:
- tb_rxb_fullchain_latency: peak |I|+|Q|=24033 at bin 0, ~80x peak/mean
  (up from PR-C's 15115 since matched filter now uses full 100 SHORT samples)
- tb_mti_canceller: 43/43 PASS with new wave_sel[1:0] input
- tb_radar_receiver_final: 8/8 PASS, ALL TESTS PASSED
- tb_system_e2e: 34/49 PASS - identical to pre-PR-D baseline (15 failures
  are pre-existing matched-filter cycle-budget skips); G8.2/G8.3 chirp_scheduler
  probes PASS
- tb_multiseg_cosim: 16/32 - same as pre-PR-D baseline
This commit is contained in:
Jason
2026-04-30 20:52:32 +05:45
parent 4238eb1b99
commit 8e8f3e60c4
15 changed files with 666 additions and 1718 deletions
+9 -9
View File
@@ -34,7 +34,7 @@ reg signed [DATA_W-1:0] range_q_in;
reg range_valid_in;
reg [5:0] range_bin_in;
reg mti_enable;
reg tb_use_long_chirp;
reg [1:0] tb_wave_sel;
wire signed [DATA_W-1:0] range_i_out;
wire signed [DATA_W-1:0] range_q_out;
@@ -65,7 +65,7 @@ mti_canceller #(
.range_valid_out(range_valid_out),
.range_bin_out(range_bin_out),
.mti_enable(mti_enable),
.use_long_chirp(tb_use_long_chirp), // driven by TB; T12 exercises boundary
.wave_sel(tb_wave_sel), // driven by TB; T12 exercises boundary
.mti_first_chirp(mti_first_chirp)
);
@@ -94,7 +94,7 @@ task do_reset;
range_q_in = 0;
range_valid_in = 0;
range_bin_in = 0;
tb_use_long_chirp = 1'b0; // default homogeneous waveform
tb_wave_sel = 2'b00; // default homogeneous waveform (SHORT)
repeat (5) @(posedge clk);
reset_n = 1;
repeat (2) @(posedge clk);
@@ -485,7 +485,7 @@ initial begin
mti_enable = 1'b1;
// Chirp A (long, val=1000) first chirp, muted by first-chirp path.
tb_use_long_chirp = 1'b1;
tb_wave_sel = 2'b10; // RP_WAVE_LONG
fork
feed_chirp_const(16'sd1000, 16'sd500);
capture_chirp;
@@ -496,7 +496,7 @@ initial begin
cap_q[0] == 16'sd0);
// Chirp B (long, val=2000) same waveform: 2000 - 1000 = 1000.
tb_use_long_chirp = 1'b1;
tb_wave_sel = 2'b10; // RP_WAVE_LONG
cap_count = 0;
fork
feed_chirp_const(16'sd2000, 16'sd1500);
@@ -511,7 +511,7 @@ initial begin
// prev buffer must be overwritten with THIS chirp (not subtracted
// against the long-waveform chirp B). If R-1 regresses, we'd see
// 5000 - 2000 = 3000 here instead of 0.
tb_use_long_chirp = 1'b0;
tb_wave_sel = 2'b00; // RP_WAVE_SHORT
cap_count = 0;
fork
feed_chirp_const(16'sd5000, 16'sd3000);
@@ -525,7 +525,7 @@ initial begin
// Chirp D (short, val=5500) same waveform as C: 5500 - 5000 = 500.
// This proves the prev buffer was correctly overwritten with C,
// not stuck on B's long-waveform profile.
tb_use_long_chirp = 1'b0;
tb_wave_sel = 2'b00; // RP_WAVE_SHORT
cap_count = 0;
fork
feed_chirp_const(16'sd5500, 16'sd3250);
@@ -538,7 +538,7 @@ initial begin
// Chirp E (short -> long) another boundary, reverse direction,
// confirms muting is symmetric.
tb_use_long_chirp = 1'b1;
tb_wave_sel = 2'b10; // RP_WAVE_LONG
cap_count = 0;
fork
feed_chirp_const(16'sd9000, 16'sd4000);
@@ -566,7 +566,7 @@ initial begin
// ================================================================
do_reset;
mti_enable = 1'b1;
tb_use_long_chirp = 1'b1;
tb_wave_sel = 2'b10; // RP_WAVE_LONG
// Chirp 1: early-terminate at bin 31 (only 32/64 bins). I=1000, Q=500.
begin : t13_partial_chirp
+23 -23
View File
@@ -32,7 +32,7 @@ localparam FFT_SIZE = 1024;
localparam SEGMENT_ADVANCE = 896; // 1024 - 128
localparam OVERLAP_SAMPLES = 128;
localparam LONG_SEGMENTS = 4;
localparam SHORT_SAMPLES = 50;
localparam SHORT_SAMPLES = 100; // chirp-v2 SHORT = 1 us (was 0.5 us / 50 samples)
localparam LONG_CHIRP_SAMPLES = 3000;
localparam TIMEOUT = 500000; // Max clocks per operation
@@ -51,15 +51,17 @@ always #(CLK_PERIOD / 2) clk = ~clk;
reg signed [17:0] ddc_i;
reg signed [17:0] ddc_q;
reg ddc_valid;
reg use_long_chirp;
reg [1:0] wave_sel;
reg [5:0] chirp_counter;
reg mc_new_chirp;
reg mc_new_elevation;
reg mc_new_azimuth;
reg chirp_pulse;
reg [15:0] ref_chirp_real;
reg [15:0] ref_chirp_imag;
reg mem_ready;
// chirp-v2 PR-D: legacy use_long_chirp removed. Tests pick a waveform via
// wave_sel directly; helper alias kept for legibility in test bodies.
wire use_long_chirp = (wave_sel == 2'b10); // RP_WAVE_LONG
wire signed [15:0] pc_i_w;
wire signed [15:0] pc_q_w;
wire pc_valid_w;
@@ -77,11 +79,9 @@ matched_filter_multi_segment dut (
.ddc_i(ddc_i),
.ddc_q(ddc_q),
.ddc_valid(ddc_valid),
.use_long_chirp(use_long_chirp),
.wave_sel(wave_sel),
.chirp_counter(chirp_counter),
.mc_new_chirp(mc_new_chirp),
.mc_new_elevation(mc_new_elevation),
.mc_new_azimuth(mc_new_azimuth),
.chirp_pulse(chirp_pulse),
.ref_chirp_real(ref_chirp_real),
.ref_chirp_imag(ref_chirp_imag),
.segment_request(segment_request),
@@ -176,11 +176,9 @@ task apply_reset;
ddc_i <= 18'd0;
ddc_q <= 18'd0;
ddc_valid <= 1'b0;
use_long_chirp <= 1'b0;
wave_sel <= 2'b00; // RP_WAVE_SHORT
chirp_counter <= 6'd0;
mc_new_chirp <= 1'b0;
mc_new_elevation <= 1'b0;
mc_new_azimuth <= 1'b0;
chirp_pulse <= 1'b0;
ref_chirp_real <= 16'd0;
ref_chirp_imag <= 16'd0;
mem_ready <= 1'b0;
@@ -301,18 +299,19 @@ initial begin
$display("\n=== TEST 2: Short Chirp (1 segment, zero-padded) ===");
apply_reset;
use_long_chirp <= 1'b0;
wave_sel <= 2'b00; // RP_WAVE_SHORT
chirp_counter <= 6'd0;
@(posedge clk);
// Trigger chirp start (rising edge on mc_new_chirp)
mc_new_chirp <= 1'b1;
// Trigger chirp start (1-cycle chirp_pulse from chirp_scheduler)
chirp_pulse <= 1'b1;
@(posedge clk);
chirp_pulse <= 1'b0;
@(posedge clk);
// Verify FSM transitioned to ST_COLLECT_DATA
check(fsm_state == 4'd1, "Short chirp: entered ST_COLLECT_DATA");
// Feed 50 short chirp samples
// Feed SHORT_SAMPLES (100) short chirp samples
for (i = 0; i < SHORT_SAMPLES; i = i + 1) begin
@(posedge clk);
ddc_i <= (i * 100 + 500) & 18'h3FFFF; // Identifiable values
@@ -365,13 +364,14 @@ initial begin
$display("\n=== TEST 3: Long Chirp (4 segments, overlap-save) ===");
apply_reset;
use_long_chirp <= 1'b1;
wave_sel <= 2'b10; // RP_WAVE_LONG
chirp_counter <= 6'd0;
@(posedge clk);
// Trigger chirp start
mc_new_chirp <= 1'b1;
// Trigger chirp start (1-cycle chirp_pulse)
chirp_pulse <= 1'b1;
@(posedge clk);
chirp_pulse <= 1'b0;
@(posedge clk);
check(fsm_state == 4'd1, "Long chirp: entered ST_COLLECT_DATA");
check(tot_seg == 3'd4, "total_segments = 4");
@@ -637,11 +637,11 @@ initial begin
// Verify we can start a new chirp after the previous one completed
check(fsm_state == 4'd0, "In IDLE before re-trigger");
// Toggle mc_new_chirp (it was left high, so toggle low then high)
mc_new_chirp <= 1'b0;
// Re-trigger via 1-cycle chirp_pulse
repeat(3) @(posedge clk);
mc_new_chirp <= 1'b1;
chirp_pulse <= 1'b1;
@(posedge clk);
chirp_pulse <= 1'b0;
@(posedge clk);
@(posedge clk);
check(fsm_state == 4'd1, "Re-trigger: entered ST_COLLECT_DATA");
@@ -1,750 +0,0 @@
`timescale 1ns / 1ps
module tb_radar_mode_controller;
// ── Parameters ─────────────────────────────────────────────
localparam CLK_PERIOD = 10.0; // 100 MHz
// Use much shorter timing for simulation (100x faster)
localparam SIM_LONG_CHIRP = 30;
localparam SIM_LONG_LISTEN = 137;
localparam SIM_GUARD = 175;
localparam SIM_SHORT_CHIRP = 5;
localparam SIM_SHORT_LISTEN = 175;
// Use small scan size for simulation
localparam SIM_CHIRPS = 4;
localparam SIM_ELEVATIONS = 3;
localparam SIM_AZIMUTHS = 2;
// ── Signals ────────────────────────────────────────────────
reg clk;
reg reset_n;
reg [1:0] mode;
reg stm32_new_chirp;
reg stm32_new_elevation;
reg stm32_new_azimuth;
reg trigger;
// Gap 2: Runtime-configurable timing inputs
reg [15:0] cfg_long_chirp_cycles;
reg [15:0] cfg_long_listen_cycles;
reg [15:0] cfg_guard_cycles;
reg [15:0] cfg_short_chirp_cycles;
reg [15:0] cfg_short_listen_cycles;
reg [5:0] cfg_chirps_per_elev;
reg [1:0] range_mode;
wire use_long_chirp;
wire mc_new_chirp;
wire mc_new_elevation;
wire mc_new_azimuth;
wire [5:0] chirp_count;
wire [5:0] elevation_count;
wire [5:0] azimuth_count;
wire scanning;
wire scan_complete;
// ── Test bookkeeping ───────────────────────────────────────
integer pass_count;
integer fail_count;
integer test_num;
integer csv_file;
integer i;
// Edge detection helpers for auto-scan counting
reg mc_new_chirp_prev;
reg mc_new_elevation_prev;
reg mc_new_azimuth_prev;
integer chirp_toggles;
integer elevation_toggles;
integer azimuth_toggles;
integer scan_completes;
// Saved values for toggle checks
reg saved_mc_new_chirp;
reg saved_mc_new_elevation;
reg saved_mc_new_azimuth;
// ── Clock ──────────────────────────────────────────────────
always #(CLK_PERIOD/2) clk = ~clk;
// ── DUT ────────────────────────────────────────────────────
radar_mode_controller #(
.CHIRPS_PER_ELEVATION (SIM_CHIRPS),
.ELEVATIONS_PER_AZIMUTH(SIM_ELEVATIONS),
.AZIMUTHS_PER_SCAN (SIM_AZIMUTHS),
.LONG_CHIRP_CYCLES (SIM_LONG_CHIRP),
.LONG_LISTEN_CYCLES (SIM_LONG_LISTEN),
.GUARD_CYCLES (SIM_GUARD),
.SHORT_CHIRP_CYCLES (SIM_SHORT_CHIRP),
.SHORT_LISTEN_CYCLES (SIM_SHORT_LISTEN)
) uut (
.clk (clk),
.reset_n (reset_n),
.mode (mode),
.stm32_new_chirp (stm32_new_chirp),
.stm32_new_elevation(stm32_new_elevation),
.stm32_new_azimuth (stm32_new_azimuth),
.trigger (trigger),
// Gap 2: Runtime-configurable timing inputs
.cfg_long_chirp_cycles (cfg_long_chirp_cycles),
.cfg_long_listen_cycles (cfg_long_listen_cycles),
.cfg_guard_cycles (cfg_guard_cycles),
.cfg_short_chirp_cycles (cfg_short_chirp_cycles),
.cfg_short_listen_cycles(cfg_short_listen_cycles),
.cfg_chirps_per_elev (cfg_chirps_per_elev),
.range_mode (range_mode),
// Outputs
.use_long_chirp (use_long_chirp),
.mc_new_chirp (mc_new_chirp),
.mc_new_elevation (mc_new_elevation),
.mc_new_azimuth (mc_new_azimuth),
.chirp_count (chirp_count),
.elevation_count (elevation_count),
.azimuth_count (azimuth_count),
.scanning (scanning),
.scan_complete (scan_complete)
);
// ── Check task ─────────────────────────────────────────────
task check;
input cond;
input [511:0] label;
begin
test_num = test_num + 1;
if (cond) begin
$display("[PASS] Test %0d: %0s", test_num, label);
pass_count = pass_count + 1;
end else begin
$display("[FAIL] Test %0d: %0s", test_num, label);
fail_count = fail_count + 1;
end
end
endtask
// ── Helper: apply reset ────────────────────────────────────
task apply_reset;
begin
reset_n = 0;
mode = 2'b11; // reserved = safe idle
stm32_new_chirp = 0;
stm32_new_elevation = 0;
stm32_new_azimuth = 0;
trigger = 0;
// Gap 2: Set cfg_* to simulation parameter defaults
cfg_long_chirp_cycles = SIM_LONG_CHIRP;
cfg_long_listen_cycles = SIM_LONG_LISTEN;
cfg_guard_cycles = SIM_GUARD;
cfg_short_chirp_cycles = SIM_SHORT_CHIRP;
cfg_short_listen_cycles = SIM_SHORT_LISTEN;
cfg_chirps_per_elev = SIM_CHIRPS;
range_mode = 2'b00; // 3 km short-chirp mode
repeat (4) @(posedge clk);
reset_n = 1;
@(posedge clk); #1;
end
endtask
// ── Stimulus ───────────────────────────────────────────────
initial begin
$dumpfile("tb_radar_mode_controller.vcd");
$dumpvars(0, tb_radar_mode_controller);
clk = 0;
pass_count = 0;
fail_count = 0;
test_num = 0;
// ════════════════════════════════════════════════════════
// TEST GROUP 1: Reset behaviour
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 1: Reset Behaviour ---");
apply_reset;
reset_n = 0;
repeat (4) @(posedge clk); #1;
check(use_long_chirp === 1'b0, "use_long_chirp=0 after reset");
check(mc_new_chirp === 1'b0, "mc_new_chirp=0 after reset");
check(mc_new_elevation === 1'b0, "mc_new_elevation=0 after reset");
check(mc_new_azimuth === 1'b0, "mc_new_azimuth=0 after reset");
check(chirp_count === 6'd0, "chirp_count=0 after reset");
check(elevation_count === 6'd0, "elevation_count=0 after reset");
check(azimuth_count === 6'd0, "azimuth_count=0 after reset");
check(scanning === 1'b0, "scanning=0 after reset");
check(scan_complete === 1'b0, "scan_complete=0 after reset");
reset_n = 1;
@(posedge clk); #1;
// ════════════════════════════════════════════════════════
// TEST GROUP 2: STM32 pass-through mode (mode 00)
// The DUT uses XOR toggle detection: when stm32_new_chirp
// changes from its previous value, the DUT detects it.
// We toggle-and-hold (don't pulse) to get exactly one detection.
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 2: STM32 Pass-through (mode 00) ---");
apply_reset;
mode = 2'b00;
@(posedge clk); #1;
// Save current mc_new_chirp
saved_mc_new_chirp = mc_new_chirp;
// Toggle stm32_new_chirp (0→1, hold at 1)
stm32_new_chirp = 1'b1;
// Wait 2 cycles: 1 for prev register update, 1 for XOR→main FSM
@(posedge clk); @(posedge clk); #1;
check(mc_new_chirp !== saved_mc_new_chirp,
"mc_new_chirp toggles on stm32 chirp change");
check(chirp_count === 6'd1, "chirp_count incremented to 1");
// Toggle again (1→0, hold at 0) — second chirp
saved_mc_new_chirp = mc_new_chirp;
stm32_new_chirp = 1'b0;
@(posedge clk); @(posedge clk); #1;
check(mc_new_chirp !== saved_mc_new_chirp,
"mc_new_chirp toggles again");
check(chirp_count === 6'd2, "chirp_count incremented to 2");
// Toggle stm32_new_elevation (0→1, hold)
saved_mc_new_elevation = mc_new_elevation;
stm32_new_elevation = 1'b1;
@(posedge clk); @(posedge clk); #1;
check(mc_new_elevation !== saved_mc_new_elevation,
"mc_new_elevation toggles on stm32 elevation change");
check(chirp_count === 6'd0,
"chirp_count resets on elevation toggle");
check(elevation_count === 6'd1,
"elevation_count incremented to 1");
// Toggle stm32_new_azimuth (0→1, hold)
saved_mc_new_azimuth = mc_new_azimuth;
stm32_new_azimuth = 1'b1;
@(posedge clk); @(posedge clk); #1;
check(mc_new_azimuth !== saved_mc_new_azimuth,
"mc_new_azimuth toggles on stm32 azimuth change");
check(elevation_count === 6'd0,
"elevation_count resets on azimuth toggle");
check(azimuth_count === 6'd1,
"azimuth_count incremented to 1");
// ════════════════════════════════════════════════════════
// TEST GROUP 3: Auto-scan mode (mode 01) — full scan
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 3: Auto-scan (mode 01) Full Scan ---");
apply_reset;
mode = 2'b01;
csv_file = $fopen("rmc_autoscan.csv", "w");
$fwrite(csv_file, "cycle,chirp,elevation,azimuth,long_chirp,scanning,scan_complete\n");
mc_new_chirp_prev = 0;
mc_new_elevation_prev = 0;
mc_new_azimuth_prev = 0;
chirp_toggles = 0;
elevation_toggles = 0;
azimuth_toggles = 0;
scan_completes = 0;
// Check: scanning starts immediately
@(posedge clk); #1;
check(scanning === 1'b1, "Scanning starts immediately in auto mode");
// Run for enough cycles to complete one full scan
for (i = 0; i < 15000; i = i + 1) begin
@(posedge clk); #1;
if (mc_new_chirp !== mc_new_chirp_prev)
chirp_toggles = chirp_toggles + 1;
if (mc_new_elevation !== mc_new_elevation_prev)
elevation_toggles = elevation_toggles + 1;
if (mc_new_azimuth !== mc_new_azimuth_prev)
azimuth_toggles = azimuth_toggles + 1;
if (scan_complete)
scan_completes = scan_completes + 1;
mc_new_chirp_prev = mc_new_chirp;
mc_new_elevation_prev = mc_new_elevation;
mc_new_azimuth_prev = mc_new_azimuth;
if (i % 100 == 0) begin
$fwrite(csv_file, "%0d,%0d,%0d,%0d,%0d,%0d,%0d\n",
i, chirp_count, elevation_count, azimuth_count,
use_long_chirp, scanning, scan_complete);
end
end
$fclose(csv_file);
$display(" Chirp toggles: %0d (expected %0d)",
chirp_toggles, SIM_CHIRPS * SIM_ELEVATIONS * SIM_AZIMUTHS);
$display(" Elevation toggles: %0d", elevation_toggles);
$display(" Azimuth toggles: %0d", azimuth_toggles);
$display(" Scan completes: %0d", scan_completes);
check(chirp_toggles >= SIM_CHIRPS * SIM_ELEVATIONS * SIM_AZIMUTHS,
"At least 24 chirp toggles in full scan");
check(scan_completes >= 1,
"At least 1 scan completion detected");
check(elevation_toggles >= SIM_AZIMUTHS,
"Elevation toggles >= number of azimuths");
check(azimuth_toggles >= 1,
"Azimuth toggles >= 1");
// ════════════════════════════════════════════════════════
// TEST GROUP 4: Auto-scan chirp timing (3 km mode, short chirps only)
// With range_mode=0, auto-scan skips long chirp and goes directly
// to short chirp. No long→guard→short transition.
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 4: Chirp Timing Sequence (3km short-only) ---");
apply_reset;
mode = 2'b01;
@(posedge clk); #1;
check(use_long_chirp === 1'b0, "3km: starts with short chirp");
repeat (SIM_SHORT_CHIRP / 2) @(posedge clk);
#1;
check(use_long_chirp === 1'b0, "3km: still short chirp midway");
// Wait through short chirp + short listen
repeat (SIM_SHORT_CHIRP / 2 + SIM_SHORT_LISTEN) @(posedge clk);
#1;
// Next chirp should also be short
repeat (2) @(posedge clk); #1;
check(use_long_chirp === 1'b0, "3km: next chirp is also short");
// ════════════════════════════════════════════════════════
// TEST GROUP 5: Single-chirp mode (mode 10)
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 5: Single-chirp Mode (mode 10) ---");
apply_reset;
mode = 2'b10;
repeat (10) @(posedge clk); #1;
check(scanning === 1'b0, "Single mode: idle without trigger");
saved_mc_new_chirp = mc_new_chirp;
// Pulse trigger (rising edge detection)
trigger = 1'b1;
@(posedge clk); #1;
trigger = 1'b0;
repeat (2) @(posedge clk); #1;
check(scanning === 1'b1, "Single mode: scanning after trigger");
check(use_long_chirp === 1'b0, "Single mode: uses short chirp (3km)");
check(mc_new_chirp !== saved_mc_new_chirp,
"Single mode: mc_new_chirp toggled");
// Wait for chirp to complete (short chirp in 3km mode)
repeat (SIM_SHORT_CHIRP + SIM_SHORT_LISTEN + 10) @(posedge clk); #1;
check(scanning === 1'b0, "Single mode: returns to idle after chirp");
// No activity without trigger
saved_mc_new_chirp = mc_new_chirp;
repeat (100) @(posedge clk); #1;
check(mc_new_chirp === saved_mc_new_chirp,
"Single mode: no activity without trigger");
// Second trigger
saved_mc_new_chirp = mc_new_chirp;
trigger = 1'b1;
@(posedge clk); #1;
trigger = 1'b0;
repeat (3) @(posedge clk); #1;
check(mc_new_chirp !== saved_mc_new_chirp,
"Single mode: 2nd trigger works");
// ════════════════════════════════════════════════════════
// TEST GROUP 6: Reserved mode (mode 11) — stays idle
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 6: Reserved Mode (mode 11) ---");
apply_reset;
mode = 2'b11;
repeat (200) @(posedge clk); #1;
check(scanning === 1'b0, "Reserved mode: stays idle");
// ════════════════════════════════════════════════════════
// TEST GROUP 7: Mode switching
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 7: Mode Switching ---");
apply_reset;
mode = 2'b01; // Auto-scan
repeat (100) @(posedge clk); #1;
check(scanning === 1'b1, "Auto mode: scanning");
mode = 2'b11;
repeat (10) @(posedge clk); #1;
check(scanning === 1'b0, "Switching to reserved: stops scanning");
mode = 2'b10;
repeat (10) @(posedge clk); #1;
check(scanning === 1'b0, "Single mode after switch: idle");
trigger = 1'b1;
@(posedge clk); #1;
trigger = 1'b0;
repeat (3) @(posedge clk); #1;
check(scanning === 1'b1, "Single mode after switch: triggers OK");
// ════════════════════════════════════════════════════════
// TEST GROUP 8: STM32 mode — chirp count wrapping
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 8: STM32 Chirp Count Wrapping ---");
apply_reset;
mode = 2'b00;
@(posedge clk); #1;
// Toggle chirp SIM_CHIRPS times (toggle-and-hold each time)
for (i = 0; i < SIM_CHIRPS; i = i + 1) begin
stm32_new_chirp = ~stm32_new_chirp; // toggle and hold
@(posedge clk); @(posedge clk); #1; // wait for detection
end
$display(" chirp_count after %0d toggles: %0d (expect 0)",
SIM_CHIRPS, chirp_count);
check(chirp_count === 6'd0,
"chirp_count wraps after CHIRPS_PER_ELEVATION toggles");
// ════════════════════════════════════════════════════════
// TEST GROUP 9: STM32 mode — full scan completion
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 9: STM32 Full Scan Completion ---");
apply_reset;
mode = 2'b00;
@(posedge clk); #1;
scan_completes = 0;
// Toggle azimuth SIM_AZIMUTHS times
for (i = 0; i < SIM_AZIMUTHS; i = i + 1) begin
stm32_new_azimuth = ~stm32_new_azimuth;
@(posedge clk); #1;
if (scan_complete) scan_completes = scan_completes + 1;
@(posedge clk); #1;
if (scan_complete) scan_completes = scan_completes + 1;
end
$display(" scan_complete pulses: %0d (expect 1)", scan_completes);
check(scan_completes == 1, "scan_complete pulses once after full azimuth sweep");
check(azimuth_count === 6'd0, "azimuth_count wraps to 0 after full scan");
// ════════════════════════════════════════════════════════
// TEST GROUP 10: Reset Mid-Scan
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 10: Reset Mid-Scan ---");
apply_reset;
mode = 2'b01; // auto-scan
// Wait ~200 cycles (partway through first chirp)
repeat (200) @(posedge clk); #1;
check(scanning === 1'b1, "Mid-scan: scanning=1 before reset");
// Assert reset for 4 cycles
reset_n = 0;
repeat (4) @(posedge clk); #1;
// Verify state during reset
check(scanning === 1'b0, "Mid-scan reset: scanning=0");
check(chirp_count === 6'd0, "Mid-scan reset: chirp_count=0");
check(elevation_count === 6'd0, "Mid-scan reset: elevation_count=0");
check(azimuth_count === 6'd0, "Mid-scan reset: azimuth_count=0");
check(use_long_chirp === 1'b0, "Mid-scan reset: use_long_chirp=0");
check(mc_new_chirp === 1'b0, "Mid-scan reset: mc_new_chirp=0");
check(mc_new_elevation === 1'b0, "Mid-scan reset: mc_new_elevation=0");
check(mc_new_azimuth === 1'b0, "Mid-scan reset: mc_new_azimuth=0");
// Release reset
reset_n = 1;
@(posedge clk); #1;
// ════════════════════════════════════════════════════════
// TEST GROUP 11: Mode-Switch State Leakage
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 11: Mode-Switch State Leakage ---");
apply_reset;
mode = 2'b01; // auto-scan
// Run for ~500 cycles
repeat (500) @(posedge clk); #1;
check(scanning === 1'b1, "Leakage: scanning=1 during auto-scan");
// Switch to reserved mode (11) — forces scan_state=S_IDLE
mode = 2'b11;
repeat (10) @(posedge clk); #1;
check(scanning === 1'b0, "Leakage: scanning=0 in reserved mode");
// Switch back to auto-scan (01)
mode = 2'b01;
// Auto-scan S_IDLE transitions to S_LONG_CHIRP on the next clock
// so after 1 cycle scan_state != S_IDLE => scanning=1
@(posedge clk); #1;
// The first cycle in mode 01 hits S_IDLE and transitions out
// scanning should be 1 now (scan_state moved to S_LONG_CHIRP)
check(scanning === 1'b1, "Leakage: auto-scan restarts cleanly (scanning=1)");
// ════════════════════════════════════════════════════════
// TEST GROUP 12: Simultaneous STM32 Toggle Events
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 12: Simultaneous STM32 Toggle Events ---");
apply_reset;
mode = 2'b00;
@(posedge clk); #1;
// Save current toggle outputs
saved_mc_new_chirp = mc_new_chirp;
saved_mc_new_elevation = mc_new_elevation;
// Toggle BOTH stm32_new_chirp AND stm32_new_elevation at the same time
stm32_new_chirp = 1'b1;
stm32_new_elevation = 1'b1;
// Wait 2 cycles for XOR detection
@(posedge clk); @(posedge clk); #1;
check(mc_new_chirp !== saved_mc_new_chirp,
"Simultaneous: mc_new_chirp toggled");
check(mc_new_elevation !== saved_mc_new_elevation,
"Simultaneous: mc_new_elevation toggled");
// Elevation toggle resets chirp_count (last-write-wins in RTL)
check(chirp_count === 6'd0,
"Simultaneous: chirp_count=0 (elevation resets it)");
// ════════════════════════════════════════════════════════
// TEST GROUP 13: Single-Chirp Mode — Multiple Rapid Triggers
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 13: Single-Chirp Multiple Rapid Triggers ---");
apply_reset;
mode = 2'b10;
@(posedge clk); #1;
saved_mc_new_chirp = mc_new_chirp;
// First trigger — should start a chirp
trigger = 1'b1;
@(posedge clk); #1;
trigger = 1'b0;
repeat (2) @(posedge clk); #1;
check(scanning === 1'b1, "Rapid trigger: first trigger starts chirp");
check(mc_new_chirp !== saved_mc_new_chirp,
"Rapid trigger: mc_new_chirp toggled on first trigger");
// Save chirp state after first trigger
saved_mc_new_chirp = mc_new_chirp;
// Send another trigger while chirp is still active (FSM not in S_IDLE)
trigger = 1'b1;
@(posedge clk); #1;
trigger = 1'b0;
repeat (2) @(posedge clk); #1;
check(scanning === 1'b1, "Rapid trigger: still scanning (didn't restart)");
check(mc_new_chirp === saved_mc_new_chirp,
"Rapid trigger: second trigger ignored (mc_new_chirp unchanged)");
// Wait for chirp to complete (short_chirp + short_listen for range_mode=0)
repeat (SIM_SHORT_CHIRP + SIM_SHORT_LISTEN + 20) @(posedge clk); #1;
check(scanning === 1'b0, "Rapid trigger: chirp completed, back to idle");
// Now trigger again — this should work
saved_mc_new_chirp = mc_new_chirp;
trigger = 1'b1;
@(posedge clk); #1;
trigger = 1'b0;
repeat (2) @(posedge clk); #1;
check(scanning === 1'b1, "Rapid trigger: third trigger works after idle");
check(mc_new_chirp !== saved_mc_new_chirp,
"Rapid trigger: mc_new_chirp toggled on third trigger");
// ════════════════════════════════════════════════════════
// TEST GROUP 14: Auto-Scan Counter Verification
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 14: Auto-Scan Counter Verification ---");
apply_reset;
mode = 2'b01; // auto-scan
mc_new_chirp_prev = 0;
chirp_toggles = 0;
scan_completes = 0;
// The first chirp toggle happens on the S_IDLE→S_SHORT_CHIRP transition.
// We need to capture it. Sample after the first posedge so we get the
// initial state right.
@(posedge clk); #1;
// After this clock, scan_state has moved to S_LONG_CHIRP and
// mc_new_chirp has already toggled once. Record its value as prev
// so we can count from here.
mc_new_chirp_prev = mc_new_chirp;
chirp_toggles = 1; // count the initial toggle
// Run until first scan_complete
// Total chirps = 4*3*2 = 24, each chirp ~523 cycles
// 24*523 = 12552, add margin
// NOTE: When scan_complete fires (S_ADVANCE full-scan branch), the DUT
// simultaneously toggles mc_new_chirp for the NEXT scan's first chirp.
// We must check scan_complete before counting the toggle so we don't
// include that restart toggle in our count of the current scan's chirps.
for (i = 0; i < 14000; i = i + 1) begin
@(posedge clk); #1;
if (scan_complete)
scan_completes = scan_completes + 1;
// Stop BEFORE counting the toggle that coincides with scan_complete
// (that toggle starts the next scan, not the current one)
if (scan_completes >= 1)
i = 14000; // break
else begin
if (mc_new_chirp !== mc_new_chirp_prev)
chirp_toggles = chirp_toggles + 1;
mc_new_chirp_prev = mc_new_chirp;
end
end
$display(" Total chirp toggles: %0d (expected 24)", chirp_toggles);
$display(" Scan completes: %0d (expected 1)", scan_completes);
// At scan_complete, the DUT wraps all counters and immediately starts
// a new chirp (transitions to S_LONG_CHIRP, not S_IDLE). The counters
// are reset to 0 in the full-scan-complete branch of S_ADVANCE.
check(scan_completes == 1, "Counter verify: exactly 1 scan_complete");
// The full-scan-complete branch resets all counters to 0:
check(chirp_count === 6'd0, "Counter verify: chirp_count=0 at scan_complete");
check(elevation_count === 6'd0, "Counter verify: elevation_count=0 at scan_complete");
check(azimuth_count === 6'd0, "Counter verify: azimuth_count=0 at scan_complete");
check(chirp_toggles == SIM_CHIRPS * SIM_ELEVATIONS * SIM_AZIMUTHS,
"Counter verify: exactly 24 chirp toggles");
// ════════════════════════════════════════════════════════
// TEST GROUP 15: STM32 Mode — Counter Persistence
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 15: STM32 Mode Counter Persistence ---");
apply_reset;
mode = 2'b00;
@(posedge clk); #1;
// Toggle chirp 3 times
for (i = 0; i < 3; i = i + 1) begin
stm32_new_chirp = ~stm32_new_chirp;
@(posedge clk); @(posedge clk); #1;
end
$display(" chirp_count after 3 toggles: %0d (expect 3)", chirp_count);
check(chirp_count === 6'd3, "Persistence: chirp_count=3 after 3 toggles");
// Switch to reserved mode (11) — does NOT reset counters
mode = 2'b11;
repeat (10) @(posedge clk); #1;
$display(" chirp_count in reserved mode: %0d (expect 3)", chirp_count);
check(chirp_count === 6'd3, "Persistence: chirp_count=3 in reserved mode");
// Switch back to STM32 mode (00)
mode = 2'b00;
@(posedge clk); #1;
$display(" chirp_count after returning to STM32: %0d (expect 3)", chirp_count);
check(chirp_count === 6'd3, "Persistence: chirp_count=3 after mode roundtrip");
// Toggle chirp once more — should wrap (3+1=4=CHIRPS, wraps to 0)
stm32_new_chirp = ~stm32_new_chirp;
@(posedge clk); @(posedge clk); #1;
$display(" chirp_count after 4th toggle: %0d (expect 0)", chirp_count);
check(chirp_count === 6'd0, "Persistence: chirp_count wraps to 0 at 4th toggle");
// ════════════════════════════════════════════════════════
// TEST GROUP 16: Runtime Timing Reconfiguration (Gap 2)
// Verify that changing cfg_* mid-simulation changes timing.
// We halve the long chirp duration and verify the chirp
// completes in fewer cycles.
// ════════════════════════════════════════════════════════
$display("\n--- Test Group 16: Runtime Timing Reconfiguration (Gap 2) ---");
apply_reset;
mode = 2'b01; // auto-scan
// Let the first chirp start (S_IDLE -> S_SHORT_CHIRP in 3km mode)
@(posedge clk); #1;
check(scanning === 1'b1, "Reconfig: auto-scan started");
check(use_long_chirp === 1'b0, "Reconfig: starts with short chirp (3km)");
// Wait ~half the default short chirp time to confirm we're still in S_SHORT_CHIRP
// S_SHORT_CHIRP state index: check via scan_state
repeat (SIM_SHORT_CHIRP / 2) @(posedge clk); #1;
// Now change cfg_short_chirp_cycles to a much shorter value mid-scan.
// The timer is already at ~SIM_SHORT_CHIRP/2, so setting cycles to 1
// means the FSM will advance on the next cycle.
cfg_short_chirp_cycles = 1;
repeat (2) @(posedge clk); #1;
// Restore default and verify scan continues
cfg_short_chirp_cycles = SIM_SHORT_CHIRP;
repeat (10) @(posedge clk); #1;
check(scanning === 1'b1, "Reconfig: scan continues after restoring default");
// Test runtime chirps_per_elev change:
// Reset and set chirps_per_elev to 2 (instead of default 4)
apply_reset;
cfg_chirps_per_elev = 6'd2;
mode = 2'b01; // auto-scan
mc_new_chirp_prev = 0;
chirp_toggles = 0;
elevation_toggles = 0;
@(posedge clk); #1;
mc_new_chirp_prev = mc_new_chirp;
mc_new_elevation_prev = mc_new_elevation;
chirp_toggles = 1; // initial toggle
// Run enough cycles for a few chirps + elevation advance
// With 2 chirps/elev: each chirp ~180 cycles (5+175) for short-only 3km mode
// 2 chirps = ~684 cycles, then elevation advance
for (i = 0; i < 2000; i = i + 1) begin
@(posedge clk); #1;
if (mc_new_chirp !== mc_new_chirp_prev)
chirp_toggles = chirp_toggles + 1;
if (mc_new_elevation !== mc_new_elevation_prev)
elevation_toggles = elevation_toggles + 1;
mc_new_chirp_prev = mc_new_chirp;
mc_new_elevation_prev = mc_new_elevation;
end
$display(" chirp_toggles=%0d elevation_toggles=%0d (cfg_chirps_per_elev=2)",
chirp_toggles, elevation_toggles);
// With 2 chirps/elev, we should get elevation toggles at every 2 chirps
check(elevation_toggles >= 1,
"Reconfig: elevation advances with cfg_chirps_per_elev=2");
// Verify the ratio: chirp_toggles should be ~2x elevation_toggles
// (first elevation has 2 chirps, then toggle. Second has 2 chirps, then toggle, etc.)
check(chirp_toggles >= 2 * elevation_toggles,
"Reconfig: chirp/elevation ratio consistent with cfg_chirps_per_elev=2");
// Restore defaults
cfg_chirps_per_elev = SIM_CHIRPS;
// ════════════════════════════════════════════════════════
// Summary
// ════════════════════════════════════════════════════════
$display("");
$display("========================================");
$display(" RADAR MODE CONTROLLER RESULTS");
$display(" PASSED: %0d / %0d", pass_count, test_num);
$display(" FAILED: %0d / %0d", fail_count, test_num);
if (fail_count == 0)
$display(" ** ALL TESTS PASSED **");
else
$display(" ** SOME TESTS FAILED **");
$display("========================================");
$display("");
#100;
$finish;
end
endmodule
@@ -41,7 +41,7 @@
//
// Strategy:
// - Uses behavioral stub for ad9484_interface_400m (no Xilinx primitives)
// - Overrides radar_mode_controller timing params for fast simulation
// - Drives chirp_scheduler timing via host_* inputs for fast simulation
// - Feeds 120 MHz tone at ADC input (IF frequency -> DDC passband)
// - Verifies structural correctness + golden comparison + bounds checks
//
@@ -103,6 +103,9 @@ reg mc_new_chirp_prev;
reg tx_frame_start;
reg [5:0] rmc_chirp_prev;
// chirp-v2 PR-D: chirp_scheduler emits chirp_pulse (1-cycle pulse) and
// sched_chirp_counter directly. mc_new_chirp toggle / rmc_chirp_count are
// gone. The probe just rides those pulses to drive the TB-side counters.
always @(posedge clk_100m or negedge reset_n) begin
if (!reset_n) begin
chirp_counter <= 6'd0;
@@ -110,17 +113,16 @@ always @(posedge clk_100m or negedge reset_n) begin
tx_frame_start <= 1'b0;
rmc_chirp_prev <= 6'd0;
end else begin
mc_new_chirp_prev <= dut.mc_new_chirp;
if (dut.mc_new_chirp != mc_new_chirp_prev) begin
if (dut.chirp_pulse) begin
chirp_counter <= chirp_counter + 1;
end
// Detect when the internal mode controller's chirp_count wraps to 0
// Detect when the scheduler's chirp_counter wraps to 0
tx_frame_start <= 1'b0;
if (dut.rmc_chirp_count == 6'd0 && rmc_chirp_prev != 6'd0) begin
if (dut.sched_chirp_counter == 6'd0 && rmc_chirp_prev != 6'd0) begin
tx_frame_start <= 1'b1;
end
rmc_chirp_prev <= dut.rmc_chirp_count;
rmc_chirp_prev <= dut.sched_chirp_counter;
end
end
@@ -163,13 +165,16 @@ radar_receiver_final dut (
.host_range_mode(2'b01), // long-range mode (dual chirp); was missing -> z
.host_trigger(1'b0),
// Gap 2: Host-configurable chirp timing — match defparam overrides below
// chirp-v2 PR-D: chirp_scheduler is host-input driven. SHORT chirp bumped
// to 100 cycles (1 µs V2). Host_chirps_per_elev is still wired to keep
// the parent port list intact, but the scheduler inside the receiver
// pins chirps_per_subframe to RP_DEF (16) — PR-G renames the host reg.
.host_long_chirp_cycles(16'd500),
.host_long_listen_cycles(16'd2000),
.host_guard_cycles(16'd500),
.host_short_chirp_cycles(16'd50),
.host_short_chirp_cycles(16'd100),
.host_short_listen_cycles(16'd1000),
.host_chirps_per_elev(6'd32),
.host_chirps_per_elev(6'd16),
// Fix 3: digital gain control — pass-through for golden reference
.host_gain_shift(4'd0),
@@ -180,19 +185,13 @@ radar_receiver_final dut (
);
// ============================================================================
// OVERRIDE TIMING PARAMETERS via defparam
// ============================================================================
// Reduce radar_mode_controller timing to keep simulation tractable.
// SIM TIMING — driven via host_* inputs above (chirp-v2 PR-D).
// chirp_scheduler is host-input driven; no defparam overrides needed.
// Real values: LONG_CHIRP=3000, LONG_LISTEN=13700, GUARD=17540,
// SHORT_CHIRP=50, SHORT_LISTEN=17450 (total ~51740 per chirp)
// Need enough DDC samples to fill MF buffer (896) plus latency buffer (3187).
// At ~1 DDC sample per sys_clk, we need at least ~5000 sys_clk per chirp.
// Use moderately reduced values: ~5000 cycles per chirp pair
defparam dut.rmc.LONG_CHIRP_CYCLES = 500;
defparam dut.rmc.LONG_LISTEN_CYCLES = 2000;
defparam dut.rmc.GUARD_CYCLES = 500;
defparam dut.rmc.SHORT_CHIRP_CYCLES = 50;
defparam dut.rmc.SHORT_LISTEN_CYCLES = 1000;
// SHORT_CHIRP=100 (V2), SHORT_LISTEN=17400 (~31040 per chirp).
// The host_* assignments above feed the same compressed timing the legacy
// defparams used.
// ============================================================================
// ============================================================================
// TEST INFRASTRUCTURE
@@ -34,15 +34,13 @@ module tb_rxb_fullchain_latency;
reg clk;
reg reset_n;
// multi_segment inputs
// multi_segment inputs (chirp-v2 PR-D wave_sel + chirp_pulse contract)
reg signed [17:0] ddc_i;
reg signed [17:0] ddc_q;
reg ddc_valid;
reg use_long_chirp;
reg [1:0] wave_sel_r; // SHORT/MEDIUM/LONG selector
reg [5:0] chirp_counter;
reg mc_new_chirp;
reg mc_new_elevation;
reg mc_new_azimuth;
reg chirp_pulse; // 1-cycle pulse on chirp start
// multi_segment <-> chirp_reference_rom interconnect
wire [1:0] segment_request;
@@ -60,8 +58,9 @@ module tb_rxb_fullchain_latency;
wire pc_valid;
wire [3:0] ms_status;
// wave_sel shim — matches radar_receiver_final.v PR-C transitional wiring.
wire [1:0] wave_sel = use_long_chirp ? `RP_WAVE_LONG : `RP_WAVE_SHORT;
// wave_sel drives both the ROM and the matched filter (chirp-v2 PR-D
// contract — no use_long_chirp shim).
wire [1:0] wave_sel = wave_sel_r;
// ----- Chirp reference ROM (chirp-v2 PR-C) -----
chirp_reference_rom chirp_rom (
@@ -97,11 +96,9 @@ module tb_rxb_fullchain_latency;
.ddc_i (ddc_i),
.ddc_q (ddc_q),
.ddc_valid (ddc_valid),
.use_long_chirp (use_long_chirp),
.wave_sel (wave_sel),
.chirp_counter (chirp_counter),
.mc_new_chirp (mc_new_chirp),
.mc_new_elevation (mc_new_elevation),
.mc_new_azimuth (mc_new_azimuth),
.chirp_pulse (chirp_pulse),
.ref_chirp_real (ref_i_d),
.ref_chirp_imag (ref_q_d),
.segment_request (segment_request),
@@ -225,11 +222,9 @@ module tb_rxb_fullchain_latency;
ddc_i = 0;
ddc_q = 0;
ddc_valid = 0;
use_long_chirp = 1'b0; // use SHORT chirp path so loader uses short_chirp_*.mem
wave_sel_r = `RP_WAVE_SHORT; // SHORT path rx_short_*.mem
chirp_counter = 6'd0;
mc_new_chirp = 1'b0;
mc_new_elevation = 1'b0;
mc_new_azimuth = 1'b0;
chirp_pulse = 1'b0;
// Load the same short-chirp samples the ROM will serve as ref,
// so signal == ref → autocorrelation. Peak should be at bin 0 if
@@ -248,12 +243,12 @@ module tb_rxb_fullchain_latency;
$display("FFT_SIZE: %0d, SHORT_LEN: %0d", FFT_SIZE, SHORT_LEN);
$display("");
// Pulse mc_new_chirp
$display("[T=%0t] Pulsing mc_new_chirp HIGH...", $time);
// Pulse chirp_pulse for one cycle (chirp-v2 PR-D contract)
$display("[T=%0t] Pulsing chirp_pulse HIGH...", $time);
@(posedge clk);
#1 mc_new_chirp = 1'b1;
repeat (4) @(posedge clk);
#1 mc_new_chirp = 1'b0;
#1 chirp_pulse = 1'b1;
@(posedge clk);
#1 chirp_pulse = 1'b0;
// Feed signal samples (same as ref → autocorrelation)
feed_short_chirp_signal;
+10 -8
View File
@@ -35,7 +35,7 @@
* chirp_reference_rom.v \
* matched_filter_multi_segment.v matched_filter_processing_chain.v \
* range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v \
* usb_data_interface.v edge_detector.v radar_mode_controller.v
* usb_data_interface.v edge_detector.v chirp_scheduler.v
*
* Run:
* vvp tb/tb_system_e2e.vvp
@@ -873,14 +873,16 @@ initial begin
check(obs_range_valid_count > saved_range_count,
"G8.1: Auto-scan generated range profile output autonomously");
// G8.2: Receiver mode controller chirp counter advanced
// Access the RX-side mode controller chirp count directly.
check(dut.rx_inst.rmc_chirp_count > 0 || dut.rx_inst.rmc_elevation_count > 0,
"G8.2: RX mode controller chirp/elevation counters advanced");
// G8.2: chirp_scheduler chirp counter advanced (chirp-v2 PR-D)
// Sub-frame id replaces "elevation" in the v2 contract.
check(dut.rx_inst.sched_chirp_counter > 0 || dut.rx_inst.sched_subframe_id > 0,
"G8.2: chirp_scheduler chirp/sub-frame counters advanced");
// G8.3: RX-side elevation counter incremented (4 chirps/elev)
check(dut.rx_inst.rmc_elevation_count >= 1,
"G8.3: RX elevation counter incremented in auto-scan");
// G8.3: sub-frame index incremented (auto-scan walks SHORT->MEDIUM->LONG).
// chirps_per_subframe = 16 in PR-D, so a sub-frame transition implies the
// first 16 chirps completed.
check(dut.rx_inst.sched_subframe_id >= 1 || dut.rx_inst.sched_chirp_counter >= 6'd1,
"G8.3: sub-frame counter or chirp counter advanced in auto-scan");
// G8.4: Switch to single-chirp mode — auto-scan stops
bfm_send_cmd(8'h01, 8'h00, 16'h0002); // mode = 10 = single chirp