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.
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 at | Read | Ready at | Answered at | Seen by the monitor |
|---|---|---|---|---|
| 35 ns | tag 1, bank 1 | 65 ns | 65 ns | 75 ns |
| 45 ns | tag 2, bank 0 | 55 ns | 55 ns | 65 ns |
| 55 ns | tag 3, bank 1 | 85 ns | 85 ns | 95 ns |
| 65 ns | tag 3, bank 0 | 75 ns | 95 ns, after the older tag 3 | 105 ns |
| 75 ns | tag 5, bank 1 | 105 ns | never: 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.
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.
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.- 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
- data prediction 22: READ tag 3, bank 1 → pending[3]
- data READ tag 3 (bank 1): READ tag 3, bank 1 → Bank 1
- 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
- data prediction 11: READ tag 3, bank 0 → pending[3]
- data READ tag 3 (bank 0): READ tag 3, bank 0 → Bank 0
- 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
- Waiting: Bank 0 — held behind the older tag 3
- 85 ns
Bank 1 drives 22 at 85 ns. The monitor samples it at the next edge.
- 95 ns
The monitor sees 22, and
pop_frontreturns 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
- data response tag 3 = 22: Bank 1 → Response tag 3 = 22
- data prediction 22: pending[3] → Response tag 3 = 22
- Released: Bank 0
- 105 ns
The monitor sees 11.
pop_frontreturns 11, the only entry left: match, andpending[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
- 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
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.
- A read is taken55 ns (illustrative)
write_reqpushes the prediction for the first tag 3 read ontopending[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
- data tag 3: expect 22: Read taken → pending[tag] push_back
- 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
- data tag 5 prediction: Read taken → pending[tag] push_back
- Waiting: pending[tag] push_back — tag 5 owed since 75 ns
- Answered: pop and compare95 ns (illustrative)
The tag 3 response finds a prediction.
pop_frontgives 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
- data response tag 3 = 22: Response seen → pop_front and compare
- data tag 3: expect 22: pending[tag] push_back → pop_front and compare
- 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
- data tag 5 older than 50 ns: pending[tag] push_back → Watchdog error, once
- Waiting: pending[tag] push_back — tag 5 flagged, still owed
- The end of the run175 ns (illustrative)
The bounded wait ends the run.
check_phasereports, 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.
- Nothing waiting: an orphan
A response whose tag has no prediction cannot be compared with anything.
write_rspcounts 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
- data response, no tag waiting: Response seen → Orphan error
- Released: pending[tag] push_back
- 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_phaseunflagged 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
- 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.
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
endtaskThree 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.
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 : 1Check yourself
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
timethreshold reports a missing response during the run, once per prediction;check_phasereports 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
- Interleaved read bursts Implement · Hard