Pipelined drivers

Drive a bus that carries two transfers at once: two driver workers, a semaphore around get() and the address phase, a response sent later with put(), and a sequence that collects responses on a second thread.

Lesson
U6.1
Time
30 min
Runs on
Verilator with UVM
Check
5 questions

A memory controller accepts a new address every clock while it is still returning the data of the previous access. Its bench passes a thousand-item regression. In the lab the first back-to-back write and read to the same address returns stale data. The bench never saw it: its driver waited for each item's data phase before asking for the next item, so no two transfers were ever on the bus together.

What the bench drove, and what the bus can carry
01234567clock edge (ns)515253545556575serial driver, addritem 1-item 2-item 3-item 4-serial driver, data-item 1-item 2-item 3-item 4pipelined, addritem 1item 2item 3item 4-pipelined, data-item 1item 2item 3item 4-addr 2 over data 1four items done

Each item has an address phase and, one clock later, a data phase. The serial driver leaves every other clock empty; the pipelined driver keeps both phases busy and finishes four items at 45 ns instead of 75 ns.

Two phases, two items in flight

The bus in this lesson has two phases. In the address phase the master drives avalid, addr, we and wdata until the memory accepts them at a clock edge. In the data phase, the next clock, the memory returns rdata. So item 2's address phase can run while item 1's data phase completes. AHB and OCP work this way, and some buses have more phases.

A driver that waits for the data phase before asking for the next item drives the bus at half speed. It also never creates the overlap the design must handle, such as a write whose data phase meets the next read's address phase. Filling the pipeline takes three changes. The driver works on two items at once, the handshake releases the sequence early, and the sequence stops waiting for each result.

U4 built the single-item handshake and U5 the choice among sequences. Here one sequence and one sequencer stay; only the driver and the shape of the sequence change. The example assumes a fixed latency, no reset and one producing sequence.

Handing off a request, completing it later

Two items in flight, two milestones eachArrows are calls and returned transactions. Follow item 1 and item 2 through the example run: each worker carries its own item through both stages.
Two items in flight, two milestones eachArrows are calls and returned transactions. Follow item 1 and item 2 through the example run: each worker carries its own item through both stages. sequence Sequence: sendthread start_item,finish_item per item sequencer Sequencer One requesthandshake at a time queue Addresssemaphore One key driver thread Worker A Item 1: address 0-5ns, data 5-15 ns driver thread Worker B Item 2: address 5-15ns, data 15-25 ns sequence Sequence: collectthread get_response loop dut Memory pins Address phase anddata phase overlap finish_item(item)get(): handoff ofitem 1get(): handoff ofitem 2holds key 0-5 nsholds key 5-15 nsdrives item 1drives item 2put(rsp): completion
  1. Handoff of item 1 (0 ns)

    Worker A takes the semaphore key and calls get(). The sequence's finish_item for item 1 returns at once, so it starts preparing item 2.

    More context

    Worker A holds the only key, so worker B is blocked on the semaphore. Item 1 reaches worker A through get(), and the sequence's finish_item returns in the same time step.

    Follow the calls, values and waits
    1. call get(1): Worker A → Address semaphore
    2. data key: Address semaphore → Worker A
    3. call get(): Worker A → Sequencer
    4. data item 1: Sequence: send thread → Sequencer
    5. data item 1: Sequencer → Worker A
    6. Waiting: Worker B — waiting for the key
    7. Released: Sequence: send thread
  2. Item 1's address accepted (5 ns)

    The memory accepts item 1's address phase at the 5 ns edge. Worker A clears avalid, then returns the key.

    More context

    Worker A drove item 1's address from 0 ns; the memory takes it at the 5 ns edge. Clearing avalid before returning the key keeps the bus from repeating the address.

    Follow the calls, values and waits
    1. data addr 1: Worker A → Memory pins
    2. return key: Worker A → Address semaphore
    3. Released: Worker B
  3. Handoff of item 2 (5 ns)

    Worker B takes the key and item 2 and starts its address phase while worker A is in item 1's data phase.

    More context

    The log shows "sent item 2" at 5 ns: handoff happens the moment worker B takes the item, before any of its pins move. Worker A is busy with item 1's data phase until 15 ns.

    Follow the calls, values and waits
    1. call get(1): Worker B → Address semaphore
    2. data key: Address semaphore → Worker B
    3. call get(): Worker B → Sequencer
    4. data item 2: Sequence: send thread → Sequencer
    5. data item 2: Sequencer → Worker B
    6. Waiting: Worker A — data phase of item 1
    7. Released: Sequence: send thread
  4. Completion of item 1 (15 ns)

    Worker A reads rdata after the data edge, builds a response with set_id_info and calls put. At the same edge the memory accepts item 2's address.

    More context

    Two things happen at the 15 ns edge: item 1's data phase ends, so its response reaches the collect thread, and item 2's address phase is accepted. This is the overlap the serial driver never produces.

    Follow the calls, values and waits
    1. data rdata 1: Memory pins → Worker A
    2. return rsp 1: Worker A → Sequence: collect thread
    3. data addr 2: Worker B → Memory pins
    4. Released: Sequence: collect thread
    5. Released: Worker A
  5. Handoff of item 3 (15 ns)

    Worker B returns the key; worker A, free again, takes it and item 3.

    More context

    Item 3's start_item has been waiting since 5 ns because both workers were busy; it is handed off at 15 ns, matching "sent item 3" in the log. That wait is the pipeline's back-pressure on the sequence.

    Follow the calls, values and waits
    1. return key: Worker B → Address semaphore
    2. call get(1): Worker A → Address semaphore
    3. data key: Address semaphore → Worker A
    4. call get(): Worker A → Sequencer
    5. data item 3: Sequence: send thread → Sequencer
    6. data item 3: Sequencer → Worker A
    7. Waiting: Worker B — data phase of item 2
    8. Released: Sequence: send thread
  6. Serial before completionbefore 15 ns (serial) (illustrative)

    In the serial alternative, finish_item stays blocked until the data phase completes at 15 ns.

    More context

    A separate, derived scenario: before 15 ns the serial sequence is still in finish_item for item 1. It returns at 15 ns, then offers item 2. The pipelined run already handed item 2 off at 5 ns.

    Follow the calls, values and waits
    1. data item 1: Sequence: send thread → Sequencer
    2. Waiting: Sequence: send thread — in finish_item until 15 ns
    3. Released: Worker A
    4. Released: Worker B
Read the diagram as text
  • Sequence: send thread: start_item, finish_item per item
  • Sequencer: One request handshake at a time
  • Address semaphore: One key
  • Worker A: Item 1: address 0-5 ns, data 5-15 ns
  • Worker B: Item 2: address 5-15 ns, data 15-25 ns
  • Sequence: collect thread: get_response loop
  • Memory pins: Address phase and data phase overlap
  • Sequence: send thread → Sequencer: finish_item(item)
  • Worker A → Sequencer: get(): handoff of item 1
  • Worker B → Sequencer: get(): handoff of item 2
  • Worker A → Address semaphore: holds key 0-5 ns
  • Worker B → Address semaphore: holds key 5-15 ns
  • Worker A → Memory pins: drives item 1
  • Worker B → Memory pins: drives item 2
  • Worker A → Sequence: collect thread: put(rsp): completion

Handoff (the worker's get) and completion (its put) are separate events. The semaphore covers request selection and the address phase only.

Every item passes two milestones. Handoff is the moment the driver takes responsibility for the request: the sequence's finish_item returns, and it may prepare the next request. Completion is the moment the result exists: the data phase has ended and the read data is known. A serial driver calls item_done after the data phase, so both milestones coincide.

The pipelined driver separates them. seq_item_port.get(req) takes the item and completes the request handshake at once; in uvm-core it is the item selection followed by item_done(). When the data phase ends, the driver builds a response, copies the request's IDs into it with rsp.set_id_info(req), and calls seq_item_port.put(rsp). put takes no time, not even a delta cycle; the sequencer routes the response to its sequence by sequence ID.

This is the same handshake U4 taught, with one more event. U4.3 tied item_done to bus acceptance. Here get releases the request handshake at handoff, and put returns a separate response after the data phase. The design decision is when the sequence may issue its next request: at handoff, which fills the pipeline, or at completion, which keeps one transfer at a time.

Handoff and completion times for the four items
ItemSerial driver: finish_item returnsPipelined: finish_item returns (handoff)Pipelined: response arrives (completion)
1 write 0x1015 ns0 ns15 ns
2 read 0x1035 ns5 ns25 ns
3 read 0x2055 ns15 ns35 ns
4 read 0x3075 ns25 ns45 ns

Pipelined times are from the example below; serial times follow from the same two-edge timing with one item in flight.

Two workers and one semaphore

The driver forks two identical workers. Each worker carries its own item through both stages: it takes the item, drives the address phase, waits through the data phase and returns the response. Neither is permanently the address thread. Two workers are enough for this two-stage, fixed-latency bus; a deeper pipeline or variable latency needs more.

One worker. The semaphore covers get() and the address phase only.
task worker(string id);
  mem_item tr, rsp;                   // this worker's own handles
  forever begin
    addr_phase.get();                 // one key: request selection and address phase
    seq_item_port.get(tr);            // handoff: finish_item returns now
    accept_tr(tr, $time);
    vif.avalid <= 1; vif.we <= tr.we; vif.addr <= tr.addr; vif.wdata <= tr.wdata;
    @(posedge vif.clk);               // the memory accepts the address phase
    void'(begin_tr(tr));              // here: the acceptance edge
    vif.avalid <= 0;                  // clear first ...
    addr_phase.put();                 // ... then let the other worker drive
    @(posedge vif.clk);               // data phase ends at this edge
    rsp = mem_item::type_id::create("rsp");
    rsp.set_id_info(tr);
    rsp.we = tr.we; rsp.addr = tr.addr; rsp.rdata = vif.rdata;
    end_tr(tr);
    seq_item_port.put(rsp);           // completion
  end
endtask

The semaphore serialises two things. First, request selection: only one worker is inside get at a time. The sequencer serves one request handshake through a one-entry FIFO, and a second request it cannot place is a fatal error whose text suggests a semaphore. Second, the address pins: only one worker drives them.

The worker clears avalid before it releases the key. The other worker's avalid <= 1 comes later in the same time step and wins; with no item waiting, the bus goes idle instead of repeating the last address. The worker reads rdata right after the data edge, before the memory's nonblocking update for the next transfer lands, so it reads its own item's data. U4.3 showed the same pre-edge read on ready.

Each worker keeps its own tr handle. The req field that uvm_driver provides is a single variable; if both workers used it, worker B's get would replace worker A's item during A's data phase. The quiz shows that variant failing.

A sequence with two threads

Serial: waits for each result

for (int i = 0; i < 4; i++) begin
  mem_item it = mem_item::type_id::create("it");
  start_item(it);
  fill(it, i);
  finish_item(it);
  get_response(rsp);     // pipeline drains here
end

Correct for register-style accesses that need each result before the next request, but only one transfer is ever in flight.

Pipelined: sends and collects in parallel

fork
  for (int i = 0; i < 4; i++) begin
    mem_item it = mem_item::type_id::create("it");
    start_item(it);
    fill(it, i);
    finish_item(it);
  end
  repeat (4) get_response(rsp);
join

The send loop is paced by the workers' get() calls; responses are read as they arrive.

With handoff early, a get_response right after finish_item would stall the send loop until the data phase ends. Create a new item for every send: the driver still reads a handed-off item during its data phase, and a reused object would also repeat one transaction ID.

A sequence that needs no results can call use_response_handler(1) and count responses in response_handler, which keeps its response queue empty. Its body() must still stay alive until the count reaches the number sent. Once a sequence has ended, the sequencer no longer finds it and drops later responses with a warning.

Timestamps on the item

With several items in flight, "when did this item happen" has three answers. accept_tr records when the driver took the item and end_tr when it completed. begin_tr records whatever the driver chooses as the start; this example calls it at the edge where the memory accepts the address phase. Without transaction recording the calls still set the three times, which get_accept_time(), get_begin_time() and get_end_time() read back.

The sequencer would record the item too. Automatic item recording is on by default. start_item begins the item at the grant, and finish_item ends it when it returns, which here is the handoff, long before the data phase ends. So the driver calls seq_item_port.disable_auto_item_recording() at the start of its run_phase, and the item's begin and end are only the driver's. U2.4 introduced the switch.

Item 3 shows each mark. Worker A takes it at 15 ns and drives its address during 15-25 ns; the memory accepts it at 25 ns and the data phase ends at 35 ns. The pipeline's back-pressure is on the sequence side: item 3's start_item began at 5 ns and waited until 15 ns, when a worker and the key were free.

When completion is out of order

This memory answers in order. A two-bank memory does not: a fast bank finishes a later read before an earlier read from the slow bank. The model below keeps the handoff, takes one item every 10 ns and gives each outstanding item its own completion thread. Address 0x80 sits in the slow bank (40 ns); 0x01 and 0x02 take 10 ns.

The same three responses, two alternative collectorsOne run at a time: the program runs strategy A as sequence a, then strategy B as sequence b. Arrows are returned responses.
The same three responses, two alternative collectorsOne run at a time: the program runs strategy A as sequence `a`, then strategy B as sequence `b`. Arrows are returned responses. driver Driver Takes items 1, 2, 3 at0, 10, 20 ns dut Slow bank 0x80: 40 ns dut Fast bank 0x01, 0x02: 10 ns queue Response queue Per sequence, depth8 alternative A: take and lookup Done at 20, 30, 40ns alternative B: wait for chosenID Done at 40, 40, 40ns read 1reads 2, 3tid 2 at 20, tid 3 at30tid 1 at 40oldest firsttid 1, then 2, then 3
  1. Read 1 to the slow bank (0 ns)

    The driver takes read 1 (0x80) and gives it its own completion thread; the slow bank needs 40 ns.

    More context

    One item every 10 ns: read 1 goes to the slow bank and will not answer until 40 ns. Times are relative to the start of each sequence.

    Follow the calls, values and waits
    1. data read 1 (0x80): Driver → Slow bank
    2. Waiting: Slow bank — busy until 40 ns
  2. Read 2 to the fast bank (10 ns)

    The driver takes read 2 (0x01); the fast bank needs 10 ns.

    More context

    Read 2 is issued after read 1 but will finish 20 ns earlier.

    Follow the calls, values and waits
    1. data read 2 (0x01): Driver → Fast bank
  3. Alternative A at 20 ns

    Response 2 arrives first. Collector A pops it, looks up request 2 by its transaction ID and retires the entry. Read 3 is taken at the same moment.

    More context

    In sequence a, read 2 completes at 20 ns: the first entry of "done by arrival 20 30 40". The queue never holds more than one response.

    Follow the calls, values and waits
    1. data read 3 (0x02): Driver → Fast bank
    2. return tid 2: Fast bank → Response queue
    3. data tid 2: Response queue → A: take and look up
    4. Released: A: take and look up
  4. Alternative A at 30 ns

    Response 3 arrives and is handled at once.

    More context

    Read 3 completes at 30 ns. Request 1 is still outstanding in the lookup table.

    Follow the calls, values and waits
    1. return tid 3: Fast bank → Response queue
    2. data tid 3: Response queue → A: take and look up
    3. Released: A: take and look up
  5. Alternative A at 40 ns

    Response 1 finally leaves the slow bank and completes read 1.

    More context

    Arrival order was 2, 3, 1, and every response matched its own request: "matched wrong 0". A naive k-th pairing would have been wrong all three times.

    Follow the calls, values and waits
    1. return tid 1: Slow bank → Response queue
    2. data tid 1: Response queue → A: take and look up
    3. Released: A: take and look up
    4. Released: Slow bank
  6. Alternative B at 20 ns

    In the second run, collector B asks for ID 1 first. Response 2 arrives and stays queued.

    More context

    Sequence b runs the same traffic after sequence a ends; times are relative to its start. Its collector waits for transaction ID 1, so response 2 waits in the queue (occupancy 1).

    Follow the calls, values and waits
    1. return tid 2: Fast bank → Response queue
    2. Waiting: B: wait for chosen ID — get_response(tid 1)
    3. Waiting: Slow bank — busy until 40 ns (alternative B)
  7. Alternative B at 30 ns

    Response 3 arrives and joins response 2 in the queue.

    More context

    Queue occupancy is now 2. Behind a slower response, more than 8 queued entries would be dropped silently in this library.

    Follow the calls, values and waits
    1. return tid 3: Fast bank → Response queue
    2. Waiting: B: wait for chosen ID — get_response(tid 1)
  8. Alternative B at 40 ns

    Response 1 lands; collector B takes 1, then 2, then 3.

    More context

    All three reads finish at 40 ns: "by id 40 40 40". Strategy B needed no table but every read waited behind the slow one.

    Follow the calls, values and waits
    1. return tid 1: Slow bank → Response queue
    2. data tid 1: Response queue → B: wait for chosen ID
    3. data tid 2: Response queue → B: wait for chosen ID
    4. data tid 3: Response queue → B: wait for chosen ID
    5. Released: B: wait for chosen ID
    6. Released: Slow bank
Read the diagram as text
  • Driver: Takes items 1, 2, 3 at 0, 10, 20 ns
  • Slow bank: 0x80: 40 ns
  • Fast bank: 0x01, 0x02: 10 ns
  • Response queue: Per sequence, depth 8
  • A: take and look up: Done at 20, 30, 40 ns
  • B: wait for chosen ID: Done at 40, 40, 40 ns
  • Driver → Slow bank: read 1
  • Driver → Fast bank: reads 2, 3
  • Fast bank → Response queue: tid 2 at 20, tid 3 at 30
  • Slow bank → Response queue: tid 1 at 40
  • Response queue → A: take and look up: oldest first
  • Response queue → B: wait for chosen ID: tid 1, then 2, then 3

Both alternatives pair every response with the right request. A finishes each read when its data exists; B needs no table but waits behind the slowest response.

The out-of-order driver and the two collectors. The full program is the second predict item below.
// driver: one thread per outstanding item
seq_item_port.get(tr);
fork complete(tr); join_none        // complete(): bank delay, set_id_info, put(rsp)
#10;

// collector A: take whatever arrives, look up its request, retire the entry
get_response(rsp);
req_of = by_tid[rsp.get_transaction_id()];
by_tid.delete(rsp.get_transaction_id());

// collector B: wait for a chosen ID, in send order
get_response(rsp, sent[k].get_transaction_id());

The naive loop pairs the k-th response with the k-th request. Measured here, the arrival order is 2, 3, 1 and all three pairings are wrong, with no warning: the sequencer routes by sequence ID only and never reorders.

Alternative A handles each result the moment it exists, at the cost of a table of outstanding requests keyed by ID. Alternative B needs no table, but a slow response holds up the ones behind it. Behind a very slow one, the queue can pass its depth of 8 and drop entries silently. On real pins the bus carries a tag with each data beat, and the driver keys its outstanding items by that tag. U8.2 builds the matching out-of-order scoreboard.

Try it

Read the log by time. Item 2's finish_item returns at 5 ns, at the same edge that accepts item 1's address phase, and item 2's address phase is on the bus during item 1's data phase. The read of 0x10 at item 2 returns 0x5a, the value item 1 wrote one clock earlier.

Each completion line carries the item's three timestamps. Accept is when a worker took the item and begin is the edge where the memory accepted its address phase, after one clock of driving. Four items complete at 45 ns; a serial driver that holds the semaphore through the data phase (the second option of the quiz) finishes at 75 ns.

Try deleting the line vif.avalid <= 0, and watch the memory accept item 4's address a second time at 45 ns. Then move get_response into the send loop and see the completions spread to every other clock.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 0 ns sequence sent item 1: write 0x10 <= 0x5a 5 ns driver A address phase of item 1 accepted 5 ns sequence sent item 2: read 0x10 15 ns driver A item 1 done accept 0 begin 5 end 15 15 ns driver B address phase of item 2 accepted 15 ns sequence response for item 1 15 ns sequence sent item 3: read 0x20 25 ns driver B item 2 done: rdata=0x5a accept 5 begin 15 end 25 25 ns driver A address phase of item 3 accepted 25 ns sequence response for item 2: rdata=0x5a 25 ns sequence sent item 4: read 0x30 35 ns driver A item 3 done: rdata=0xa0 accept 15 begin 25 end 35 35 ns driver B address phase of item 4 accepted 35 ns sequence response for item 3: rdata=0xa0 45 ns driver B item 4 done: rdata=0xb0 accept 25 begin 35 end 45 45 ns sequence response for item 4: rdata=0xb0 45 ns test all four responses are in UVM_ERROR : 0

Check yourself

Predict the outputTwo driver workers share a semaphore around get() and a 10 ns address phase, then spend 10 ns in a data phase before put. 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)
  function new(string name = "item_c"); super.new(name); endfunction
endclass
class drv_c extends uvm_driver #(item_c);
  `uvm_component_utils(drv_c)
  semaphore lock = new(1);
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    fork
      thread();
      thread();
    join
  endtask
  task thread();
    item_c tr, rsp;
    forever begin
      lock.get();
      seq_item_port.get(tr);
      #10;                 // address phase
      lock.put();
      #10;                 // data phase
      rsp = item_c::type_id::create("rsp");
      rsp.set_id_info(tr);
      seq_item_port.put(rsp);
    end
  endtask
endclass
class seq_c extends uvm_sequence #(item_c);
  `uvm_object_utils(seq_c)
  int sent[3], done[3];
  function new(string name = "seq_c"); super.new(name); endfunction
  task body();
    fork
      send_all();
      collect_all();
    join
    $display("sent %0d %0d %0d  done %0d %0d %0d", sent[0], sent[1], sent[2], done[0], done[1], done[2]);
  endtask
  task send_all();
    for (int i = 0; i < 3; i++) begin
      item_c it = item_c::type_id::create("it");
      start_item(it);
      finish_item(it);
      sent[i] = $time;
    end
  endtask
  task collect_all();
    for (int i = 0; i < 3; i++) begin
      get_response(rsp);
      done[i] = $time;
    end
  endtask
endclass
class my_test extends uvm_test;
  `uvm_component_utils(my_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);
    seq_c seq = seq_c::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(sqr);
    phase.drop_objection(this);
  endtask
endclass
module tb;
  initial run_test("my_test");
endmodule
Spot the bugThe sequence writes 0x5a to 0x10, then reads 0x10, 0x20 and 0x30, and checks every response against the memory's contents. Each option is the body of one driver worker's forever loop; drive_address puts the item on the pins, waits for the accepting edge and clears avalid. Which option returns wrong results?
ConceptWhy does the pipelined driver hold the semaphore across get() and the address phase, and release it before the data phase?
Predict the outputThe full out-of-order program from the last section. Sequence a takes responses as they arrive; sequence b waits for each ID in send order; times are relative to each sequence's start. Fill in the line it prints, of the form arrival A B C; naive wrong N; matched wrong M; done by arrival T T T, by id T T T.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
class mem_item extends uvm_sequence_item;
  `uvm_object_utils(mem_item)
  bit [7:0] addr, rdata;
  function new(string name = "mem_item"); super.new(name); endfunction
endclass
// Takes one item every 10 ns. Addresses 0x80 and up sit in a slow bank (40 ns); the rest take 10 ns.
class ooo_driver extends uvm_driver #(mem_item);
  `uvm_component_utils(ooo_driver)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      mem_item tr;
      seq_item_port.get(tr);
      fork complete(tr); join_none          // one thread per outstanding item
      #10;
    end
  endtask
  task complete(mem_item t);
    mem_item rsp;
    #(t.addr[7] ? 40 : 10);
    rsp = mem_item::type_id::create("rsp");
    rsp.set_id_info(t);
    rsp.addr = t.addr; rsp.rdata = t.addr + 1;
    seq_item_port.put(rsp);
  endtask
endclass
class read_seq extends uvm_sequence #(mem_item);
  `uvm_object_utils(read_seq)
  bit by_id;                 // 0: take responses as they arrive; 1: wait for each ID in send order
  int arrival[$], done[$], naive_wrong, matched_wrong;
  mem_item sent[$];
  mem_item by_tid[int];
  function new(string name = "read_seq"); super.new(name); endfunction
  task body();
    bit [7:0] addrs[3] = '{8'h80, 8'h01, 8'h02};
    time t0 = $time;
    fork
      foreach (addrs[i]) begin
        mem_item it = mem_item::type_id::create("it");
        start_item(it);
        it.addr = addrs[i];
        finish_item(it);
        sent.push_back(it);
        by_tid[it.get_transaction_id()] = it;
      end
      for (int k = 0; k < 3; k++) begin
        if (by_id) begin
          wait (sent.size() > k);
          get_response(rsp, sent[k].get_transaction_id());
        end else begin
          get_response(rsp);
          if (rsp.rdata != sent[k].addr + 1) naive_wrong++;          // k-th answer to k-th request
          if (rsp.rdata != by_tid[rsp.get_transaction_id()].addr + 1) matched_wrong++;
          by_tid.delete(rsp.get_transaction_id());                       // retire the entry
        end
        arrival.push_back(rsp.get_transaction_id());
        done.push_back($time - t0);
      end
    join
  endtask
endclass
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  uvm_sequencer #(mem_item) sqr;
  ooo_driver drv;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    sqr = uvm_sequencer #(mem_item)::type_id::create("sqr", this);
    drv = ooo_driver::type_id::create("drv", 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);
    read_seq a = read_seq::type_id::create("a");
    read_seq b = read_seq::type_id::create("b");
    b.by_id = 1;
    phase.raise_objection(this);
    a.start(sqr);
    b.start(sqr);
    $display("arrival %0d %0d %0d; naive wrong %0d; matched wrong %0d; done by arrival %0d %0d %0d, by id %0d %0d %0d",
             a.arrival[0], a.arrival[1], a.arrival[2], a.naive_wrong, a.matched_wrong,
             a.done[0], a.done[1], a.done[2], b.done[0], b.done[1], b.done[2]);
    phase.drop_objection(this);
  endtask
endclass
module tb;
  initial run_test("my_test");
endmodule
Read the waveformIn the example's run, when does finish_item for item 3 (the read of 0x20) return?
The example run, by clock edge
01234edge (ns)515253545addr phase accepteditem 1item 2item 3item 4-data phase ends-item 1item 2item 3item 4worker Aaddr 1data 1, take 3addr 3data 3-worker Btake 2addr 2data 2, take 4addr 4data 4A takes item 3

Item 3 is taken by worker A at 15 ns, driven during 15-25 ns, accepted at 25 ns and completed at 35 ns.

Remember

  • Every item has two milestones: handoff, when finish_item returns, and completion, when the result exists. A pipelined driver separates them so the sequence can issue the next request at handoff.
  • get() hands the request off at once; put(rsp) returns the result later, in zero time, routed by the IDs set_id_info copied.
  • Two identical workers each carry their own item, with their own handle, through both phases; a one-key semaphore serialises request selection and the address phase.
  • A pipelined sequence sends on one thread and collects on another, with a new item per send, and stays alive until every response has arrived.
  • When completion reorders, match by transaction ID: look up each arriving response, or wait for a chosen ID and accept the head-of-line wait.

Practice