Q007FreeSystemVerilog
How to Build a Synchronous FIFO in SystemVerilog
Question
Design a parameterized synchronous FIFO, then write a small bus-functional model that can reset it, push one item, and pop one item without testbench races. W and DEPTH are positive integers; leave PW and CW at their derived defaults. One caller owns each BFM instance and serializes its reset, push and pop calls.
Implementation scaffold
module sync_fifo #(
parameter int unsigned W = 8,
parameter int unsigned DEPTH = 16,
parameter int unsigned PW = (DEPTH <= 1) ? 1 : $clog2(DEPTH),
parameter int unsigned CW = (DEPTH <= 1) ? 1 : $clog2(DEPTH + 1)
) (
input logic clk,
input logic rst_n,
input logic wr_en,
input logic [W-1:0] wr_data,
output logic full,
input logic rd_en,
output logic [W-1:0] rd_data,
output logic empty
);
logic [W-1:0] mem [0:DEPTH-1];
logic [PW-1:0] wr_ptr_q, rd_ptr_q;
logic [CW-1:0] count_q;
logic rd_fire, wr_fire;
assign empty = (count_q == 0);
assign full = (count_q == DEPTH);
assign rd_fire = rd_en && !empty;
assign wr_fire = wr_en && (!full || rd_fire);
always_ff @(posedge clk or negedge rst_n) begin : fifo_state
// TODO: Implement fifo_state using the supplied state and interface.
end
endmodule
interface fifo_if #(parameter int unsigned W = 8) (input logic clk);
logic rst_n, wr_en, rd_en, full, empty;
logic [W-1:0] wr_data, rd_data;
clocking cb @(posedge clk);
default input #1step output #0;
output rst_n, wr_en, wr_data, rd_en;
input full, empty, rd_data;
endclocking
modport TB (clocking cb);
endinterface
class fifo_bfm #(int unsigned W = 8);
virtual fifo_if #(W).TB vif;
function new(virtual fifo_if #(W).TB vif);
this.vif = vif;
endfunction
task reset();
// TODO: implement this body.
endtask
task push(input logic [W-1:0] data);
// TODO: implement this body.
endtask
task pop(output logic [W-1:0] data);
// TODO: implement this body.
endtask
endclassTrace one case
cycle 0: rst_n=0
cycle 1: rst_n=1, push=1, data=0x2A, pop=0
cycle 2: push=1, data=0x7C, pop=1
cycle 3: push=0, pop=1cycle 0: empty=1, count=0
cycle 1: accept 0x2A, count=1
cycle 2: pop 0x2A and accept 0x7C, count remains 1
cycle 3: pop 0x7C, empty=1, count=0The simultaneous push/pop cycle preserves occupancy, and the race-free BFM samples accepted transfers only on the active clock edge.
Requirements
- Expose full and empty, reject reads while empty, and reject writes while full unless a same-cycle read creates space.
- Define registered read-data semantics and handle simultaneous accepted reads and writes without corrupting occupancy.
- Support non-power-of-two depths with explicit pointer wrap logic.
- Use an interface clocking block so the BFM drives and samples on deterministic scheduler regions.
Short answer
Model occupancy from accepted reads and writes, not raw requests. Permit a write at full only when a read also succeeds, register the popped word, update count from the push/pop combination, and wrap pointers explicitly at DEPTH - 1. In the BFM, use a clocking block and sample registered read data on the following edge.
Why this reasoning works
Define read_accept as rd_en while nonempty. Define write_accept as wr_en when capacity exists, or when a simultaneous accepted read frees one entry. Only these accepted events move pointers. The count increments for write-only, decrements for read-only, and remains constant when both occur, which prevents occupancy drift under concurrent traffic.
Non-power-of-two depths require an explicit comparison against the final legal index before returning a pointer to zero. Because read data is registered, the BFM should issue and hold a request through an acceptance edge, then wait until the register has updated before sampling. Clocking-block skews keep those actions out of DUT scheduling races.
Interview takeaways
- Count accepted operations
- Wrap non-power-of-two pointers
- Sample registered reads later
