Skip to content
VLSI Mentor

UART · Module 10

TX and RX FIFO Architecture

A UART FIFO is an asynchronous FIFO only when its two sides genuinely sit in different clock domains — which, for a receiver whose input was already synchronised, is usually not the case.

Chapter 10.1 treated the FIFO as a box with a depth. This chapter opens it, and it is the module's main RTL chapter: everything from 10.3 onward is written against the contract fixed here.

It starts with a question that sounds like it has an obvious answer and does not:

Does a UART FIFO need to be an asynchronous FIFO?

The word asynchronous appears in the protocol's name, the receive pin is genuinely asynchronous, and asynchronous FIFOs are a well-known structure — so the reflex is yes. §2 shows that for the architecture this curriculum has built, the answer is no, and that building one anyway imports Gray-code pointers, synchroniser chains and CDC review into a design with a single clock.

1. What the FIFO Must Store

Two instances, opposite roles, and the same storage primitive:

RX FIFOTX FIFO
Producerthe receive engine (Module 6)software, a bus master or DMA
Consumersoftware, a bus master or DMAthe transmit engine (Module 7)
Push ratefixed by the line — one per T_framebursty, whenever the producer runs
Pop rateburstyfixed by the line
Entry contentsdata + per-character status — §8data only

The producer and consumer swap sides; the queue does not change. One parameterised module serves both, and building two is duplicated verification for no benefit. The only asymmetry is the entry width, which §8 develops.

2. The Clocking Question

3. State, and Why Occupancy Is Explicit

The minimum state is a memory array, a write pointer and a read pointer. That is not quite enough, and the reason is worth seeing:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wr_ptr == rd_ptr   means the queue is EMPTY
wr_ptr == rd_ptr   means the queue is FULL

Both are true. After DEPTH pushes the write pointer has wrapped all the way round and equals the read pointer again, which is the identical condition as never having pushed at all. Pointer equality cannot distinguish the two.

The classical resolutions are an extra pointer bit, or a separate occupancy counter. This design keeps an explicit occupancy counter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
empty = (level == 0)          full = (level == DEPTH)

It costs a counter and buys three things. The full/empty ambiguity disappears; level is directly available to the thresholds of Chapter 10.3 and the flow control of Chapter 10.5, which both need occupancy rather than flags; and arbitrary depths work without the extra-bit trick, which assumes a power-of-two wrap.

The extra-pointer-bit form is equally valid and cheaper by a few flip-flops. It is the wrong choice here only because two later chapters need level anyway, so the counter would have to exist regardless.

4. The Contract

Fixed here, used unchanged by every later chapter of this module.

Clock domainone. clk for both sides.
push_i / pop_irequests, not guarantees
pop_firepop_i && !empty_o
push_firepush_i && (!full_o || pop_fire) — §6
pop_data_othe combinational head, valid when !empty_o, undefined when empty. Show-ahead / first-word fall-through. pop_i advances past it.
empty_olevel_o == 0
full_olevel_o == DEPTH
overflow_evt_oone cycle: a push was requested and not accepted
underflow_evt_oone cycle: a pop was requested and not accepted
Resetpointers, occupancy and events cleared. Memory is not reset — §9
Wrapexplicit comparison against DEPTH-1, so any DEPTH ≥ 1 works

The read-data contract is the one most often left ambiguous, and both forms are common. This design is show-ahead: the head is continuously visible, so the port composes directly with the valid/ready handshakes of Chapter 6.5 and Chapter 7.4 — !empty_o is a valid, pop_i is a ready. A registered-output FIFO, where pop_i is a strobe and data appears a cycle later, needs a skid buffer to present the same interface. Neither is wrong; publishing RTL without saying which is.

5. The RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Synthesizable SystemVerilog — the canonical FIFO of §4.

module uart_sync_fifo #(
    parameter int unsigned WIDTH = 8,
    parameter int unsigned DEPTH = 16
) (
    input  logic             clk,
    input  logic             rst_n,

    input  logic             push_i,
    input  logic [WIDTH-1:0] push_data_i,

    input  logic             pop_i,
    output logic [WIDTH-1:0] pop_data_o,

    output logic             empty_o,
    output logic             full_o,
    output logic [$clog2(DEPTH+1)-1:0] level_o,

    output logic             overflow_evt_o,
    output logic             underflow_evt_o
);
    // Pointers span 0 .. DEPTH-1, so they need $clog2(DEPTH) bits, guarded
    // because $clog2(1) is 0 and a zero-width vector is illegal.
    localparam int unsigned PTR_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH);
    // Occupancy spans 0 .. DEPTH inclusive, so it must REPRESENT DEPTH — one
    // more value than the pointers. Hence DEPTH+1 rather than DEPTH.
    localparam int unsigned CNT_W = $clog2(DEPTH + 1);

    initial begin
        if (DEPTH < 1) $fatal(1, "uart_sync_fifo: DEPTH = %0d must be >= 1", DEPTH);
        if (WIDTH < 1) $fatal(1, "uart_sync_fifo: WIDTH = %0d must be >= 1", WIDTH);
    end

    logic [WIDTH-1:0] mem [DEPTH];
    logic [PTR_W-1:0] wr_ptr_q, rd_ptr_q;
    logic [CNT_W-1:0] level_q;

    logic push_fire, pop_fire;

    assign empty_o = (level_q == '0);
    assign full_o  = (level_q == CNT_W'(DEPTH));
    assign level_o = level_q;

    assign pop_fire  = pop_i  && !empty_o;
    // A push into a full FIFO is accepted when a pop frees an entry on the
    // same cycle. Chapter 10.2 §5 argues this against the simpler
    // `push_i && !full_o`, which needlessly drops a byte at the boundary.
    assign push_fire = push_i && (!full_o || pop_fire);

    // Show-ahead head. Combinational read of the memory array.
    assign pop_data_o = mem[rd_ptr_q];

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_ptr_q        <= '0;
            rd_ptr_q        <= '0;
            level_q         <= '0;
            overflow_evt_o  <= 1'b0;
            underflow_evt_o <= 1'b0;
        end else begin
            overflow_evt_o  <= push_i && !push_fire;
            underflow_evt_o <= pop_i  && !pop_fire;

            if (push_fire) begin
                mem[wr_ptr_q] <= push_data_i;
                // EXPLICIT wrap. `wr_ptr_q + 1` relies on truncation, which
                // is correct only when DEPTH is a power of two — §4.
                wr_ptr_q <= (wr_ptr_q == PTR_W'(DEPTH - 1)) ? '0 : wr_ptr_q + 1'b1;
            end

            if (pop_fire) begin
                rd_ptr_q <= (rd_ptr_q == PTR_W'(DEPTH - 1)) ? '0 : rd_ptr_q + 1'b1;
            end

            // ONE explicit four-way update. Two separate conditional
            // increments would let the later assignment silently win — §4.
            case ({push_fire, pop_fire})
                2'b10:   level_q <= level_q + 1'b1;
                2'b01:   level_q <= level_q - 1'b1;
                default: level_q <= level_q;      // 00 and 11 both hold
            endcase
        end
    end
endmodule

6. The Four Decisions Inside That Code

Widths are derived, and the two are different

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam int unsigned PTR_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH);
localparam int unsigned CNT_W = $clog2(DEPTH + 1);

Pointers span 0 … DEPTH−1, so $clog2(DEPTH) bits — with the guard, because $clog2(1) is 0 and a zero-width vector is illegal.

Occupancy spans 0 … DEPTH inclusive, so it must represent DEPTH — one more value than the pointers. At DEPTH = 16 that is 5 bits against the pointers' 4. Writing $clog2(DEPTH) for the counter gives 4 bits, which cannot hold 16, so full_o is never asserted and the FIFO silently overwrites itself.

This is the same +1 reasoning Chapter 9.4 §4 needed for the break threshold, and it is worth recognising as a pattern: a counter that must reach a limit needs one more value than a counter that must count up to it.

Wrap is explicit, not inherited from truncation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wr_ptr_q <= (wr_ptr_q == PTR_W'(DEPTH - 1)) ? '0 : wr_ptr_q + 1'b1;

The tempting form is wr_ptr_q <= wr_ptr_q + 1'b1, relying on the register wrapping at 2^PTR_W. That equals DEPTH only when DEPTH is a power of two. At DEPTH = 5 the pointer is 3 bits and wraps at 8, so the queue silently uses eight slots of a five-slot memory — reading entries that were never written and losing entries that were.

Verified: the suite runs DEPTH = 1, 2, 3, 5, 10 and 16, and the non-power-of-two cases are where this would fail.

Occupancy updates once, in one place

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
case ({push_fire, pop_fire})
    2'b10:   level_q <= level_q + 1'b1;
    2'b01:   level_q <= level_q - 1'b1;
    default: level_q <= level_q;      // 00 and 11 both hold
endcase

The natural-looking alternative is two independent conditionals:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — on a simultaneous push and pop the later assignment wins and the
// level decrements. The FIFO then slowly empties itself under balanced load.
if (push_fire) level_q <= level_q + 1'b1;
if (pop_fire)  level_q <= level_q - 1'b1;

Both are non-blocking assignments to the same register, so the second silently overrides the first. The defect only appears when push and pop coincide, which a directed fill-then-drain test never produces — it needs randomised concurrent traffic, and the suite in §10 recorded 621 simultaneous transactions at DEPTH = 16 alone.

Push into a full FIFO is accepted when a pop frees a slot

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign pop_fire  = pop_i  && !empty_o;
assign push_fire = push_i && (!full_o || pop_fire);

The simpler push_i && !full_o rejects a push whenever the FIFO is currently full, even if a pop is removing an entry on the same edge. The slot is free by the end of the cycle, and the byte was dropped for nothing.

7. Simultaneous Push and Pop, Enumerated

The full truth table, because Chapter 10.4 reasons about the boundary rows:

statepush_ipop_ipush_firepop_firelevel nextevents
any0000unchanged—
not full1010+1—
not empty0101−1—
mid-range1111unchanged—
full1000unchangedoverflow
full1111unchanged—
empty0100unchangedunderflow
empty1110+1underflow

The last row is worth reading twice. An empty FIFO with a simultaneous push and pop accepts the push and rejects the pop — there is nothing to read this cycle, and the arriving byte is not forwarded combinationally to the read port. This design is show-ahead but not bypass: a byte pushed into an empty FIFO is visible on pop_data_o from the next cycle, not the same one.

That is a deliberate boundary. Adding same-cycle bypass would make pop_data_o depend combinationally on push_data_i, lengthening the path from the producer straight through to the consumer, and it buys one cycle of latency in a design where the producer is a UART delivering a byte every thousands of cycles.

8. The RX Entry Carries Its Status

Chapter 6.4 established per-character status and its publish discipline; Chapter 9.6 §6 established that the per-character form is the primitive and sticky status is derived from it. A FIFO is where that distinction becomes concrete.

If the RX FIFO stores only data, the association is destroyed. A byte and the flags describing it arrive together at the receiver's output; if the queue holds the byte for several frame times while the flags are a global sticky register, the consumer reading byte n sees status that may belong to byte n+3. Everything Chapter 6.4 built to keep status attached to its frame is undone by the buffer.

So the entry is wider than the data:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Conceptual SystemVerilog — the RX FIFO's entry layout.
// One parameterised FIFO serves both directions; only the width differs.
localparam int unsigned RX_STATUS_W = 2;                    // parity, framing
localparam int unsigned RX_ENTRY_W  = DATA_W + RX_STATUS_W;

// push side, from the receiver of Module 6:
assign rx_push_data = {rx_parity_err_o, rx_frame_err_o, rx_data_o};

// pop side, to the consumer:
assign {consumer_parity_err, consumer_frame_err, consumer_data} = rx_pop_data;

Overrun is not in the entry, and the asymmetry is instructive: a parity or framing error describes a byte that exists, so it travels with that byte. An overrun describes a byte that does not exist — one that was dropped — so there is no entry to attach it to. Chapter 10.4 develops what that means for reporting it.

The TX entry is data only. There is no status to carry: the producer knows what it wrote, and the transmit engine has nothing to report about a byte it has not yet sent.

The register interface through which software reads a byte and its flags together is Module 13's. What this chapter fixes is that the information is available to be read atomically, which a data-only FIFO would have made impossible.

Two instances of the same parameterised synchronous FIFO within a UART. On the receive path, the receive engine is the producer: each completed character, together with its parity error and framing error flags, is pushed as a single wider entry, and software or a bus master pops entries from the other end. On the transmit path the roles are reversed: software or a bus master pushes data bytes, and the transmit engine pops them as it becomes ready to send each frame. Both instances are the same module with different parameters, differing only in entry width, because the receive entry must carry status alongside the data while the transmit entry does not.RX engineModule 6RX FIFODATA_W + 2 wideconsumersoftware / DMAbyte + flagsassociation keptproducersoftware / DMATX FIFODATA_W wideTX engineModule 7tx_oone frame at a timepushpopreadspushpopserialise12
Figure 1 — the two instances. The same parameterised queue appears twice with its producer and consumer exchanged, and the only structural difference is the entry width: the receive entry carries the per-character status alongside the byte so the association survives buffering, while the transmit entry carries data alone.

9. Reset and Memory Inference

Reset clears the pointers, the occupancy and the event outputs. It does not clear the memory.

The memory does not need clearing because its contents are unreadable while the queue is empty — pop_data_o is only meaningful when !empty_o, and after reset level_q is zero. Resetting the array would be functionally harmless and is deliberately avoided for a different reason.

Reset style affects memory inference. Most FPGA block RAMs have no reset on their storage array, so a FIFO whose memory is written inside a reset branch cannot map to block RAM and is implemented in registers or distributed RAM instead. At 16 × 10 bits that is fine; at 1,024 × 10 bits it is a large and unnecessary cost.

What depends on tool and target, and should not be promised: whether any particular depth maps to block RAM, how the read port is implemented, and whether a show-ahead read infers a distributed-RAM read or a registered one with bypass. Those vary by vendor, by device family and by the synthesis options in use. Module 12 covers implementation behaviour properly; what belongs here is the coding consequence — do not reset the array — and the reason for it.

10. The Same FIFO in Three Languages

§5's listing is SystemVerilog. The contract is not, and every decision in §6 survives translation — including the two that are easiest to get wrong, the explicit pointer wrap and the single four-way occupancy update.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_sync_fifo_v — Synthesizable Verilog-2001
//
//  The canonical single-clock FIFO, same hardware as the SystemVerilog of
//  Chapter 10.2 section 4.
//
//  NOTE THE PORT STYLE. Ports are declared Verilog-1995 style -- names in
//  the header, widths in the body -- because an ANSI port list cannot
//  reference a localparam that does not exist yet, and `level_o` is
//  clog2(DEPTH+1) bits wide. SystemVerilog solves this with $clog2 inline in
//  the port list; $clog2 is a Verilog-2005 system function and is not
//  available in strict 2001. This style is, and it is portable everywhere.
//
//  Two width rules, and they are DIFFERENT:
//    PTR_W spans 0 .. DEPTH-1   -> clog2(DEPTH)
//    CNT_W spans 0 .. DEPTH     -> clog2(DEPTH+1)
//  Occupancy must REPRESENT DEPTH, one more value than any pointer holds.
//  Sizing the counter like a pointer makes `full_o` unreachable.
//===========================================================================
module uart_sync_fifo_v (
    clk, rst_n,
    push_i, push_data_i,
    pop_i, pop_data_o,
    empty_o, full_o, level_o,
    overflow_evt_o, underflow_evt_o
);
    parameter WIDTH = 8;
    parameter DEPTH = 16;

    function integer clog2;
        input integer value;
        integer i;
        begin
            clog2 = 1;
            for (i = value - 1; i > 1; i = i >> 1) clog2 = clog2 + 1;
        end
    endfunction

    // clog2(1) would be 0 and a zero-width vector is illegal, hence the guard.
    localparam PTR_W = (DEPTH <= 1) ? 1 : clog2(DEPTH);
    localparam CNT_W = clog2(DEPTH + 1);

    input              clk;
    input              rst_n;
    input              push_i;
    input  [WIDTH-1:0] push_data_i;
    input              pop_i;
    output [WIDTH-1:0] pop_data_o;
    output             empty_o;
    output             full_o;
    output [CNT_W-1:0] level_o;
    output reg         overflow_evt_o;
    output reg         underflow_evt_o;

    reg [WIDTH-1:0] mem [0:DEPTH-1];
    reg [PTR_W-1:0] wr_ptr_q, rd_ptr_q;
    reg [CNT_W-1:0] level_q;

    assign empty_o = (level_q == {CNT_W{1'b0}});
    assign full_o  = (level_q == DEPTH[CNT_W-1:0]);
    assign level_o = level_q;

    wire pop_fire  = pop_i && !empty_o;
    // A push into a full FIFO is accepted when a pop frees an entry on the
    // SAME cycle. The simpler `push_i && !full_o` drops a byte at exactly
    // the boundary where a consumer is keeping up -- 10.2 section 5.
    wire push_fire = push_i && (!full_o || pop_fire);

    // Show-ahead head: the output follows the read pointer combinationally,
    // so pop_data_o is already valid on the cycle pop_i is asserted.
    assign pop_data_o = mem[rd_ptr_q];

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            wr_ptr_q        <= {PTR_W{1'b0}};
            rd_ptr_q        <= {PTR_W{1'b0}};
            level_q         <= {CNT_W{1'b0}};
            overflow_evt_o  <= 1'b0;
            underflow_evt_o <= 1'b0;
        end else begin
            overflow_evt_o  <= push_i && !push_fire;
            underflow_evt_o <= pop_i  && !pop_fire;

            if (push_fire) begin
                mem[wr_ptr_q] <= push_data_i;
                // EXPLICIT wrap. Relying on `wr_ptr_q + 1` to truncate is
                // correct only when DEPTH is a power of two.
                wr_ptr_q <= (wr_ptr_q == (DEPTH[PTR_W-1:0] - 1'b1))
                            ? {PTR_W{1'b0}} : wr_ptr_q + 1'b1;
            end

            if (pop_fire) begin
                rd_ptr_q <= (rd_ptr_q == (DEPTH[PTR_W-1:0] - 1'b1))
                            ? {PTR_W{1'b0}} : rd_ptr_q + 1'b1;
            end

            // ONE explicit four-way update. Two separate conditional
            // increments would let the later assignment silently win when a
            // push and a pop coincide.
            case ({push_fire, pop_fire})
                2'b10:   level_q <= level_q + 1'b1;
                2'b01:   level_q <= level_q - 1'b1;
                default: level_q <= level_q;      // 00 and 11 both hold
            endcase
        end
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  uart_sync_fifo -- Synthesizable VHDL-2008
--
--  The same single-clock FIFO. VHDL needs no port-declaration trick: a
--  generic is visible to the port clause, so the derived widths can be
--  written directly with a function call. This is the one place where VHDL
--  is straightforwardly tidier than either Verilog dialect.
--
--  empty, full and level are READ by the architecture (push_fire depends on
--  full, pop_fire on empty), and VHDL forbids reading an out port -- so each
--  is an internal signal with a concurrent driver. One flop, one net, in all
--  three languages; VHDL simply names them separately.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;

package uart_fifo_pkg is
    -- ceil(log2(n)), floored at 1 so a one-deep FIFO still yields a legal
    -- vector. math_real is elaboration-only; nothing real-valued reaches
    -- the netlist.
    function cnt_width (n : positive) return positive;
end package uart_fifo_pkg;

package body uart_fifo_pkg is
    function cnt_width (n : positive) return positive is
    begin
        if n <= 2 then
            return 1;
        else
            return positive(ceil(log2(real(n))));
        end if;
    end function;
end package body uart_fifo_pkg;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.uart_fifo_pkg.all;

entity uart_sync_fifo is
    generic (
        WIDTH : positive := 8;
        DEPTH : positive := 16
    );
    port (
        clk             : in  std_logic;
        rst_n           : in  std_logic;

        push_i          : in  std_logic;
        push_data_i     : in  std_logic_vector(WIDTH-1 downto 0);

        pop_i           : in  std_logic;
        pop_data_o      : out std_logic_vector(WIDTH-1 downto 0);

        empty_o         : out std_logic;
        full_o          : out std_logic;
        level_o         : out std_logic_vector(cnt_width(DEPTH+1)-1 downto 0);

        overflow_evt_o  : out std_logic;
        underflow_evt_o : out std_logic
    );
end entity uart_sync_fifo;

architecture rtl of uart_sync_fifo is

    -- PTR_W spans 0 .. DEPTH-1; CNT_W spans 0 .. DEPTH. They are different,
    -- and sizing the counter like a pointer makes `full` unreachable.
    constant PTR_W : positive := cnt_width(DEPTH);
    constant CNT_W : positive := cnt_width(DEPTH + 1);

    type mem_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);

    signal mem      : mem_t;
    signal wr_ptr_q : unsigned(PTR_W-1 downto 0) := (others => '0');
    signal rd_ptr_q : unsigned(PTR_W-1 downto 0) := (others => '0');
    signal level_q  : unsigned(CNT_W-1 downto 0) := (others => '0');

    signal empty_s, full_s           : std_logic;
    signal push_fire_s, pop_fire_s   : std_logic;

begin

    empty_s <= '1' when level_q = 0 else '0';
    full_s  <= '1' when level_q = to_unsigned(DEPTH, CNT_W) else '0';

    empty_o <= empty_s;
    full_o  <= full_s;
    level_o <= std_logic_vector(level_q);

    pop_fire_s  <= pop_i and not empty_s;
    -- A push into a full FIFO is accepted when a pop frees an entry on the
    -- SAME cycle -- section 5 of chapter 10.2.
    push_fire_s <= push_i and (not full_s or pop_fire_s);

    -- Show-ahead head: combinational read of the array.
    pop_data_o <= mem(to_integer(rd_ptr_q));

    process (clk, rst_n)
    begin
        if rst_n = '0' then
            wr_ptr_q        <= (others => '0');
            rd_ptr_q        <= (others => '0');
            level_q         <= (others => '0');
            overflow_evt_o  <= '0';
            underflow_evt_o <= '0';
        elsif rising_edge(clk) then
            overflow_evt_o  <= push_i and not push_fire_s;
            underflow_evt_o <= pop_i  and not pop_fire_s;

            if push_fire_s = '1' then
                mem(to_integer(wr_ptr_q)) <= push_data_i;
                -- EXPLICIT wrap: relying on truncation is correct only when
                -- DEPTH is a power of two.
                if wr_ptr_q = to_unsigned(DEPTH-1, PTR_W) then
                    wr_ptr_q <= (others => '0');
                else
                    wr_ptr_q <= wr_ptr_q + 1;
                end if;
            end if;

            if pop_fire_s = '1' then
                if rd_ptr_q = to_unsigned(DEPTH-1, PTR_W) then
                    rd_ptr_q <= (others => '0');
                else
                    rd_ptr_q <= rd_ptr_q + 1;
                end if;
            end if;

            -- ONE explicit four-way update. Two separate conditional
            -- increments would let the later assignment silently win when a
            -- push and a pop coincide.
            if    push_fire_s = '1' and pop_fire_s = '0' then
                level_q <= level_q + 1;
            elsif push_fire_s = '0' and pop_fire_s = '1' then
                level_q <= level_q - 1;
            else
                level_q <= level_q;              -- 00 and 11 both hold
            end if;
        end if;
    end process;

end architecture rtl;

VHDL needs no trick at all. A generic is visible to the port clause, so cnt_width(DEPTH+1) can be written directly where the width belongs. This is the one place in the whole UART curriculum where VHDL is straightforwardly tidier than either Verilog dialect, and it is worth saying so.

PropertyVerilog-2001SystemVerilogVHDL-2008Agree?
depth, widthparametersparametersgenericsyes
pointer widthclog2(DEPTH)$clog2(DEPTH)cnt_width(DEPTH)yes
occupancy widthclog2(DEPTH+1)$clog2(DEPTH+1)cnt_width(DEPTH+1)yes — and different from the pointer
acceptance rule`push && (!fullpop_fire)`same
pointer wrapexplicit compareexplicit compareexplicit compareyes
occupancy updateone 4-way caseone 4-way caseone 4-way if/elsifyes
head readcombinational (show-ahead)samesameyes
port declaration stylenon-ANSIANSIANSIno — language rule
reading full/empty internallyreads the netreads the netinternal signalsno — language rule

Two deliberate differences, both properties of the languages. The port style is explained above. The second is the rule Modules 7 and 9 already met: VHDL forbids a process or concurrent expression from reading an out port, and push_fire depends on full. One net either way; VHDL names it separately.

11. The Self-Checking Testbenches

INDEPENDENCE: each checker keeps its own queue — an array with a head, a tail and a count — advanced by the same stimulus. None of them reads wr_ptr_q, rd_ptr_q, level_q or mem. A checker assembled from the design's pointer arithmetic would reproduce a wrap bug rather than catch it, which is not a hypothetical: §7 below records a mutation that survived until the queue comparison was extended.

Two depths run side by side, and the second one is the whole point:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam DEPTH  = 16;      // power of two
localparam DEPTH3 = 5;       // NOT a power of two

At DEPTH = 16 a truncating pointer is correct — the wrap and the truncation coincide. Only at a non-power-of-two depth do they diverge, so a test matrix that uses round numbers cannot see the difference §6 is about.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_sync_fifo_v — self-checking Verilog-2001 testbench
//
//  INDEPENDENCE: the checker keeps its OWN queue -- a plain array with a
//  head, a tail and a count -- and drives it from the same stimulus. It
//  never reads wr_ptr_q, rd_ptr_q, level_q or mem. A checker built from the
//  DUT's pointer arithmetic would reproduce a wrap bug rather than catch it.
//
//  Two DEPTHs run side by side, one a power of two and one NOT, because the
//  explicit-wrap decision is only observable at a non-power-of-two depth.
//
//  Mandatory timeout: the watchdog turns a stalled FIFO into a reported
//  failure rather than a hung regression.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_sync_fifo_v;

    localparam WIDTH  = 8;
    localparam DEPTH  = 16;      // power of two
    localparam DEPTH3 = 5;       // NOT a power of two

    reg clk = 1'b0, rst_n = 1'b0;
    always #5 clk = ~clk;

    reg              push = 1'b0, pop = 1'b0;
    reg  [WIDTH-1:0] push_data = 8'h00;
    wire [WIDTH-1:0] pop_data;
    wire             empty, full;
    wire [4:0]       level;
    wire             ovf, unf;

    uart_sync_fifo_v #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
        .clk(clk), .rst_n(rst_n),
        .push_i(push), .push_data_i(push_data),
        .pop_i(pop),   .pop_data_o(pop_data),
        .empty_o(empty), .full_o(full), .level_o(level),
        .overflow_evt_o(ovf), .underflow_evt_o(unf));

    // the non-power-of-two twin, driven by the SAME stimulus
    wire [WIDTH-1:0] pop_data3;
    wire             empty3, full3;
    wire [2:0]       level3;
    wire             ovf3, unf3;

    uart_sync_fifo_v #(.WIDTH(WIDTH), .DEPTH(DEPTH3)) dut3 (
        .clk(clk), .rst_n(rst_n),
        .push_i(push), .push_data_i(push_data),
        .pop_i(pop),   .pop_data_o(pop_data3),
        .empty_o(empty3), .full_o(full3), .level_o(level3),
        .overflow_evt_o(ovf3), .underflow_evt_o(unf3));

    integer checks = 0, failures = 0;

    task check;
        input cond;
        input [8*56-1:0] name;
        begin
            checks = checks + 1;
            if (cond) $display("  PASS %0s", name);
            else begin failures = failures + 1; $display("  FAIL %0s", name); end
        end
    endtask

    //-----------------------------------------------------------------------
    //  The independent model: a queue, not a pointer pair.
    //-----------------------------------------------------------------------
    reg [WIDTH-1:0] q   [0:63];
    integer         qh = 0, qt = 0, qn = 0;      // head, tail, count
    reg [WIDTH-1:0] q3  [0:63];
    integer         q3h = 0, q3t = 0, q3n = 0;

    integer mism_data = 0, mism_level = 0, mism_flags = 0;
    integer mism_ovf  = 0, mism_unf   = 0;
    integer n_ovf = 0, n_unf = 0, ovf_wide = 0;
    reg     ovf_prev = 1'b0;

    // Model the SAME acceptance rule the contract states -- in queue terms,
    // not in pointer terms.
    task model_step;
        input do_push, do_pop;
        input [WIDTH-1:0] d;
        reg pf, of;
        begin
            of = do_pop  && (qn > 0);
            pf = do_push && ((qn < DEPTH) || of);
            if (of) begin qh = (qh + 1) % 64; qn = qn - 1; end
            if (pf) begin q[qt] = d; qt = (qt + 1) % 64; qn = qn + 1; end
            of = do_pop  && (q3n > 0);
            pf = do_push && ((q3n < DEPTH3) || of);
            if (of) begin q3h = (q3h + 1) % 64; q3n = q3n - 1; end
            if (pf) begin q3[q3t] = d; q3t = (q3t + 1) % 64; q3n = q3n + 1; end
        end
    endtask

    // Compare FIRST -- at this instant both the DUT's outputs and the model
    // describe the queue after the PREVIOUS edge -- then advance the model
    // with the stimulus this edge is consuming. Advancing earlier puts the
    // model one transaction ahead and every comparison becomes noise.
    always @(posedge clk) if (rst_n) begin
        if (qn > 0 && pop_data !== q[qh])            mism_data  = mism_data + 1;
        // The DEPTH=5 instance exists to expose a truncating pointer, and
        // only its DATA can show it: the level counter is independent of the
        // pointers, so a wrap bug moves bytes without moving the count.
        // Checking level3 alone was a hole -- a mutation survived it.
        if (q3n > 0 && pop_data3 !== q3[q3h])        mism_data  = mism_data + 1;
        if (level !== qn[4:0])                       mism_level = mism_level + 1;
        if (empty !== (qn == 0) || full !== (qn == DEPTH)) mism_flags = mism_flags + 1;
        if (level3 !== q3n[2:0])                     mism_level = mism_level + 1;
        if (ovf) begin
            n_ovf = n_ovf + 1;
            if (ovf_prev) ovf_wide = ovf_wide + 1;
        end
        if (unf) n_unf = n_unf + 1;
        ovf_prev = ovf;

        model_step(push, pop, push_data);
    end

    // Drive one cycle of traffic and advance the model in lockstep.
    task step;
        input do_push, do_pop;
        input [WIDTH-1:0] d;
        begin
            @(negedge clk);
            push = do_push; pop = do_pop; push_data = d;
            @(negedge clk);
            push = 1'b0; pop = 1'b0;
        end
    endtask

    // overflow_evt_o and underflow_evt_o are REGISTERED, and the observer
    // samples a registered output, so a count is two active edges behind the
    // stimulus that caused it. Reading a count earlier tests the testbench.
    task settle;
        begin repeat (2) @(negedge clk); end
    endtask

    integer i, base_ovf, base_unf, seed;
    reg [7:0] r;

    initial begin
        #2_000_000;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: VERILOG FIFO TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_sync_fifo_v : self-checking Verilog testbench ==");

        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        check(empty === 1'b1 && full === 1'b0, "reset: empty, not full");
        check(level === 5'd0,                  "reset: level zero");
        check(ovf === 1'b0 && unf === 1'b0,    "reset: no events");
        @(negedge clk) rst_n = 1'b1;
        @(negedge clk);

        // ---- one byte in, one byte out -----------------------------------
        step(1'b1, 1'b0, 8'hA5);
        check(level === 5'd1 && empty === 1'b0, "one push: level 1, not empty");
        check(pop_data === 8'hA5,               "show-ahead: head is visible");
        step(1'b0, 1'b1, 8'h00);
        check(level === 5'd0 && empty === 1'b1, "one pop: back to empty");

        // ---- fill exactly to DEPTH ---------------------------------------
        for (i = 0; i < DEPTH; i = i + 1) step(1'b1, 1'b0, 8'h10 + i[7:0]);
        check(level === DEPTH[4:0] && full === 1'b1, "filled to DEPTH: full");
        check(mism_data == 0,                        "head tracked the model throughout");

        // ---- pushing into a full FIFO overflows and loses the byte -------
        base_ovf = n_ovf;
        step(1'b1, 1'b0, 8'hFF);
        settle;
        check(n_ovf == base_ovf + 1, "push when full: one overflow event");
        check(level === DEPTH[4:0],  "push when full: level unchanged");
        check(pop_data === 8'h10,    "push when full: head undisturbed");

        // ---- THE boundary: push and pop on the same cycle when FULL ------
        // The contract accepts this. `push_i && !full_o` would reject it and
        // drop a byte at exactly the moment a consumer is keeping up.
        base_ovf = n_ovf;
        step(1'b1, 1'b1, 8'hC3);
        settle;
        check(n_ovf == base_ovf,    "push+pop when full: NO overflow");
        check(level === DEPTH[4:0], "push+pop when full: level holds");

        // ---- drain it completely -----------------------------------------
        for (i = 0; i < DEPTH; i = i + 1) step(1'b0, 1'b1, 8'h00);
        check(empty === 1'b1 && level === 5'd0, "drained: empty again");
        check(mism_data == 0,                    "every byte came out in order");

        // ---- popping an empty FIFO underflows -----------------------------
        base_unf = n_unf;
        step(1'b0, 1'b1, 8'h00);
        settle;
        check(n_unf == base_unf + 1, "pop when empty: one underflow event");
        check(level === 5'd0,        "pop when empty: level stays zero");

        // ---- push+pop when EMPTY: the push lands, the pop underflows ------
        base_unf = n_unf;
        step(1'b1, 1'b1, 8'h7E);
        settle;
        check(n_unf == base_unf + 1, "push+pop when empty: underflow reported");
        check(level === 5'd1,        "push+pop when empty: the push still landed");
        step(1'b0, 1'b1, 8'h00);

        // ---- wrap: push and pop far more than DEPTH ----------------------
        // This is what the explicit wrap exists for. The DEPTH=5 twin is the
        // one that proves it -- a truncating pointer is correct at 16 and
        // wrong at 5.
        for (i = 0; i < 200; i = i + 1) begin
            step(1'b1, 1'b0, 8'h20 + i[7:0]);
            step(1'b0, 1'b1, 8'h00);
        end
        check(mism_data == 0,  "200 wrap cycles: data order never diverged");
        check(mism_level == 0, "200 wrap cycles: level never diverged");
        check(empty === 1'b1 && empty3 === 1'b1, "both depths empty at the end");

        // ---- long pseudo-random traffic against the model ----------------
        // No constrained randomisation in Icarus; a deterministic LFSR-ish
        // sequence is reproducible, which is better for a published run.
        seed = 32'h1234_5678;
        for (i = 0; i < 1500; i = i + 1) begin
            seed = (seed * 1103515245 + 12345);
            r    = seed[23:16];
            step(r[0] | r[3], r[1] & ~r[4], r);
        end
        check(mism_data  == 0, "1500 random cycles: data matched the model");
        check(mism_level == 0, "1500 random cycles: level matched the model");
        check(mism_flags == 0, "1500 random cycles: empty/full matched");
        check(n_ovf > 0,       "random traffic exercised overflow");
        check(n_unf > 0,       "random traffic exercised underflow");
        check(ovf_wide == 0,   "overflow event never wider than one clock");

        // ---- reset while holding data -------------------------------------
        step(1'b1, 1'b0, 8'h55);
        step(1'b1, 1'b0, 8'h66);
        check(level !== 5'd0, "data held before reset");
        @(negedge clk) rst_n = 1'b0;
        repeat (2) @(negedge clk);
        check(empty === 1'b1 && level === 5'd0, "reset discards contents");
        @(negedge clk) rst_n = 1'b1;
        @(negedge clk);

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL VERILOG FIFO TESTS PASSED");
        else               $display("   RESULT: VERILOG FIFO TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_sync_fifo_sv — self-checking Verilog-2001 testbench
//
//  INDEPENDENCE: the checker keeps its OWN queue -- a plain array with a
//  head, a tail and a count -- and drives it from the same stimulus. It
//  never reads wr_ptr_q, rd_ptr_q, level_q or mem. A checker built from the
//  DUT's pointer arithmetic would reproduce a wrap bug rather than catch it.
//
//  Two DEPTHs run side by side, one a power of two and one NOT, because the
//  explicit-wrap decision is only observable at a non-power-of-two depth.
//
//  Mandatory timeout: the watchdog turns a stalled FIFO into a reported
//  failure rather than a hung regression.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_sync_fifo_sv;

    localparam WIDTH  = 8;
    localparam DEPTH  = 16;      // power of two
    localparam DEPTH3 = 5;       // NOT a power of two

    logic clk = 1'b0, rst_n = 1'b0;
    always #5ns clk = ~clk;

    logic push = 1'b0, pop = 1'b0;
    logic [WIDTH-1:0] push_data = 8'h00;
    logic [WIDTH-1:0] pop_data;
    logic empty, full;
    logic [4:0]       level;
    logic ovf, unf;

    uart_sync_fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (
        .clk(clk), .rst_n(rst_n),
        .push_i(push), .push_data_i(push_data),
        .pop_i(pop),   .pop_data_o(pop_data),
        .empty_o(empty), .full_o(full), .level_o(level),
        .overflow_evt_o(ovf), .underflow_evt_o(unf));

    // the non-power-of-two twin, driven by the SAME stimulus
    logic [WIDTH-1:0] pop_data3;
    logic empty3, full3;
    logic [2:0]       level3;
    logic ovf3, unf3;

    uart_sync_fifo #(.WIDTH(WIDTH), .DEPTH(DEPTH3)) dut3 (
        .clk(clk), .rst_n(rst_n),
        .push_i(push), .push_data_i(push_data),
        .pop_i(pop),   .pop_data_o(pop_data3),
        .empty_o(empty3), .full_o(full3), .level_o(level3),
        .overflow_evt_o(ovf3), .underflow_evt_o(unf3));

    int unsigned checks = 0, failures = 0;

    task automatic check(input bit cond, input string name);
        checks++;
        if (cond) $display("  PASS %0s", name);
        else begin failures++; $display("  FAIL %0s", name); end
    endtask

    //-----------------------------------------------------------------------
    //  The independent model: a queue, not a pointer pair.
    //-----------------------------------------------------------------------
    logic [WIDTH-1:0] q  [0:63];
    int qh = 0, qt = 0, qn = 0;      // head, tail, count
    logic [WIDTH-1:0] q3  [0:63];
    int q3h = 0, q3t = 0, q3n = 0;

    int mism_data = 0, mism_level = 0, mism_flags = 0;
    int mism_ovf  = 0, mism_unf   = 0;
    int n_ovf = 0, n_unf = 0, ovf_wide = 0;
    logic ovf_prev = 1'b0;

    // Model the SAME acceptance rule the contract states -- in queue terms,
    // not in pointer terms.
    task automatic model_step(input bit do_push, input bit do_pop,
                              input logic [WIDTH-1:0] d);
        bit pf, pf3, ofire, ofire3;
        ofire = do_pop  && (qn > 0);
        pf    = do_push && ((qn < DEPTH) || ofire);
        if (ofire) begin qh = (qh + 1) % 64; qn--; end
        if (pf)    begin q[qt] = d; qt = (qt + 1) % 64; qn++; end
        ofire3 = do_pop  && (q3n > 0);
        pf3    = do_push && ((q3n < DEPTH3) || ofire3);
        if (ofire3) begin q3h = (q3h + 1) % 64; q3n--; end
        if (pf3)    begin q3[q3t] = d; q3t = (q3t + 1) % 64; q3n++; end
    endtask

    // Compare FIRST -- at this instant both the DUT's outputs and the model
    // describe the queue after the PREVIOUS edge -- then advance the model
    // with the stimulus this edge is consuming. Advancing earlier puts the
    // model one transaction ahead and every comparison becomes noise.
    always @(posedge clk) if (rst_n) begin
        if (qn > 0 && pop_data !== q[qh])            mism_data  = mism_data + 1;
        // The DEPTH=5 instance exists to expose a truncating pointer, and
        // only its DATA can show it: the level counter is independent of the
        // pointers. Checking level3 alone was a hole -- a mutation survived.
        if (q3n > 0 && pop_data3 !== q3[q3h])        mism_data  = mism_data + 1;
        if (level !== qn[4:0])                       mism_level = mism_level + 1;
        if (empty !== (qn == 0) || full !== (qn == DEPTH)) mism_flags = mism_flags + 1;
        if (level3 !== q3n[2:0])                     mism_level = mism_level + 1;
        if (ovf) begin
            n_ovf = n_ovf + 1;
            if (ovf_prev) ovf_wide = ovf_wide + 1;
        end
        if (unf) n_unf = n_unf + 1;
        ovf_prev = ovf;

        model_step(push, pop, push_data);
    end

    // Drive one cycle of traffic and advance the model in lockstep.
    task automatic step(input bit do_push, input bit do_pop,
                        input logic [WIDTH-1:0] d);
        @(negedge clk);
        push = do_push; pop = do_pop; push_data = d;
        @(negedge clk);
        push = 1'b0; pop = 1'b0;
    endtask

    // overflow_evt_o and underflow_evt_o are REGISTERED, and the observer
    // samples a registered output, so a count is two active edges behind the
    // stimulus that caused it. Reading a count earlier tests the testbench.
    task automatic settle(); repeat (2) @(negedge clk); endtask

    int i, base_ovf, base_unf, seed;
    logic [7:0] r;

    initial begin
        #2ms;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: SYSTEMVERILOG FIFO TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_sync_fifo : self-checking Verilog testbench ==");

        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        check(empty === 1'b1 && full === 1'b0, "reset: empty, not full");
        check(level === 5'd0,                  "reset: level zero");
        check(ovf === 1'b0 && unf === 1'b0,    "reset: no events");
        @(negedge clk) rst_n = 1'b1;
        @(negedge clk);

        // ---- one byte in, one byte out -----------------------------------
        step(1'b1, 1'b0, 8'hA5);
        check(level === 5'd1 && empty === 1'b0, "one push: level 1, not empty");
        check(pop_data === 8'hA5,               "show-ahead: head is visible");
        step(1'b0, 1'b1, 8'h00);
        check(level === 5'd0 && empty === 1'b1, "one pop: back to empty");

        // ---- fill exactly to DEPTH ---------------------------------------
        for (i = 0; i < DEPTH; i = i + 1) step(1'b1, 1'b0, 8'h10 + i[7:0]);
        check(level === DEPTH[4:0] && full === 1'b1, "filled to DEPTH: full");
        check(mism_data == 0,                        "head tracked the model throughout");

        // ---- pushing into a full FIFO overflows and loses the byte -------
        base_ovf = n_ovf;
        step(1'b1, 1'b0, 8'hFF);
        settle();
        check(n_ovf == base_ovf + 1, "push when full: one overflow event");
        check(level === DEPTH[4:0],  "push when full: level unchanged");
        check(pop_data === 8'h10,    "push when full: head undisturbed");

        // ---- THE boundary: push and pop on the same cycle when FULL ------
        // The contract accepts this. `push_i && !full_o` would reject it and
        // drop a byte at exactly the moment a consumer is keeping up.
        base_ovf = n_ovf;
        step(1'b1, 1'b1, 8'hC3);
        settle();
        check(n_ovf == base_ovf,    "push+pop when full: NO overflow");
        check(level === DEPTH[4:0], "push+pop when full: level holds");

        // ---- drain it completely -----------------------------------------
        for (i = 0; i < DEPTH; i = i + 1) step(1'b0, 1'b1, 8'h00);
        check(empty === 1'b1 && level === 5'd0, "drained: empty again");
        check(mism_data == 0,                    "every byte came out in order");

        // ---- popping an empty FIFO underflows -----------------------------
        base_unf = n_unf;
        step(1'b0, 1'b1, 8'h00);
        settle();
        check(n_unf == base_unf + 1, "pop when empty: one underflow event");
        check(level === 5'd0,        "pop when empty: level stays zero");

        // ---- push+pop when EMPTY: the push lands, the pop underflows ------
        base_unf = n_unf;
        step(1'b1, 1'b1, 8'h7E);
        settle();
        check(n_unf == base_unf + 1, "push+pop when empty: underflow reported");
        check(level === 5'd1,        "push+pop when empty: the push still landed");
        step(1'b0, 1'b1, 8'h00);

        // ---- wrap: push and pop far more than DEPTH ----------------------
        // This is what the explicit wrap exists for. The DEPTH=5 twin is the
        // one that proves it -- a truncating pointer is correct at 16 and
        // wrong at 5.
        for (i = 0; i < 200; i = i + 1) begin
            step(1'b1, 1'b0, 8'h20 + i[7:0]);
            step(1'b0, 1'b1, 8'h00);
        end
        check(mism_data == 0,  "200 wrap cycles: data order never diverged");
        check(mism_level == 0, "200 wrap cycles: level never diverged");
        check(empty === 1'b1 && empty3 === 1'b1, "both depths empty at the end");

        // ---- long pseudo-random traffic against the model ----------------
        // No constrained randomisation in Icarus; a deterministic LFSR-ish
        // sequence is reproducible, which is better for a published run.
        seed = 32'h1234_5678;
        for (i = 0; i < 1500; i = i + 1) begin
            seed = (seed * 1103515245 + 12345);
            r    = seed[23:16];
            step(r[0] | r[3], r[1] & ~r[4], r);
        end
        check(mism_data  == 0, "1500 random cycles: data matched the model");
        check(mism_level == 0, "1500 random cycles: level matched the model");
        check(mism_flags == 0, "1500 random cycles: empty/full matched");
        check(n_ovf > 0,       "random traffic exercised overflow");
        check(n_unf > 0,       "random traffic exercised underflow");
        check(ovf_wide == 0,   "overflow event never wider than one clock");

        // ---- reset while holding data -------------------------------------
        step(1'b1, 1'b0, 8'h55);
        step(1'b1, 1'b0, 8'h66);
        check(level !== 5'd0, "data held before reset");
        @(negedge clk) rst_n = 1'b0;
        repeat (2) @(negedge clk);
        check(empty === 1'b1 && level === 5'd0, "reset discards contents");
        @(negedge clk) rst_n = 1'b1;
        @(negedge clk);

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL SYSTEMVERILOG FIFO TESTS PASSED");
        else               $display("   RESULT: SYSTEMVERILOG FIFO TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  tb_uart_sync_fifo -- self-checking VHDL-2008 testbench
--
--  INDEPENDENCE: the checker keeps its OWN queue -- an array with a head, a
--  tail and a count -- driven from the same stimulus. It never reads
--  wr_ptr_q, rd_ptr_q, level_q or mem.
--
--  Two DEPTHs run side by side, one a power of two and one NOT, because the
--  explicit-wrap decision is only observable at a non-power-of-two depth.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.env.finish;
use work.uart_fifo_pkg.all;

entity tb_uart_sync_fifo is
end entity tb_uart_sync_fifo;

architecture sim of tb_uart_sync_fifo is

    constant WIDTH      : positive := 8;
    constant DEPTH      : positive := 16;   -- power of two
    constant DEPTH3     : positive := 5;    -- NOT a power of two
    constant CLK_PERIOD : time     := 10 ns;

    signal clk, rst_n, done : std_logic := '0';
    signal push, pop        : std_logic := '0';
    signal push_data        : std_logic_vector(WIDTH-1 downto 0) := (others => '0');

    signal pop_data  : std_logic_vector(WIDTH-1 downto 0);
    signal empty, full, ovf, unf : std_logic;
    signal level     : std_logic_vector(cnt_width(DEPTH+1)-1 downto 0);

    signal pop_data3 : std_logic_vector(WIDTH-1 downto 0);
    signal empty3, full3, ovf3, unf3 : std_logic;
    signal level3    : std_logic_vector(cnt_width(DEPTH3+1)-1 downto 0);

    signal mism_data, mism_level, mism_flags : natural := 0;
    signal n_ovf, n_unf, ovf_wide            : natural := 0;
    signal q_count                           : natural := 0;

begin

    clk <= not clk after CLK_PERIOD / 2 when done = '0' else '0';

    dut : entity work.uart_sync_fifo
        generic map (WIDTH => WIDTH, DEPTH => DEPTH)
        port map (clk => clk, rst_n => rst_n,
                  push_i => push, push_data_i => push_data,
                  pop_i => pop, pop_data_o => pop_data,
                  empty_o => empty, full_o => full, level_o => level,
                  overflow_evt_o => ovf, underflow_evt_o => unf);

    dut3 : entity work.uart_sync_fifo
        generic map (WIDTH => WIDTH, DEPTH => DEPTH3)
        port map (clk => clk, rst_n => rst_n,
                  push_i => push, push_data_i => push_data,
                  pop_i => pop, pop_data_o => pop_data3,
                  empty_o => empty3, full_o => full3, level_o => level3,
                  overflow_evt_o => ovf3, underflow_evt_o => unf3);

    ------------------------------------------------------------------------
    --  The model, and the comparison. Compare FIRST -- both descriptions
    --  refer to the state after the previous edge -- then advance the model
    --  with the stimulus this edge consumes.
    ------------------------------------------------------------------------
    model : process (clk)
        type q_t is array (0 to 63) of std_logic_vector(WIDTH-1 downto 0);
        variable q, q3       : q_t := (others => (others => '0'));
        variable qh, qt, qn  : natural := 0;
        variable q3h, q3t, q3n : natural := 0;
        variable md, ml, mf, no, nu, ow : natural := 0;
        variable ovf_prev    : std_logic := '0';
        variable pf, ofire   : boolean;
    begin
        if rising_edge(clk) and rst_n = '1' then
            -- compare
            if qn > 0 and pop_data /= q(qh) then md := md + 1; end if;
            -- the DEPTH=5 instance exposes a truncating pointer, and only
            -- its DATA can show it: the level counter is independent of the
            -- pointers. Checking level3 alone was a hole.
            if q3n > 0 and pop_data3 /= q3(q3h) then md := md + 1; end if;
            if to_integer(unsigned(level))  /= qn  then ml := ml + 1; end if;
            if to_integer(unsigned(level3)) /= q3n then ml := ml + 1; end if;
            if (empty = '1') /= (qn = 0) or (full = '1') /= (qn = DEPTH) then
                mf := mf + 1;
            end if;
            if ovf = '1' then
                no := no + 1;
                if ovf_prev = '1' then ow := ow + 1; end if;
            end if;
            if unf = '1' then nu := nu + 1; end if;
            ovf_prev := ovf;

            -- advance
            ofire := (pop = '1') and (qn > 0);
            pf    := (push = '1') and ((qn < DEPTH) or ofire);
            if ofire then qh := (qh + 1) mod 64; qn := qn - 1; end if;
            if pf    then q(qt) := push_data; qt := (qt + 1) mod 64; qn := qn + 1; end if;

            ofire := (pop = '1') and (q3n > 0);
            pf    := (push = '1') and ((q3n < DEPTH3) or ofire);
            if ofire then q3h := (q3h + 1) mod 64; q3n := q3n - 1; end if;
            if pf    then q3(q3t) := push_data; q3t := (q3t + 1) mod 64; q3n := q3n + 1; end if;

            mism_data <= md; mism_level <= ml; mism_flags <= mf;
            n_ovf <= no; n_unf <= nu; ovf_wide <= ow; q_count <= qn;
        end if;
    end process model;

    watchdog : process
    begin
        wait for 2 ms;
        report "watchdog: simulation did not finish" severity error;
        report "   RESULT: VHDL FIFO TESTS FAILED (timeout)" severity failure;
        wait;
    end process watchdog;

    stim : process
        variable checks, failures : natural := 0;
        variable base_ovf, base_unf : natural;
        variable seed : unsigned(31 downto 0) := x"12345678";
        variable r    : unsigned(7 downto 0);

        procedure check(cond : boolean; name : string) is
        begin
            checks := checks + 1;
            if cond then report "  PASS " & name;
            else failures := failures + 1; report "  FAIL " & name severity error; end if;
        end procedure;

        procedure step(p, o : std_logic; d : std_logic_vector) is
        begin
            wait until falling_edge(clk);
            push <= p; pop <= o; push_data <= d;
            wait until falling_edge(clk);
            push <= '0'; pop <= '0';
        end procedure;

        -- the event outputs are registered and the observer samples a
        -- registered output, so counts are two active edges behind
        procedure settle is
        begin
            for i in 0 to 1 loop wait until falling_edge(clk); end loop;
        end procedure;
    begin
        report "== uart_sync_fifo (VHDL) : self-checking testbench ==";

        rst_n <= '0';
        for i in 0 to 3 loop wait until falling_edge(clk); end loop;
        check(empty = '1' and full = '0', "reset: empty, not full");
        check(unsigned(level) = 0,        "reset: level zero");
        check(ovf = '0' and unf = '0',    "reset: no events");
        rst_n <= '1';
        wait until falling_edge(clk);

        step('1','0', x"A5");
        check(unsigned(level) = 1 and empty = '0', "one push: level 1, not empty");
        check(pop_data = x"A5",                    "show-ahead: head is visible");
        step('0','1', x"00");
        check(unsigned(level) = 0 and empty = '1', "one pop: back to empty");

        for i in 0 to DEPTH-1 loop
            step('1','0', std_logic_vector(to_unsigned(16#10# + i, WIDTH)));
        end loop;
        check(unsigned(level) = DEPTH and full = '1', "filled to DEPTH: full");
        check(mism_data = 0,                          "head tracked the model throughout");

        base_ovf := n_ovf;
        step('1','0', x"FF");
        settle;
        check(n_ovf = base_ovf + 1,      "push when full: one overflow event");
        check(unsigned(level) = DEPTH,   "push when full: level unchanged");
        check(pop_data = x"10",          "push when full: head undisturbed");

        base_ovf := n_ovf;
        step('1','1', x"C3");
        settle;
        check(n_ovf = base_ovf,        "push+pop when full: NO overflow");
        check(unsigned(level) = DEPTH, "push+pop when full: level holds");

        for i in 0 to DEPTH-1 loop step('0','1', x"00"); end loop;
        check(empty = '1' and unsigned(level) = 0, "drained: empty again");
        check(mism_data = 0,                        "every byte came out in order");

        base_unf := n_unf;
        step('0','1', x"00");
        settle;
        check(n_unf = base_unf + 1, "pop when empty: one underflow event");
        check(unsigned(level) = 0,  "pop when empty: level stays zero");

        base_unf := n_unf;
        step('1','1', x"7E");
        settle;
        check(n_unf = base_unf + 1, "push+pop when empty: underflow reported");
        check(unsigned(level) = 1,  "push+pop when empty: the push still landed");
        step('0','1', x"00");

        for i in 0 to 199 loop
            step('1','0', std_logic_vector(to_unsigned((16#20# + i) mod 256, WIDTH)));
            step('0','1', x"00");
        end loop;
        check(mism_data = 0,  "200 wrap cycles: data order never diverged");
        check(mism_level = 0, "200 wrap cycles: level never diverged");
        check(empty = '1' and empty3 = '1', "both depths empty at the end");

        -- a reproducible pseudo-random sequence; VHDL integers would
        -- overflow on the C-style constants, so the LFSR is unsigned
        for i in 0 to 1499 loop
            seed := seed(30 downto 0) & (seed(31) xor seed(21) xor seed(1) xor seed(0));
            r := seed(23 downto 16);
            step(r(0) or r(3), r(1) and not r(4), std_logic_vector(r));
        end loop;
        check(mism_data  = 0, "1500 random cycles: data matched the model");
        check(mism_level = 0, "1500 random cycles: level matched the model");
        check(mism_flags = 0, "1500 random cycles: empty/full matched");
        check(n_ovf > 0,      "random traffic exercised overflow");
        check(n_unf > 0,      "random traffic exercised underflow");
        check(ovf_wide = 0,   "overflow event never wider than one clock");

        step('1','0', x"55");
        step('1','0', x"66");
        check(unsigned(level) /= 0, "data held before reset");
        wait until falling_edge(clk); rst_n <= '0';
        for i in 0 to 1 loop wait until falling_edge(clk); end loop;
        check(empty = '1' and unsigned(level) = 0, "reset discards contents");
        wait until falling_edge(clk); rst_n <= '1';
        wait until falling_edge(clk);

        report "== " & integer'image(checks) & " checks, " &
               integer'image(failures) & " failures ==";
        if failures = 0 then report "   RESULT: ALL VHDL FIFO TESTS PASSED";
        else report "   RESULT: VHDL FIFO TESTS FAILED" severity error; end if;
        done <= '1';
        finish;
    end process stim;

end architecture sim;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ iverilog -g2001 -o sim uart_sync_fifo_v.v tb_uart_sync_fifo_v.v && ./sim
== uart_sync_fifo_v : self-checking Verilog testbench ==
  PASS reset: empty, not full
  PASS one push: level 1, not empty
  PASS show-ahead: head is visible
  PASS filled to DEPTH: full
  PASS push when full: one overflow event
  PASS push when full: head undisturbed
  PASS push+pop when full: NO overflow
  PASS drained: empty again
  PASS every byte came out in order
  PASS pop when empty: one underflow event
  PASS push+pop when empty: the push still landed
  PASS 200 wrap cycles: data order never diverged
  PASS 1500 random cycles: data matched the model
  PASS 1500 random cycles: level matched the model
  PASS 1500 random cycles: empty/full matched
  PASS overflow event never wider than one clock
  PASS reset discards contents
== 30 checks, 0 failures ==
   RESULT: ALL VERILOG FIFO TESTS PASSED

30 checks in each language, 0 failures — the same thirty, including 1,500 cycles of reproducible pseudo-random push/pop traffic compared against the queue model on every clock.

Proving these checks can fail

MutationWhat it breaksFailures
occupancy counter sized like a pointer§3's width rule — full unreachable19 / 30
push && !full — reject a push that coincides with a pop§7's boundary case9 / 30
two conditional increments instead of one case§6's single-update rule7 / 30
pointers truncate instead of wrapping explicitly§6's explicit wrap3 / 30

The last row is the one with a story, and §7 tells it.

12. Verification

The scoreboard must be structurally independent. A reference queue that consults the DUT's pointers to predict the DUT's behaviour proves nothing. This suite maintains its own queue and compares order, occupancy and flags on every cycle.

Results, across six configurations:

WIDTHDEPTHchecksfailurespushespopssimultaneousoverflowunderflow
812,63609797217485
825,21202652657279100
837,787041641610282138
8512,944072572516466211
121025,90501,5821,58237870231
81641,41202,6012,60162119316
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
TOTAL: 4,805,704 checks, 0 failures

Four properties of that table matter more than the totals.

DEPTH = 1, 3, 5 and 10 are there deliberately. Three of them are not powers of two, which is where the wrap defect of §6 lives, and DEPTH = 1 is where the width guard and the full-plus-simultaneous case meet.

The simultaneous column is non-empty everywhere, because a randomised run with overlapping push and pop probabilities produces the case a directed test never does.

Overflow and underflow counts are non-zero, meaning the boundaries were genuinely exercised rather than avoided.

2,601 pushes at DEPTH = 16 is 162 pointer wraps. A FIFO can pass fill-and-drain and fail after wrapping; this run wraps repeatedly in every configuration.

Directed cases alongside the random run: reset state, push-one/pop-one, fill to full, rejected overflow push, full plus simultaneous push and pop, drain to empty, rejected underflow pop, empty plus simultaneous push and pop, and a final drain confirming the reference queue empties in step.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Assertion — occupancy never exceeds the depth. The counter is wide enough
// to violate this, so it states a contract rather than a tautology.
property p_level_bounded;
    @(posedge clk) disable iff (!rst_n)  level_o <= CNT_W'(DEPTH);
endproperty
assert property (p_level_bounded);

// Assertion — the flags are exactly the occupancy extremes.
property p_empty_iff_zero;
    @(posedge clk) disable iff (!rst_n)  empty_o == (level_o == '0);
endproperty
assert property (p_empty_iff_zero);

property p_full_iff_depth;
    @(posedge clk) disable iff (!rst_n)  full_o == (level_o == CNT_W'(DEPTH));
endproperty
assert property (p_full_iff_depth);

// Assertion — the four-way level update of §6, stated as a property.
// This is what fails on the two-independent-conditionals bug.
property p_level_delta;
    @(posedge clk) disable iff (!rst_n)
        ##1 level_o == ($past(level_o) + $past(push_fire) - $past(pop_fire));
endproperty
assert property (p_level_delta);

// Assertion — a rejected push is always reported.
property p_overflow_reported;
    @(posedge clk) disable iff (!rst_n)
        (push_i && !push_fire) |=> overflow_evt_o;
endproperty
assert property (p_overflow_reported);

The fourth property is the one to keep. It expresses the entire occupancy contract in one line, it holds for all four combinations including the simultaneous case, and it fails immediately on the two-conditionals defect that randomised traffic is otherwise needed to expose.

13. Debugging

14. What This Means on an FPGA

Shallow FIFOs are registers or distributed RAM. Sixteen entries of ten bits is 160 flip-flops or a handful of LUT-RAMs — negligible next to a UART.

Depth is where the implementation changes character. Hundreds of entries want block RAM, and that brings the reset restriction of §9, a different read-port structure, and a possible extra cycle of read latency that the show-ahead contract would then need a bypass register to hide.

The occupancy comparator is cheap relative to the memory, which is why the counter-based form costs little even at large depths — a 10-bit comparator against a 1,024-entry RAM is noise.

Probe accepted transactions, not requests. A logic analyser capture of push_i tells you what was asked for; push_fire tells you what happened. The difference between them is exactly the overflow, and it is the measurement Chapter 10.4 needs.

Instrument the high-water mark, as Chapter 10.1 §9 argued. It costs a comparator and a register, and it is the only direct evidence of how much margin a chosen depth has on the real system.

15. Understanding Check

16. Summary

A UART FIFO is asynchronous only when its two sides genuinely sit in different clock domains. The receive pin's asynchrony was resolved by the synchroniser at the input boundary, so for this architecture a synchronous FIFO is correct — and building an asynchronous one anyway imports Gray-coded pointers, synchronisers and CDC review for a crossing that does not exist.

Pointer equality cannot distinguish full from empty, so occupancy is tracked explicitly. It costs a counter and pays for itself because 10.3 and 10.5 both need level anyway.

The contract of §4 is fixed for the whole module: requests versus accepted transactions, pop_data_o as the combinational show-ahead head, and overflow/underflow as one-cycle events.

Four decisions inside the RTL: derived widths where the counter needs one more value than the pointers; explicit wrap because truncation is only correct at power-of-two depths; one four-way occupancy update because two conditionals let the later silently win; and push accepted while full when a pop frees a slot, which is safe even at DEPTH = 1 because the read is combinational and the write is not.

An empty FIFO with a simultaneous push and pop accepts the push and rejects the pop — show-ahead, deliberately not bypass.

The RX entry carries its status, or buffering destroys the association the publish discipline was built to protect. Overrun is the exception, because it describes a byte that does not exist.

Verified across six configurations including three non-power-of-two depths: 4,805,704 checks, 0 failures, with simultaneous transactions, overflow, underflow and 162 pointer wraps all genuinely exercised.

17. What Comes Next

The FIFO reports full_o and empty_o. Both are boundaries, and by the time either is asserted the opportunity to act gracefully has passed.

Chapter 10.3 adds the early warning. It defines trigger levels and watermarks precisely, explains why the receive and transmit senses are opposite, derives a threshold from a latency budget rather than choosing one that looks reasonable, and introduces the hysteresis that stops a single threshold from chattering while occupancy sits on the boundary.

Browse the full path on the UART tutorials index. For the per-character status this entry carries, read back to Chapter 6.4.

Continue learning

Where this fits

Part of the UART curriculum.