The integrated UVM FIFO capstone
Assemble a complete UVM bench for a four-entry FIFO, follow one value from the test to the scoreboard, and end the run with a ledger, coverage and a verdict that a broken design cannot pass.
- Lesson
- U16.3
- Time
- 35 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
Every piece of a FIFO bench passed its own small test: the driver, the monitor, the scoreboard and a reset sequence. Put together, the first test with a reset in the middle fails with a data error. The driver had reset the design, so the FIFO was empty again, but nobody told the scoreboard, which still expected the old values.
| Step | Design holds | Scoreboard expects | Result |
|---|---|---|---|
| Write 10, write 20 | 10, 20 | 10, 20 | - |
| Reset | nothing | 10, 20 (not told) | - |
| Write 30 | 30 | 10, 20, 30 | - |
| Read | - | 10 expected | UVM_ERROR [DATA] got 30, expected 10 |
Each part was right on its own. The bug lived between them: the reset reached the design but not the model.
The bench, piece by piece
The capstone bench uses every idea from this course. Read the table as a map of fifo_bench.svh, the file the example includes.
| Piece | Class | Job | Course |
|---|---|---|---|
| Item | fifo_item | One bus cycle: reset, write, read, data | U2, U4 |
| Sequences | random_seq, directed_seq | Decide which items to send | U4, U10 |
| Driver | fifo_driver | Turns an item into pins for one clock; idles when no item waits | U4 |
| Monitor | fifo_monitor | Samples the pins at each edge and publishes a fifo_sample | U7 |
| Agent | fifo_agent | Holds sequencer, driver and monitor; passive builds the monitor only | U11 |
| Scoreboard | fifo_scoreboard | Queue model: checks every accepted read and both flags | U7, U10 |
| Coverage | fifo_coverage | Counts the nine bins from U16.2 | U16.2 |
| Configuration | fifo_cfg | Interface handle, active or passive, coverage on or off | U2, U11 |
| Test | capstone_test | Sets the configuration, a factory override and the plan | U1, U2 |
The parts of the capstone bench and where each was introduced.
The test never touches the environment's insides at build time. It fills a fifo_cfg object and puts it in the configuration database for env, which passes it to the agent. Everything below the test is reusable: another test changes only configure() and run_phase.
One value, end to end
Follow the write of one value, say 8'h3c, through the bench. Every hand-off is one you have seen in an earlier lesson.
Sequence
A sequence creates a
fifo_itemthrough the factory. The override returns awrite_heavy_item. It callsstart_item, randomizes it to a write of 3c, and callsfinish_item.Driver
At the falling edge the driver takes the item with
try_next_itemand setswrandwdata. It waits for the rising edge, then callsitem_done, which releases the sequence.Design
At that rising edge the FIFO is not full, so it stores 3c and its count goes up by one.
Monitor
At the same edge the monitor reads the pins before the design updates them. It sees a write while not full, so it marks the sample as an accepted write.
Scoreboard and coverage
The analysis port delivers the sample to both subscribers. The scoreboard appends 3c to its queue, and coverage adds a hit to the new fill level.
Later read
When a read is accepted, the scoreboard pops the front of its queue and compares it with
rdata. A difference is aDATAerror.
The monitor decides what the design accepted from the pins alone, never from the driver's intent. That is why a reset counts in the scoreboard: the monitor sees rst high at an edge and publishes a reset sample. The scoreboard then moves every stored value into its canceled count, which fixes the hook's bug.
The test plan as one run
The capstone test runs five stages, each aimed at something the earlier stages cannot show.
Random stages explore; directed stages hit what the random ones cannot, then leave the FIFO empty.
Stage 1 is random traffic. A factory override replaces every fifo_item with write_heavy_item, which never sends a read on its own, so the FIFO fills and writes get refused. Stage 2 resets the design while values are stored, and stage 3 reads the empty FIFO. Stage 4 checks that the bench still works after the reset. Stage 5 sends five reads, enough to empty a four-entry FIFO whatever it holds.
The override is one line in configure(). No sequence or component changed. That is the reason the bench creates every item through the factory.
A type override reaches every fifo_item the bench creates, the directed ones included. A first version of directed_seq randomized its reads with rd == 1 and wr == 0. Under the override every such call failed: six RAND errors, a drain that read nothing, and three values left stored. So directed_seq now assigns its fields, and no constraint applies to them.
The plan also fixes its own length. Stages 2, 3 and 5 send 7 items, and the two random stages send 24 each, so the driver must report 55 items. The test counts what it planned and compares that with the driver's count at the end.
A ledger that must balance
At the end, every value the design accepted must be accounted for exactly once. It was read and checked, canceled by a reset, or is still stored. The ledger is that sum: writes equal reads plus canceled plus stored.
The verdict in report_phase requires six things together. The driver drove every planned item, and at least one read was checked. Nothing is still stored, and the ledger balances. Coverage is 9 of 9, and there are no errors or fatals. Each condition catches a different kind of broken bench. A monitor that sees nothing fails the read count. A driver that hangs before item_done is different: finish_item never returns, the test keeps its objection and report_phase never runs. Only a timeout catches that, such as uvm_root::get().set_timeout(...) or a watchdog in tb.
A wrong flag would slip past a scoreboard that only compares data, because the monitor decides acceptance from the design's own full and empty. A FIFO that claimed to be full at three entries would refuse the fourth write, and the model would agree. So the scoreboard also checks both flags against its queue size at every edge.
Challenge the bench with a broken design
A bench is only trusted once you have seen it fail. Break the design on purpose, one fault at a time, and run the same test. Store bus.wdata + 1 and the first read must give a DATA error. Remove the reset of count, or narrow count to two bits, and the flag check must fire.
A broken design that still passes points to a gap: a missing check, a missing bin or a stage that never reaches the fault. Fix the bench, then run the fault again.
Keep each broken design as a small, named experiment that anyone can rerun. When the bench changes later, run the experiments again. A check that once caught a fault can stop catching it after a refactor, and only a rerun shows that.
Know what this bench does not show. It drives one clock domain and resets on a clock edge, so it says nothing about clock-domain crossings or asynchronous reset. Its queue model is written in SystemVerilog. A C model can replace it through DPI, as module 8 shows, if it is fed the same accepted samples and told about the same resets.
Try it
The run prints three lines of accounting before its verdict. Check the ledger by hand: writes must equal reads plus canceled plus stored, and stored must be 0 after the drain.
Now break the design in fifo_bench.svh. Change memory[wp] <= bus.wdata; to memory[wp] <= bus.wdata + 1; and look for DATA errors and a failed verdict. Restore it, then delete count <= 0; from the reset branch and see which check fires first.
items: 55 planned, 55 driven ledger: 24 writes = 21 reads + 3 canceled + 0 stored checks: 0 mismatches; coverage 9 of 9, holes: none TEST PASSED UVM_ERROR : 0Read included file: fifo_bench.svh
// The four-entry FIFO and its UVM bench.
interface fifo_if (input bit clk);
bit rst = 1, wr = 0, rd = 0;
bit [7:0] wdata = 0;
wire [7:0] rdata;
wire full, empty;
endinterface
module fifo_dut (fifo_if bus);
bit [7:0] memory[4];
bit [1:0] wp = 0, rp = 0;
bit [2:0] count = 0;
wire push = bus.wr && !bus.full; // a write is refused when full
wire pop = bus.rd && !bus.empty; // a read is refused when empty
assign bus.full = (count == 4);
assign bus.empty = (count == 0);
assign bus.rdata = memory[rp];
always @(posedge bus.clk)
if (bus.rst) begin wp <= 0; rp <= 0; count <= 0; end
else begin
if (push) begin memory[wp] <= bus.wdata; wp <= wp + 1'b1; end
if (pop) rp <= rp + 1'b1;
count <= count + push - pop;
end
endmodule
// One bus cycle the sequence asks for.
class fifo_item extends uvm_sequence_item;
`uvm_object_utils(fifo_item)
rand bit reset, wr, rd;
rand bit [7:0] data;
constraint c_no_reset { reset == 0; }
function new(string name = "fifo_item"); super.new(name); endfunction
endclass
// What the monitor saw at one clock edge.
class fifo_sample extends uvm_sequence_item;
`uvm_object_utils(fifo_sample)
bit reset, push, pop, write_full, read_empty, full, empty;
bit [7:0] wdata, rdata;
function new(string name = "fifo_sample"); super.new(name); endfunction
endclass
class fifo_driver extends uvm_driver #(fifo_item);
`uvm_component_utils(fifo_driver)
virtual fifo_if vif;
int items;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
task run_phase(uvm_phase phase);
forever begin
@(negedge vif.clk);
seq_item_port.try_next_item(req);
if (req == null) begin // no item: an idle cycle
vif.rst <= 0; vif.wr <= 0; vif.rd <= 0;
end else begin
vif.rst <= req.reset; vif.wr <= req.wr; vif.rd <= req.rd; vif.wdata <= req.data;
@(posedge vif.clk);
items++;
seq_item_port.item_done();
end
end
endtask
endclass
class fifo_monitor extends uvm_monitor;
`uvm_component_utils(fifo_monitor)
virtual fifo_if vif;
uvm_analysis_port #(fifo_sample) ap;
function new(string name, uvm_component parent); super.new(name, parent); ap = new("ap", this); endfunction
task run_phase(uvm_phase phase);
forever begin
fifo_sample s;
@(posedge vif.clk); // values just before the design updates
s = fifo_sample::type_id::create("s");
s.reset = vif.rst;
s.push = !vif.rst && vif.wr && !vif.full;
s.pop = !vif.rst && vif.rd && !vif.empty;
s.write_full = !vif.rst && vif.wr && vif.full;
s.read_empty = !vif.rst && vif.rd && vif.empty;
s.full = vif.full; s.empty = vif.empty;
s.wdata = vif.wdata; s.rdata = vif.rdata;
ap.write(s);
end
endtask
endclass
// Checks every accepted read against a queue model.
class fifo_scoreboard extends uvm_subscriber #(fifo_sample);
`uvm_component_utils(fifo_scoreboard)
bit [7:0] expected[$];
int writes, reads, canceled, mismatches;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void write(fifo_sample t);
if (t.reset) begin canceled += expected.size(); expected.delete(); return; end
if (t.full != (expected.size() == 4) || t.empty != (expected.size() == 0)) begin
mismatches++;
`uvm_error("FLAGS", $sformatf("full=%0d empty=%0d with %0d values stored", t.full, t.empty, expected.size()))
end
if (t.pop) begin
bit [7:0] want = expected.pop_front();
reads++;
if (t.rdata != want) begin
mismatches++;
`uvm_error("DATA", $sformatf("read %0d: got %02h, expected %02h", reads, t.rdata, want))
end
end
if (t.push) begin expected.push_back(t.wdata); writes++; end
endfunction
function void check_phase(uvm_phase phase);
if (expected.size() != 0) `uvm_error("PENDING", $sformatf("%0d values never read", expected.size()))
endfunction
endclass
// Counts which situations the stimulus reached. Each name is one bin.
class fifo_coverage extends uvm_subscriber #(fifo_sample);
`uvm_component_utils(fifo_coverage)
string names[9] = '{"level 0", "level 1", "level 2", "level 3", "level 4",
"write when full", "read when empty", "write and read", "reset with data"};
int hits[9];
int level;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void write(fifo_sample t);
if (t.reset) begin
if (level > 0) hits[8]++;
level = 0;
return;
end
if (t.write_full) hits[5]++;
if (t.read_empty) hits[6]++;
if (t.push && t.pop) hits[7]++;
level += t.push - t.pop;
hits[level]++;
endfunction
function int covered();
foreach (hits[i]) if (hits[i] > 0) covered++;
endfunction
function string holes();
foreach (hits[i]) if (hits[i] == 0) holes = {holes, holes == "" ? "" : ", ", names[i]};
if (holes == "") holes = "none";
endfunction
endclass
// Settings the test chooses for the environment.
class fifo_cfg extends uvm_object;
`uvm_object_utils(fifo_cfg)
virtual fifo_if vif;
uvm_active_passive_enum is_active = UVM_ACTIVE;
bit has_coverage = 1;
function new(string name = "fifo_cfg"); super.new(name); endfunction
endclass
class fifo_agent extends uvm_agent;
`uvm_component_utils(fifo_agent)
fifo_cfg cfg;
uvm_sequencer #(fifo_item) sqr;
fifo_driver drv;
fifo_monitor mon;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
if (!uvm_config_db #(fifo_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "no fifo_cfg for the agent")
mon = fifo_monitor::type_id::create("mon", this);
mon.vif = cfg.vif;
if (cfg.is_active == UVM_ACTIVE) begin
sqr = uvm_sequencer #(fifo_item)::type_id::create("sqr", this);
drv = fifo_driver::type_id::create("drv", this);
drv.vif = cfg.vif;
end
endfunction
function void connect_phase(uvm_phase phase);
if (cfg.is_active == UVM_ACTIVE) drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclass
class fifo_env extends uvm_env;
`uvm_component_utils(fifo_env)
fifo_cfg cfg;
fifo_agent agent;
fifo_scoreboard sb;
fifo_coverage cov;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
if (!uvm_config_db #(fifo_cfg)::get(this, "", "cfg", cfg))
`uvm_fatal("NOCFG", "no fifo_cfg for the environment")
uvm_config_db #(fifo_cfg)::set(this, "agent", "cfg", cfg);
agent = fifo_agent::type_id::create("agent", this);
sb = fifo_scoreboard::type_id::create("sb", this);
if (cfg.has_coverage) cov = fifo_coverage::type_id::create("cov", this);
endfunction
function void connect_phase(uvm_phase phase);
agent.mon.ap.connect(sb.analysis_export);
if (cfg.has_coverage) agent.mon.ap.connect(cov.analysis_export);
endfunction
endclass
// Builds the configuration and the environment. Tests extend this class.
class fifo_base_test extends uvm_test;
`uvm_component_utils(fifo_base_test)
fifo_cfg cfg;
fifo_env env;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
cfg = fifo_cfg::type_id::create("cfg");
if (!uvm_config_db #(virtual fifo_if)::get(this, "", "vif", cfg.vif))
`uvm_fatal("NOVIF", "no virtual interface")
configure(cfg);
uvm_config_db #(fifo_cfg)::set(this, "env", "cfg", cfg);
env = fifo_env::type_id::create("env", this);
endfunction
virtual function void configure(fifo_cfg cfg); endfunction // tests change settings here
endclass
// Random traffic: n items, each a write, a read, both or neither.
class random_seq extends uvm_sequence #(fifo_item);
`uvm_object_utils(random_seq)
int n = 16;
function new(string name = "random_seq"); super.new(name); endfunction
task body();
repeat (n) begin
fifo_item it = fifo_item::type_id::create("it");
start_item(it);
if (!it.randomize()) `uvm_error("RAND", "randomize failed")
finish_item(it);
end
endtask
endclass
Check yourself
Remember
- The test fills a configuration object and sets factory overrides; the environment below it stays unchanged between tests.
- The monitor decides what the design accepted from the pins, including resets, so the scoreboard can cancel values on a reset.
- Close a run with a ledger: writes equal reads plus canceled plus stored, and stored is zero after the drain.
- A verdict combines item counts, the ledger, coverage and the error count; each catches a different broken bench.
- Trust the bench only after it has failed on designs broken on purpose.