Skip to main content

The Generator

From Simple to Layered flagged this as the first concrete gap: SystemVerilog's own capstone randomized and put()-ed transactions directly inside tb_top's initial block, tying stimulus generation to that one specific test. This page pulls that logic out into its own class.

A standalone stimulus source​

class generator;
mailbox #(fifo_txn) gen2drv;
int num_txns;

function new(mailbox #(fifo_txn) gen2drv, int num_txns);
this.gen2drv = gen2drv;
this.num_txns = num_txns;
endfunction

task run();
repeat (num_txns) begin
fifo_txn txn = new();
if (!txn.randomize())
$error("Randomization failed");
gen2drv.put(txn);
end
endtask
endclass

run() creates a fresh fifo_txn, randomizes it, and put()s it into gen2drv — repeated num_txns times, a number handed in through the constructor rather than hardcoded, which is already an improvement over inline generation: the same class now works for a 20-transaction smoke test and a 2000-transaction regression run, just by constructing it differently.

Checking randomize()'s return value isn't optional

randomize() returns a bit indicating success — a constraint that can never be satisfied (contradictory bounds, an impossible combination) makes it return 0 silently otherwise. Every call in this topic checks it, exactly as Randomization and Constraints established — a habit worth keeping even here, where fifo_txn has no constraints to conflict with yet.

The mailbox's size is a design decision too​

gen2drv is declared with no size argument, which SystemVerilog treats as unbounded: put() on an unbounded mailbox never blocks, no matter how far the generator gets ahead of whatever's draining gen2drv on the other end. That's convenient here — nothing in run() needs to coordinate with the driver's pace — but it's a choice, not the only option. Constructing a mailbox with a positive size instead (new(1), say) makes it bounded: once it holds that many un-collected items, a further put() blocks until a get() makes room.

A bounded mailbox is a deliberate back-pressure mechanism — it forces the generator to slow down to the driver's actual pace rather than silently queuing transactions arbitrarily far ahead, which matters most when the two need to stay roughly in lockstep (a driver that must react to DUT state before accepting the next transaction, for instance). This generator doesn't need that coupling — an unbounded gen2drv is the right default when the driver is expected to keep up — but recognizing bounded-vs-unbounded as an explicit sizing decision, not just default behavior, is worth carrying forward: the same choice reappears wherever a mailbox connects two independently-paced testbench components.

Whichever sizing is chosen, mailbox always preserves order: it's a strict first-in-first-out queue, so the driver's gen2drv.get(txn) always retrieves transactions in exactly the order the generator's put() calls produced them, regardless of bounded/unbounded sizing. That ordering guarantee is precisely why nothing in this pipeline needs a sequence number or a timestamp to know which transaction is "next" — the mailbox itself already enforces it.

fifo_txn's op field (The Transaction Class) is declared plain rand, which means each call to txn.randomize() here picks a new WRITE/READ value uniformly at random — including, entirely legally, the same value several calls in a row (WRITE, WRITE, WRITE, READ, WRITE, ...). SystemVerilog has a second random qualifier, randc ("random cyclic"), that behaves differently: a randc variable's solver won't repeat a value until every value in its declared range has appeared at least once, only then starting a fresh cycle. Had op been declared randc op_e op; instead, run()'s num_txns calls would guarantee a WRITE and a READ alternate far more evenly — useful when a test specifically needs to exercise every enum value quickly and repeatedly, rather than trusting uniform randomness to eventually cover all of them. Plain rand remains the right choice for op here precisely because part of this DUT's real behavior — several consecutive writes, then several consecutive reads, potentially pushing full or empty — is exactly the kind of legal-but-uneven sequence randc would make harder to generate, not easier.

What this generator can't do​

Named honestly, because the gap is exactly what motivates UVM's answer to it later:

  • No arbitration. There's exactly one generator, feeding exactly one mailbox. If a test ever needs two independent sources of stimulus — normal traffic plus an occasional error-injection transaction, say — nothing here has a concept of interleaving or prioritizing between them.
  • No mid-run reconfiguration. num_txns is fixed at construction. Changing the stimulus shape mid-test (react to something the scoreboard just saw, for instance) isn't something this class's shape supports at all.
  • Stimulus and drive timing are still coupled through the mailbox's blocking behavior, but nothing about how those transactions get onto the DUT's pins is this class's concern — that's the next page's job entirely.

UVM's own Sequencer & Sequence, and later its Sequence Libraries & Arbitration, are the direct answer to the first two gaps — a generalized version of exactly this class, with a real arbitration mechanism behind it. Worth reading now, briefly, if only to see how closely generator.run()'s shape maps onto a UVM sequence's body().

What's next​

The generator has stimulus ready in a mailbox. Section C builds the two components that actually touch the DUT: a driver that consumes it, and a monitor that independently watches what happens as a result — starting with the mechanism that lets a class reach real hardware pins at all.