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.
182 lines
8.2 KiB
Verilog
182 lines
8.2 KiB
Verilog
`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
|