Queues and ordering

Use a queue as a first-in, first-out list of expected responses, guard every pop, and keep one queue per ID when the bus orders responses only within an ID.

Lesson
2.2
Time
20 min
Runs on
Verilator
Check
5 questions

A read scoreboard pushes each expected response onto one queue and pops the front whenever a response arrives. The new interconnect may return responses for different IDs in any order, and it does: a read for ID 2 finishes before an earlier read for ID 1. The scoreboard reports 1,200 data mismatches against a DUT that is behaving correctly. The team spends two days hunting a bug that lives in the checker.

Two IDs, one queue
01234requestID1 AID2 BidleresponseidleID2 BID1 Aidlequeue (front first)AA, BBemptyB compared with A: mismatchA compared with B: mismatch

The single queue assumes every response returns in request order, which this bus does not promise.

A queue grows and shrinks at both ends

A queue is an ordered list whose length changes as you add and remove elements. You declare it with a dollar sign in the brackets, as in int exp_q [$];, and it starts empty.

Its methods work at either end. push_back adds at the end and pop_front removes from the front, so using that pair gives first-in, first-out order, or FIFO. Using push_back with pop_back gives last-in, first-out order instead, like a stack.

Operation on q = {10, 20, 30}Resultq afterwards
q.size()3{10, 20, 30}
q[0] and q[$]10 and 30unchanged
q.push_back(40)none{10, 20, 30, 40}
q.pop_front()10{20, 30}
q.insert(1, 15)none{10, 15, 20, 30}
q.delete(0)none{20, 30}

Each row starts again from {10, 20, 30}. $ as an index means the last element.

You never call new for a queue. Indexing works as it does for arrays, and foreach visits the elements from front to back. A range gives a new queue: q[1:$] holds every element except the first.

Guard every pop

Popping an empty queue does not stop the simulation. It returns the element type's default value, 0 for an int, and the simulator may print a warning. Icarus Verilog prints one, but Verilator 5.052 stays silent.

In a scoreboard, an empty queue at the moment a response arrives is itself a failure: the DUT sent a response that nobody asked for. Check size() first, and report that case by name instead of comparing against a made-up 0.

Pop without looking

exp = exp_q.pop_front();      // 0 if empty
if (rdata != exp)
  $error("expected %0h got %0h", exp, rdata);

An unexpected response with data 0 matches the default value and passes.

Check, then pop

if (exp_q.size() == 0)
  $error("unexpected response %0h", rdata);
else begin
  exp = exp_q.pop_front();
  if (rdata != exp)
    $error("expected %0h got %0h", exp, rdata);
end

Reports the unexpected response, and compares only real expectations.

Order is part of the protocol

A single FIFO of expectations is correct for a bus that returns every response in request order. For that bus, popping the front and comparing is the whole check, and it also catches responses that come back out of order.

An AXI-style bus makes a weaker promise. Responses that share an ID return in request order, but responses with different IDs may overtake each other. The checker must allow the second kind of reordering while still catching the first.

In the opening story, the bus returned B before A. A and B have different IDs, so that order is legal, yet the single queue compared B with A and failed. Had two reads with ID 1 come back swapped, the same check would have failed for a good reason.

Sorting the responses before comparing them looks like a fix, but it hides real bugs. If two reads with the same ID come back swapped, a sorted comparison still passes.

One queue per ID

The structure that matches the rule is one FIFO per ID. With 4-bit IDs, int exp_q [16][$]; declares a fixed array of 16 queues, and exp_q[3] is the queue for ID 3.

A request with ID arid pushes its expected data onto exp_q[arid]. A response with ID rid checks exp_q[rid].size(), then pops the front of that queue only. Responses for other IDs never touch it, so they may arrive in any order.

Replay the opening story with this structure. The request for ID 1 pushes A onto exp_q[1], and the request for ID 2 pushes B onto exp_q[2]. Response B pops exp_q[2] and matches, then response A pops exp_q[1] and matches.

At the end of the test, every queue must be empty. A leftover entry is a read that never got its response. When IDs are too wide for a fixed array, the next lesson replaces the outer array with an associative array.

Queues never block

Queue methods act immediately. pop_front never waits for an element to arrive, so a loop that polls an empty queue at the same simulation time can hang the run. Producer and consumer processes need a mailbox, which Module 3 covers.

A bounded queue sets a highest index: int q [$:15] holds at most 16 elements. A push beyond that limit is dropped, with a warning in Icarus and silently in Verilator 5.052, so a bounded queue does not slow down the sender.

Try it

The single queue compares the ID 2 response with the oldest expectation, which belongs to ID 1, and reports a false mismatch. The per-ID queues match each response with its own ID, and the second ID 1 response is reported as unexpected.

Add a second request for ID 1 with data 'ha3, then make the ID 1 responses arrive as a3 before a1. Predict which line reports the error.

RUNVerilator · simulator service
Edit the code, then run it
Expected: single queue: response b2 vs a1 -> MISMATCH per-ID queues: ID 2 response b2 -> match per-ID queues: ID 1 response a1 -> match per-ID queues: ID 1 response a1 -> unexpected response left over: ID 1=0 ID 2=0

Check yourself

Predict the outputPredict the exact output.
module tb;
  int q [$];
  initial begin
    q.push_back(1);
    q.push_back(2);
    q.push_front(3);
    void'(q.pop_back());
    q.push_back(4);
    $display("%0d %0d %0d", q[0], q[$], q.size());
  end
endmodule
ConceptWhich pair of methods gives first-in, first-out order?
ConceptResponses with different IDs may return in any order, while responses with the same ID must keep request order. Which scoreboard structure checks exactly that rule?
Read the waveformFour reads were issued in this order: ID 1 A1, ID 2 B1, ID 1 A2, ID 2 B2. Which response breaks the ordering rule?
Responses on the read data channel
0123rvalidrid212rdataB1A2A1B2

Same-ID responses must keep request order; different IDs may interleave.

Spot the bugEach option is the body of function automatic bit rsp_ok(int rid, int rdata), which returns 1 only when a response matches the front of the per-ID queue exp_q[rid], and 0 for a mismatch or an unexpected response. Which option accepts an unexpected response?

Remember

  • A queue [$] starts empty and grows and shrinks at both ends; push_back with pop_front gives FIFO order.
  • Popping an empty queue returns a default value; check size() and report unexpected responses by name.
  • Match the scoreboard to the ordering rule: one FIFO per ID when only same-ID responses keep their order.
  • Sorting or searching by value hides ordering bugs.
  • Queue methods never wait, and a bounded queue drops extra elements rather than slowing the sender.

Practice