Skip to main content

The FIFO Testbench Plan

Every remaining page in this topic builds one piece of a single testbench, for one DUT. This page introduces that DUT, the interface connecting to it, and the complete block diagram the rest of the topic fills in one component at a time.

The DUT​

A synchronous FIFO with a simple push/pop interface — identical to the one UVM's own curriculum builds a testbench for, deliberately, so the two can be compared component for component later:

interface fifo_if (input bit clk, input bit rst_n);
logic wr_en;
logic [7:0] wr_data;
logic full;
logic rd_en;
logic [7:0] rd_data;
logic empty;
endinterface

wr_en/wr_data push a byte in, full reports when it can't accept more; rd_en pops the oldest byte out onto rd_data, empty reports when there's nothing left. Small enough to hold in your head completely, with just enough internal state (an actual queue, a full/empty condition depending on what's already inside) that a hand-written directed test genuinely can't cover it as convincingly as randomized stimulus checked against an independent reference model can.

One behavior worth stating explicitly, since it's exactly the kind of edge case a reference model has to get right: this DUT is overflow/underflow-protected. Asserting wr_en while full is already high doesn't corrupt the queue or silently overwrite anything — the write is simply ignored, the same way rd_en while empty is high returns no new data rather than popping something that isn't there. A generator that randomizes wr_en/rd_en without any awareness of full/empty will, sooner or later, produce exactly this scenario — which is precisely the kind of legal-but-edge-of-the-state-space stimulus constrained-random testing is good at surfacing, and exactly the case a correct reference model must also refuse to advance on, not just the DUT.

What real FIFOs often add, deliberately left out here​

fifo_if above is intentionally minimal — full and empty only. Many real FIFO designs go further, adding almost_full and almost_empty flags that assert one (or a programmable number of) entries before the hard limit, giving upstream/downstream logic advance warning to throttle before an actual overflow/underflow condition is reached, rather than reacting only at the last possible cycle. Some designs make that threshold itself programmable, rather than fixed at "one entry away." None of that appears in this DUT's interface, and that's a deliberate scope choice, not an oversight: full/empty alone are already enough to teach every testbench-architecture concept this topic builds — a generator, driver, monitor, scoreboard, and reference model behave identically in shape whether the DUT reports two flags or four. A verification plan for a real FIFO with programmable almost-full/almost-empty thresholds would need additional coverage bins and directed corner cases around each threshold boundary — real, extra work this deliberately-simple DUT doesn't require.

Why the reference model is a separate, independent implementation​

It's tempting to assume the reference model could just be the RTL, reused as its own check — but that would prove nothing: a bug baked into the RTL's algorithm would be baked into "the check" too, and every comparison would trivially pass. A reference model earns its name by being an independent implementation of the same specification — ideally written differently enough (a plain behavioral queue model here, rather than anything resembling synthesizable RTL) that a bug in one is unlikely to also exist, identically, in the other. This is the same reasoning real verification teams use when building a golden/reference model in a different language or abstraction level entirely from the design it checks — independence is the entire point, not an implementation detail.

The block diagram​

Generator puts into gen2drv mailbox, Driver gets from it and drives the FIFO DUT via vif; the DUT drives Monitor via vif, Monitor puts into mon2sb mailbox, Scoreboard gets from it and compares against the Reference Model

Reading it left to right: a generator creates and randomizes transactions, put()-ing them into a mailbox. A driver get()s them out and drives the FIFO's pins through a virtual interface — the only component that ever writes to fifo_if. Independently, a monitor watches those same pins (read-only) and reconstructs what it observes into transactions of its own, put()-ing them into a second, separate mailbox. A scoreboard get()s those and compares them against an independently computed reference model — a plain model of "what a correct FIFO would have done," with no access to the DUT's own internals.

Two structural properties are worth naming before any code exists:

  • Two separate mailboxes, not one. gen2drv carries intended stimulus; mon2sb carries observed behavior. Collapsing them into one would mean the scoreboard is checking what was supposed to happen against itself, not against what the DUT actually did — the entire value of an independent monitor is that it never simply trusts the generator's data.
  • The driver never talks to the scoreboard, and the monitor never talks to the driver. Every arrow in the diagram above goes through a mailbox or a virtual interface — no component holds a direct handle to another component's internals. This is what makes each piece independently testable and, later, independently reusable.

What each page builds​

ComponentPageSection
Transaction"The Transaction Class"B
Generator"The Generator"B
Virtual interface handle"The Virtual Interface Bridge"C
Driver"The Driver"C
Monitor"The Monitor"C
Reference model"The Reference Model"D
Scoreboard"The Scoreboard"D
Environment"The Environment"E
Test"The Test"E

What's next​

Section B starts at the left edge of the diagram: the transaction class every other component passes around, and the generator that creates and randomizes it.