mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-10 23:41:18 +00:00
doppler: gate S_IDLE→S_ACCUMULATE on frame_start_pulse (AUDIT-S3)
Pre-fix S_IDLE had two independent if-branches: one for frame_start_pulse (resets pointers) and one for data_valid (transitions to S_ACCUMULATE). A data_valid arriving before frame_start_pulse would advance the FSM with whatever pointers happened to be live, and the BRAM write block would write the sample into mem_write_addr = (write_chirp_index*RANGE_BINS) + 0. In current operation the race is benign — end-of-S_ACCUMULATE always zeros write_chirp_index/write_range_bin (line 287-288) and the MF pipeline latency (~165 µs) is millions of cycles longer than the frame_start CDC latency (~50 ns), so frame_start always arrives first. But the FSM relies on an undocumented system-level invariant; a future code path that leaves pointers stale on entry to S_IDLE would silently corrupt the first sample. Fix: add a `frame_armed` register set when frame_start_pulse arrives in S_IDLE, cleared on transition to S_ACCUMULATE. Both the FSM transition and the BRAM write block gate on `(frame_start_pulse || frame_armed)`. The OR admits the same-cycle case where both arrive together (write to addr 0 still resolves correctly because both blocks use the same gate). Verification: tb_doppler_frame_start_gate 21/21 PASS, quick regression 32/32 PASS (was 31/31; +1 new test, 0 regressions). tb_doppler_realdata (full FFT pipeline) still passes — gate transparent to normal operation.
This commit is contained in:
@@ -134,6 +134,17 @@ reg frame_buffer_full;
|
|||||||
reg [9:0] chirps_received;
|
reg [9:0] chirps_received;
|
||||||
reg [1:0] chirp_state;
|
reg [1:0] chirp_state;
|
||||||
|
|
||||||
|
// AUDIT-S3 fix: arm-on-frame-start gating. Set when frame_start_pulse arrives
|
||||||
|
// in S_IDLE; cleared when the FSM transitions to S_ACCUMULATE. Prevents stale
|
||||||
|
// data_valid from prior MF pipeline residue from advancing S_IDLE → S_ACCUMULATE
|
||||||
|
// before the new frame is officially started, which would write the first
|
||||||
|
// sample(s) into addr 0 of the previous frame's buffer if write_chirp_index
|
||||||
|
// happened to be non-zero. The pointer-reset invariant (line 287-288 always
|
||||||
|
// zeros pointers at end of S_ACCUMULATE) makes this race benign in current
|
||||||
|
// operation, but the gate makes the FSM robust against future code paths
|
||||||
|
// that might leave pointers stale on entry to S_IDLE.
|
||||||
|
reg frame_armed;
|
||||||
|
|
||||||
// Sub-frame tracking
|
// Sub-frame tracking
|
||||||
reg current_sub_frame; // 0=processing long, 1=processing short
|
reg current_sub_frame; // 0=processing long, 1=processing short
|
||||||
|
|
||||||
@@ -246,6 +257,7 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
range_bin <= 0;
|
range_bin <= 0;
|
||||||
sub_frame <= 0;
|
sub_frame <= 0;
|
||||||
current_sub_frame <= 0;
|
current_sub_frame <= 0;
|
||||||
|
frame_armed <= 0;
|
||||||
end else begin
|
end else begin
|
||||||
doppler_valid <= 0;
|
doppler_valid <= 0;
|
||||||
fft_input_valid <= 0;
|
fft_input_valid <= 0;
|
||||||
@@ -262,14 +274,24 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
write_range_bin <= 0;
|
write_range_bin <= 0;
|
||||||
frame_buffer_full <= 0;
|
frame_buffer_full <= 0;
|
||||||
chirps_received <= 0;
|
chirps_received <= 0;
|
||||||
|
frame_armed <= 1; // AUDIT-S3: arm on frame_start_pulse
|
||||||
end
|
end
|
||||||
|
|
||||||
if (data_valid && !frame_buffer_full) begin
|
// AUDIT-S3 fix: only transition to S_ACCUMULATE when armed,
|
||||||
|
// i.e., when this frame has been officially started by a
|
||||||
|
// frame_start_pulse. Pre-fix code accepted any data_valid in
|
||||||
|
// S_IDLE and could race with a missing/late frame_start_pulse.
|
||||||
|
// (frame_start_pulse || frame_armed) admits the same-cycle case
|
||||||
|
// where both pulse and data_valid arrive together — write to
|
||||||
|
// addr 0 still resolves correctly because the BRAM write block
|
||||||
|
// uses the same gate.
|
||||||
|
if ((frame_start_pulse || frame_armed) && data_valid && !frame_buffer_full) begin
|
||||||
state <= S_ACCUMULATE;
|
state <= S_ACCUMULATE;
|
||||||
write_range_bin <= 1;
|
write_range_bin <= 1;
|
||||||
|
frame_armed <= 0; // disarm; S_ACCUMULATE handles its own pointers
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
S_ACCUMULATE: begin
|
S_ACCUMULATE: begin
|
||||||
if (data_valid) begin
|
if (data_valid) begin
|
||||||
if (write_range_bin < RANGE_BINS - 1) begin
|
if (write_range_bin < RANGE_BINS - 1) begin
|
||||||
@@ -393,14 +415,19 @@ always @(posedge clk) begin
|
|||||||
|
|
||||||
case (state)
|
case (state)
|
||||||
S_IDLE: begin
|
S_IDLE: begin
|
||||||
if (data_valid && !frame_buffer_full) begin
|
// AUDIT-S3 fix: gate BRAM write on frame_armed so stale
|
||||||
|
// data_valid arriving before frame_start_pulse cannot
|
||||||
|
// overwrite addr 0 of the buffer. Same gate as the FSM's
|
||||||
|
// S_IDLE → S_ACCUMULATE transition above, so the two blocks
|
||||||
|
// stay coherent.
|
||||||
|
if ((frame_start_pulse || frame_armed) && data_valid && !frame_buffer_full) begin
|
||||||
mem_we <= 1;
|
mem_we <= 1;
|
||||||
mem_waddr_r <= mem_write_addr;
|
mem_waddr_r <= mem_write_addr;
|
||||||
mem_wdata_i <= range_data[15:0];
|
mem_wdata_i <= range_data[15:0];
|
||||||
mem_wdata_q <= range_data[31:16];
|
mem_wdata_q <= range_data[31:16];
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
S_ACCUMULATE: begin
|
S_ACCUMULATE: begin
|
||||||
if (data_valid) begin
|
if (data_valid) begin
|
||||||
mem_we <= 1;
|
mem_we <= 1;
|
||||||
|
|||||||
@@ -554,6 +554,10 @@ run_test "FT2232H Frame Drop Counter (AUDIT-C12)" \
|
|||||||
tb/tb_ft2232h_frame_drop.vvp \
|
tb/tb_ft2232h_frame_drop.vvp \
|
||||||
tb/tb_ft2232h_frame_drop.v usb_data_interface_ft2232h.v
|
tb/tb_ft2232h_frame_drop.v usb_data_interface_ft2232h.v
|
||||||
|
|
||||||
|
run_test "Doppler Frame-Start Gate (AUDIT-S3)" \
|
||||||
|
tb/tb_doppler_frame_start_gate.vvp \
|
||||||
|
tb/tb_doppler_frame_start_gate.v doppler_processor.v xfft_16.v fft_engine.v
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// tb_doppler_frame_start_gate.v — AUDIT-S3 regression
|
||||||
|
// ============================================================================
|
||||||
|
// Verifies the post-fix gating in doppler_processor_optimized's S_IDLE state.
|
||||||
|
//
|
||||||
|
// AUDIT-S3 (pre-fix bug): S_IDLE had two independent if-branches — one for
|
||||||
|
// frame_start_pulse (resets pointers) and one for data_valid (transitions to
|
||||||
|
// S_ACCUMULATE). A data_valid arriving BEFORE frame_start_pulse would
|
||||||
|
// transition the FSM with whatever pointers happened to be set, and the BRAM
|
||||||
|
// write block would write into addr (write_chirp_index*RANGE_BINS+0).
|
||||||
|
//
|
||||||
|
// Post-fix: a `frame_armed` register is set on frame_start_pulse and cleared
|
||||||
|
// on transition to S_ACCUMULATE. Both the FSM transition and the BRAM write
|
||||||
|
// are gated on `(frame_start_pulse || frame_armed)`. data_valid arriving
|
||||||
|
// before frame_start_pulse is silently dropped at the input.
|
||||||
|
//
|
||||||
|
// Tests:
|
||||||
|
// T1 data_valid alone (no frame_start) → state stays S_IDLE, mem_we=0
|
||||||
|
// T2 frame_start_pulse alone (no data_valid) → state stays S_IDLE,
|
||||||
|
// frame_armed=1, mem_we=0
|
||||||
|
// T3 frame_start, then data_valid 5 cycles later (normal MF latency case)
|
||||||
|
// → state→S_ACCUMULATE on the data_valid cycle, mem_we=1, mem_waddr_r=0
|
||||||
|
// T4 frame_start and data_valid on the SAME cycle
|
||||||
|
// → state→S_ACCUMULATE this cycle, mem_we=1, mem_waddr_r=0
|
||||||
|
// T5 long burst of data_valid in S_IDLE before any frame_start_pulse
|
||||||
|
// → state stays S_IDLE for the entire burst, mem_we never asserts
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
`include "radar_params.vh"
|
||||||
|
|
||||||
|
module tb_doppler_frame_start_gate;
|
||||||
|
|
||||||
|
localparam CLK_PERIOD = 10.0; // 100 MHz
|
||||||
|
|
||||||
|
// Tiny config for fast simulation. We only exercise the S_IDLE gating; the
|
||||||
|
// rest of the FSM (S_ACCUMULATE, FFT pipeline) is not driven to completion.
|
||||||
|
localparam DOPPLER_FFT_SIZE = 16; // unchanged
|
||||||
|
localparam RANGE_BINS = 8;
|
||||||
|
localparam CHIRPS_PER_FRAME = 4;
|
||||||
|
localparam CHIRPS_PER_SUBFRAME = 2;
|
||||||
|
localparam DATA_WIDTH = 16;
|
||||||
|
|
||||||
|
reg clk;
|
||||||
|
reg reset_n;
|
||||||
|
reg [31:0] range_data;
|
||||||
|
reg data_valid;
|
||||||
|
reg new_chirp_frame;
|
||||||
|
|
||||||
|
wire [31:0] doppler_output;
|
||||||
|
wire doppler_valid;
|
||||||
|
wire [4:0] doppler_bin;
|
||||||
|
wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin;
|
||||||
|
wire sub_frame;
|
||||||
|
wire processing_active;
|
||||||
|
wire frame_complete;
|
||||||
|
wire [3:0] dut_status;
|
||||||
|
|
||||||
|
doppler_processor_optimized #(
|
||||||
|
.DOPPLER_FFT_SIZE(DOPPLER_FFT_SIZE),
|
||||||
|
.RANGE_BINS(RANGE_BINS),
|
||||||
|
.CHIRPS_PER_FRAME(CHIRPS_PER_FRAME),
|
||||||
|
.CHIRPS_PER_SUBFRAME(CHIRPS_PER_SUBFRAME),
|
||||||
|
.DATA_WIDTH(DATA_WIDTH)
|
||||||
|
) dut (
|
||||||
|
.clk(clk),
|
||||||
|
.reset_n(reset_n),
|
||||||
|
.range_data(range_data),
|
||||||
|
.data_valid(data_valid),
|
||||||
|
.new_chirp_frame(new_chirp_frame),
|
||||||
|
.doppler_output(doppler_output),
|
||||||
|
.doppler_valid(doppler_valid),
|
||||||
|
.doppler_bin(doppler_bin),
|
||||||
|
.range_bin(range_bin),
|
||||||
|
.sub_frame(sub_frame),
|
||||||
|
.processing_active(processing_active),
|
||||||
|
.frame_complete(frame_complete),
|
||||||
|
.status(dut_status)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Hierarchical refs to internal state
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
wire [2:0] dut_state = dut.state;
|
||||||
|
wire dut_armed = dut.frame_armed;
|
||||||
|
wire dut_mem_we = dut.mem_we;
|
||||||
|
wire [`RP_DOPPLER_MEM_ADDR_W-1:0] dut_mem_waddr = dut.mem_waddr_r;
|
||||||
|
|
||||||
|
localparam [2:0] S_IDLE = 3'b000;
|
||||||
|
localparam [2:0] S_ACCUMULATE = 3'b001;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Clock
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
initial clk = 0;
|
||||||
|
always #(CLK_PERIOD / 2) clk = ~clk;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Test bookkeeping
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
integer pass_count = 0;
|
||||||
|
integer fail_count = 0;
|
||||||
|
integer test_num = 0;
|
||||||
|
|
||||||
|
task check;
|
||||||
|
input cond;
|
||||||
|
input [255:0] label;
|
||||||
|
begin
|
||||||
|
test_num = test_num + 1;
|
||||||
|
if (cond) begin
|
||||||
|
$display("[PASS %0d] %0s", test_num, label);
|
||||||
|
pass_count = pass_count + 1;
|
||||||
|
end else begin
|
||||||
|
$display("[FAIL %0d] %0s (state=%0d armed=%0b mem_we=%0b waddr=%0d)",
|
||||||
|
test_num, label, dut_state, dut_armed, dut_mem_we, dut_mem_waddr);
|
||||||
|
fail_count = fail_count + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task do_reset;
|
||||||
|
begin
|
||||||
|
reset_n = 0;
|
||||||
|
data_valid = 0;
|
||||||
|
new_chirp_frame = 0;
|
||||||
|
range_data = 32'h0;
|
||||||
|
repeat (4) @(posedge clk);
|
||||||
|
#1 reset_n = 1;
|
||||||
|
@(posedge clk); #1;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Stimulus
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
initial begin
|
||||||
|
$dumpfile("tb_doppler_frame_start_gate.vcd");
|
||||||
|
$dumpvars(0, tb_doppler_frame_start_gate);
|
||||||
|
|
||||||
|
$display("\n=== AUDIT-S3 regression: doppler S_IDLE frame_start gating ===\n");
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// T1: data_valid alone (no frame_start) → no transition, no write
|
||||||
|
// ============================================================
|
||||||
|
do_reset;
|
||||||
|
range_data = 32'hAAAA_BBBB;
|
||||||
|
data_valid = 1;
|
||||||
|
@(posedge clk); #1;
|
||||||
|
check(dut_state == S_IDLE, "T1.a: state stays S_IDLE under data_valid w/o frame_start");
|
||||||
|
check(dut_armed == 1'b0, "T1.b: frame_armed remains 0");
|
||||||
|
check(dut_mem_we == 1'b0, "T1.c: mem_we does NOT fire");
|
||||||
|
|
||||||
|
// Hold for 5 more cycles to confirm no late transition / no write
|
||||||
|
repeat (5) @(posedge clk);
|
||||||
|
#1;
|
||||||
|
check(dut_state == S_IDLE, "T1.d: state still S_IDLE after 5 more cycles");
|
||||||
|
check(dut_mem_we == 1'b0, "T1.e: mem_we still 0 after 5 more cycles");
|
||||||
|
|
||||||
|
data_valid = 0;
|
||||||
|
@(posedge clk); #1;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// T2: frame_start_pulse alone, no data_valid → frame_armed=1, no write
|
||||||
|
// ============================================================
|
||||||
|
do_reset;
|
||||||
|
new_chirp_frame = 1; // edge-detector inside DUT will fire one-shot
|
||||||
|
@(posedge clk); #1;
|
||||||
|
new_chirp_frame = 0;
|
||||||
|
@(posedge clk); #1; // frame_start_pulse fires this cycle
|
||||||
|
check(dut_armed == 1'b1, "T2.a: frame_armed set after frame_start_pulse");
|
||||||
|
check(dut_state == S_IDLE, "T2.b: state stays S_IDLE without data_valid");
|
||||||
|
check(dut_mem_we == 1'b0, "T2.c: mem_we does NOT fire (no data_valid)");
|
||||||
|
|
||||||
|
// Wait some cycles — armed should persist
|
||||||
|
repeat (10) @(posedge clk);
|
||||||
|
#1;
|
||||||
|
check(dut_armed == 1'b1, "T2.d: frame_armed persists across idle cycles");
|
||||||
|
check(dut_state == S_IDLE, "T2.e: state still S_IDLE");
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// T3: armed (from T2), then data_valid → transition + write to addr 0
|
||||||
|
// ============================================================
|
||||||
|
range_data = 32'h1234_5678;
|
||||||
|
data_valid = 1;
|
||||||
|
@(posedge clk); #1;
|
||||||
|
check(dut_state == S_ACCUMULATE, "T3.a: state→S_ACCUMULATE on first data_valid after frame_start");
|
||||||
|
check(dut_armed == 1'b0, "T3.b: frame_armed cleared on transition");
|
||||||
|
check(dut_mem_we == 1'b1, "T3.c: mem_we asserted on transition");
|
||||||
|
check(dut_mem_waddr == 0, "T3.d: first sample writes to addr 0");
|
||||||
|
|
||||||
|
data_valid = 0;
|
||||||
|
@(posedge clk); #1;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// T4: same-cycle frame_start_pulse + data_valid → transition + write
|
||||||
|
// ============================================================
|
||||||
|
do_reset;
|
||||||
|
new_chirp_frame = 1;
|
||||||
|
@(posedge clk); #1; // 1st cycle of new_chirp_frame=1
|
||||||
|
new_chirp_frame = 0;
|
||||||
|
range_data = 32'hDEAD_BEEF;
|
||||||
|
data_valid = 1;
|
||||||
|
// On THIS cycle: new_chirp_frame_d1=1 (latched last cycle), new_chirp_frame=0
|
||||||
|
// → frame_start_pulse = 0 (XOR is 1 only on rising edge)
|
||||||
|
// We need to time the data_valid to coincide with the rising-edge cycle.
|
||||||
|
// So actually: drive new_chirp_frame=1 simultaneously with data_valid=1
|
||||||
|
// for ONE cycle, starting from a state where new_chirp_frame_d1=0.
|
||||||
|
@(posedge clk); #1; // give the FSM a cycle to settle (T4 prep)
|
||||||
|
do_reset; // reclean
|
||||||
|
range_data = 32'hDEAD_BEEF;
|
||||||
|
data_valid = 1;
|
||||||
|
new_chirp_frame = 1;
|
||||||
|
@(posedge clk); #1; // first cycle: new_chirp_frame_d1 still 0
|
||||||
|
// → frame_start_pulse fires THIS cycle.
|
||||||
|
// Same cycle as data_valid → transition+write.
|
||||||
|
check(dut_state == S_ACCUMULATE, "T4.a: same-cycle pulse+data → state→S_ACCUMULATE");
|
||||||
|
check(dut_mem_we == 1'b1, "T4.b: same-cycle → mem_we fires");
|
||||||
|
check(dut_mem_waddr == 0, "T4.c: same-cycle → write to addr 0");
|
||||||
|
check(dut_armed == 1'b0, "T4.d: same-cycle → frame_armed disarmed");
|
||||||
|
|
||||||
|
new_chirp_frame = 0;
|
||||||
|
data_valid = 0;
|
||||||
|
@(posedge clk); #1;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// T5: long data_valid burst with no frame_start → no transition, no write
|
||||||
|
// (regression for the audit's specific concern)
|
||||||
|
// ============================================================
|
||||||
|
do_reset;
|
||||||
|
range_data = 32'hCAFE_F00D;
|
||||||
|
data_valid = 1;
|
||||||
|
begin : t5_burst
|
||||||
|
integer i;
|
||||||
|
integer mem_we_count;
|
||||||
|
mem_we_count = 0;
|
||||||
|
for (i = 0; i < 100; i = i + 1) begin
|
||||||
|
@(posedge clk); #1;
|
||||||
|
if (dut_mem_we) mem_we_count = mem_we_count + 1;
|
||||||
|
if (dut_state != S_IDLE) begin
|
||||||
|
$display("[FAIL] T5: state left S_IDLE at cycle %0d (state=%0d)", i, dut_state);
|
||||||
|
fail_count = fail_count + 1;
|
||||||
|
disable t5_burst;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
check(mem_we_count == 0, "T5.a: 100 cycles of data_valid w/o frame_start → 0 BRAM writes");
|
||||||
|
check(dut_state == S_IDLE, "T5.b: state still S_IDLE after 100-cycle burst");
|
||||||
|
check(dut_armed == 1'b0, "T5.c: frame_armed never set");
|
||||||
|
end
|
||||||
|
data_valid = 0;
|
||||||
|
@(posedge clk); #1;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Summary
|
||||||
|
// ============================================================
|
||||||
|
$display("\n=== AUDIT-S3 frame-start gate regression ===");
|
||||||
|
$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("");
|
||||||
|
|
||||||
|
#20 $finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Timeout safety
|
||||||
|
initial begin
|
||||||
|
#500_000;
|
||||||
|
$display("[FAIL] Watchdog timeout — TB hung");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
Reference in New Issue
Block a user