Capstone: the complete layered testbench
Assemble every layer into an environment and a test, decide pass or fail on more than an error count, and see how the same structure maps onto UVM.
- Lesson
- 7.7
- Time
- 40 min
- Runs on
- Verilator
- Check
- 5 questions
A testbench refactor left the scoreboard reading from a mailbox that nothing wrote to. For a week every test printed PASS: 0 errors, because a scoreboard that checks nothing finds nothing wrong. The verdict counted errors and asked nothing about the work done, so a disconnected checker looked exactly like a perfect design.
One column per nightly run. A verdict that looks only at errors cannot tell these runs from good ones.
The whole testbench
Every layer is now built, and each one reuses a module of this course. The classes come from Module 4, the queue model from Module 2 and the random stimulus from Module 5. The processes and mailboxes are Module 3's, and the interface, clocking blocks and assertions are Module 6's.
| Layer | Component | Job | Reuses | Status |
|---|---|---|---|---|
| Test | fifo_test, fifo_config | chooses depth, seed and length, runs the environment, gives the verdict | Module 4 classes | this lesson |
| Generator | fifo_generator | creates fifo_op objects: directed, then seeded random | Modules 4 and 5 | built in 7.3 |
| Mailboxes | mailbox #(fifo_op), mailbox #(fifo_obs) | carry operations and observations between processes | Module 3 | built in 7.3 |
| Driver | fifo_driver | drives one operation per clock through drv_cb | Module 6 | built in 7.3 |
| Monitor | fifo_monitor | samples the pins through mon_cb into a new fifo_obs | Module 6 | built in 7.4 |
| Scoreboard | fifo_scoreboard | predicts each edge with the queue exp_q and compares | Modules 2 and 4 | built in 7.4 |
| Assertions | fifo_props | checks timing rules on the pins at every edge | Module 6 | built in 7.4 |
| Coverage | fifo_coverage, fifo_scenarios | measures which situations the stimulus reached | Module 5 stimulus | built in 7.5 |
| DUT | fifo | the design under test | lesson 7.1 rules | built in 7.2 |
The layered testbench after lesson 7.7.
Configure
fifo_testfills afifo_configwith the depth, the seed and the number of random operations, and prints them.Build and connect
fifo_envcreates the two mailboxes and every component, and hands the driver and monitor the virtual interface.Run
The environment forks the generator, driver, monitor and the scoreboard loop; the assertions run on the pins on their own.
Drain
It waits until every operation is driven, then two more edges, so the last answer is checked.
Decide
report()prints the counts and the verdict, which depends on more than errors.
Environment and test
fifo_env is the one place where components meet. It owns the mailboxes, so no component creates a channel for another, and each component sees only the handles it is given. Swapping the monitor or the scoreboard means changing one line in the environment.
Each channel also carries data in one direction only. The generator only puts operations and the driver only gets them; the monitor only puts observations and the scoreboard only gets them. That makes a hang easy to place, because the side that stopped moving is the one to inspect.
fifo_test holds nothing but choices. A second test, say a long run at depth 3 with another seed, is a few lines that build a different fifo_config and reuse the same environment. That is the payoff of keeping settings out of the components.
The top module stays small: the clock, the interface, the DUT, the assertions, one test object and a watchdog. Everything that verifies lives in classes, which is what lets the same environment check a second FIFO instance or a later version of the design.
Pass means more than zero errors
The verdict in report() requires four things. The scoreboard found no errors, and the model is empty after the drain. Both mailboxes are empty, so nothing was left unprocessed. Finally, every scenario in the test plan happened, from a write refused at full to two laps of the ring.
Errors only
pass = sb.errors == 0;
Passes a run that checked nothing, or one whose stimulus never reached the hard cases.
Errors, leftovers and coverage
pass = sb.errors == 0
&& sb.exp_q.size() == 0
&& req.num() == 0 && obs_mb.num() == 0
&& sc.missing() == "";
Fails if anything was left unchecked or any planned scenario never happened.
A disconnected monitor, as in the opening story, now fails: with no observations, no scenario is ever counted, so sc.missing() lists every one of them. A good verdict makes the testbench prove that it did its work.
Before you trust the testbench, run it with several seeds and at more than one depth, and keep the broken designs from lesson 7.5 in the regression. Together they show that the stimulus reaches the corners, that the checks can fail, and that the verdict notices a run that did not do its work.
From here to UVM
UVM, the Universal Verification Methodology, is a class library that standardises exactly this structure, so testbenches from different teams fit together. Almost every piece you wrote has a UVM counterpart.
| This testbench | UVM |
|---|---|
fifo_op | uvm_sequence_item |
fifo_generator | a uvm_sequence running on a uvm_sequencer |
fifo_driver | uvm_driver |
fifo_monitor and mailbox #(fifo_obs) | uvm_monitor with an analysis port |
fifo_scoreboard | uvm_scoreboard |
fifo_coverage | a uvm_subscriber holding covergroups |
fifo_env, fifo_test | uvm_env, uvm_test |
fifo_config handed to each component | uvm_config_db |
| the end-of-test wait | objections, raised and dropped by sequences and the test |
The same layers, with UVM's names.
UVM supplies the plumbing: building, connecting, phasing and reporting. The judgement stays yours: the model, the timing of each check, the scenarios that must happen and the verdict. The UVM track builds testbenches of this shape, several of them around a FIFO, and the problem bank's FIFO problems use this same design and rules.
One difference you will meet first: UVM splits the generator into a sequence, which decides the operations, and a sequencer, which hands them to the driver when it asks. The driver requests each item instead of reading a mailbox, but the rhythm of one operation per clock is the same.
Try it
The test prints its configuration first, then the environment reports 215 checked edges for 212 operations, every scenario of the plan at least once, and an empty model and mailboxes. Only then does it print PASS.
Change the test to new(bus, DEPTH, 0, 2024), so only the directed cases run, and predict the verdict and the missing scenarios.
fifo_test: depth=4 seed=2024 random operations=200 215 edges, 86 writes, 84 reads, 0 errors scenarios: laps=20 write_blocked=6 read_blocked=28 both_at_full=4 both_at_empty=9 both_between=30 reset_holding=1 PASS: 212 operations driven, model and mailboxes emptyCheck yourself
Remember
- The environment builds every component, owns the channels, runs the layers and gives the verdict.
- A test is a configuration plus a run; new tests reuse the same environment.
- Pass only when there are no errors, nothing is left unchecked, and every planned scenario happened.
- UVM standardises the same layers; the model, the checks and the verdict remain your design.
Practice
- Premature test completion Fix · Easy