PLFM_RADAR/9_Firmware/9_2_FPGA/cdc_async_fifo.v
Jason 58d2e1ba10 AUDIT-C11: replace Gray-CDC at CIC→FIR with home-grown async FIFO
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.
2026-04-30 10:47:31 +05:45

182 lines
8.2 KiB
Verilog
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

`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