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 FIFO | TX FIFO | |
|---|---|---|
| Producer | the receive engine (Module 6) | software, a bus master or DMA |
| Consumer | software, a bus master or DMA | the transmit engine (Module 7) |
| Push rate | fixed by the line — one per T_frame | bursty, whenever the producer runs |
| Pop rate | bursty | fixed by the line |
| Entry contents | data + per-character status — §8 | data 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:
wr_ptr == rd_ptr means the queue is EMPTY
wr_ptr == rd_ptr means the queue is FULLBoth 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:
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 domain | one. clk for both sides. |
push_i / pop_i | requests, not guarantees |
pop_fire | pop_i && !empty_o |
push_fire | push_i && (!full_o || pop_fire) — §6 |
pop_data_o | the combinational head, valid when !empty_o, undefined when empty. Show-ahead / first-word fall-through. pop_i advances past it. |
empty_o | level_o == 0 |
full_o | level_o == DEPTH |
overflow_evt_o | one cycle: a push was requested and not accepted |
underflow_evt_o | one cycle: a pop was requested and not accepted |
| Reset | pointers, occupancy and events cleared. Memory is not reset — §9 |
| Wrap | explicit 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
// 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
endmodule6. The Four Decisions Inside That Code
Widths are derived, and the two are different
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
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
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
endcaseThe natural-looking alternative is two independent conditionals:
// 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
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:
| state | push_i | pop_i | push_fire | pop_fire | level next | events |
|---|---|---|---|---|---|---|
| any | 0 | 0 | 0 | 0 | unchanged | — |
| not full | 1 | 0 | 1 | 0 | +1 | — |
| not empty | 0 | 1 | 0 | 1 | −1 | — |
| mid-range | 1 | 1 | 1 | 1 | unchanged | — |
| full | 1 | 0 | 0 | 0 | unchanged | overflow |
| full | 1 | 1 | 1 | 1 | unchanged | — |
| empty | 0 | 1 | 0 | 0 | unchanged | underflow |
| empty | 1 | 1 | 1 | 0 | +1 | underflow |
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:
// 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.
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.
//===========================================================================
// 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--==========================================================================
-- 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.
| Property | Verilog-2001 | SystemVerilog | VHDL-2008 | Agree? |
|---|---|---|---|---|
| depth, width | parameters | parameters | generics | yes |
| pointer width | clog2(DEPTH) | $clog2(DEPTH) | cnt_width(DEPTH) | yes |
| occupancy width | clog2(DEPTH+1) | $clog2(DEPTH+1) | cnt_width(DEPTH+1) | yes — and different from the pointer |
| acceptance rule | `push && (!full | pop_fire)` | same | |
| pointer wrap | explicit compare | explicit compare | explicit compare | yes |
| occupancy update | one 4-way case | one 4-way case | one 4-way if/elsif | yes |
| head read | combinational (show-ahead) | same | same | yes |
| port declaration style | non-ANSI | ANSI | ANSI | no — language rule |
reading full/empty internally | reads the net | reads the net | internal signals | no — 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:
localparam DEPTH = 16; // power of two
localparam DEPTH3 = 5; // NOT a power of twoAt 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.
//===========================================================================
// 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//===========================================================================
// 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--==========================================================================
-- 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;$ 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 PASSED30 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
| Mutation | What it breaks | Failures |
|---|---|---|
| occupancy counter sized like a pointer | §3's width rule — full unreachable | 19 / 30 |
push && !full — reject a push that coincides with a pop | §7's boundary case | 9 / 30 |
two conditional increments instead of one case | §6's single-update rule | 7 / 30 |
| pointers truncate instead of wrapping explicitly | §6's explicit wrap | 3 / 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:
WIDTH | DEPTH | checks | failures | pushes | pops | simultaneous | overflow | underflow |
|---|---|---|---|---|---|---|---|---|
| 8 | 1 | 2,636 | 0 | 97 | 97 | 21 | 74 | 85 |
| 8 | 2 | 5,212 | 0 | 265 | 265 | 72 | 79 | 100 |
| 8 | 3 | 7,787 | 0 | 416 | 416 | 102 | 82 | 138 |
| 8 | 5 | 12,944 | 0 | 725 | 725 | 164 | 66 | 211 |
| 12 | 10 | 25,905 | 0 | 1,582 | 1,582 | 378 | 70 | 231 |
| 8 | 16 | 41,412 | 0 | 2,601 | 2,601 | 621 | 19 | 316 |
TOTAL: 4,805,704 checks, 0 failuresFour 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.
// 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
Related tutorials
- Related topic
Parity Generation, Checking and Error Detection
One interval, one XOR reduction, and a detection guarantee with a sharp edge: parity catches every corruption that flips an odd number of protected bits and provably misses every even-numbered one — demonstrated, not asserted.
- Related topic
Frame Configurations: 8N1 and the Configuration Space
8N1 names three of the four choices a UART link depends on and omits the one most likely to be wrong. Reading the shorthand, computing what each configuration costs in intervals and line time, and why a longer frame spends timing margin as well as throughput.
- Related topic
Integer Dividers and Baud-Rate Error
The ratio is a fraction and a counter holds an integer, so rounding is a design decision with a measurable cost. Three policies, the actual rate each produces, and why the error belongs in units of a bit period rather than as a bare percentage.
- Related topic
Fractional Baud Generation
An accumulator keeps the remainder an integer divider discards, lengthening an occasional interval so the average converges on the ideal. That improves long-run accuracy by orders of magnitude while making individual intervals unequal — and a UART receiver never measures the average.
Where this fits
Part of the UART curriculum.
