The baseline uses an occupancy counter and explicit pointer wrap, so depth need not be a power of two. Output data is registered on an accepted pop, matching the synchronous-read behavior expected by many SRAM and FPGA memory blocks.
Those choices now turn the telemetry contract into RTL: the count generates status, bump_ptr enforces the five-entry address range, and dout_valid_o marks the registered pop result.
The memory array is never reset.
Read and write pointers advance only on accepted operations.
Simultaneous accepted push and pop leave count unchanged.
The conservative full policy rejects a push observed while full, even if a pop occurs on that edge.
dout_valid_o pulses for one cycle after an accepted pop and qualifies dout_o.
// snippet: complete architecture appears in the verification lesson
localparam int AW = (DEPTH <= 1) ? 1 : $clog2(DEPTH);
localparam int CW = $clog2(DEPTH + 1);
function automatic logic [AW-1:0] bump_ptr(input logic [AW-1:0] ptr);
return (ptr == DEPTH-1) ? '0 : ptr + 1'b1;
endfunction
assign push_take = push_i && !full_o;
assign pop_take = pop_i && !empty_o;
assign full_o = (count == DEPTH);
assign empty_o = (count == 0);After a pop, dout_o contains the removed word. Before the first pop, after reset, and in cycles without a pop, its bits alone carry no transaction meaning. dout_valid_o prevents downstream logic from treating a held old value as a new word.
Each slot is one clock cycle. The FIFO holds words A and B before slot 0. pop_i stays high for three slots, and the three pop edges are the page's E0, E1 and E2: E0 ends slot 0, E1 ends slot 1 and E2 ends slot 2. The table lists the values after each edge.
Edge | Pre-edge words | Accepted pop |
|
|
|---|---|---|---|---|
E0 | A, B | yes | 0 | 1 / A |
E1 | B | yes | 1 | 1 / B |
E2 | none | no | 1 | 0 / B (bits held) |
Slot 1: the pop at E0 has landed. dout_valid is 1 and dout shows A. The consumer captures A at E1, using the pre-edge valid and data.
Slot 2: the pop at E1 has landed and emptied the FIFO. empty is 1, yet dout_valid is still 1 and dout shows B. Consecutive pops replace the result each cycle, so valid may stay high. pop_i is still high, but E2 rejects it because the FIFO is empty. The consumer captures B at E2.
Slot 3: dout_valid drops to 0 while dout holds B. Those held bits mean nothing; only valid marks a new word.
This is a pop-result pulse interface with no ready input. Reserve capacity before requesting a pop; capture each result even when empty_o is also high. Never gate result-valid with !empty_o: that discards the final word. A stallable consumer needs a result buffer and credits for both buffered words and accepted pops whose results have not yet been captured. Issue a pop only when buffered + pending < result_capacity (or account explicitly for a same-edge delivery). Its valid/ready offer must then remain stable through the accepting edge.
Read the picture from the top: a pop is issued only when the buffer can still take its result. The dashed arrows are the two counts the check adds up.
The FIFO cannot wait. Its result arrives one cycle after the pop, with no ready input, so something must catch it.
The buffer can wait. It holds the word and offers it to the consumer with valid and ready for as long as the consumer stalls.
The credit check counts both. Words already in the buffer and pops whose results have not yet landed both use buffer space.
A generic RTL FIFO cannot guarantee that every ASIC or FPGA tool maps the same array to the desired memory macro. The coding template expresses one read and one write per clock, but the target library's read-during-write mode and port shape still need a synthesis report check.
DEPTH-1 instead of relying on bit overflow?It makes arbitrary depths safe. A 10-entry FIFO wraps 9 to 0 rather than walking through invalid addresses 10 through 15.
dout_o with dout_valid_o?Because dout_o holds its previous bits when no pop occurs. Valid marks the cycle containing a newly removed FIFO word.