From 58d2e1ba10078b9eff4384d393d43e69623396b1 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:47:31 +0545 Subject: [PATCH] =?UTF-8?q?AUDIT-C11:=20replace=20Gray-CDC=20at=20CIC?= =?UTF-8?q?=E2=86=92FIR=20with=20home-grown=20async=20FIFO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cdc_adc_to_processing carries multi-bit data across 400→100 MHz via TWO independent synchronizer chains (data Gray-encoded + a separate 2-bit toggle). Under metastability, the chains can resolve on different cycles, letting the destination latch a half-resolved Gray word that decodes to an arbitrary value. Audit C-11. Practical MTBF is years per event but the design is non-conformant for arbitrary multi-bit data — Gray code's single-bit-flip protection only holds for ±1 transitions, not for CIC samples that can change by hundreds of LSBs. Replace with cdc_async_fifo, a Cummings SNUG-2002 style #2 async FIFO. Data does NOT cross domains; it sits in dual-clock distRAM (write port src_clk, read port dst_clk). Only the read/write Gray-coded POINTERS cross — and pointers genuinely change ±1 per increment, so Gray code's protection is correct by construction. Home-grown rather than XPM_FIFO_ASYNC: vendor-neutral (iverilog can simulate it directly, no SIM stub), keeps the project's existing home-grown CDC convention (3 sibling primitives in cdc_modules.v), and avoids XPM library version skew. Port shape is preserved (same WIDTH=18, same dst_data/dst_valid/ overrun semantics — 1-cycle pulse per read in steady state) so the swap is local to two instantiations in ddc_400m.v. Sticky-overrun aggregation downstream is unchanged. XDC: project already has blanket set_false_path on clk_100m ↔ adc_dco_p, which covers both new pointer crossings. Synchronizer FFs carry ASYNC_REG="TRUE" for placement-aware MTBF. No XDC change needed. New TB tb_cdc_async_fifo.v exercises 7 groups (28 checks): reset, single-sample passthrough, multi-Gray-bit-flip (0x00000 ↔ 0x3FFFF — audit's recommended coverage point, asserts NO intermediate values appear at dst_data), matched-rate continuous stream, sustained-burst overrun, drain-to-empty, and mid-stream reset. Resource: 8 LUTRAMs per instance × 2 instances = 16 LUTRAMs (~0.05% of XC7A50T budget). Verified: full FPGA regression 42/42 PASS (was 41/41; +1 new test, 0 regressions in DDC Chain / Doppler Co-Sim / Full-Chain Real-Data / Receiver Integration / System Top / System E2E / MF Co-Sim — all of which exercise the swap path through the production signal chain). 0 lint errors. --- 9_Firmware/9_2_FPGA/cdc_async_fifo.v | 181 +++++++++++ 9_Firmware/9_2_FPGA/ddc_400m.v | 15 +- 9_Firmware/9_2_FPGA/run_regression.sh | 9 +- 9_Firmware/9_2_FPGA/tb/tb_cdc_async_fifo.v | 338 +++++++++++++++++++++ 4 files changed, 537 insertions(+), 6 deletions(-) create mode 100644 9_Firmware/9_2_FPGA/cdc_async_fifo.v create mode 100644 9_Firmware/9_2_FPGA/tb/tb_cdc_async_fifo.v diff --git a/9_Firmware/9_2_FPGA/cdc_async_fifo.v b/9_Firmware/9_2_FPGA/cdc_async_fifo.v new file mode 100644 index 0000000..6dbc455 --- /dev/null +++ b/9_Firmware/9_2_FPGA/cdc_async_fifo.v @@ -0,0 +1,181 @@ +`timescale 1ns / 1ps + +// ============================================================================ +// cdc_async_fifo — Cummings-style asynchronous FIFO (SNUG 2002, style #2) +// ============================================================================ +// Replaces cdc_adc_to_processing for multi-bit data crossings where the data +// can change by arbitrary amounts between src_valid events (e.g. CIC samples +// at the 400→100 MHz boundary). The Gray-CDC anti-pattern that +// cdc_adc_to_processing exposes — independent data and toggle synchronizer +// chains that can skew under metastability and let the destination capture a +// half-resolved Gray word — does not apply here, because: +// +// - Data does NOT cross domains; it sits in a dual-clock distRAM +// (write port: src_clk, read port: dst_clk). +// - Only the read/write Gray-coded POINTERS cross between domains. Pointer +// counters genuinely change ±1 per increment, so Gray code's single-bit- +// flip metastability protection holds by construction. +// +// Reference: Clifford Cummings, "Simulation and Synthesis Techniques for +// Asynchronous FIFO Design", SNUG 2002 (style #2, registered empty/full). +// +// Output semantics — drop-in compatible with cdc_adc_to_processing: +// - dst_valid pulses HIGH for one dst_clk cycle per FIFO read. +// - In matched-rate steady state (src_rate ≤ dst_rate) dst_valid is HIGH +// every dst_clk cycle while data is flowing — same level shape that +// cdc_adc_to_processing produced when the toggle changed every cycle. +// - When the FIFO is empty, dst_valid stays LOW. +// +// Overrun semantics: +// - overrun pulses HIGH for one src_clk cycle whenever src_valid arrives +// while the FIFO is full. The write is dropped (no stomp on data already +// in the FIFO). External logic latches/counts as needed (matches the +// audit-F-1.2 sticky-overrun pattern in ddc_400m.v). +// +// XDC timing constraints: +// The project XDC files (xc7a50t_ftg256.xdc, xc7a200t_fbg484.xdc) already +// contain a blanket `set_false_path` between `clk_100m` and `adc_dco_p` +// (the 100/400 MHz domains). This automatically covers both pointer +// crossings here (wptr Gray src→dst and rptr Gray dst→src) — no XDC change +// is needed. The 2-stage synchronizers carry ASYNC_REG="TRUE" so Vivado +// places them in the same slice for MTBF; placement is unaffected by the +// blanket false-path. This matches the project convention already used by +// cdc_adc_to_processing and other CDC primitives in cdc_modules.v. +// +// Resource estimate: distributed-RAM FIFO, depth 16 × width 18 → 8 LUTRAMs +// per instance. Two instances on the CIC→FIR boundary = 16 LUTRAMs (~0.05% +// of XC7A50T LUT budget). +// +// Reset semantics: src and dst sides reset independently (async-reset on +// negedge of each domain's reset_n). The FIFO comes out of reset in the +// EMPTY state from both sides; writes are gated on `~full` so a write +// arriving before the dst side has come out of reset is safely held in the +// FIFO and drained once dst_reset_n deasserts. +// ============================================================================ +module cdc_async_fifo #( + parameter WIDTH = 18, + parameter DEPTH = 16 // must be a power of 2 +)( + input wire src_clk, + input wire dst_clk, + input wire src_reset_n, + input wire dst_reset_n, + input wire [WIDTH-1:0] src_data, + input wire src_valid, + output reg [WIDTH-1:0] dst_data, + output reg dst_valid, + output reg overrun +); + + localparam ADDR_W = $clog2(DEPTH); + + // ---------- Storage (dual-clock distRAM; Vivado infers SLICEM LUTRAM) ---------- + // Note: no reset on `mem` — distRAM has no reset semantics, and forcing one + // would block LUTRAM inference. Reads are gated on `~empty`, so a cell is + // never read before it has been written; X-propagation is impossible in + // sim by construction. The `initial` block zeroes cells purely for + // simulator cleanliness; synthesis honors it as LUTRAM init values. + reg [WIDTH-1:0] mem [0:DEPTH-1]; + + integer init_i; + initial begin + for (init_i = 0; init_i < DEPTH; init_i = init_i + 1) + mem[init_i] = {WIDTH{1'b0}}; + end + + // ---------- Source domain registers ---------- + reg [ADDR_W:0] wptr_bin; // ADDR_W+1 bits: extra MSB enables full detect + reg [ADDR_W:0] wptr_gray; + reg full; + wire [ADDR_W-1:0] waddr = wptr_bin[ADDR_W-1:0]; + + // ---------- Destination domain registers ---------- + reg [ADDR_W:0] rptr_bin; + reg [ADDR_W:0] rptr_gray; + reg empty; + wire [ADDR_W-1:0] raddr = rptr_bin[ADDR_W-1:0]; + + // ---------- CDC: Gray pointer crossings (the only domain-crossing signals) ---------- + (* ASYNC_REG = "TRUE" *) reg [ADDR_W:0] wptr_gray_dst [0:1]; + (* ASYNC_REG = "TRUE" *) reg [ADDR_W:0] rptr_gray_src [0:1]; + + // ---------- Pointer-next combinational ---------- + wire do_write = src_valid & ~full; + wire do_read = ~empty; + wire [ADDR_W:0] wptr_bin_next = wptr_bin + do_write; + wire [ADDR_W:0] wptr_gray_next = wptr_bin_next ^ (wptr_bin_next >> 1); + wire [ADDR_W:0] rptr_bin_next = rptr_bin + do_read; + wire [ADDR_W:0] rptr_gray_next = rptr_bin_next ^ (rptr_bin_next >> 1); + + // ---------- Cummings full/empty conditions (style #2: registered) ---------- + // full: next-write-Gray equals synchronized-read-Gray with the two MSBs + // inverted. This is the canonical "Gray pointer match with MSB twist" + // detection that distinguishes "wrote one full lap and caught up" from + // "wptr == rptr because both are at 0". + wire wfull_val = (wptr_gray_next == + {~rptr_gray_src[1][ADDR_W:ADDR_W-1], + rptr_gray_src[1][ADDR_W-2:0]}); + wire rempty_val = (rptr_gray_next == wptr_gray_dst[1]); + + // ============================================================================ + // SOURCE DOMAIN + // ============================================================================ + always @(posedge src_clk or negedge src_reset_n) begin + if (!src_reset_n) begin + wptr_bin <= {(ADDR_W+1){1'b0}}; + wptr_gray <= {(ADDR_W+1){1'b0}}; + full <= 1'b0; + overrun <= 1'b0; + end else begin + if (do_write) mem[waddr] <= src_data; + wptr_bin <= wptr_bin_next; + wptr_gray <= wptr_gray_next; + full <= wfull_val; + overrun <= src_valid & full; // 1-cycle pulse on dropped write + end + end + + // Synchronize destination read pointer (Gray) into source domain + always @(posedge src_clk or negedge src_reset_n) begin + if (!src_reset_n) begin + rptr_gray_src[0] <= {(ADDR_W+1){1'b0}}; + rptr_gray_src[1] <= {(ADDR_W+1){1'b0}}; + end else begin + rptr_gray_src[0] <= rptr_gray; + rptr_gray_src[1] <= rptr_gray_src[0]; + end + end + + // ============================================================================ + // DESTINATION DOMAIN + // ============================================================================ + always @(posedge dst_clk or negedge dst_reset_n) begin + if (!dst_reset_n) begin + rptr_bin <= {(ADDR_W+1){1'b0}}; + rptr_gray <= {(ADDR_W+1){1'b0}}; + empty <= 1'b1; + dst_data <= {WIDTH{1'b0}}; + dst_valid <= 1'b0; + end else begin + if (do_read) begin + dst_data <= mem[raddr]; // capture the read data + rptr_bin <= rptr_bin_next; + rptr_gray <= rptr_gray_next; + end + empty <= rempty_val; + dst_valid <= do_read; // 1-cycle pulse per read + end + end + + // Synchronize source write pointer (Gray) into destination domain + always @(posedge dst_clk or negedge dst_reset_n) begin + if (!dst_reset_n) begin + wptr_gray_dst[0] <= {(ADDR_W+1){1'b0}}; + wptr_gray_dst[1] <= {(ADDR_W+1){1'b0}}; + end else begin + wptr_gray_dst[0] <= wptr_gray; + wptr_gray_dst[1] <= wptr_gray_dst[0]; + end + end + +endmodule diff --git a/9_Firmware/9_2_FPGA/ddc_400m.v b/9_Firmware/9_2_FPGA/ddc_400m.v index 604de4c..1abbf5d 100644 --- a/9_Firmware/9_2_FPGA/ddc_400m.v +++ b/9_Firmware/9_2_FPGA/ddc_400m.v @@ -643,9 +643,16 @@ wire [17:0] fir_d_in_i, fir_d_in_q; wire cdc_fir_i_overrun; wire cdc_fir_q_overrun; -cdc_adc_to_processing #( +// AUDIT-C11: replaced cdc_adc_to_processing (Gray-CDC anti-pattern: independent +// data and toggle synchronizer chains can skew under metastability and let the +// destination capture a half-resolved Gray word) with cdc_async_fifo (Cummings +// SNUG-2002 design — data sits in dual-clock distRAM; only ±1-incrementing +// Gray-coded read/write pointers cross domains). Port shape is preserved so +// the swap is local to these two instantiations. Sticky-overrun aggregation +// at line ~680 is unchanged. +cdc_async_fifo #( .WIDTH(18), - .STAGES(3) + .DEPTH(16) )CDC_FIR_i( .src_clk(clk_400m), .dst_clk(clk_100m), @@ -658,9 +665,9 @@ cdc_adc_to_processing #( .overrun(cdc_fir_i_overrun) ); -cdc_adc_to_processing #( +cdc_async_fifo #( .WIDTH(18), - .STAGES(3) + .DEPTH(16) )CDC_FIR_q( .src_clk(clk_400m), .dst_clk(clk_100m), diff --git a/9_Firmware/9_2_FPGA/run_regression.sh b/9_Firmware/9_2_FPGA/run_regression.sh index 2c08888..f399506 100755 --- a/9_Firmware/9_2_FPGA/run_regression.sh +++ b/9_Firmware/9_2_FPGA/run_regression.sh @@ -59,6 +59,7 @@ PROD_RTL=( nco_400m_enhanced.v cic_decimator_4x_enhanced.v cdc_modules.v + cdc_async_fifo.v fir_lowpass.v ddc_input_interface.v chirp_memory_loader_param.v @@ -99,7 +100,7 @@ RECEIVER_RTL=( radar_mode_controller.v tb/ad9484_interface_400m_stub.v ddc_400m.v nco_400m_enhanced.v cic_decimator_4x_enhanced.v - cdc_modules.v fir_lowpass.v ddc_input_interface.v + cdc_modules.v cdc_async_fifo.v fir_lowpass.v ddc_input_interface.v chirp_memory_loader_param.v matched_filter_multi_segment.v matched_filter_processing_chain.v range_bin_decimator.v doppler_processor.v xfft_16.v fft_engine.v @@ -585,7 +586,7 @@ echo "--- PHASE 2: Integration Tests ---" run_test "DDC Chain (NCO→CIC→FIR)" \ tb/tb_ddc_reg.vvp \ tb/tb_ddc_cosim.v ddc_400m.v nco_400m_enhanced.v \ - cic_decimator_4x_enhanced.v fir_lowpass.v cdc_modules.v + cic_decimator_4x_enhanced.v fir_lowpass.v cdc_modules.v cdc_async_fifo.v # Real-data co-simulation: committed golden hex vs RTL (exact match required). # These catch architecture mismatches (e.g. 32-pt → dual 16-pt Doppler FFT) @@ -705,6 +706,10 @@ run_test "CDC Modules (3 variants)" \ tb/tb_cdc_reg.vvp \ tb/tb_cdc_modules.v cdc_modules.v +run_test "CDC Async FIFO (AUDIT-C11)" \ + tb/tb_cdc_async_fifo_reg.vvp \ + tb/tb_cdc_async_fifo.v cdc_async_fifo.v + run_test "Edge Detector" \ tb/tb_edge_reg.vvp \ tb/tb_edge_detector.v edge_detector.v diff --git a/9_Firmware/9_2_FPGA/tb/tb_cdc_async_fifo.v b/9_Firmware/9_2_FPGA/tb/tb_cdc_async_fifo.v new file mode 100644 index 0000000..6e630a8 --- /dev/null +++ b/9_Firmware/9_2_FPGA/tb/tb_cdc_async_fifo.v @@ -0,0 +1,338 @@ +`timescale 1ns / 1ps + +// ============================================================================ +// tb_cdc_async_fifo — exercises the home-grown Cummings async FIFO that +// replaces cdc_adc_to_processing on the CIC→FIR (400→100 MHz) crossing. +// +// Coverage objectives (audit C-11 recommendation): +// 1. Reset behaviour on both domains. +// 2. Single sample passthrough (data integrity). +// 3. Continuous stream at matched src/dst rate (steady-state bandwidth). +// 4. Multi-Gray-bit-flip stimulus — alternate 0x00000 ↔ 0x3FFFF: each +// transition flips ALL Gray bits at once, which is exactly the input +// pattern that exposes the cdc_adc_to_processing skew hazard. This +// FIFO must never present an intermediate value at dst_data — only +// the alternating extremes. +// 5. Sustained burst: src_valid every src_clk cycle (4× the dst drain +// rate). FIFO fills, overrun pulses, no data corruption on cells +// already in flight. +// 6. Drain to empty: after src stops, dst_valid pulses exactly N times +// for N writes that fit in the FIFO, then dst_valid stays LOW. +// +// Clock ratio mirrors production: src=400 MHz, dst=100 MHz (4:1). +// ============================================================================ +module tb_cdc_async_fifo; + + localparam SRC_CLK_PERIOD = 2.5; // 400 MHz + localparam DST_CLK_PERIOD = 10.0; // 100 MHz + localparam WIDTH = 18; + localparam DEPTH = 16; + + integer pass_count; + integer fail_count; + integer test_num; + + 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 + + // ── DUT signals ─────────────────────────────────────────────── + reg src_clk; + reg dst_clk; + reg src_reset_n; + reg dst_reset_n; + reg [WIDTH-1:0] src_data; + reg src_valid; + wire [WIDTH-1:0] dst_data; + wire dst_valid; + wire overrun; + + always #(SRC_CLK_PERIOD/2) src_clk = ~src_clk; + always #(DST_CLK_PERIOD/2) dst_clk = ~dst_clk; + + cdc_async_fifo #( + .WIDTH(WIDTH), + .DEPTH(DEPTH) + ) dut ( + .src_clk (src_clk), + .dst_clk (dst_clk), + .src_reset_n (src_reset_n), + .dst_reset_n (dst_reset_n), + .src_data (src_data), + .src_valid (src_valid), + .dst_data (dst_data), + .dst_valid (dst_valid), + .overrun (overrun) + ); + + // ── Captured-output queue (collected on every dst_valid pulse) ──── + reg [WIDTH-1:0] capture_q [0:1023]; + integer capture_n; + always @(posedge dst_clk) begin + if (!dst_reset_n) begin + capture_n <= 0; + end else if (dst_valid) begin + capture_q[capture_n] <= dst_data; + capture_n <= capture_n + 1; + end + end + + // ── Overrun pulse counter (src domain) ──────────────────────────── + integer overrun_count; + always @(posedge src_clk) begin + if (!src_reset_n) overrun_count <= 0; + else if (overrun) overrun_count <= overrun_count + 1; + end + + // ── Helpers ─────────────────────────────────────────────────────── + task drive_sample; + input [WIDTH-1:0] v; + begin + @(posedge src_clk); + src_data <= v; + src_valid <= 1'b1; + @(posedge src_clk); + src_valid <= 1'b0; + end + endtask + + // Drive src_valid HIGH every src_clk cycle for n cycles, with + // a counter pattern as data so we can verify ordering. + task drive_burst; + input integer n; + input [WIDTH-1:0] base; + integer k; + begin + for (k = 0; k < n; k = k + 1) begin + @(posedge src_clk); + src_data <= base + k; + src_valid <= 1'b1; + end + @(posedge src_clk); + src_valid <= 1'b0; + end + endtask + + task wait_dst_cycles; + input integer n; + integer k; + begin + for (k = 0; k < n; k = k + 1) @(posedge dst_clk); + end + endtask + + // ── Main ────────────────────────────────────────────────────────── + integer i, expected_overrun; + integer alt_count, intermediate_count; + + initial begin + $dumpfile("tb_cdc_async_fifo.vcd"); + $dumpvars(0, tb_cdc_async_fifo); + + src_clk = 0; + dst_clk = 0; + src_reset_n = 0; + dst_reset_n = 0; + src_data = {WIDTH{1'b0}}; + src_valid = 1'b0; + pass_count = 0; + fail_count = 0; + test_num = 0; + + // ════════════════════════════════════════════════════════ + // Group 1: Reset + // ════════════════════════════════════════════════════════ + $display("\n=== Group 1: Reset ==="); + #100; + check(dst_valid === 1'b0, "G1.1: dst_valid LOW during reset"); + check(dst_data === {WIDTH{1'b0}}, "G1.2: dst_data 0 during reset"); + check(overrun === 1'b0, "G1.3: overrun LOW during reset"); + + // Release dst first (out-of-order reset deassertion is allowed) + @(posedge dst_clk); dst_reset_n = 1'b1; + wait_dst_cycles(2); + check(dst_valid === 1'b0, "G1.4: dst_valid LOW (src still in reset)"); + + @(posedge src_clk); src_reset_n = 1'b1; + wait_dst_cycles(4); + check(dst_valid === 1'b0, "G1.5: dst_valid LOW after both resets release (FIFO empty)"); + + // ════════════════════════════════════════════════════════ + // Group 2: Single sample passthrough + // ════════════════════════════════════════════════════════ + $display("\n=== Group 2: Single sample passthrough ==="); + capture_n = 0; + drive_sample(18'h12345); + wait_dst_cycles(8); + check(capture_n === 1, "G2.1: exactly 1 sample emitted"); + check(capture_q[0] === 18'h12345, "G2.2: data integrity (0x12345)"); + + // ════════════════════════════════════════════════════════ + // Group 3: Multi-Gray-bit-flip stimulus (audit C-11 coverage) + // Alternate 0x00000 ↔ 0x3FFFF — these two values differ by ALL + // 18 bits in binary, so Gray code also flips many bits between + // them. cdc_adc_to_processing's data/toggle skew failure mode + // would manifest as an intermediate value (anything other than + // these two). The FIFO must NEVER produce an intermediate. + // ════════════════════════════════════════════════════════ + $display("\n=== Group 3: Multi-Gray-bit-flip (0x00000 <-> 0x3FFFF) ==="); + capture_n = 0; + for (i = 0; i < 32; i = i + 1) begin + drive_sample((i & 1) ? 18'h3FFFF : 18'h00000); + // small inter-sample gap to let dst drain so FIFO doesn't fill + wait_dst_cycles(2); + end + wait_dst_cycles(16); + check(capture_n === 32, "G3.1: 32 samples emitted (no drops)"); + + intermediate_count = 0; + alt_count = 0; + for (i = 0; i < capture_n; i = i + 1) begin + if (capture_q[i] === 18'h00000 || capture_q[i] === 18'h3FFFF) begin + alt_count = alt_count + 1; + end else begin + intermediate_count = intermediate_count + 1; + end + end + check(intermediate_count === 0, + "G3.2: NO intermediate values (every sample is 0x00000 or 0x3FFFF)"); + check(alt_count === 32, + "G3.3: all 32 samples are one of the two extremes"); + + // Verify ordering: even index → 0x00000, odd index → 0x3FFFF + // (sampling order matches src_valid order because FIFO is FIFO) + for (i = 0; i < capture_n; i = i + 1) begin + if ((i & 1) ? (capture_q[i] !== 18'h3FFFF) + : (capture_q[i] !== 18'h00000)) begin + $display(" [trace] G3 ordering mismatch at index %0d: 0x%h", i, capture_q[i]); + end + end + // Just one summary check on ordering: + check((capture_q[0] === 18'h00000) && (capture_q[1] === 18'h3FFFF) && + (capture_q[30] === 18'h00000) && (capture_q[31] === 18'h3FFFF), + "G3.4: alternation order preserved across the run"); + + // ════════════════════════════════════════════════════════ + // Group 4: Continuous stream — counter pattern, no overrun + // src writes one sample every 4 src cycles (100 MHz src valid + // rate, matched to dst drain rate) + // ════════════════════════════════════════════════════════ + $display("\n=== Group 4: Matched-rate continuous stream ==="); + capture_n = 0; + overrun_count = 0; + // Drive 64 samples with 3-cycle gap → 100 MHz effective src rate + for (i = 0; i < 64; i = i + 1) begin + @(posedge src_clk); src_data <= i[WIDTH-1:0]; src_valid <= 1'b1; + @(posedge src_clk); src_valid <= 1'b0; + @(posedge src_clk); + @(posedge src_clk); + end + wait_dst_cycles(32); + check(overrun_count === 0, "G4.1: no overrun at matched src/dst rate"); + check(capture_n === 64, "G4.2: all 64 samples passed through"); + check(capture_q[0] === 18'd0, "G4.3: first sample = 0"); + check(capture_q[63] === 18'd63, "G4.4: last sample = 63"); + // Spot-check ordering + check(capture_q[10] === 18'd10 && capture_q[42] === 18'd42, + "G4.5: monotonic counter pattern preserved"); + + // ════════════════════════════════════════════════════════ + // Group 5: Overrun — sustained burst, src_valid every cycle + // FIFO has DEPTH=16, drain is 4× slower than fill, so after + // ~21 src writes the FIFO fills and overrun starts pulsing. + // ════════════════════════════════════════════════════════ + $display("\n=== Group 5: Burst overrun ==="); + capture_n = 0; + overrun_count = 0; + // Wait for dst to fully drain anything pending + wait_dst_cycles(8); + @(posedge src_clk); + + drive_burst(64, 18'h10000); // src_valid HIGH for 64 src cycles + wait_dst_cycles(48); // let FIFO drain + + check(overrun_count > 0, "G5.1: overrun fired during sustained burst"); + // 64 writes; FIFO drains 1 entry per 4 src cycles = 16 drained during burst. + // So writes that succeed = 16 (in FIFO) + 16 (drained) = ~32; drops = 64-32 = ~32. + check(capture_n >= 16 && capture_n <= 48, + "G5.2: capture count in expected range (FIFO depth + drained)"); + // Whatever made it through must be a contiguous prefix-suffix of the + // counter pattern — first sample MUST be 0x10000 (the burst base); + // the FIFO never reorders. + check(capture_q[0] === 18'h10000, "G5.3: first captured sample = burst base"); + + // ════════════════════════════════════════════════════════ + // Group 6: Drain to empty + idle behaviour + // ════════════════════════════════════════════════════════ + $display("\n=== Group 6: Drain + idle ==="); + wait_dst_cycles(64); + capture_n = 0; + overrun_count = 0; + wait_dst_cycles(32); + check(capture_n === 0, "G6.1: no spurious dst_valid while idle"); + check(dst_valid === 1'b0, "G6.2: dst_valid LOW after drain"); + check(overrun_count === 0, "G6.3: no overrun while idle"); + + // Single-shot post-idle write should still work + drive_sample(18'h2AAAA); + wait_dst_cycles(8); + check(capture_n === 1, "G6.4: post-idle single sample emitted"); + check(capture_q[0] === 18'h2AAAA, "G6.5: post-idle data integrity"); + + // ════════════════════════════════════════════════════════ + // Group 7: Reset mid-stream — both pointers must zero, dst_valid LOW + // ════════════════════════════════════════════════════════ + $display("\n=== Group 7: Reset mid-stream ==="); + wait_dst_cycles(8); + // Pre-load a few samples + drive_sample(18'h11111); + drive_sample(18'h22222); + // Assert reset on both before they drain + @(posedge dst_clk); dst_reset_n = 1'b0; + @(posedge src_clk); src_reset_n = 1'b0; + capture_n = 0; + wait_dst_cycles(8); + check(dst_valid === 1'b0, "G7.1: dst_valid LOW under mid-stream reset"); + check(capture_n === 0, "G7.2: no captures during reset"); + + // Release + @(posedge dst_clk); dst_reset_n = 1'b1; + @(posedge src_clk); src_reset_n = 1'b1; + wait_dst_cycles(8); + check(dst_valid === 1'b0, "G7.3: dst_valid LOW after reset release (FIFO empty)"); + + // Post-reset write + drive_sample(18'h3CCCC); + wait_dst_cycles(8); + check(capture_q[0] === 18'h3CCCC, "G7.4: post-reset write succeeds"); + + // ── Final summary ───────────────────────────────────────── + $display("\n============================================"); + $display(" RESULTS: %0d passed / %0d failed / %0d total", + pass_count, fail_count, test_num); + $display("============================================"); + if (fail_count == 0) $display(" STATUS: ALL TESTS PASSED"); + else $display(" STATUS: FAILURES DETECTED"); + + $finish; + end + + // Watchdog + initial begin + #200000; + $display("[FAIL] WATCHDOG: simulation hung"); + $finish; + end + +endmodule