Skip to article

Designs and Projects

Synchronous FIFO in Verilog

Build and self-check a four-entry single-clock FIFO with registered reads, full and empty protection, simultaneous read/write behavior, and explicit memory-mapping limits.

5 min read · Updated


A synchronous FIFO is a first-in, first-out queue whose read and write control use the same clock. It stores words in an array, advances separate read and write pointers only when an operation is accepted, and uses occupancy state to prevent reading an empty queue or writing a full one.

Prerequisites: clocked registers, non-blocking assignments, arrays of registers, pointer wraparound, equality comparison, and a self-checking testbench. This article is intentionally single-clock. It is not an asynchronous FIFO and it is not a clock-domain-crossing solution.

A four-word storage array between write-data and registered read-data paths, with separate read and write pointers plus a count block generating full and empty
The FIFO accepts operations through full and empty guards, advances pointers on accepted transfers, and tracks occupancy from zero to four.

Exact behavior of this example

Ambiguity at the boundaries is where FIFO examples become dangerous, so this design states its policy before showing code:

  • A write is accepted only when full == 0.
  • A read is accepted only when empty == 0.
  • A simultaneous accepted read and write keeps count unchanged.
  • A read returns data through the registered data_out on the accepting rising edge.
  • A write requested while full is rejected even if a read is requested on that edge.
  • A read requested while empty is rejected even if a write is requested on that edge.

Those last two conservative rules avoid same-address read/write collision semantics at the full and empty boundaries. More permissive production FIFOs can support replacement-on-full or first-word fall-through, but only with an explicit, verified contract.

Predict before running

The test writes A1 B2 C3 D4 and rejects a write-only E5 while full. It then requests read and write together while still full. Before reading the code, complete this state trace using the boundary policy above. “Returned” means the registered data_out immediately after the edge.

Situation Count before Write request Read request Accept write? Accept read? Returned Count after
Full boundary 4 E5 yes ? ? ? ?
Next edge 3 E5 yes ? ? ? ?
Empty boundary 0 F6 yes ? ? ? ?
Recovery 1 no yes ? ? ? ?

Finally, predict the drain order after the first two rows.

`timescale 1ns/1ps

module sync_fifo4 (
  input  wire       clk,
  input  wire       rst,
  input  wire       write_en,
  input  wire       read_en,
  input  wire [7:0] data_in,
  output reg  [7:0] data_out,
  output wire       full,
  output wire       empty,
  output reg  [2:0] count
);
  reg [7:0] memory [0:3];
  reg [1:0] write_ptr;
  reg [1:0] read_ptr;

  wire accept_write = write_en && !full;
  wire accept_read  = read_en && !empty;

  assign full  = (count == 3'd4);
  assign empty = (count == 3'd0);

  always @(posedge clk) begin
    if (rst) begin
      write_ptr <= 2'd0;
      read_ptr  <= 2'd0;
      count     <= 3'd0;
      data_out  <= 8'h00;
    end else begin
      if (accept_write) begin
        memory[write_ptr] <= data_in;
        write_ptr <= write_ptr + 2'd1;
      end

      if (accept_read) begin
        data_out <= memory[read_ptr];
        read_ptr <= read_ptr + 2'd1;
      end

      case ({accept_write, accept_read})
        2'b10: count <= count + 3'd1;
        2'b01: count <= count - 3'd1;
        default: count <= count;
      endcase
    end
  end
endmodule

module tb;
  reg clk, rst, write_en, read_en;
  reg [7:0] data_in;
  wire [7:0] data_out;
  wire full, empty;
  wire [2:0] count;

  sync_fifo4 dut (
    .clk(clk), .rst(rst), .write_en(write_en), .read_en(read_en),
    .data_in(data_in), .data_out(data_out), .full(full), .empty(empty), .count(count)
  );

  always #5 clk = ~clk;

  task transfer;
    input do_write;
    input do_read;
    input [7:0] value;
    begin
      @(negedge clk);
      write_en = do_write;
      read_en = do_read;
      data_in = value;
      @(posedge clk); #1;
    end
  endtask

  task require_read;
    input [7:0] expected;
    begin
      if (data_out !== expected) begin
        $display("FAIL READ expected=%02h actual=%02h", expected, data_out);
        $finish;
      end
    end
  endtask

  initial begin
    clk = 1'b0;
    rst = 1'b1;
    write_en = 1'b0;
    read_en = 1'b0;
    data_in = 8'h00;

    repeat (2) @(posedge clk);
    #1;
    $display("RESET empty=%0b full=%0b count=%0d", empty, full, count);
    if (!empty || full || count !== 3'd0) begin $display("FAIL RESET"); $finish; end
    @(negedge clk);
    rst = 1'b0;

    transfer(1'b1, 1'b0, 8'hA1);
    transfer(1'b1, 1'b0, 8'hB2);
    transfer(1'b1, 1'b0, 8'hC3);
    transfer(1'b1, 1'b0, 8'hD4);
    $display("FULL count=%0d empty=%0b full=%0b", count, empty, full);
    if (!full || empty || count !== 3'd4) begin $display("FAIL FULL"); $finish; end

    transfer(1'b1, 1'b0, 8'hE5);
    $display("OVERFLOW_BLOCKED count=%0d", count);
    if (count !== 3'd4) begin $display("FAIL OVERFLOW"); $finish; end

    transfer(1'b1, 1'b1, 8'hE5);
    require_read(8'hA1);
    $display("FULL_BOTH read=%02h write_blocked count=%0d", data_out, count);
    if (count !== 3'd3) begin $display("FAIL FULL BOUNDARY"); $finish; end

    transfer(1'b1, 1'b1, 8'hE5);
    require_read(8'hB2);
    $display("SIMULTANEOUS read=%02h wrote=e5 count=%0d", data_out, count);
    if (count !== 3'd3) begin $display("FAIL SIMULTANEOUS"); $finish; end

    transfer(1'b0, 1'b1, 8'h00);
    require_read(8'hC3);
    $display("DRAIN data=%02h", data_out);
    transfer(1'b0, 1'b1, 8'h00);
    require_read(8'hD4);
    $display("DRAIN data=%02h", data_out);
    transfer(1'b0, 1'b1, 8'h00);
    require_read(8'hE5);
    $display("DRAIN data=%02h count=%0d empty=%0b", data_out, count, empty);

    transfer(1'b1, 1'b1, 8'hF6);
    require_read(8'hE5);
    $display("EMPTY_BOTH read_blocked wrote=f6 count=%0d", count);
    if (empty || full || count !== 3'd1) begin $display("FAIL EMPTY BOUNDARY"); $finish; end

    transfer(1'b0, 1'b1, 8'h00);
    require_read(8'hF6);
    $display("RECOVER data=%02h count=%0d empty=%0b", data_out, count, empty);

    transfer(1'b0, 1'b1, 8'h00);
    require_read(8'hF6);
    $display("UNDERFLOW_BLOCKED data=%02h count=%0d", data_out, count);
    if (!empty || count !== 3'd0) begin $display("FAIL UNDERFLOW"); $finish; end

    $display("PASS");
    $finish;
  end
endmodule
Expected output — reveal after you predict
RESET empty=1 full=0 count=0
FULL count=4 empty=0 full=1
OVERFLOW_BLOCKED count=4
FULL_BOTH read=a1 write_blocked count=3
SIMULTANEOUS read=b2 wrote=e5 count=3
DRAIN data=c3
DRAIN data=d4
DRAIN data=e5 count=0 empty=1
EMPTY_BOTH read_blocked wrote=f6 count=1
RECOVER data=f6 count=0 empty=1
UNDERFLOW_BLOCKED data=f6 count=0
PASS

Compare your boundary trace

Situation Count before Accept write? Accept read? Returned Count after
Full boundary 4 no yes A1 3
Next edge 3 yes (E5) yes B2 3
Empty boundary 0 yes (F6) no unchanged (E5) 1
Recovery 1 no yes F6 0

The final drain order is C3 D4 E5. Pointer values wrap because they are two bits wide; occupancy does not come from comparing the pointers alone, because equal pointers can mean either empty or full. The separate 3-bit count disambiguates those states.

Hardware it describes

The RTL describes a four-word storage array, two 2-bit pointer registers, a 3-bit occupancy register, increment/decrement logic, equality comparisons for full and empty, control muxing, and a separate data_out register. Conservative Yosys prep recognizes the array as a generic memory cell; the simulation proves that this RTL updates data_out only on an accepted rising-edge read. Those are deliberately separate claims: the test does not inspect or claim a clocked read-port mode for a target memory primitive.

Simulation, synthesis, and implementation are different claims

  • Simulation: checks reset, fill order, write-only full protection, simultaneous read/write at full, an accepted simultaneous read/write away from a boundary, complete FIFO order, simultaneous read/write at empty, read-only empty protection, and registered output behavior.
  • Generic synthesis: conservative Yosys prep requires a generic memory cell plus sequential/control logic, and a separate complete synth script must finish. This proves recognized synthesizable storage and a completed generic flow—not a specific memory read-port mode or block-RAM inference on a device.
  • Implementation: a vendor tool may map this tiny array to flip-flops, LUT/distributed RAM, block RAM, or other resources depending on size, read mode, target, constraints, and tool settings. Same-address collision and reset support also vary by primitive and mapping flow.

Limitations

  • Single clock only. Do not use this design to cross clock domains.
  • Fixed width and depth. A reusable parameterized FIFO needs power-of-two checks, derived pointer widths, and wider verification.
  • Conservative boundary policy: no write-through while full and no fall-through while empty.
  • No almost_full, almost_empty, packet boundaries, error flag, flush, backpressure protocol, or asynchronous reset.
  • The testbench is strong for this four-entry contract but is not formal proof and does not explore arbitrary long sequences.

Sources and verification

Example provenance: SkillLift Labs authored the FIFO, boundary policy, self-checking testbench, diagram, and timing explanation. tests/scripts/tutorial-verified-examples.test.ts compiles and simulates the complete example, requires exact published output, checks an executable queue model including both simultaneous boundary cases, and reruns through the real article-to-playground source split. tests/scripts/tutorial-tut8-synthesis.test.ts also compiles with warnings, inspects a conservative Yosys prep netlist for generic memory plus sequential/control cells, and separately requires complete generic synth. No asynchronous behavior, metastability, target memory read mode, block-RAM mapping, board result, or physical timing was tested or claimed.