Why does UVM need both wait_for_grant() and wait_for_item_done()?

Hi everyone,

I understand the basic purpose of these methods:

wait_for_grant();
send_request(req);
wait_for_item_done();

  • wait_for_grant() waits until the sequencer grants permission to the sequence.

  • send_request() sends the item through the sequencer to the driver.

  • wait_for_item_done() waits until the driver indicates completion of the item.

However, I’m struggling to understand why UVM needs both waiting mechanisms.

My current thinking is:

If multiple sequences are running on the same sequencer, then the sequencer arbitrates based on priority and grants one sequence at a time. The granted sequence sends its transaction to the driver. Once the driver completes that transaction, the sequencer could simply grant the next waiting sequence.

If that’s how the flow works, then it seems like a single wait condition should be enough. Why do we need both:

wait_for_grant() and wait_for_item_done()

Specifically:

  1. Is my understanding above incorrect?

  2. Can the sequencer grant another sequence even though a previously granted transaction has not yet completed in the driver?

  3. Does the sequencer keep requests queued while the driver is still working on earlier transactions?

  4. What would break if UVM only had wait_for_grant() and no wait_for_item_done()?

I’m trying to understand the actual communication flow and timing relationship between sequence, sequencer, and driver, rather than just the individual API descriptions.

Thanks!

The preferred mechanism is start_item()/finish_item(), which encapsulates the lower-level wait_for_grant()/send_request() API. The lower-level methods exist primarily for advanced use cases and to expose the underlying sequencer protocol.

One of the reasons UVM separates the grant and send phases is to support what we call late randomization. Instead of randomizing an item before requesting the sequencer, a sequence can wait until it has actually won arbitration:

start_item(req);
assert(req.randomize());
finish_item(req);

Here, start_item() does not return until the sequence has been granted access to the sequencer. The item is then randomized and sent immediately. This ensures the randomization reflects the state of the testbench at the time the request is issued, not at some earlier time when the sequence first became ready.

More generally, the work done between start_item() and finish_item() is not limited to randomization. Any computation that needs to occur at the point of request can be performed there: consulting a model, examining previous responses, calculating an address, updating sequence state, etc.

This explains why wait_for_grant() and wait_for_item_done() are fundamentally different. wait_for_grant() (or start_item()) synchronizes with the point where a sequence is allowed to prepare and issue its next request. wait_for_item_done() synchronizes with a later event—the driver finishing a previously issued request.

A lot of how these methods are used depends on whether the driver is pipelined. In a simple non-pipelined driver, a sequence often issues one request and then immediately waits for completion before issuing the next. In that case, the distinction between grant and item completion may not seem very important because they occur in a tightly coupled sequence.

With a pipelined driver, however, the distinction becomes essential. A sequence may receive a grant, perform late randomization, send a request, and then another sequence may be granted and send additional requests before the first request completes. The grant event controls ownership of the sequencer for issuing requests, while item_done tracks completion of requests that have already been issued. Those are independent events that can be separated by many clock cycles.

How does the sequencer decide that THIS sequence gets the grant?