Out-of-order checking

Check a design that answers out of order by keying predictions on the transaction tag, keep same-tag transactions in order, and report orphans and the reads that never came back.

Lesson
U8.2
Time
30 min
Runs on
Verilator with UVM
Check
5 questions

A two-bank memory passes every directed test and fails the first random one with twenty mismatches. Every value it returned was right. The scoreboard popped the oldest prediction for each response, and the fast bank had answered before the slow one.

Bank 0 overtakes bank 1
01234edge (ns)3545556575read taken (tag, bank)1, b12, b0-response tag-21in-order scoreboard expects-12mismatchmismatch

Both responses are right. The queue-based scoreboard from U7.3 pairs each with the wrong prediction.

A design that may reorder

The design in this lesson is a memory with two banks. A read carries a 4-bit tag, a bank bit and an address, and the response carries the tag and the data. Bank 0 can answer two edges after a read is taken, bank 1 four; those are the uncontended latencies as the monitor sees them. Only one response fits per edge, and when both banks are ready bank 0 goes first, so a response can wait, as the table's second tag 3 shows.

So responses leave in a different order from the requests, and that is correct behaviour. Fabrics, memory controllers and caches all do it: a resource may be busy, so the design accepts the next request rather than wait. Advanced SoC buses such as AXI allow it and use an ID to tie responses back to requests.

They also impose a rule, and this memory follows it: reads that share a tag are answered in the order they were issued, whatever their bank. The two tag 3 reads in the table show it, and the next section follows them step by step.

Taken atReadReady atAnswered atSeen by the monitor
35 nstag 1, bank 165 ns65 ns75 ns
45 nstag 2, bank 055 ns55 ns65 ns
55 nstag 3, bank 185 ns85 ns95 ns
65 nstag 3, bank 075 ns95 ns, after the older tag 3105 ns
75 nstag 5, bank 1105 nsnever: the bench's fault switch drops it-

The example's five reads. Tag 2 overtakes tag 1; the second tag 3 waits for the first.

Choosing the key

An out-of-order comparator stores predictions until the matching response arrives, so it needs a key: something the response carries that identifies its request. Here the tag is that key. When the protocol has an ID, the ID is the key, and nothing else is as good. The key decides everything downstream: how predictions are stored, how a response finds its own, and what the end-of-test report can name.

Without an ID the key has to be built from the payload. A packet switch might key on a sequence number inside the packet, or on a hash of the whole payload. The Cookbook's out-of-order comparator asks the transaction for an index_id() function and uses whatever it returns. An address is a poor key when the same address can be in flight twice, and a value the response does not carry cannot be a key at all. A key is unique only within a scope. Two requesters on a fabric may both issue tag 3, so the key there is the pair of port and tag, and the scoreboard stores that composite. A repeated key is two requests that both still owe a response; a duplicate response is one request answered twice, and only the second is an error.

An associative array of queues

The storage is mem_rsp pending[bit [3:0]][$]: an associative array keyed by tag, whose elements are queues of predictions, oldest first. A read pushes its prediction onto the queue of its tag. A response pops the front of the queue of its tag and compares. The array grows a key the first time a tag is used and keeps it, empty or not, until the test ends.

Lookup by key, then the oldest prediction with that key.
mem_rsp pending[bit [3:0]][$];            // by tag, oldest first

// on a READ request
pending[t.tag].push_back(e);

// on a response
if (!pending.exists(r.tag) || pending[r.tag].size() == 0) begin
  orphans++;
  `uvm_error("SB", $sformatf("response %s has no read waiting", r.convert2string()))
  return;
end
e = pending[r.tag].pop_front();
if (!e.compare(r)) `uvm_error("SB", ...)

Why a queue and not one slot per tag? Because the same tag may be outstanding twice, and the oldest read is answered first.

Two reads with tag 3, two banks, one queueArrows from a read go to its bank and to the scoreboard's pending[3] (a push); arrows from a bank carry the response; the arrow from pending[3] is the pop. The steps run in time order.
Two reads with tag 3, two banks, one queueArrows from a read go to its bank and to the scoreboard's `pending[3]` (a push); arrows from a bank carry the response; the arrow from `pending[3]` is the pop. The steps run in time order. request READ tag 3, bank1 taken at 55 ns; modelsays 22 request READ tag 3, bank0 taken at 65 ns; modelsays 11 queue pending[3] [22] at 55 ns, [22,11] at 65 ns bank Bank 1 ready and driven at85 ns bank Bank 0 ready at 75 ns, helduntil the older tag 3has gone; driven at95 ns response Response tag 3 =22 seen at 95 ns:pop_front gives 22,match response Response tag 3 =11 seen at 105 ns:pop_front gives 11,match push 22seen 95seen 105pop_front
  1. 55 ns

    The first tag 3 read is taken. The scoreboard pushes its prediction, 22; the read goes to bank 1.

    More context

    The prediction is made when the read is taken, from the model's byte at address 2. Both happen at the same edge.

    Follow the calls, values and waits
    1. data prediction 22: READ tag 3, bank 1 → pending[3]
    2. data READ tag 3 (bank 1): READ tag 3, bank 1 → Bank 1
  2. 65 ns

    The second tag 3 read is taken. Its prediction, 11, goes behind 22; the read goes to bank 0.

    More context

    A repeated tag is legal while the first read is in flight. pending[3] now holds [22, 11], oldest first.

    Follow the calls, values and waits
    1. data prediction 11: READ tag 3, bank 0 → pending[3]
    2. data READ tag 3 (bank 0): READ tag 3, bank 0 → Bank 0
  3. 75 ns

    Bank 0 is ready first, but an older tag 3 read is still in bank 1, so the same-tag rule holds it back.

    Follow the calls, values and waits
    1. Waiting: Bank 0 — held behind the older tag 3
  4. 85 ns

    Bank 1 drives 22 at 85 ns. The monitor samples it at the next edge.

  5. 95 ns

    The monitor sees 22, and pop_front returns 22: match. Bank 0 may now drive 11, at 95 ns.

    More context

    The log line "95 ns rsp tag=3 data=22 matches READ tag=3 bank=1 ... taken at 55 ns" is this pair. The front entry answers the oldest read with the tag.

    Follow the calls, values and waits
    1. data response tag 3 = 22: Bank 1 → Response tag 3 = 22
    2. data prediction 22: pending[3] → Response tag 3 = 22
    3. Released: Bank 0
  6. 105 ns

    The monitor sees 11. pop_front returns 11, the only entry left: match, and pending[3] is empty.

    More context

    With one slot per tag instead of a queue, the second push would have overwritten 22, and the 95 ns response would have mismatched.

    Follow the calls, values and waits
    1. data response tag 3 = 11: Bank 0 → Response tag 3 = 11
Read the diagram as text
  • READ tag 3, bank 1: taken at 55 ns; model says 22
  • READ tag 3, bank 0: taken at 65 ns; model says 11
  • pending[3]: [22] at 55 ns, [22, 11] at 65 ns
  • Bank 1: ready and driven at 85 ns
  • Bank 0: ready at 75 ns, held until the older tag 3 has gone; driven at 95 ns
  • Response tag 3 = 22: seen at 95 ns: pop_front gives 22, match
  • Response tag 3 = 11: seen at 105 ns: pop_front gives 11, match
  • READ tag 3, bank 1 → pending[3]: push 22
  • READ tag 3, bank 0 → pending[3]
  • READ tag 3, bank 1 → Bank 1
  • READ tag 3, bank 0 → Bank 0
  • Bank 1 → Response tag 3 = 22: seen 95
  • Bank 0 → Response tag 3 = 11: seen 105
  • pending[3] → Response tag 3 = 22: pop_front

The queue per tag is the same-tag ordering rule written as storage: its front entry is always the one the next response with that tag answers.

A plain pending[tag] = e silently overwrites the first prediction. The quiz program shows the symptoms: the first response mismatches against the second prediction, and the second response finds nothing.

The whole example run, edge by edge
The example run, by edge
01234567edge (ns)35455565758595105read taken1235-response-21-3waiting tags after11 21 2 31 3 33 3 53 55tag 2 before tag 1tag 5 stays

Each response removes exactly one prediction, the oldest with its tag. Tag 5 is never answered.

Orphans, duplicates and reads that never return

Three things can go wrong around the array, and each needs a different report.

Which report fires, and whenArrows show where each prediction or response goes. Every path that loses one ends in an error.
Which report fires, and whenArrows show where each prediction or response goes. Every path that loses one ends in an error. event Read taken write_req event Response seen write_rsp queue pending[tag]push_back a repeated tag islegal: same-tag readsmay be in flight check pop_front andcompare the oldest predictionof this tag error Orphan error nothing waiting forthis tag error Watchdog error,once oldest prediction of atag older thanmax_age (50 ns);flagged, still waiting error check_phaseerror per tag: waitingpredictions thewatchdog has notreported predictiontag has a predictiontag has noneoldest firsttoo oldstill waiting at theend
  1. A read is taken55 ns (illustrative)

    write_req pushes the prediction for the first tag 3 read onto pending[3].

    More context

    A second tag 3 read at 65 ns pushes behind it. A repeated tag is not an error here.

    Follow the calls, values and waits
    1. data tag 3: expect 22: Read taken → pending[tag] push_back
  2. A read that will not return75 ns (illustrative)

    The tag 5 read is taken and its prediction pushed. The bench's fault switch makes the memory drop it.

    More context

    tag_mem #(.LOSE_TAG(5)) takes reads with tag 5 and never answers them: a planted fault that proves the reports work.

    Follow the calls, values and waits
    1. data tag 5 prediction: Read taken → pending[tag] push_back
    2. Waiting: pending[tag] push_back — tag 5 owed since 75 ns
  3. Answered: pop and compare95 ns (illustrative)

    The tag 3 response finds a prediction. pop_front gives the oldest one, 22, and the pair matches.

    More context

    A mismatch would be reported here, in write_rsp, with the read it answers.

    Follow the calls, values and waits
    1. data response tag 3 = 22: Response seen → pop_front and compare
    2. data tag 3: expect 22: pending[tag] push_back → pop_front and compare
  4. Too old: the watchdog130 ns (illustrative)

    Every 10 ns the watchdog looks at the oldest prediction of each tag. Tag 5's is now older than 50 ns: one error, and the prediction is flagged but stays in the array.

    More context

    The log reads "130 ns watchdog tag 5: READ tag=5 bank=1 addr=2 taken at 75 ns, unanswered after 50 ns". The flag stops a second report.

    Follow the calls, values and waits
    1. data tag 5 older than 50 ns: pending[tag] push_back → Watchdog error, once
    2. Waiting: pending[tag] push_back — tag 5 flagged, still owed
  5. The end of the run175 ns (illustrative)

    The bounded wait ends the run. check_phase reports, per tag, waiting predictions the watchdog has not flagged; tag 5 is flagged, so the run ends with the watchdog's one error.

    More context

    The ledger still counts tag 5: reads 5, checked 4, waiting 1.

  6. Nothing waiting: an orphan

    A response whose tag has no prediction cannot be compared with anything. write_rsp counts an orphan and reports it at once.

    More context

    The example has no orphans. The one-slot-per-tag quiz program produces one: its second tag 3 response finds the slot already gone.

    Follow the calls, values and waits
    1. data response, no tag waiting: Response seen → Orphan error
    2. Released: pending[tag] push_back
  7. Lost late in the run

    A read lost within the last 50 ns of a run never grows old enough for the watchdog. Its prediction reaches check_phase unflagged and is reported there, with its tag and time.

    More context

    Either way the report names the read. A count alone would say only that something was lost.

    Follow the calls, values and waits
    1. data unflagged prediction: pending[tag] push_back → check_phase error
Read the diagram as text
  • Read taken: write_req
  • Response seen: write_rsp
  • pending[tag] push_back: a repeated tag is legal: same-tag reads may be in flight
  • pop_front and compare: the oldest prediction of this tag
  • Orphan error: nothing waiting for this tag
  • Watchdog error, once: oldest prediction of a tag older than max_age (50 ns); flagged, still waiting
  • check_phase error: per tag: waiting predictions the watchdog has not reported
  • Read taken → pending[tag] push_back: prediction
  • Response seen → pop_front and compare: tag has a prediction
  • Response seen → Orphan error: tag has none
  • pending[tag] push_back → pop_front and compare: oldest first
  • pending[tag] push_back → Watchdog error, once: too old
  • pending[tag] push_back → check_phase error: still waiting at the end

A duplicate prediction is legal for this protocol; a protocol that forbids it would report it in write_req.

The example turns on a fault switch in the bench: tag_mem #(.LOSE_TAG(5)) makes the memory take reads with tag 5 and never answer them. That is how you prove the report works before you need it. In the example the watchdog reports the lost read during the run: tag 5: READ tag=5 bank=1 addr=2 taken at 75 ns, unanswered after 50 ns. A read lost within the last 50 ns of a run would reach check_phase instead, as tag 5: 1 read(s) never answered, first: READ tag=5 bank=1 addr=2 at 75. Either way the report names the read. A count alone would say that something was lost; the tag, bank, address and time say where to look in the waveform.

A long test should not wait until the end to learn about a hole. A per-key watchdog finds it near the cause. The example scoreboard's run_phase checks every 10 ns whether the oldest prediction of any tag is older than max_age, this test's limit of 50 ns. If one is, it reports that tag at once, while the waveform around the request is still short enough to read. Tag 5 is reported at 130 ns, 45 ns before the test ends.

The watchdog marks what it reports and leaves it in the array, so the ledger still counts it and the end-of-test report skips it.
time max_age = 50ns;                        // a time, so the comparison is exact
task run_phase(uvm_phase phase);
  forever begin
    #10ns;
    foreach (pending[t])
      if (pending[t].size() != 0 && !pending[t][0].timed_out && $time - pending[t][0].at > max_age) begin
        pending[t][0].timed_out = 1;              // report each prediction once
        `uvm_error("SB", $sformatf("tag %0d: %s taken at %0t still unanswered after %0d ns",
                   t, pending[t][0].req.convert2string(), pending[t][0].at, max_age))
      end
  end
endtask

Three decisions make a watchdog usable. It reports each prediction once, through a flag, instead of once per cycle. It never removes the prediction: the read is still owed, so waiting() keeps counting it and the ledger of U8.4 stays honest. And it watches only the oldest prediction of each tag. A younger one with the same tag cannot be answered first, and check_phase reports it with its tag at the end.

Ending a test when a result may never come

U7.3 ends a run on planned work plus a quiet window. With a lost read the work never completes, and an unbounded wait would end the test only at the global timeout, with a message about the timeout, not about tag 5. The example bounds the wait: a fork of the wait and a 100 ns delay, join_any, disable fork, with a flag that records which branch finished. Whichever finishes first, the objection drops and the watchdog's report and check_phase tell the real story. When the counts do balance, the U8.1 rule applies: keep observing for a quiet window before ending. This memory has no architectural maximum latency: bank 0 always wins the output, so a bank 1 read can wait behind any number of bank 0 reads. The 50 ns is this test's policy, the limit the watchdog enforces, chosen above the 40 ns longest latency this script produces. After a balance the example observes 50 ns more, so a late extra response still reaches the scoreboard as an orphan. U10.3 covers bounded waits in more depth.

Try it

Tag 2 is answered before tag 1 and both match, because each response looks up its own tag. The two tag 3 reads come back in issue order, bank 1 first, so pop_front pairs them correctly. The one error is the watchdog's report for tag 5 at 130 ns, 55 ns after the read the bench's fault switch lost on purpose; check_phase skips a prediction the watchdog has already reported, so the count stays at one.

Set LOSE_TAG to 16 in tb and the run is clean. Then change pop_front to pop_back and watch the tag 3 pair mismatch.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 65 ns rsp tag=2 data=11 matches READ tag=2 bank=0 addr=1 taken at 45 ns, waiting: 1 3 3 75 ns rsp tag=1 data=22 matches READ tag=1 bank=1 addr=2 taken at 35 ns, waiting: 3 3 5 95 ns rsp tag=3 data=22 matches READ tag=3 bank=1 addr=2 taken at 55 ns, waiting: 3 5 105 ns rsp tag=3 data=11 matches READ tag=3 bank=0 addr=1 taken at 65 ns, waiting: 5 130 ns watchdog tag 5: READ tag=5 bank=1 addr=2 taken at 75 ns, unanswered after 50 ns reads 5, checked 4, mismatches 0, orphans 0, waiting 1 (tags: 5) UVM_ERROR : 1

Check yourself

Predict the outputThis scoreboard keeps one slot per tag, mem_rsp slot[bit [3:0]], so a new prediction replaces any older one with the same tag, and a response with no slot is an orphan. The fault switch is off and the sequence is the example's. What line does report_phase print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

interface mem_if (input logic clk);
  logic       req_valid = 0;
  logic [1:0] req_kind  = 0;    // 0 read, 1 write, 2 control write
  logic [3:0] req_tag   = 0;
  logic       req_bank  = 0;
  logic [3:0] req_addr  = 0;
  logic [7:0] req_data  = 0;
  logic       rsp_valid = 0;
  logic [3:0] rsp_tag   = 0;
  logic [7:0] rsp_data  = 0;
endinterface

// A two-bank memory with tagged reads. Bank 0 is ready to answer one edge after a read is taken,
// bank 1 three edges after; the response is driven at that edge and a monitor sampling at rising
// edges sees it one edge later (two and four edges after the read, when nothing is in the way).
// One response per edge; bank 0 goes first when both are ready. Reads that share a tag
// answer in request order whatever their bank. A control write sets inv; a read taken while inv
// is 1 returns the stored byte inverted. The power-up contents are unspecified. LOSE_TAG is a
// fault switch for the bench: reads with that tag are taken and never answered (16 = off).
module tag_mem #(parameter int LOSE_TAG = 16) (mem_if bus);
  typedef struct { bit [3:0] tag; bit [7:0] data; int due; int seq; } pend_t;
  bit [7:0] mem0 [16], mem1 [16];
  bit       inv = 0;
  pend_t    q0[$], q1[$];
  int       cycle = 0, seqno = 0;
  initial foreach (mem0[i]) begin mem0[i] = 8'hC0 | i[3:0]; mem1[i] = 8'hD0 | i[3:0]; end
  // An older read with the same tag is still waiting in the other bank.
  function automatic bit held(pend_t head, pend_t other[$]);
    foreach (other[i]) if (other[i].tag == head.tag && other[i].seq < head.seq) return 1;
    return 0;
  endfunction
  always @(posedge bus.clk) begin
    pend_t p;
    cycle++;
    if (bus.req_valid) case (bus.req_kind)
      0: begin
        p.tag  = bus.req_tag;
        p.data = (bus.req_bank ? mem1[bus.req_addr] : mem0[bus.req_addr]) ^ {8{inv}};
        p.due  = cycle + (bus.req_bank ? 3 : 1);
        p.seq  = seqno++;
        if (bus.req_tag != LOSE_TAG[3:0] || LOSE_TAG > 15) begin
          if (bus.req_bank) q1.push_back(p); else q0.push_back(p);
        end
      end
      1: if (bus.req_bank) mem1[bus.req_addr] = bus.req_data; else mem0[bus.req_addr] = bus.req_data;
      2: inv = bus.req_data[0];
      default: ;
    endcase
    bus.rsp_valid <= 0;
    if (q0.size() != 0 && q0[0].due <= cycle && !held(q0[0], q1)) begin
      p = q0.pop_front();
      bus.rsp_valid <= 1; bus.rsp_tag <= p.tag; bus.rsp_data <= p.data;
    end else if (q1.size() != 0 && q1[0].due <= cycle && !held(q1[0], q0)) begin
      p = q1.pop_front();
      bus.rsp_valid <= 1; bus.rsp_tag <= p.tag; bus.rsp_data <= p.data;
    end
  end
endmodule

class mem_req extends uvm_sequence_item;
  `uvm_object_utils(mem_req)
  typedef enum bit [1:0] { READ, WRITE, CTRL } kind_e;
  kind_e    kind;
  bit [3:0] tag;
  bit       bank;
  bit [3:0] addr;
  bit [7:0] data;
  time      at;                // when the monitor saw the request taken
  function new(string name = "mem_req"); super.new(name); endfunction
  function string convert2string();
    if (kind == READ) return $sformatf("READ tag=%0d bank=%0d addr=%0d", tag, bank, addr);
    return $sformatf("%s tag=%0d bank=%0d addr=%0d data=%02h", kind.name(), tag, bank, addr, data);
  endfunction
endclass

class mem_rsp extends uvm_sequence_item;
  `uvm_object_utils(mem_rsp)
  bit [3:0] tag;
  bit [7:0] data;
  bit [7:0] mask = 8'hFF;      // 1 = this data bit is checked (a prediction's mask decides)
  bit       timed_out;         // a prediction the watchdog has already reported
  time      at;                // when the monitor saw it (a prediction: when the read was taken)
  mem_req   req;               // a prediction remembers the read it answers
  function new(string name = "mem_rsp"); super.new(name); endfunction
  function bit do_compare(uvm_object rhs, uvm_comparer comparer);
    mem_rsp o;
    if (!$cast(o, rhs)) return 0;
    return comparer.compare_field_int("tag", tag, o.tag, 4) &
           comparer.compare_field_int("data", data, o.data, 8);
  endfunction
  function string convert2string(); return $sformatf("tag=%0d data=%02h", tag, data); endfunction
endclass

class script_seq extends uvm_sequence #(mem_req);
  `uvm_object_utils(script_seq)
  function new(string name = "script_seq"); super.new(name); endfunction
  task send(mem_req::kind_e kind, bit [3:0] tag, bit bank, bit [3:0] addr, bit [7:0] data = 0);
    mem_req it = mem_req::type_id::create("it");
    start_item(it);
    it.kind = kind; it.tag = tag; it.bank = bank; it.addr = addr; it.data = data;
    finish_item(it);
  endtask
  task body();
    send(mem_req::WRITE, 0, 0, 1, 8'h11);   // bank 0, addr 1 = 11
    send(mem_req::WRITE, 0, 1, 2, 8'h22);   // bank 1, addr 2 = 22
    send(mem_req::READ,  1, 1, 2);          // slow bank first ...
    send(mem_req::READ,  2, 0, 1);          // ... the fast bank answers before it
    send(mem_req::READ,  3, 1, 2);          // tag 3 twice: bank 1 ...
    send(mem_req::READ,  3, 0, 1);          // ... then bank 0, which must wait its turn
    send(mem_req::READ,  5, 1, 2);          // the bench's fault switch loses this one
  endtask
endclass

// One request per cycle: offered at a falling edge, taken at the next rising edge.
class mem_driver extends uvm_driver #(mem_req);
  `uvm_component_utils(mem_driver)
  virtual mem_if vif;
  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
        vif.req_valid <= 0;
        continue;
      end
      vif.req_valid <= 1;
      vif.req_kind  <= req.kind;
      vif.req_tag   <= req.tag;
      vif.req_bank  <= req.bank;
      vif.req_addr  <= req.addr;
      vif.req_data  <= req.data;
      @(posedge vif.clk);
      seq_item_port.item_done();
    end
  endtask
endclass

// Publishes every request the memory takes and every response it gives, stamped with the time.
class mem_monitor extends uvm_monitor;
  `uvm_component_utils(mem_monitor)
  virtual mem_if vif;
  uvm_analysis_port #(mem_req) req_ap;
  uvm_analysis_port #(mem_rsp) rsp_ap;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    req_ap = new("req_ap", this);
    rsp_ap = new("rsp_ap", this);
  endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (vif.req_valid) begin
        mem_req t = mem_req::type_id::create("t");
        t.kind = mem_req::kind_e'(vif.req_kind);
        t.tag  = vif.req_tag;
        t.bank = vif.req_bank;
        t.addr = vif.req_addr;
        t.data = vif.req_data;
        t.at   = $time;
        req_ap.write(t);
      end
      if (vif.rsp_valid) begin
        mem_rsp r = mem_rsp::type_id::create("r");
        r.tag  = vif.rsp_tag;
        r.data = vif.rsp_data;
        r.at   = $time;
        rsp_ap.write(r);
      end
    end
  endtask
endclass

`uvm_analysis_imp_decl(_req)
`uvm_analysis_imp_decl(_rsp)

class tag_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(tag_scoreboard)
  uvm_analysis_imp_req #(mem_req, tag_scoreboard) req_imp;
  uvm_analysis_imp_rsp #(mem_rsp, tag_scoreboard) rsp_imp;
  bit [7:0] model0 [16], model1 [16];     // the reference model: what each location holds
  mem_rsp   pending[bit [3:0]][$];        // predictions waiting for a response, by tag, oldest first
  int       reads, checked, mismatches, orphans;
  uvm_event changed = new("changed");     // triggered on every request or response it receives
  mem_rsp   slot[bit [3:0]];              // one prediction per tag
  function new(string name, uvm_component parent);
    super.new(name, parent);
    req_imp = new("req_imp", this);
    rsp_imp = new("rsp_imp", this);
  endfunction
  function int waiting();
    int n = 0;
    foreach (pending[t]) n += pending[t].size();
    return n;
  endfunction
  function string waiting_tags();
    string s = "";
    foreach (pending[t]) repeat (pending[t].size()) s = {s, $sformatf(" %0d", t)};
    return s.len() ? s : " none";
  endfunction
  function void write_req(mem_req t);
    changed.trigger();                    // wakes a test that waits for the counts to change
    case (t.kind)
      mem_req::WRITE: begin
        if (t.bank) model1[t.addr] = t.data; else model0[t.addr] = t.data;
      end
      mem_req::CTRL: begin
        
      end
      mem_req::READ: begin
        mem_rsp e = mem_rsp::type_id::create("e");
        e.tag = t.tag;
        e.at  = t.at;
        e.req = t;
        e.data = t.bank ? model1[t.addr] : model0[t.addr];
        slot[t.tag] = e;
        reads++;
      end
    endcase
  endfunction
  function void write_rsp(mem_rsp r);
    mem_rsp e;
    changed.trigger();
    if (!slot.exists(r.tag)) begin
      orphans++;
      `uvm_error("SB", $sformatf("response %s at %0t has no read waiting", r.convert2string(), r.at))
      return;
    end
    e = slot[r.tag];
    slot.delete(r.tag);
    checked++;
    if (!e.compare(r)) begin
      mismatches++;
      `uvm_error("SB", $sformatf("%s, predicted data=%02h for %s taken at %0t",
                 r.convert2string(), e.data, e.req.convert2string(), e.at))
    end
  endfunction

  // Per tag: every waiting prediction the watchdog has not already reported.
  function void check_phase(uvm_phase phase);
    foreach (pending[t]) begin
      int unreported = 0, oldest = -1;
      foreach (pending[t][i]) if (!pending[t][i].timed_out) begin
        unreported++;
        if (oldest < 0) oldest = i;
      end
      if (unreported != 0)
        `uvm_error("SB", $sformatf("tag %0d: %0d read(s) never answered, first: %s at %0t",
                   t, unreported, pending[t][oldest].req.convert2string(), pending[t][oldest].at))
    end
  endfunction
endclass

class predict_test extends uvm_test;
  `uvm_component_utils(predict_test)
  uvm_sequencer #(mem_req) sqr;
  mem_driver     drv;
  mem_monitor    mon;
  tag_scoreboard sb;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual mem_if vif;
    sqr = uvm_sequencer #(mem_req)::type_id::create("sqr", this);
    drv = mem_driver::type_id::create("drv", this);
    mon = mem_monitor::type_id::create("mon", this);
    sb  = tag_scoreboard::type_id::create("sb", this);
    if (!uvm_config_db #(virtual mem_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "no virtual interface")
    drv.vif = vif;
    mon.vif = vif;
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
    mon.req_ap.connect(sb.req_imp);
    mon.rsp_ap.connect(sb.rsp_imp);
  endfunction
  task run_phase(uvm_phase phase);
    script_seq seq = script_seq::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(sqr);
    #100ns;
    phase.drop_objection(this);
  endtask
  function void report_phase(uvm_phase phase);
    $display("reads %0d, checked %0d, mismatches %0d, orphans %0d", sb.reads, sb.checked, sb.mismatches, sb.orphans);
  endfunction
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  mem_if bus (clk);
  tag_mem #(.LOSE_TAG(16)) dut (bus);
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    uvm_config_db #(virtual mem_if)::set(null, "uvm_test_top", "vif", bus);
    run_test("predict_test");
  end
endmodule
Spot the bugEach option is the line in write_rsp that takes the prediction out of pending after the orphan check. The sequence is the example's with the fault switch off, so all five reads are answered. Which option fails?
ConceptWhich property of the design lets the scoreboard keep a queue per tag and pop the oldest prediction?
ConceptA response arrives whose tag has no prediction waiting. What should write_rsp do?
ConceptWhy does the end-of-test report name the tag and the first unanswered request rather than print a count of missing responses?

Remember

  • A design that answers out of order needs predictions keyed by what the response carries, usually a tag or ID.
  • Use an associative array of queues: the key finds the predictions, and the queue keeps same-key transactions in issue order, as AXI-style protocols require.
  • An orphan response is always an error; a duplicate prediction is legal only when the protocol allows same-ID transactions in flight.
  • A per-key watchdog with a typed time threshold reports a missing response during the run, once per prediction; check_phase reports the waiting predictions it has not reported, naming the request.
  • Bound the end-of-test wait, so a lost response still ends the test and the report explains it.

Practice