Q036FreeSystemVerilog
Model a CDC-safe asynchronous FIFO
Question
Adapt the FIFO pointer model to independent write and read clock domains. Model safe pointer crossing and compute full and empty without a shared unsynchronized occupancy counter. Use one clock_tick call for all local edges at a timestamp, with write_edge/read_edge marking the domains that tick. When both edges coincide, each first synchronizer stage samples the opposite domain's pre-edge registered Gray pointer; neither may see that timestamp's newly advanced pointer. The supplied write_tick/read_tick wrappers are only for timestamps with a single local edge. Acceptance uses pre-edge synchronized flags, and construction resets the model; in-run reset is outside this interface. This is an ideal sampled digital pointer model, not a metastability simulation. Physical RTL integration separately requires registered Gray sources, destination synchronizers, suitable Gray-bus skew/delay constraints and safe local reset release.
Implementation scaffold
class AsyncFifoPointerModel #(int DEPTH = 16);
localparam int ADDR_W = $clog2(DEPTH);
typedef logic [ADDR_W:0] ptr_t;
local ptr_t write_binary;
local ptr_t write_gray;
local ptr_t read_binary;
local ptr_t read_gray;
local ptr_t read_gray_sync1_write;
local ptr_t read_gray_sync2_write;
local ptr_t write_gray_sync1_read;
local ptr_t write_gray_sync2_read;
function new();
if (DEPTH < 2 || (DEPTH & (DEPTH - 1)) != 0)
$fatal(1, "asynchronous FIFO depth must be a power of two >= 2");
write_binary = '0;
write_gray = '0;
read_binary = '0;
read_gray = '0;
read_gray_sync1_write = '0;
read_gray_sync2_write = '0;
write_gray_sync1_read = '0;
write_gray_sync2_read = '0;
endfunction
function automatic ptr_t bin_to_gray(ptr_t binary);
// Implement here: bin_to_gray.
endfunction
function bit read_empty();
// Implement here: read_empty.
endfunction
function bit write_full();
// Implement here: write_full.
endfunction
// One call per timestamp; snapshot both remote pointers before either update.
function void clock_tick(bit write_edge, bit push,
bit read_edge, bit pop,
output bit did_write, output bit did_read);
// Implement here: clock_tick.
endfunction
// Supplied convenience wrappers: use only when this is the sole edge at a timestamp.
function bit write_tick(bit push);
bit did_write, did_read;
clock_tick(1, push, 0, 0, did_write, did_read);
return did_write;
endfunction
function bit read_tick(bit pop);
bit did_write, did_read;
clock_tick(0, 0, 1, pop, did_write, did_read);
return did_read;
endfunction
endclassTrace one case
DEPTH=4; write_tick(push=1) twice; read_tick(pop=0) twice; then read_tick(pop=1) twicewrite pointer advances twice; read_empty stays asserted until the two read-domain synchronizer ticks; the two legal pops then advance the read pointer twiceThis model checks only pointer and flag behavior: read_empty changes after synchronized Gray write-pointer progress, without making claims about payload data.
Requirements
- Use a power-of-two depth of at least two.
- Maintain an address-width-plus-one binary pointer and corresponding Gray pointer in each local domain.
- Pass each Gray pointer through two modeled synchronizer stages in the opposite domain.
- Compute empty in the read domain from the synchronized write pointer.
- Compute full in the write domain by comparing the write pointer with the synchronized read pointer at the opposite wrap phase.
- Update write-side state only from write-clock ticks and read-side state only from read-clock ticks.
- Do not maintain a shared count across domains.
