The environment has four parts. Stimulus is a policy-layered item. Each port has an abstract BFM whose implementation lives inside the port's interface. One monitor samples all three ports at the rising edge and feeds the checkers and coverage. The test is a sequence of phases, each setting the policy knobs and the back-pressure.
test phases A to D ─── knobs ──► cap_item (policy layers) sink: ready_pct
│ randomize() │
▼ ▼
req_bfm[0], req_bfm[1] (abstract) rsp_bfm (abstract)
bfm_impl inside mc_req_if #(PORT) bfm_impl inside mc_rsp_if
│ clocking drv @(negedge clk) │
▼ ▼
ooo_memctl #(BUG) pins ◄─────────────┘
│ sampled at posedge
▼
monitor ─► scoreboard + memory model, coverage, watchdogThe item uses the compile-time policy layers from Policy Classes: id_focus #(write_mix #(hot_window #(mc_item))). (That lesson is optional. All you need here: each layer is a class that extends the layer inside it and adds one constraint, so the item carries the address window, the write share and the ID focus together.) The runtime policy-list form is not used because Verilator 5.048 does not honour constraints reached through a back-reference. Each layer adds one rule with its own knob, so a phase changes knobs, not classes.
Phase | Window | Writes | ID focus | Response ready | Plan rows it targets |
|---|---|---|---|---|---|
A: random mix | words 0-7 | 40% | off | 70% | P1, P2 (cross-ID reorder, strobes, read-after-write) |
B: bursts under stall | words 0-3 | 30% | 70% on one ID per port | 25% | P1 same-ID depth, P3 table full, P4 reset |
C: after reset | words 0-15 | 45% | 50% | 60% | P4 ID reuse after reset, P5 contention |
D: directed empty table | words 0-15 | 0% | 50% | 60% | P3 empty table: wait until nothing is outstanding, then one read, five times |
Phase B is where the risky states live. A narrow window forces reads right behind writes; focusing each port on one ID builds same-ID queues; a 25% ready rate fills the table. The mid-traffic reset is applied 200 cycles into phase B, when the table is busy.
The env holds req_bfm and rsp_bfm handles, the abstract-BFM pattern from Abstract BFM. The implementations sit inside the interfaces, so the env never names a virtual interface. Two details of this capstone are driven by tool behaviour we measured:
Drive through a clocking block. In this bench on Verilator 5.048, when the class inside each interface wrote the request pins with plain nonblocking assignments and the interface had no clocking block, the controller's combinational ready, which depends on both ports' valid, was not re-evaluated. Both ports were accepted in the same cycle. That is a Verilator scheduling bug, not a language rule: nonblocking writes from a class to interface variables are legal and must wake the dependent always_comb. A 40-line reduction did not reproduce it, so the trigger is narrower than this description. Declaring clocking drv @(negedge clk) in the interface and driving drv.valid <= ... removed the problem. A clocking block is the right style anyway: it states the drive edge in one place.
Give each port interface a different parameter. With two instances of the same interface specialization, the class inside the second instance drove the first instance's pins in a Verilator 5.048 test. That is also a tool bug: each interface instance has its own items. The PORT parameter makes mc_req_if #(0) and mc_req_if #(1) different specializations, which avoids that and also lets each BFM report its port number.
Neither issue appeared in the Abstract BFM lesson, because its memory is sequential and its two interfaces already had different widths. They show up when the design has combinational ready logic, which most real handshakes do.
Each driver call waits for ready at the rising edge, and returns early in two cases: reset (the request is dropped by the spec, so the phase stops) and 400 cycles without acceptance (a driver failure is reported by name). A driver that waits forever turns a DUT hang into a simulator timeout with no message, which is the failure mode the drain lesson warns about.
The monitor, not the driver, decides what was accepted. Expected reads enter the scoreboard when the monitor sees the handshake, so a request dropped by reset never becomes an expectation.
In UVM terms, req_bfm is what the driver and monitor call instead of holding a virtual interface, the phases are sequences, and the knobs are sequence fields or config objects.
A wait on a condition can wait forever if a bug or a short test never reaches it, and then the test hangs. A fixed time always fires; coverage row CV8 then records whether the table was busy at that moment, so a weak reset shows up as a hole instead of a hang.
From the abstract API, implemented inside each interface where the parameters are in scope. req_bfm::port_num() returns PORT; a width() method would work the same way.