An interviewer asks you to draw a TLM 2.0 non-blocking transfer. You draw four arrows: BEGIN_REQ, END_REQ, BEGIN_RESP, END_RESP. Good. Now the model you are integrating answers BEGIN_REQ with COMPLETED. Is that a protocol violation? Many candidates say yes. It is legal, and so are two other shortcuts. Why does the protocol have four phases, and when may a transfer take fewer calls?
A real bus transfer has two places where either side may need to wait: the target may not be ready to accept the request, and the initiator may not be ready to accept the response. The TLM 2.0 base protocol models each with a begin and an end phase: BEGIN_REQ (request offered), END_REQ (request accepted), BEGIN_RESP (response offered), END_RESP (response accepted). That lets one model pipeline requests and another apply backpressure, which b_transport cannot show. It is like a restaurant with a separate order counter and pickup counter: ordering and collecting are different moments, and either queue can back up.
Each phase is delivered by a function call that returns at once: nb_transport_fw on the forward path, initiator to target, carries BEGIN_REQ and END_RESP; nb_transport_bw on the backward path, target to initiator, carries END_REQ and BEGIN_RESP. The phase is passed by reference, so the callee can move it along in its return, and the return value says what happened:
Return ( | Meaning | What happens next |
|---|---|---|
| Phase received; nothing changed | The callee will call back later on the other path |
| The callee advanced the phase argument | The caller continues from the new phase |
| The transfer is finished | No more calls; the payload is free |
The base protocol allows shorter paths, and all of them are legal: the target may return UVM_TLM_COMPLETED to BEGIN_REQ (early completion, one call); it may send BEGIN_RESP without a separate END_REQ, which implies `END_REQ`; and the initiator may return UVM_TLM_COMPLETED to BEGIN_RESP instead of sending END_RESP. A transfer therefore takes four, three, two or one call. What is illegal is a phase on the wrong path or out of order.
Every call also carries a uvm_tlm_time delay, as in b_transport, but its meaning is narrower: it says how far after the current simulation time the phase being delivered takes effect. A target that accepts a request "in 5 ns" returns UPDATED with END_REQ and a 5 ns delay, and the initiator treats the request as accepted only once that time has passed.
A non-blocking target implements nb_transport_fw and calls nb_transport_bw through its socket; a non-blocking initiator implements nb_transport_bw and calls nb_transport_fw. Both are functions: they must return immediately, so any waiting happens in a separate thread that later makes the next call.
// snippet: target side, inside the class named as IMP of a uvm_tlm_nb_target_socket
function uvm_tlm_sync_e nb_transport_fw(uvm_tlm_generic_payload gp,
ref uvm_tlm_phase_e phase,
input uvm_tlm_time delay);
case (phase)
UVM_TLM_BEGIN_REQ: begin
pending.push_back(gp); // a thread will send BEGIN_RESP on tsock.nb_transport_bw
phase = UVM_TLM_END_REQ; // accept the request in the return itself
return UVM_TLM_UPDATED; // or execute now and return UVM_TLM_COMPLETED
end
UVM_TLM_END_RESP: return UVM_TLM_COMPLETED;
default: return UVM_TLM_ACCEPTED;
endcase
endfunction
// initiator side, inside the class named as IMP of a uvm_tlm_nb_initiator_socket
function uvm_tlm_sync_e nb_transport_bw(uvm_tlm_generic_payload gp,
ref uvm_tlm_phase_e phase,
input uvm_tlm_time delay);
case (phase)
UVM_TLM_END_REQ: return UVM_TLM_ACCEPTED; // free to send the next request
UVM_TLM_BEGIN_RESP: begin
// sample the response here, then close the transfer in the return
return UVM_TLM_COMPLETED; // implies END_RESP
end
default: return UVM_TLM_ACCEPTED;
endcase
endfunctionBecause both callbacks return at once, the initiator's sequential logic lives in a task that sends BEGIN_REQ, interprets the return, and otherwise waits for the backward callback to signal it. Events tie the two together:
// snippet: initiator side; nb_transport_bw triggers req_accepted and resp_done
event req_accepted, resp_done;
task do_transfer(uvm_tlm_generic_payload gp);
uvm_tlm_phase_e phase = UVM_TLM_BEGIN_REQ;
uvm_tlm_time delay = new("delay");
uvm_tlm_sync_e sync = isock.nb_transport_fw(gp, phase, delay);
case (sync)
UVM_TLM_COMPLETED: return; // early completion: one call
UVM_TLM_UPDATED: if (phase == UVM_TLM_BEGIN_RESP) begin
// the target answered in the return: close it ourselves
phase = UVM_TLM_END_RESP;
void'(isock.nb_transport_fw(gp, phase, delay));
return;
end
default: ; // ACCEPTED, or UPDATED to END_REQ
endcase
@resp_done; // set by nb_transport_bw on BEGIN_RESP
endtaskA real initiator also honours the delay argument at each step and waits for req_accepted before it sends the next BEGIN_REQ. The structure is the point: functions for the callbacks, a task for the sequence of steps, and events between them.
Ownership follows the phases. The target may read the payload from BEGIN_REQ on. The initiator must not modify or reuse the payload until the transfer completes, by a COMPLETED return or the final END_RESP. The base protocol also allows only one request phase and one response phase in progress per socket at a time, which is how END_REQ acts as backpressure: an initiator may not send the next BEGIN_REQ until the previous request has been accepted.
The plain-SystemVerilog model below is not the UVM library; it captures the base protocol's rules as a state machine. It accepts the four-, three-, two- and one-call forms and flags phases on the wrong path or out of order:
typedef enum {PH_BEGIN_REQ, PH_END_REQ, PH_BEGIN_RESP, PH_END_RESP} ph_e;
typedef enum {FW, BW} path_e;
// Tracks one transaction through the TLM-2.0 base protocol.
// state 0: idle, 1: request open, 2: request accepted, 3: response open
class phase_tracker;
int state;
bit [31:0] held;
function new();
state = 0;
held = '0;
endfunction
// A legal call returns 1 and advances the state; an illegal call returns 0.
function bit call(path_e p, ph_e ph, bit [31:0] wdata);
bit ok;
ok = 0;
case (state)
0: if (p == FW && ph == PH_BEGIN_REQ) begin state = 1; held = wdata; ok = 1; end
1: begin
if (p == BW && ph == PH_END_REQ) begin state = 2; ok = 1; end
else if (p == BW && ph == PH_BEGIN_RESP) begin state = 3; ok = 1; end // implies END_REQ
end
2: if (p == BW && ph == PH_BEGIN_RESP) begin state = 3; ok = 1; end
3: if (p == FW && ph == PH_END_RESP) begin state = 0; ok = 1; end
default: ok = 0;
endcase
return ok;
endfunction
// A UVM_TLM_COMPLETED return closes the transaction from any open state.
function void completed();
state = 0;
endfunction
endclass
PASS: 4-, 3-, 2- and 1-call transfers accepted, 3 illegal phases flagged, payload intactA team wrote a protocol checker for their non-blocking adapter that insisted on all four phases, in order, for every transfer. It worked against their own target. Then they connected a vendor's SystemC model through co-simulation, and every single register read was flagged as a protocol violation: the vendor's model completed simple register accesses early, returning UVM_TLM_COMPLETED to BEGIN_REQ, as the base protocol allows. A week went into a bug report against the vendor before someone read the standard. A second, real bug was hiding in the same adapter: after receiving UPDATED with END_REQ, it also waited for a separate END_REQ call that never came. A checker for TLM 2.0 must accept every legal form and flag only wrong paths, wrong order and ownership violations, exactly what the tracker above does.
UVM_TLM_UPDATED with the phase now END_REQ. Who makes the next call, and with which phase?The target, on the backward path, with BEGIN_RESP once the response is ready. The UPDATED return already delivered END_REQ, so no separate END_REQ call follows.
BEGIN_RESP arrives and it returns UVM_TLM_ACCEPTED. What is wrong?Returning ACCEPTED means the initiator will send END_RESP later, so the transfer is not complete and the payload still belongs to it. Reusing the object now overwrites the response. Either return COMPLETED after sampling the response, or wait until after END_RESP.
BEGIN_REQ with UVM_TLM_COMPLETED. Where is the read data, and when may the initiator trust it?In the payload's data array, and the response status is already set, at the moment the forward call returns. Early completion is the non-blocking equivalent of b_transport: the whole transfer happened inside one call, so the initiator checks the status and uses the data immediately, honouring the returned delay if it is not zero.
A last practical point: a non-blocking interface is only worth its complexity when the model actually overlaps transfers, accepting a new request while an earlier response is still pending. A register block that serves one access at a time gains nothing from it, which is why many SystemC models offer both interfaces and complete simple accesses early.
Next: phases model a bus accurately, at the cost of many calls. Temporal decoupling goes the other way, trading timing accuracy for speed.