Seeds, coverage and directed closure

Record what makes a random run repeatable, measure which situations the stimulus reached, and close the holes that randomness cannot reach with a few directed items.

Lesson
U16.2
Time
25 min
Runs on
Verilator with UVM
Check
5 questions

A FIFO regression runs 64 random items per test, and its coverage report has been stuck at 7 of 9 bins for a week. More items and more seeds close one hole now and then, but never the other, "reset with data": a reset while the FIFO holds values. The item class has constraint c_no_reset { reset == 0; }, so no seed can ever produce that reset.

Coverage of the FIFO bench as the run goes on
012items sent32 random64 random+6 directedbins hit6 of 97 of 99 of 9write when fullreset with datadirected reset

Measured by the example below. More random items cannot hit a bin that the item's constraints forbid.

A seed names one run

Random stimulus looks different every time you change the seed, but it is not unpredictable. A simulator starts its random number generator from a seed. With the same seed, the same code and the same tool version, every random choice repeats exactly, so a failing run can be replayed.

In UVM the simulator seed flows into one value, uvm_global_random_seed, which the library reads from $urandom when it starts. Every seed UVM hands out later is derived from it. A regression therefore records, for each run: the test name, the seed, the source revision, the UVM version and the simulator version. Change any of them and the run may no longer reproduce.

Say a nightly regression fails on seed 81 of 200. Rerun seed 81 on the same build first, to confirm the failure repeats. After the fix, rerun seed 81 to show it passes, then let new seeds run. A seed that passes today proves nothing about the other 199.

UVM reseeds every item by name

A bench changes all the time. If adding a component shifted every random value after it, every old seed would become useless. UVM avoids that with random stability: it gives each object its own seed. That seed is built from the global seed, the object's type name, its full name, and a count of earlier seed requests for that type and name. Reseeding a reused object advances that count too.

start_item does this to the item. It calls the item's reseed(), which reseeds by default because use_uvm_seeding is on. So the item's stream depends on the sequence and the item's name, not on what other threads did first. Run the same test with an extra component elsewhere, and this sequence still produces the same items.

One consequence surprises people. Calling it.srandom(42) before start_item has no effect, because start_item reseeds the item right after it. If you really must pin one item's values, seed it between start_item and randomize(). Usually you should not: pin the run with the global seed instead.

Coverage: did the stimulus reach the cases that matter

A passing scoreboard says that every value it checked was right. It says nothing about which situations were tried. Functional coverage answers that: you list the situations the design must face, each one a bin, and count how often the stimulus reached each.

BinsHit when the monitor sees
level 0 to level 4The FIFO holding that many values after an edge
write when fullA write offered while full, so the design refuses it
read when emptyA read offered while empty
write and readA write and a read accepted at the same edge
reset with dataA reset while values are still stored

The nine bins of the FIFO bench. Every bin comes from the design's specification, not from its code.

In the bench, a subscriber called fifo_coverage receives the monitor's samples on the same analysis port as the scoreboard. It keeps a hit count per bin and can list the holes, the bins with no hits. A covergroup does the same job in standard SystemVerilog, with a coverpoint per variable and a bin per range.

Write the bins from the specification, before looking at the results. A bin list copied from the code only confirms that the code does what it does.

Holes that randomness cannot reach

Two kinds of hole look the same in a report. An unlucky hole needs more items or other seeds. An unreachable hole cannot be hit by this stimulus at all, and the hook's reset bin is one: c_no_reset forbids it. Before adding items, ask whether the constraints even allow the bin.

The example has one hole of each kind. "Write when full" is unlucky. Random writes and reads keep the level near the middle, so four net writes in a row are rare. "Reset with data" is unreachable.

The fix is a short directed sequence aimed at the holes, a step called coverage closure. Five writes make the last one meet a full FIFO, whatever level the random stage left. A reset then finds the FIFO holding data. Six items close the two bins that 64 random items missed.

Keep the directed items few and aimed, and keep the random stages too. Random traffic finds the combinations nobody thought to write down. Directed items then cover the few cases that the specification names and the constraints rule out.

Asks for a reset the class forbids

start_item(it);
if (!it.randomize() with { reset == 1; })
  `uvm_error("RAND", "reset item failed")
finish_item(it);

The inline constraint conflicts with c_no_reset, so randomize() fails and the fields keep their old values. The item goes out without a reset.

Switches the class constraint off first

start_item(it);
it.c_no_reset.constraint_mode(0);
if (!it.randomize() with { reset == 1; })
  `uvm_error("RAND", "reset item failed")
finish_item(it);

Only this item may reset. The other fields are still random, and every other item keeps c_no_reset.

Always check what randomize() returns. A failed call leaves the fields unchanged, and the item is still sent; only the check turns a silent no-op into an error you can see.

Checking and coverage answer different questions

Coverage and checking answer two separate questions, and a run needs both. A hit bin says that the situation happened. It does not say the design handled it well; the scoreboard decides that, value by value.

So close coverage with a working scoreboard attached, as the example does: 9 of 9 bins, and zero mismatches across the same run. A bin hit while the scoreboard is switched off proves only that the stimulus can get there.

Try it

The bench, from the FIFO and its monitor to the coverage subscriber, is in fifo_bench.svh. The test sends 64 random items, then six directed ones, and prints the holes after each stage.

Delete the constraint_mode(0) line and run again: randomize() fails, a RAND error appears, and the last line still shows the reset hole. Then add uvm_global_random_seed = 7; before run_test in tb and compare the random stages.

RUNVerilator + UVM 2020.3.1 ยท simulator service
Edit the code, then run it
Expected among the UVM messages: record: test closure_test, seed 3271625496 after 32 random items: 6 of 9 bins; holes: level 4, write when full, reset with data after 64 random items: 7 of 9 bins; holes: write when full, reset with data after 6 directed items: 9 of 9 bins; holes: none scoreboard: 24 writes, 20 reads, 4 canceled, 0 mismatches UVM_ERROR : 0
Read 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

Predict the outputEach call to send seeds a new item with srandom(42), either before or after start_item, then randomizes it. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

class item_c extends uvm_sequence_item;
  `uvm_object_utils(item_c)
  rand bit [7:0] x;
  function new(string name = "item_c"); super.new(name); endfunction
endclass
class drv_c extends uvm_driver #(item_c);
  `uvm_component_utils(drv_c)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      #10;
      seq_item_port.item_done();
    end
  endtask
endclass
class seed_seq extends uvm_sequence #(item_c);
  `uvm_object_utils(seed_seq)
  function new(string name = "seed_seq"); super.new(name); endfunction
  task send(bit seed_first, output bit [7:0] x);
    item_c it = item_c::type_id::create("it");
    if (seed_first) it.srandom(42);     // seed, then start_item
    start_item(it);
    if (!seed_first) it.srandom(42);    // start_item, then seed
    if (!it.randomize()) `uvm_error("RAND", "randomize failed")
    x = it.x;
    finish_item(it);
  endtask
  task body();
    bit [7:0] a, b, c, d;
    string r1, r2;
    send(1, a); send(1, b);
    send(0, c); send(0, d);
    r1 = "different"; r2 = "different";
    if (a == b) r1 = "same";
    if (c == d) r2 = "same";
    $display("seed first: %s, start_item first: %s", r1, r2);
  endtask
endclass
class seed_test extends uvm_test;
  `uvm_component_utils(seed_test)
  drv_c drv;
  uvm_sequencer #(item_c) sqr;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    drv = drv_c::type_id::create("drv", this);
    sqr = uvm_sequencer #(item_c)::type_id::create("sqr", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
  task run_phase(uvm_phase phase);
    seed_seq seq = seed_seq::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(sqr);
    phase.drop_objection(this);
  endtask
endclass
module tb;
  initial run_test("seed_test");
endmodule
Spot the bugThe sequence stores one value, then must send a reset item to hit the "reset with data" bin. fifo_item has constraint c_no_reset { reset == 0; }. Each option fills in the second item between start_item and finish_item. Which one misses the bin?
ConceptCoverage has stayed at 8 of 9 bins for 50 seeds. What should you check first?
ConceptA failing run must be replayed tomorrow on the same machine. Which record is enough to reproduce its random stimulus?
ConceptA run hits 9 of 9 bins, but the scoreboard was disabled to save time. What does the run show?

Remember

  • Record the test name, seed, source revision and tool versions; together they make a random run repeatable.
  • start_item reseeds each item from its name, so a seed set before start_item is discarded.
  • Coverage bins list the situations the specification requires; holes are bins with no hits.
  • An unreachable hole needs a directed item, and sometimes constraint_mode(0), not more seeds.
  • Check every randomize() result: a failure leaves the old values, and the item is still sent.