Build the monitor and scoreboard

Watch the pins with a monitor that only observes, predict every edge with a queue model, and check each answer one edge after the read that asked for it.

Lesson
7.4
Time
30 min
Runs on
Verilator
Check
5 questions

The first run of a new scoreboard reported 40 errors on a FIFO that was working, one for every read. At the edge where a read was accepted, the scoreboard popped its model and compared the result with read_data straight away. The FIFO's answer only appears after that edge, so every comparison looked at the previous read's data.

A read and its answer, one edge apart
01234read_en (sampled)read_valid (sampled)read_data (sampled)001122too-early check-ERROR-ERROR-the answer to edge 1

One column per rising edge, with values sampled just before it. The answer to the read at edge 1 is visible at edge 2.

A monitor only watches

The monitor reads the pins and writes nothing. At every rising edge it waits for @(vif.mon_cb) and copies all eight signals from the clocking block, which samples them just before the edge (#1step, Module 6). For the inputs, that is the request this edge acts on; for the outputs, it is the result of the previous edge.

It puts its findings into a new fifo_obs object for every edge and sends it through mailbox #(fifo_obs). Creating a new object each time matters for the same reason as in the generator. An object in a mailbox belongs to the receiver, and the monitor must never change it again.

Keeping the monitor apart from the driver also means it reports what really happened on the pins, including anything the driver got wrong.

Predict at one edge, check at the next

The scoreboard holds the model from lesson 7.1: the queue exp_q and its depth. For each observation, it does four things in a fixed order.

  1. Check the answer to the previous edge

    If that edge accepted a read, due is 1 and due_data holds the item it should return. read_valid must equal due, and when it is 1, read_data must equal due_data.

  2. Check the flags

    full and empty must match the size of exp_q before this edge.

  3. Decide this edge

    From the size of exp_q, decide take_w and take_r, exactly as the rules say.

  4. Update the model

    On a read, pop the oldest item into due_data and set due; on a write, push write_data. On reset, empty the queue and expect no answer.

Step 1 is what the opening story got wrong: the answer is checked one edge after the decision, because the FIFO registers it. due is the scoreboard's memory of a read that is still on its way.

The scoreboard also counts edges, writes and reads as it goes. Those counts show that checking really happened, and lesson 7.7 builds a verdict that demands proof that checking happened. Each error message names the edge and both values, and printing stops after three, so a broken design cannot bury the first and most useful message.

Because the scoreboard handles one observation at a time, you can also test it on its own by feeding it observations by hand, as the first quiz item below does. That is far quicker than debugging the scoreboard through a full simulation.

Keep the model independent

The scoreboard decides acceptance from its own count, never from the FIFO's flags. If it trusted full, a FIFO whose full never rose would look right: the scoreboard would accept every write the FIFO accepted, and both would agree on a wrong answer.

Trusting the DUT's flag

take_w = o.write_en && !o.full;

If full is broken, the model copies the bug, and nothing is reported.

Using the model's own count

take_w = o.write_en && exp_q.size() < depth;

A broken full now disagrees with the model, and step 2 reports it.

For the same reason, the model is a queue rather than a copy of the RTL's pointers. A wrap bug copied into the checker would wrap the same wrong way and agree with the FIFO.

Assertions on the pins

fifo_props adds the pin-level checks from Module 6 as concurrent assertions. full and empty are never 1 together, and reset leaves the FIFO empty with read_valid at 0. An accepted read is answered at the next edge, and no other edge raises read_valid.

The two kinds of check cover different ground. Assertions see timing rules the moment they break and name the rule; the scoreboard checks data and order across many cycles, which no single property can.

Where the testbench stands

LayerComponentJobReusesStatus
Testfifo_test, fifo_configchooses depth, seed and length, runs the environment, gives the verdictModule 4 classeslesson 7.7
Generatorfifo_generatorcreates fifo_op objects: directed, then seeded randomModules 4 and 5built in 7.3
Mailboxesmailbox #(fifo_op), mailbox #(fifo_obs)carry operations and observations between processesModule 3built in 7.3
Driverfifo_driverdrives one operation per clock through drv_cbModule 6built in 7.3
Monitorfifo_monitorsamples the pins through mon_cb into a new fifo_obsModule 6this lesson
Scoreboardfifo_scoreboardpredicts each edge with the queue exp_q and comparesModules 2 and 4this lesson
Assertionsfifo_propschecks timing rules on the pins at every edgeModule 6this lesson
Coveragefifo_coverage, fifo_scenariosmeasures which situations the stimulus reachedModule 5 stimuluslesson 7.5
DUTfifothe design under testlesson 7.1 rulesbuilt in 7.2

The layered testbench after lesson 7.4.

The example runs the directed cases and 40 random operations through every layer built so far. It waits until the driver has driven everything, then two more edges: one for the last operation to be sampled and one for its answer to be checked.

Try it

Every edge was checked: 55 observations, 20 accepted writes and 17 accepted reads, and nothing left in the model after the drain. The 3 writes not read back were discarded by resets.

Now break the RTL: change read_valid <= take_r; to if (take_r) read_valid <= 1; and predict which check reports first, the scoreboard or an assertion.

RUNVerilator · simulator service
Edit the code, then run it
Expected: edges=55 writes=20 reads=17 left=0 errors=0

Check yourself

Predict the outputObservations are fed to the scoreboard by hand. This FIFO answers a read at the same edge, instead of one edge later. Predict the exact output.
// The FIFO's pins, with the clocking blocks from module 6.
interface fifo_if #(parameter int WIDTH = 8) (input bit clk);
  bit             reset, write_en, read_en;
  bit [WIDTH-1:0] write_data;
  bit             full, empty, read_valid;
  bit [WIDTH-1:0] read_data;
  clocking drv_cb @(posedge clk);
    default input #1step output #2;
    output reset, write_en, read_en, write_data;
    input  full, empty;
  endclocking
  clocking mon_cb @(posedge clk);
    default input #1step;
    input reset, write_en, read_en, write_data, full, empty, read_valid, read_data;
  endclocking
endinterface

// What the monitor saw at one rising edge: every value is from just before the edge.
class fifo_obs;
  bit       reset, write_en, read_en, full, empty, read_valid;
  bit [7:0] write_data, read_data;
endclass

// Samples the pins through mon_cb and publishes one fifo_obs per edge.
class fifo_monitor;
  virtual fifo_if     vif;
  mailbox #(fifo_obs) obs_mb;
  function new(virtual fifo_if vif, mailbox #(fifo_obs) obs_mb);
    this.vif = vif; this.obs_mb = obs_mb;
  endfunction
  task run();
    forever begin
      fifo_obs o = new();                      // a fresh object for every edge
      @(vif.mon_cb);
      o.reset      = vif.mon_cb.reset;
      o.write_en   = vif.mon_cb.write_en;
      o.read_en    = vif.mon_cb.read_en;
      o.write_data = vif.mon_cb.write_data;
      o.full       = vif.mon_cb.full;
      o.empty      = vif.mon_cb.empty;
      o.read_valid = vif.mon_cb.read_valid;
      o.read_data  = vif.mon_cb.read_data;
      obs_mb.put(o);
    end
  endtask
endclass

// Checks every edge against a queue model (module 2) of what the FIFO should hold.
class fifo_scoreboard;
  int       depth;
  bit [7:0] exp_q [$];      // what the FIFO should hold, oldest first
  bit       due;            // a read was accepted at the previous edge...
  bit [7:0] due_data;       // ...and must show this value now
  int       edges, writes, reads, errors;
  int       max_msgs = 3;     // how many error messages to print
  function new(int depth);
    this.depth = depth;
  endfunction
  function void error(string what);
    errors++;
    if (errors <= max_msgs) $display("ERROR at edge %0d: %s", edges, what);
  endfunction
  function void check(fifo_obs o);
    bit take_w, take_r;
    edges++;
    // 1. The response to the previous edge.
    if (o.read_valid != due) error($sformatf("read_valid=%0d, expected %0d", o.read_valid, due));
    else if (due && o.read_data != due_data) error($sformatf("read_data=%h, expected %h", o.read_data, due_data));
    // 2. The flags, against the model's occupancy before this edge.
    if (o.full != (exp_q.size() == depth) || o.empty != (exp_q.size() == 0))
      error($sformatf("full=%0d empty=%0d with %0d stored", o.full, o.empty, exp_q.size()));
    // 3. What this edge does, decided on the occupancy before it.
    due = 0;
    if (o.reset) begin
      exp_q.delete();
    end else begin
      take_w = o.write_en && exp_q.size() < depth;
      take_r = o.read_en  && exp_q.size() > 0;
      if (take_r) begin due_data = exp_q.pop_front(); due = 1; reads++; end
      if (take_w) begin exp_q.push_back(o.write_data); writes++; end
    end
  endfunction
endclass

module tb;
  fifo_scoreboard sb = new(2);
  function automatic fifo_obs obs(bit w, bit r, bit [7:0] d, bit empty, bit valid, bit [7:0] rd);
    fifo_obs o = new();
    o.write_en = w; o.read_en = r; o.write_data = d;
    o.empty = empty; o.read_valid = valid; o.read_data = rd;
    return o;
  endfunction
  initial begin
    sb.max_msgs = 0;
    sb.check(obs(1, 0, 8'h11, 1, 0, 8'h00));   // write 11 into an empty FIFO
    sb.check(obs(0, 1, 8'h00, 0, 1, 8'h11));   // read; the answer is already there
    sb.check(obs(0, 0, 8'h00, 1, 0, 8'h11));   // idle
    $display("errors=%0d", sb.errors);
  end
endmodule
Spot the bugEach option replaces step 3 and 4 of check() for the non-reset case. The FIFO under test is correct. Which option makes the scoreboard report errors on it?
ConceptWhy does the monitor create a new fifo_obs at every edge instead of filling one object again and again?
ConceptA FIFO's full output never rises, but the FIFO still refuses writes when it is full. Which scoreboard notices?
ConceptWhich check is a good fit for an assertion rather than the scoreboard?

Remember

  • The monitor samples through mon_cb, writes nothing, and publishes a new fifo_obs for every edge.
  • The scoreboard checks the previous edge's answer, checks the flags, then decides and updates this edge.
  • An accepted read is checked one edge later; due and due_data remember it until then.
  • Decide acceptance from the model's own count, never from the DUT's flags.
  • Assertions catch pin timing rules at once; the scoreboard checks data and order over time.

Practice