The Environment
Every component built so far — generator, driver, monitor, reference model, scoreboard — exists independently, with no single place that owns building and connecting all of them. From Simple to Layered named exactly this as SystemVerilog's own capstone's third gap: driver and scoreboard wired directly inside tb_top. This page fixes it.
One class, owning construction and connection
class environment;
virtual fifo_if vif;
mailbox #(fifo_txn) gen2drv;
mailbox #(fifo_txn) mon2sb;
generator gen;
driver drv;
monitor mon;
fifo_ref_model model;
scoreboard sb;
function new(virtual fifo_if vif, int num_txns);
this.vif = vif;
gen2drv = new();
mon2sb = new();
gen = new(gen2drv, num_txns);
drv = new(vif, gen2drv);
mon = new(vif, mon2sb);
model = new();
sb = new(mon2sb, model);
endfunction
task run();
fork
drv.run();
mon.run();
sb.run();
join_none
gen.run();
endtask
endclass
The constructor is the single place every mailbox gets created and every component gets built and wired — vif flows to drv and mon, gen2drv connects gen to drv, mon2sb connects mon to sb, model connects to sb. Nothing about this wiring lives anywhere else; a new test that wants the identical environment just constructs one environment, instead of hand-assembling five components itself.
fork/join_none: three components running for the whole test
drv.run(), mon.run(), and sb.run() are each infinite forever loops — they need to run concurrently, for the entire duration of the test, exactly the requirement Processes and Fork-Join flagged this topic would need. fork ... join_none launches all three as independent background processes and returns immediately, without waiting for any of them to finish (they never will, on their own). gen.run() runs directly, not forked — it's the one process that's actually supposed to finish, once it's produced its fixed number of transactions.
run() returning doesn't mean the test is doneThe moment gen.run() returns, environment.run() returns too — but drv, mon, and sb are still running in the background, quite possibly still draining the last few transactions through the pipeline. Nothing in this class knows or cares whether that draining has actually finished. This is a real, unresolved problem, not a minor detail — the next page has to solve it before a test can reliably know when to check results and end.
Why fork comes before gen.run(), not after
The order in run() isn't arbitrary: fork ... join_none launching drv/mon/sb happens before the direct call to gen.run(), not after. Swap the two and the environment would still eventually process every transaction correctly — gen2drv is unbounded, so gen.run() could still push every transaction into it without ever blocking — but the pipeline would behave very differently in the meantime: gen.run() (a direct, non-forked call) would run to completion first, entirely on its own, before drv, mon, or sb had ever started running at all. Only once every transaction had already been generated would the driver even begin pulling the first one out.
With fork issued first, drv.run(), mon.run(), and sb.run() are already alive and blocked at their own first @(posedge vif.clk) or mailbox call by the time gen.run() starts pushing transactions — so the driver can pick up the very first transaction while the generator is still producing the rest. This is what makes the testbench genuinely pipelined (driving, monitoring, and scoring transaction N while transaction N+1 is still being generated) rather than a two-phase "generate everything, then process everything" sequence that happens to reach the same final result far more slowly and without ever exercising realistic overlap.
What's next
The environment is fully built and wired, and can be launched with one line. The next page is the piece that actually constructs it, runs one test scenario, and — the open problem flagged above — decides when the test is actually finished.