A synthesizable, parameterized sync FIFO using extra-MSB pointers. Copy it into the simulator and drive a small testbench.
module sync_fifo #(
parameter WIDTH = 8,
parameter DEPTH = 16, // must be power of 2
parameter ADDR_W = $clog2(DEPTH)
)(
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [WIDTH-1:0] wdata,
output wire full,
input wire rd_en,
output reg [WIDTH-1:0] rdata,
output wire empty
);
localparam PTR_W = ADDR_W + 1;
reg [WIDTH-1:0] mem [0:DEPTH-1];
reg [PTR_W-1:0] wptr, rptr;
wire wr_fire = wr_en && !full;
wire rd_fire = rd_en && !empty;
assign empty = (wptr == rptr);
assign full = (wptr == {~rptr[PTR_W-1], rptr[PTR_W-2:0]});
always @(posedge clk or negedge rst_n) begin
if (!rst_n) wptr <= {PTR_W{1'b0}};
else if (wr_fire) wptr <= wptr + 1'b1;
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) rptr <= {PTR_W{1'b0}};
else if (rd_fire) rptr <= rptr + 1'b1;
end
always @(posedge clk) begin
if (wr_fire) mem[wptr[ADDR_W-1:0]] <= wdata;
end
always @(posedge clk) begin
if (rd_fire) rdata <= mem[rptr[ADDR_W-1:0]];
end
endmodule
Why wptr[ADDR_W-1:0] for the RAM address: the extra MSB is only a wrap counter. The memory has DEPTH rows.
Why rdata is a register: standard (not FWFT) FIFO โ data appears the cycle after a successful read. FWFT would mux combinational RAM read; that is a different timing contract.
wr_fire โ never on a blocked wr_en.rd_fire.rdata on rd_fire.full in time if you compute full from next pointer โ see trap below.The listing uses full from current pointers. A write that fills the last slot updates wptr on the clock edge; full rises after that edge. That is correct: the write that filled it was legal. The next cycle full=1 blocks another write. Do not also write when full is already 1.
module tb;
reg clk=0, rst_n=0, wr_en=0, rd_en=0;
reg [7:0] wdata=0;
wire [7:0] rdata;
wire full, empty;
always #5 clk = ~clk;
sync_fifo #(.WIDTH(8), .DEPTH(4)) dut (
.clk(clk), .rst_n(rst_n),
.wr_en(wr_en), .wdata(wdata), .full(full),
.rd_en(rd_en), .rdata(rdata), .empty(empty)
);
integer i;
initial begin
#12 rst_n = 1;
// fill
for (i=0;i<4;i=i+1) begin
@(posedge clk); wr_en<=1; wdata<=i+8'hA0;
end
@(posedge clk); wr_en<=0;
if (!full) $display("FAIL expected full");
// drain
for (i=0;i<4;i=i+1) begin
@(posedge clk); rd_en<=1;
end
@(posedge clk); rd_en<=0;
if (!empty) $display("FAIL expected empty");
else $display("ALL_TESTS_PASSED");
$finish;
end
endmodule
The mem[] write is clocked and not reset โ that is what you want for a BRAM/SRAM inference. Resetting every RAM location is expensive and usually not required; only pointers and rdata (if you care) are reset. Some linters flag unread memory after reset: document that occupancy is empty so old RAM data is unreachable.
Split the pointer always blocks so write-enable of RAM is a simple if (wr_fire). Combining RAM and pointers in one block can prevent BRAM inference on FPGA. Async reset on pointers is fine; do not put async reset on the RAM write process.
Keep a SystemVerilog queue in the testbench: q.push_back(wdata) on wr_fire, compare q.pop_front() with rdata on the cycle it becomes valid (one cycle after rd_fire for this RTL). Assert q.size() <= DEPTH always. That golden model does not care whether clocks are sync or async โ only that fires are the commits. Lesson 2.3 adds overflow/underflow language on top of this scoreboard.
Addressing uses the low ADDR_W bits of a wrapping binary pointer. Non-power-of-two depths need a wrap mux or a different pointer coding. The async Gray FIFO later also requires power-of-two depth.
This is a standard FIFO: memory is sampled on rd_fire and appears the following cycle. FWFT would present data combinationally from the read address.
The write that fills DEPTH is legal because full was still 0. After that wr_clk/clk edge, pointers compare as full, so the next cycle blocks wr_fire.
Push DEPTH unique beats, see full, refuse extra writes, pop them in order, see empty, and never see X on rdata after a legal read.