While basic SystemC is relatively straightforward—the most naive approach involves writing C++ code in an RTL-like manner—this approach is not ideal. Writing at such a low level defeats the purpose, as one might aswell write RTL directly. When working with SystemC at this stage, the goal is typically to create "abstract models with critical timing and complete functionality." These models don't require accurate interface implementations or dependency-aware timing. Instead, the entire hardware is abstracted into several major functional blocks, where communication between blocks uses "transaction transfers" instead of physical signal wiring, and internal block behavior uses timing descriptions rather than dependent timing implementations.
For example, when a CPU accesses memory, TLM abstracts this access as a memory read/write "transaction" rather than implementing it through physical signal interfaces like wen, addr, din, and dout. Additionally, the transaction doesn't need to specify detailed physical signal timing relationships—it only needs to define the start and end times of communication and describe the time interval directly.
TLM2.0 Transaction Concepts
In TLM2.0, a "transaction" is abstracted as a socket. The transaction initiator is called the initiator, and the responding entity is called the target. Information exchange occurs between initiator sockets and target sockets.
This information exchange can be implemented in two ways: blocking transport (b_transport) and non-blocking transport (nb_transport). The former is typically used for simple functional modeling, while the latter is suitable for complex performance modeling.
Blocking Transport
For blocking transport, a b_transport function must be registered with the target socket. During execution, the initiator calls b_transport to send information to the target. The b_transport function interface includes tlm_generic_payload and sc_time—the former is a standardized information format, and the latter represents time based on the SystemC simulator.
Non-Blocking Transport
For non-blocking transport, nb_transport functions must be constructed in both the initiator and target. The initiator registers nb_transport_bw with the initiator socket, while the target registers nb_transport_fw with the target socket. In addition to tlm_generic_payload and sc_time present in blocking transport, nb_transport introduces an additional tlm_phase parameter to identify the state of non-blocking transport. Default tlm_phase values include: BEGIN_REQ, END_REQ, BEGIN_RESP, and END_RESP. For more complex protocols, TLM_DECLARE_EXTENDED_PHASE can be used to add more phases.
Code Examples and Analysis
Blocking Transport Implementation
#include <systemc>
#include "tlm_utils/simple_initiator_socket.h"
#include "tlm_utils/simple_target_socket.h"
class MasterDevice : public sc_core::sc_module
{
public:
SC_HAS_PROCESS(MasterDevice);
explicit MasterDevice(sc_core::sc_module_name name)
: sc_core::sc_module(name),
m_master_socket("master_socket")
{
SC_THREAD(ProcessingLoop);
}
void ProcessingLoop()
{
int t_counter = 0;
sc_core::sc_time t_interval = sc_core::SC_ZERO_TIME;
while (true)
{
t_counter++;
t_interval = sc_core::sc_time(t_counter, sc_core::SC_NS);
tlm::tlm_generic_payload *t_data = new tlm::tlm_generic_payload();
t_data->set_address(0x20000 + t_counter);
std::cout << "\033[32m [" << sc_core::sc_time_stamp() << "]"
<< " invoking b_transport at address " << std::hex << t_data->get_address()
<< " with interval " << t_counter << " SC_NS"
<< " \033[0m" << std::endl;
m_master_socket->b_transport(*t_data, t_interval);
wait(1, sc_core::SC_NS);
}
}
tlm_utils::simple_initiator_socket<MasterDevice> m_master_socket;
};
class SlaveDevice : public sc_core::sc_module
{
public:
SC_HAS_PROCESS(SlaveDevice);
explicit SlaveDevice(sc_core::sc_module_name name)
: sc_core::sc_module(name), m_slave_socket("slave_socket")
{
m_slave_socket.register_b_transport(this, &SlaveDevice::HandleRequest);
}
void HandleRequest(tlm::tlm_generic_payload &payload, sc_core::sc_time &t_interval)
{
wait(t_interval);
std::cout << "\033[33m [" << sc_core::sc_time_stamp() << "]"
<< " HandleRequest received address " << std::hex << payload.get_address()
<< " \033[0m" << std::endl;
}
tlm_utils::simple_target_socket<SlaveDevice> m_slave_socket;
};
class TestEnvironment : public sc_core::sc_module
{
public:
SC_HAS_PROCESS(TestEnvironment);
explicit TestEnvironment(sc_core::sc_module_name name)
: sc_core::sc_module(name),
m_master("master"),
m_slave("slave")
{
m_master.m_master_socket.bind(m_slave.m_slave_socket);
}
MasterDevice m_master;
SlaveDevice m_slave;
};
int sc_main(int argc, char **argv)
{
TestEnvironment m_test_env("test_environment");
sc_core::sc_start(20, sc_core::SC_NS);
return 0;
}
This code creates three classes: MasterDevice, SlaveDevice, and TestEnvironment. Each class inherits from sc_module, equivalent to using the SC_MODULE macro.
The MasterDevice constructor declares the SC_HAS_PROCESS macro, creates the ProcessingLoop thread using SC_THREAD, and initializes the m_master_socket member (the initiator socket using TLM's simple_initiator_socket). The ProcessingLoop initiates a transaction each cycle by creating a new tlm_generic_payload, setting its address, and calling b_transport with the payload and time interval.
The SlaveDevice constructor initializes m_slave_socket (the target socket using simple_target_socket), and calls register_b_transport to automatically invoke HandleRequest when receiving payloads from the initiator.
The TestEnvironment instantiates one MasterDevice and one SlaveDevice, connecting them via the bind method.
The simulation runs for 20ns in sc_main.
Execution output:
[0 s] invoking b_transport at address 20001 with interval 1 SC_NS
[1 ns] HandleRequest received address 20001
[2 ns] invoking b_transport at address 20002 with interval 2 SC_NS
[4 ns] HandleRequest received address 20002
[5 ns] invoking b_transport at address 20003 with interval 3 SC_NS
[8 ns] HandleRequest received address 20003
[9 ns] invoking b_transport at address 20004 with interval 4 SC_NS
[13 ns] HandleRequest received address 20004
[14 ns] invoking b_transport at address 20005 with interval 5 SC_NS
[19 ns] HandleRequest received address 20005
Timing analysis: The initiator requires 1ns to initiate each transaction and blocks until the transaction completes. The target's receive delay is controlled by the initiator's t_interval parameter, which increases with each transmission. The overall timing sequence: at 0s, the initiator sends the first transaction; at 1ns, the target waits 1ns and receives it; at 2ns, the initiator waits 1ns and sends the second transaction; at 4ns, the target waits 2ns and receives it, and so on. Simulation ends at 20ns.
Non-Blocking Transport Implementation
#include <systemc>
#include "tlm_utils/simple_initiator_socket.h"
#include "tlm_utils/simple_target_socket.h"
#include "tlm_utils/peq_with_get.h"
class MasterDeviceNb : public sc_core::sc_module
{
public:
SC_HAS_PROCESS(MasterDeviceNb);
explicit MasterDeviceNb(sc_core::sc_module_name name)
: sc_core::sc_module(name),
m_master_socket("master_socket_nb"), m_response_queue("response_queue")
{
m_master_socket.register_nb_transport_bw(this, &MasterDeviceNb::BackwardPathHandler);
SC_THREAD(RequestThread);
SC_THREAD(ResponseCompletionThread);
sensitive << m_response_queue.get_event();
}
void RequestThread()
{
int t_counter = 0;
tlm::tlm_phase t_phase = tlm::BEGIN_REQ;
sc_core::sc_time t_interval = sc_core::SC_ZERO_TIME;
while (true)
{
t_counter++;
t_interval = sc_core::sc_time(t_counter, sc_core::SC_NS);
tlm::tlm_generic_payload *t_data = new tlm::tlm_generic_payload();
t_data->set_address(0x20000 + t_counter);
std::cout << this->name() << "\033[32m [" << sc_core::sc_time_stamp() << "]"
<< " invoking nb_transport_fw, BEGIN_REQ phase, addr=0x" << std::hex
<< t_data->get_address() << " interval " << t_counter
<< " \033[0m" << std::endl;
m_master_socket->nb_transport_fw(*t_data, t_phase, t_interval);
wait(m_request_complete_event);
wait(1, sc_core::SC_NS);
}
}
tlm::tlm_sync_enum BackwardPathHandler(tlm::tlm_generic_payload &payload,
tlm::tlm_phase &phase, sc_core::sc_time &interval)
{
switch (phase)
{
case tlm::END_REQ:
std::cout << this->name()
<< "\033[36m [" << sc_core::sc_time_stamp() << "]"
<< " BackwardPathHandler received END_REQ phase, addr=0x" << std::hex
<< payload.get_address() << " \033[0m" << std::endl;
m_request_complete_event.notify();
break;
case tlm::BEGIN_RESP:
std::cout << this->name() << "\033[36m [" << sc_core::sc_time_stamp() << "]"
<< " BackwardPathHandler received BEGIN_RESP phase, addr=0x" << std::hex
<< payload.get_address() << " \033[0m" << std::endl;
m_response_queue.notify(payload);
break;
default:
assert(false);
}
return tlm::TLM_ACCEPTED;
}
void ResponseCompletionThread()
{
tlm::tlm_generic_payload *t_item = nullptr;
tlm::tlm_phase t_phase = tlm::END_RESP;
sc_core::sc_time t_interval = sc_core::SC_ZERO_TIME;
while (true)
{
wait();
while ((t_item = m_response_queue.get_next_transaction()) != nullptr)
{
std::cout << this->name() << "\033[32m [" << sc_core::sc_time_stamp() << "]"
<< " invoking nb_transport_fw, END_RESP phase, addr=0x" << std::hex
<< t_item->get_address() << " \033[0m" << std::endl;
m_master_socket->nb_transport_fw(*t_item, t_phase, t_interval);
t_item = nullptr;
}
}
}
sc_core::sc_event m_request_complete_event;
tlm_utils::simple_initiator_socket<MasterDeviceNb> m_master_socket;
tlm_utils::peq_with_get<tlm::tlm_generic_payload> m_response_queue;
};
class SlaveDeviceNb : public sc_core::sc_module
{
public:
SC_HAS_PROCESS(SlaveDeviceNb);
explicit SlaveDeviceNb(sc_core::sc_module_name name)
: sc_core::sc_module(name), m_slave_socket("slave_socket_nb")
{
m_slave_socket.register_nb_transport_fw(this, &SlaveDeviceNb::ForwardPathHandler);
SC_THREAD(RequestHandlerThread);
SC_THREAD(ResponseHandlerThread);
}
tlm::tlm_sync_enum ForwardPathHandler(tlm::tlm_generic_payload &payload,
tlm::tlm_phase &phase, sc_core::sc_time &interval)
{
wait(interval);
switch (phase)
{
case tlm::BEGIN_REQ:
m_request_buffer.write(&payload);
std::cout << this->name()
<< "\033[33m [" << sc_time_stamp() << "]"
<< " ForwardPathHandler received BEGIN_REQ phase, addr=0x" << std::hex
<< payload.get_address() << " \033[0m" << std::endl;
break;
case tlm::END_RESP:
std::cout << this->name() << "\033[33m [" << sc_core::sc_time_stamp() << "]"
<< " ForwardPathHandler received END_RESP phase, addr=0x" << std::hex
<< payload.get_address() << " \033[0m\n"
<< std::endl;
break;
default:
assert(false);
}
return tlm::TLM_ACCEPTED;
}
void RequestHandlerThread()
{
tlm::tlm_phase t_phase = tlm::END_REQ;
sc_core::sc_time t_interval = sc_core::SC_ZERO_TIME;
while (true)
{
tlm::tlm_generic_payload *t_payload = m_request_buffer.read();
std::cout << this->name() << "\033[31m [" << sc_core::sc_time_stamp() << "]"
<< " invoking nb_transport_bw, END_REQ phase, addr=0x" << std::hex
<< t_payload->get_address() << " \033[0m" << std::endl;
m_slave_socket->nb_transport_bw(*t_payload, t_phase, t_interval);
m_response_buffer.write(t_payload);
wait(1, sc_core::SC_NS);
}
}
void ResponseHandlerThread()
{
tlm::tlm_phase t_phase = tlm::BEGIN_RESP;
sc_core::sc_time t_interval = sc_core::sc_time(1, sc_core::SC_NS);
while (true)
{
tlm::tlm_generic_payload *t_payload = m_response_buffer.read();
std::cout << this->name() << "\033[31m [" << sc_core::sc_time_stamp() << "]"
<< " invoking nb_transport_bw, BEGIN_RESP phase, addr=0x" << std::hex
<< t_payload->get_address() << " \033[0m" << std::endl;
m_slave_socket->nb_transport_bw(*t_payload, t_phase, t_interval);
wait(1, sc_core::SC_NS);
}
}
tlm_utils::simple_target_socket<SlaveDeviceNb> m_slave_socket;
sc_core::sc_fifo<tlm::tlm_generic_payload *> m_request_buffer;
sc_core::sc_fifo<tlm::tlm_generic_payload *> m_response_buffer;
};
class TestEnvironmentNb : public sc_core::sc_module
{
public:
SC_HAS_PROCESS(TestEnvironmentNb);
explicit TestEnvironmentNb(sc_core::sc_module_name name)
: sc_core::sc_module(name),
m_master("master_nb"),
m_slave("slave_nb")
{
m_master.m_master_socket.bind(m_slave.m_slave_socket);
}
MasterDeviceNb m_master;
SlaveDeviceNb m_slave;
};
int sc_main(int argc, char **argv)
{
TestEnvironmentNb m_test_env("test_environment_nb");
sc_core::sc_start(20, sc_core::SC_NS);
return 0;
}
The non-blocking transport implementation is significantly more complex than blocking transport. This code creates three classes: MasterDeviceNb, SlaveDeviceNb, and TestEnvironmentNb.
The MasterDeviceNb constructor initializes m_master_socket and m_response_queue. The response queue is a specialized utility (using TLM's peq_with_get class) for managing timing during information transfer. The constructor registers nb_transport_bw via simple_initiator_socket to handle incoming responses through BackwardPathHandler, and creates two threads: RequestThread and ResponseCompletionThread.
As previously discussed, non-blocking transport involves four phases: BEGIN_REQ, END_REQ, BEGIN_RESP, and END_RESP. The sequence is: initiator sends BEGIN_REQ, target responds; target sends END_REQ, initiator responds; target sends BEGIN_RESP, initiator responds; initiator sends END_RESP, target responds—completing the four-phase non-blocking transaction.
The BackwardPathHandler processes target's END_REQ and BEGIN_RESP responses. When receiving END_REQ, it calls notify() on m_request_complete_event to signal the RequestThread. When receiving BEGIN_RESP, it calls m_response_queue.notify() to schedule the payload for future processing (defaulting to SC_ZERO_TIME if no time is specified).
RequestThread and ResponseCompletionThread correspond to the initiator's BEGIN_REQ and END_RESP phases respectively. RequestThread sends the transaction and waits for END_REQ from the target. ResponseCompletionThread waits for the m_response_queue event, then calls get_next_transaction() to process all queued transactions. No delays or waits should be introduced here, as this would affect transaction timing accuracy.
The SlaveDeviceNb constructor registers nb_transport_fw via simple_target_socket and creates two threads: RequestHandlerThread and ResponseHandlerThread. It also initializes two FIFOs (m_request_buffer and m_response_buffer) for cross-thread communication—these block on write when full and on read when empty.
ForwardPathHandler processes BEGIN_REQ and END_RESP responses. In the BEGIN_REQ phase, it writes the payload to m_request_buffer. RequestHandlerThread sends END_REQ after reading from m_request_buffer, then writes to m_response_buffer. ResponseHandlerThread sends BEGIN_RESP after reading from m_response_buffer.
TestEnvironmentNb instantiates both devices and connects their sockets. The simulation runs for 20ns.
Execution output demonstrates the four-phase protocol clearly:
test_environment_nb.master [0 s] invoking nb_transport_fw, BEGIN_REQ phase, addr=0x20001 interval 1
test_environment_nb.slave [1 ns] ForwardPathHandler received BEGIN_REQ phase, addr=0x20001
test_environment_nb.slave [1 ns] invoking nb_transport_bw, END_REQ phase, addr=0x20001
test_environment_nb.master [1 ns] BackwardPathHandler received END_REQ phase, addr=0x20001
test_environment_nb.slave [1 ns] invoking nb_transport_bw, BEGIN_RESP phase, addr=0x20001
test_environment_nb.master [1 ns] BackwardPathHandler received BEGIN_RESP phase, addr=0x20001
test_environment_nb.master [1 ns] invoking nb_transport_fw, END_RESP phase, addr=0x20001
test_environment_nb.slave [1 ns] ForwardPathHandler received END_RESP phase, addr=0x20001
...
Each complete non-blocking transaction follows the sequence: initiator sends BEGIN_REQ, target receives and sends END_REQ, initiator receives and target sends BEGIN_RESP, initiator receives and sends END_RESP, target receives. The RequestThread increments its delay counter each cycle, and ForwardPathHandler waits for the specified delay. No additional delays exist between operations.
TLM2.0 Summary
TLM2.0 doesn't replace SystemC functionality but enhances its abstract modeling capabilities. A significant challenge with raw SystemC is that implementing complex data transfer behaviors (such as various bus protocols) using the native sc_port mechanism is time-consuming. TLM (Transaction Level Modeling) provides efficient abstraction for these behaviors, simplifying them into blocking and non-blocking transport. Notab, C++ TLM2.0 can seamlessly interface with SystemVerilog UVM through sockets, greatly facilitating verification. TLM2.0 can also be used in conjunction with the sc_port mechanism as needed.