Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
    • TLM 1: Ports, Exports and Analysis
    • TLM 2.0 Sockets
      • Initiator and Target Sockets
      • Blocking Transport
      • Non-Blocking Transport
      • Temporal Decoupling
      • TLM 2.0 Generic Payload: Extensions and Status
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹TLM 2.0 Sockets
  1. Topics
  2. UVM Verification
  3. Transaction-Level Modeling (TLM)
  4. TLM 2.0 Sockets
  5. Blocking Transport

Part 4 · TLM & Analysis · Intermediate

Blocking Transport

One call per transfer with b_transport, the uvm_tlm_time delay argument, and the two ways an initiator and target can account for delay without double-counting it.

On this page

    A register sequence runs against the DMA's TLM model in a fraction of the RTL run time, which is the point. Then someone compares the timestamps and finds every access took twice the latency the model's documentation promises. Both sides were handling the delay: the target waited 10 ns, and then the initiator waited another 10 ns because the target had also left the 10 ns in the delay argument. How is a b_transport supposed to account for time?

    The idea: one call, and an agreed place to spend the time

    b_transport(payload, delay) is a task. The initiator calls it, the target does the whole transfer, and when the call returns the transfer is complete: the payload holds the read data and the response status. The second argument, a uvm_tlm_time object, is a timing annotation: an offset from the current simulation time at which the transfer is considered to happen. On entry it says how far ahead of simulation time the initiator already is; on return it says how far ahead the transfer's end is.

    That leaves a choice. The target can consume the delay, waiting in simulation time and returning a delay of zero. Or it can annotate, adding its latency to the delay and returning at once, leaving the initiator to wait (or to keep running ahead, which is temporal decoupling). Both are legal. What is not legal is both sides spending the same nanoseconds. It is like a restaurant bill: either the waiter adds the tip or you do, never both.

    The picture

    Two cases: an annotating target returns immediately with 10 ns in the delay and the initiator waits; a consuming target waits 10 ns itself and returns a zero delay.
    Either side may spend the time, but only one of them. The annotating style is faster and lets the initiator run ahead; the consuming style keeps simulation time exact at every call.

    Build it

    The target annotates

    The memory model in the lesson's TLM2 test checks the request, executes it, adds 10 ns to the delay and returns without waiting. Error responses return early, before the delay is added:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // snippet: from tlm2_gp_test in tlm_tb.sv, checks trimmed
    task b_transport(uvm_tlm_generic_payload gp, uvm_tlm_time delay);
      byte unsigned data[];
      bit [63:0]   a   = gp.get_address();
      int unsigned len = gp.get_data_length();
      if (a + len > 64'h100) begin
        gp.set_response_status(UVM_TLM_ADDRESS_ERROR_RESPONSE);
        return;                                         // nothing executed, no delay added
      end
      gp.get_data(data);
      for (int i = 0; i < len; i++)
        if (gp.is_write()) mem[a + i] = data[i];
        else               data[i] = mem.exists(a + i) ? mem[a + i] : 8'h00;
      if (gp.is_read()) gp.set_data(data);
      delay.incr(10ns, 1ns);                            // annotate 10 ns of latency
      gp.set_response_status(UVM_TLM_OK_RESPONSE);
    endtask

    The initiator honours what comes back

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // snippet: from tlm2_gp_test in tlm_tb.sv
    uvm_tlm_time delay = new("delay");                  // starts at 0
    // ... build gp (command, address, data, lengths, status = INCOMPLETE)
    isock.b_transport(gp, delay);
    #(delay.get_realtime(1ns));                         // spend the annotated time once
    if (!gp.is_response_ok())
      `uvm_error("GP", gp.get_response_string())

    uvm_tlm_time method

    What it does

    incr(t, 1ns) / decr(t, 1ns)

    Add or subtract t, given in the caller's time unit, scaled by the literal (here 1 ns)

    get_realtime(1ns)

    The value in the caller's time unit, ready for #(...)

    get_abstime(1e-9) / set_abstime(v, 1e-9)

    Read or set the value in absolute seconds-based units, independent of the caller's timescale

    reset()

    Back to zero

    Note

    In IEEE 1800.2: the class is named uvm_time, and uvm_tlm_time is a typedef of it, so code written for UVM 1.2 still compiles.

    The same target, consuming instead

    A consuming target spends the time itself: it first catches up with any offset the initiator passed in, then waits its own latency, and returns a zero delay so the initiator has nothing left to wait for.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet
    task b_transport(uvm_tlm_generic_payload gp, uvm_tlm_time delay);
      #(delay.get_realtime(1ns));         // catch up with the initiator's offset
      delay.reset();
      // ... the same checks and execution as above ...
      #10ns;                              // spend the access latency here
      gp.set_response_status(UVM_TLM_OK_RESPONSE);
    endtask                               // returns with delay = 0

    Choose consuming when the target interacts with anything that lives in simulation time, such as RTL signals, an interrupt it raises, or other SystemVerilog threads that must see the access happen at the right moment. Choose annotating when the target is a pure model and speed matters, because it lets a decoupled initiator batch its waits. A wrapper can convert between the two, which is often where a co-simulation bridge does its timing work.

    The rules of the call

    • The delay argument must not be null; the socket reports UVM/TLM/NULLDELAY and returns.

    • From the call until it returns, the payload belongs to the target. The initiator must not read results or change fields from another thread in the meantime.

    • When the call returns, the transfer is complete: no separate completion check exists, and the initiator may reuse the payload object.

    • b_transport may be called by several initiators at once through an interconnect, so a target that keeps state between # delays must be written to tolerate overlapping calls.

    Watch it run

    The first three transfers of the TLM2 test, each with the annotating target above and the waiting initiator:

    Diagram
    UVM_INFO tlm_tb.sv(502) @ 10000: uvm_test_top.ini [GP]  UVM_TLM_WRITE_COMMAND [0x0000000000000010] = 11 22 33 44 (status=OK) -> OK
    UVM_INFO tlm_tb.sv(502) @ 20000: uvm_test_top.ini [GP]  UVM_TLM_WRITE_COMMAND [0x0000000000000010] = aa bb -- -- (status=OK) -> OK
    UVM_INFO tlm_tb.sv(502) @ 30000: uvm_test_top.ini [GP]  UVM_TLM_READ_COMMAND [0x0000000000000010] = aa bb 33 44 (status=OK) -> OK
    UVM_INFO tlm_tb.sv(502) @ 30000: uvm_test_top.ini [GP]  UVM_TLM_READ_COMMAND [0x00000000000000fe] = aa bb 33 44 (status=ADDRESS_ERROR) -> ADDRESS_ERROR
    • Each successful transfer advanced simulation time by exactly 10 ns (times are in ps): spent once, by the initiator.

    • The read at 0xFE ran past the 256-byte model, so the target returned ADDRESS_ERROR before adding any delay: the time did not move. The data shown is just the stale buffer, which is why an initiator checks the status before trusting data.

    • The second write used byte enables (the -- bytes); the generic payload lesson explains them.

    What breaks: the latency counted twice

    The opening bug, exactly. The model's author wrote a consuming target: #(delay.get_realtime(1ns)) to wait out any incoming offset, then #10ns for the access, but then also delay.incr(10ns, 1ns) as documentation of the latency. The bench's initiator, written for annotating targets, waited out the returned delay. Every access cost 20 ns. Functionally nothing failed, so it went unnoticed until a performance model was checked against the RTL. The fix is a one-line contract written down for every TLM interface in the project: targets annotate, initiators wait, or the reverse, but one rule. A consuming target must return the delay reset to zero (delay.reset()).

    Interview

    "In b_transport, who is responsible for the delay?"

    A weak answer: "the target waits for the delay." That is one legal style, stated as the only one.

    A strong answer: "Either side, but exactly one. The delay argument is an offset from current simulation time. A target can consume it, waiting in simulation time and returning zero, or annotate, adding its latency and returning at once so the initiator waits or runs ahead under temporal decoupling. On return the transfer is complete and the response status is valid; until then the payload belongs to the target. The project should fix one convention, or latencies get counted twice."

    Expect the follow-ups: What happens with a null delay? (UVM/TLM/NULLDELAY error) and When can the initiator reuse the payload? (as soon as the call returns).

    Check yourself A target returns UVM_TLM_OK_RESPONSE and a delay of 25 ns. The initiator logs the result immediately without waiting. What is wrong, and when does it matter?

    The initiator is acting on a transfer 25 ns before it is considered complete. In a loosely timed model that is acceptable only if the initiator keeps a local time offset and synchronizes later (temporal decoupling). If it interacts with anything in simulation time, such as an RTL interrupt, it must wait the 25 ns first.

    Check yourself The initiator reuses one payload object for a write and then a read. When is that safe?

    As soon as the write's b_transport returns: return means the transfer is complete and the target has no further claim on the object. Before that, the object belongs to the target.

    Check yourself Two initiators reach the same annotating memory target through an interconnect, and both call b_transport at the same simulation time. The target keeps a busy flag between a # delay and the end of the access. What can go wrong, and which target style avoids it?

    The calls overlap: the second call enters while the first is still inside its delay, sees or overwrites busy, and the two accesses corrupt each other's state. A target must be re-entrant: keep per-call state in automatic variables. An annotating target that never waits inside b_transport cannot overlap with itself at all, which is one more reason pure models annotate.

    Next: one blocking call hides the request and response phases of a real bus. Non-blocking transport exposes them, with up to four calls per transfer.

    Related topics

    • Initiator and target sockets

    • Non-blocking transport

    • Temporal decoupling

    Previous

    69 / 280

    Next: Non-Blocking Transport

    Up next

    Up nextNon-Blocking Transport

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Temporal Decoupling
    • TLM 2.0 Generic Payload: Extensions and Status