Scenario answers show planning before components. Each answer below states the plan first, then the environment, then the checks that close it.
Structure the answer as plan, environment, checks, coverage, closure. Plan extracts features and risk-ranks the full, empty and reset corners highest.
The environment pairs an interface with clocking blocks, a generator-mailbox-driver chain, independent monitors and a queue reference model. Three checkers divide the work: scoreboard for data, assertions for protocol, coverage for proof of exercise.
gen ─mb─► drv ─┐ ┌─► in_mon ──► ┌────────────┐
fifo_if ──► DUT ──► fifo_if ─► out_mon ─► │ scoreboard │
assertions bound to DUT ▲ │ (queue mdl)│
(full/empty/count/data) │ └────────────┘
coverage: boundaries, crosses, backpressureName the corner inventory explicitly: exact boundary levels, flag transitions, simultaneous push and pop near full and empty, a push offered at full, and reset mid-traffic. Closure is a seed sweep with merged coverage and hole analysis.
Four active agents drive, one passive checker watches. The scoreboard checks three property families: exclusivity, legality and fairness within the documented bound.
Stimulus engineers contention instead of hoping for it: all-request bursts, staggered arrivals, one hogging master, and randomized priorities where programmable. Exact rotation needs a policy model; bounds alone cannot prove it.
The properties below are fragments, so this course's checks do not run them. Placed in a module with the signals declared, they run under verilator --assert.
// snippet
// a_excl: assert property (@(posedge clk) disable iff (!rst_n)
// $onehot0(gnt));
// a_legal: assert property (@(posedge clk) disable iff (!rst_n)
// |gnt |-> |(gnt & req));
// a_starv: assert property (@(posedge clk) disable iff (!rst_n)
// (req[0] && !gnt[0]) |-> ##[1:64] gnt[0]);The same two safety properties run here as procedural checks over all 16 request patterns. Exclusivity is a one-bit trick: a one-hot value ANDed with itself minus one is zero.
module prio_arb4 (
input logic [3:0] req,
output logic [3:0] gnt
);
always_comb begin
gnt = 4'b0;
if (req[0]) gnt = 4'b0001;
else if (req[1]) gnt = 4'b0010;
else if (req[2]) gnt = 4'b0100;
else if (req[3]) gnt = 4'b1000;
end
endmodule
module tb_prio_arb4;
timeunit 1ns; timeprecision 1ps;
logic [3:0] req, gnt;
prio_arb4 dut (.*);
int errors = 0;
initial begin
for (int r = 0; r < 16; r++) begin
req = r[3:0]; #1;
if ((gnt & (gnt - 1)) !== 4'b0) errors++; // at most one grant
if ((gnt & ~req) !== 4'b0) errors++; // grant implies request
if ((req == 4'b0) !== (gnt == 4'b0)) errors++;
end
if (errors == 0) $display("PASS: all 16 request patterns grant at most one requester");
else $fatal(1, "FAIL: arbiter grant wrong");
$finish;
end
endmoduleLayer | Tool | What it proves |
|---|---|---|
Structural | CDC lint | Every crossing has a legal synchronizer |
Protocol | Assertions | Stability, handshake hold, one-bit gray steps |
Dynamic | Async ratios plus injection | Tolerance to real skew and metastability |
Simulation alone is structurally blind here: RTL has no metastability, so a violated window still resolves cleanly and deterministically. That sentence is the senior marker.
The contracts below are fragments, so this course's checks do not run them. Placed in a module with the signals declared, they run under verilator --assert.
// snippet
// a_gray: assert property (@(posedge wr_clk) disable iff (!rst_n)
// $countones(wr_ptr_gray ^ $past(wr_ptr_gray)) <= 1);
// a_hold: assert property (@(posedge src_clk) disable iff (!rst_n)
// (req && !ack_sync) |=> req);The gray-code contract runs here as a plain loop: all 16 counter steps, including the wrap, change exactly one bit.
module tb_gray_step;
int errors = 0;
function automatic logic [3:0] to_gray(input logic [3:0] b);
return (b >> 1) ^ b;
endfunction
initial begin : main
logic [3:0] g_now, g_next, diff;
int w, ones;
for (int v = 0; v < 16; v++) begin
w = (v + 1) % 16;
g_now = to_gray(v[3:0]);
g_next = to_gray(w[3:0]);
diff = g_now ^ g_next;
ones = 0;
for (int b = 0; b < 4; b++) if (diff[b]) ones++;
if (ones !== 1) errors++;
end
if (errors == 0) $display("PASS: all 16 gray steps change exactly one bit");
else $fatal(1, "FAIL: gray step wrong");
$finish;
end
endmoduleLocate first, then classify. Frozen sim time means a zero-delay loop; advancing time means something blocks eternally. The blocking checklist is short: mailbox get, missed event triggers, stuck fork branches, leaked semaphore keys, or a DUT handshake with no ack.
HANG TRIAGE TREE
sim time frozen?
├─ YES → zero-delay loop: comb feedback, wait that never yields
└─ NO → blocking wait:
├─ mailbox get: producer dead?
├─ event wait: same-step missed trigger?
├─ join: which branch is stuck?
├─ semaphore: key leaked by disable fork?
└─ DUT handshake: req with no ack?Pause in the debugger and inspect process states, or bisect with timestamped prints at component boundaries. Ship every bench with a global watchdog plus per-component heartbeats, so hangs arrive as reports naming the stuck part.
Question | Decides |
|---|---|
Reproducible on the tapeout RTL, seed pinned | Real bug or artifact |
DUT behavior or checker artifact | Who owns the fix |
Trigger conditions in real traffic | Field likelihood |
Workaround in software or straps | Ship-with-erratum option |
RTL delta blast radius | Re-verification cost |
Triage for decision-grade facts in hours, not a fix. The deliverable is a decision package with options and risk: fix-and-slip, ship-with-erratum, or respin. Heroics are not the answer.
A zero-delay loop: combinational feedback or a wait that never yields time. Pause and profile the spinning process.
Legality caught it. The scoreboard adds the policy model: whether the right requester won under rotation.