Non-Blocking TLM and Protocol Phases
How BEGIN_REQ, END_REQ, BEGIN_RESP, and END_RESP express pipelined transaction protocols.
How to Read This Lesson
Blocking transport models a transaction as one call. Non-blocking transport models a transaction as a sequence of phases. According to the IEEE 1666-2023 LRM Section 14, non-blocking transport is used when protocol ordering, pipelining, and response timing matter.
Standard and source context
The Four-Phase Base Protocol
The TLM-2.0 base protocol commonly uses:
BEGIN_REQ: initiator begins a requestEND_REQ: target accepts the requestBEGIN_RESP: target begins the responseEND_RESP: initiator accepts the response
This lets a target accept a request now, freeing the initiator to do other work, and complete it later. The initiator calls the forward path (nb_transport_fw), and the target can call back on the backward path (nb_transport_bw). The split reflects protocol direction.
Return Status
Non-blocking transport returns a synchronization enum (tlm_sync_enum):
TLM_ACCEPTED: transaction accepted, more phases will arrive later via the backward/forward path.TLM_UPDATED: phase and delay were updated immediately.TLM_COMPLETED: transaction completed without more callbacks.
The status is part of the protocol. Ignoring it is a violation of the TLM-2.0 standard.
Payload Event Queue and Complete Example
tlm_utils::peq_with_get helps targets schedule transactions for later processing. A PEQ keeps the target from trying to complete everything inside the transport call.
Here is a fully compilable, standards-compliant AT (Approximately-Timed) non-blocking example utilizing the Payload Event Queue.
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <tlm_utils/peq_with_get.h>
using namespace sc_core;
SC_MODULE(NBTarget) {
tlm_utils::simple_target_socket<NBTarget> socket{"socket"};
tlm_utils::peq_with_get<tlm::tlm_generic_payload> peq;
SC_CTOR(NBTarget) : peq("peq") {
socket.register_nb_transport_fw(this, &NBTarget::nb_transport_fw);
SC_THREAD(process_transactions);
}
tlm::tlm_sync_enum nb_transport_fw(tlm::tlm_generic_payload& trans,
tlm::tlm_phase& phase, sc_time& delay) {
if (phase == tlm::BEGIN_REQ) {
// Accept request and schedule for processing
peq.notify(trans, delay);
phase = tlm::END_REQ;
return tlm::TLM_UPDATED;
}
if (phase == tlm::END_RESP) {
return tlm::TLM_COMPLETED;
}
return tlm::TLM_ACCEPTED;
}
void process_transactions() {
while (true) {
wait(peq.get_event()); // Wait for PEQ notification
tlm::tlm_generic_payload* trans;
while ((trans = peq.get_next_transaction()) != nullptr) {
// Process scheduled work here.
wait(10, SC_NS); // Simulated processing time
trans->set_response_status(tlm::TLM_OK_RESPONSE);
tlm::tlm_phase bw_phase = tlm::BEGIN_RESP;
sc_time bw_delay = SC_ZERO_TIME;
// Call back to initiator
socket->nb_transport_bw(*trans, bw_phase, bw_delay);
}
}
}
};
SC_MODULE(NBInitiator) {
tlm_utils::simple_initiator_socket<NBInitiator> socket{"socket"};
SC_CTOR(NBInitiator) {
socket.register_nb_transport_bw(this, &NBInitiator::nb_transport_bw);
SC_THREAD(run);
}
void run() {
tlm::tlm_generic_payload trans;
sc_time delay = SC_ZERO_TIME;
tlm::tlm_phase phase = tlm::BEGIN_REQ;
unsigned char data = 0xFF;
trans.set_command(tlm::TLM_WRITE_COMMAND);
trans.set_address(0x100);
trans.set_data_ptr(&data);
trans.set_data_length(1);
// Send request
tlm::tlm_sync_enum status = socket->nb_transport_fw(trans, phase, delay);
if (status == tlm::TLM_UPDATED && phase == tlm::END_REQ) {
std::cout << "Target accepted request at " << sc_time_stamp() << "\n";
}
wait(); // Yield
}
tlm::tlm_sync_enum nb_transport_bw(tlm::tlm_generic_payload& trans,
tlm::tlm_phase& phase, sc_time& delay) {
if (phase == tlm::BEGIN_RESP) {
std::cout << "Initiator received response at " << sc_time_stamp() << "\n";
phase = tlm::END_RESP;
return tlm::TLM_COMPLETED;
}
return tlm::TLM_ACCEPTED;
}
};
int sc_main(int argc, char* argv[]) {
NBInitiator init("init");
NBTarget tgt("tgt");
init.socket.bind(tgt.socket);
sc_start(100, SC_NS);
return 0;
}When to Avoid Non-Blocking TLM
Do not use non-blocking transport just because it feels more professional. Use it when the model needs:
- multiple outstanding transactions
- pipelined request/response behavior
- approximately timed modeling
- timing points visible to both initiator and target
- protocol-level backpressure
For simple memory-mapped platforms doing sequential fetch-decode-execute loops, blocking transport (b_transport) is often better and significantly faster.
Under the Hood: The TLM Base Protocol State Machine
The non-blocking transport (nb_transport_fw and nb_transport_bw) implements a state machine using the tlm_phase enum.
In src/tlm_core/tlm_2/tlm_generic_payload/tlm_phase.h, the base protocol defines four standard phases: BEGIN_REQ, END_REQ, BEGIN_RESP, END_RESP.
When a master calls nb_transport_fw(trans, phase, delay), it passes the phase by reference. The target can update the phase and return TLM_UPDATED, or return TLM_ACCEPTED indicating it will send the response later via nb_transport_bw. This entirely avoids SystemC wait() calls, achieving high simulation speeds while accurately modeling pipelined bus architectures like AXI.
Can you answer these clearly?
Keep moving when you can answer each question without looking back at the lesson.
Comments and Corrections