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?
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 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:
// 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// 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())
| What it does |
|---|---|
| Add or subtract |
| The value in the caller's time unit, ready for |
| Read or set the value in absolute seconds-based units, independent of the caller's timescale |
| Back to zero |
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.
// 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 = 0Choose 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 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.
The first three transfers of the TLM2 test, each with the annotating target above and the waiting initiator:
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_ERROREach 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.
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()).
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.
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.
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.