TLM-2.0, Generic Payloads, and Sockets
The practical model for fast transaction-level platforms using initiator and target sockets.
How to Read This Lesson
For TLM, resist the temptation to picture pins. Picture a C++ function call carrying a transaction object, then add timing only where the architectural question needs it.
TLM-2.0, Generic Payloads, and Sockets
Transaction Level Modeling (TLM) changes the unit of communication. Instead of toggling pins and evaluating clock edges delta-cycle by delta-cycle, a model sends a high-level transaction: read this address, write these bytes, wait this long, return this response.
TLM-2.0 is the official IEEE standard methodology built on top of SystemC to standardize how IPs communicate in Virtual Platforms, ensuring interoperability between models from different vendors.
Source and LRM Trail
For TLM, use the IEEE 1666 TLM clauses in Docs/LRMs/SystemC_LRM_1666-2023.pdf as the portable contract. Then inspect .codex-src/systemc/src/tlm_core/tlm_2: tlm_generic_payload, tlm_fw_transport_if, tlm_bw_transport_if, tlm_initiator_socket, tlm_target_socket, tlm_dmi, and tlm_quantumkeeper.
The Generic Payload (tlm_generic_payload)
The core of TLM-2.0 is the tlm_generic_payload. According to the LRM, this single class encapsulates all attributes necessary to model standard memory-mapped bus transactions (like AXI, AHB, APB, PCIe).
The standard payload carries:
- Command: Read (
TLM_READ_COMMAND), Write (TLM_WRITE_COMMAND), or Ignore. - Address: A 64-bit unsigned integer representing the memory-mapped address.
- Data Pointer and Length: An
unsigned char*array and its length. - Byte Enables: A pointer and length for masking specific bytes during writes.
- Streaming Width: Used to model burst transfers to a fixed FIFO address.
- Response Status: Updated by the target (e.g.,
TLM_OK_RESPONSE,TLM_ADDRESS_ERROR_RESPONSE). - Extensions: A mechanism to attach custom bus-specific metadata (like AXI secure bits) without altering the base payload.
By enforcing a single generic payload, TLM-2.0 ensures that a CPU modeled by ARM can talk directly to a memory controller modeled by Synopsys without needing a protocol adapter.
Initiator and Target Sockets
TLM-2.0 groups ports, exports, and interfaces into a unified concept called Sockets.
- Initiator Socket: Sends transactions (e.g., CPUs, DMAs).
- Target Socket: Receives transactions (e.g., RAMs, Peripherals).
The tlm_utils namespace provides simple_initiator_socket and simple_target_socket, which hide the complex interface multi-inheritance rules required by the LRM, making TLM-2.0 modeling highly accessible.
Loosely Timed (LT) Modeling (Blocking Transport)
The most common modeling style for firmware development is Loosely Timed (LT) modeling, which uses the b_transport (blocking transport) interface.
In b_transport, the initiator calls a function on the target, passing the payload and a sc_time reference. The target performs the memory operation instantly in C++ execution time, updates the sc_time reference to indicate how long the hardware would have taken, and returns.
This skips thousands of simulated clock cycles instantly, enabling Virtual Platforms to boot Linux in seconds.
Complete Example: Initiator to Target Communication
Here is a complete, compilable sc_main demonstrates a simple DMA (Initiator) writing data to a Memory block (Target) using standard TLM-2.0 blocking transport and generic payloads.
#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
#include <iostream>
// ---------------------------------------------------------
// INITIATOR (e.g., CPU, DMA)
// ---------------------------------------------------------
SC_MODULE(Initiator) {
tlm_utils::simple_initiator_socket<Initiator> socket{"socket"};
SC_CTOR(Initiator) {
SC_THREAD(run);
}
void run() {
tlm::tlm_generic_payload trans;
sc_core::sc_time delay = sc_core::SC_ZERO_TIME;
// Data to write
uint32_t data = 0xDEADBEEF;
// Configure the Generic Payload
trans.set_command(tlm::TLM_WRITE_COMMAND);
trans.set_address(0x1000);
trans.set_data_ptr(reinterpret_cast<unsigned char*>(&data));
trans.set_data_length(4);
trans.set_streaming_width(4); // Required by LRM
trans.set_byte_enable_ptr(nullptr); // No byte masking
trans.set_dmi_allowed(false);
trans.set_response_status(tlm::TLM_INCOMPLETE_RESPONSE);
std::cout << "@ " << sc_core::sc_time_stamp()
<< " [Initiator] Sending WRITE to 0x1000.\n";
// Perform the Blocking Transport call
socket->b_transport(trans, delay);
// Advance local time based on the target's annotated delay
wait(delay);
if (trans.is_response_error()) {
SC_REPORT_ERROR("TLM", "Transaction returned error");
} else {
std::cout << "@ " << sc_core::sc_time_stamp()
<< " [Initiator] Transaction Complete. Target consumed delay.\n";
}
}
};
// ---------------------------------------------------------
// TARGET (e.g., Memory, Peripheral)
// ---------------------------------------------------------
SC_MODULE(Target) {
tlm_utils::simple_target_socket<Target> socket{"socket"};
SC_CTOR(Target) {
// Register the callback function for blocking transport
socket.register_b_transport(this, &Target::b_transport);
}
void b_transport(tlm::tlm_generic_payload& trans, sc_core::sc_time& delay) {
tlm::tlm_command cmd = trans.get_command();
uint64_t adr = trans.get_address();
if (cmd == tlm::TLM_WRITE_COMMAND) {
std::cout << " -> [Target] Processing WRITE at 0x" << std::hex << adr << "\n";
// In a real model, we would copy trans.get_data_ptr() into our memory array here.
}
// LRM Mandate: Target must set response status
trans.set_response_status(tlm::TLM_OK_RESPONSE);
// Annotate how long this operation takes in hardware (e.g., 20ns memory access)
delay += sc_core::sc_time(20, sc_core::SC_NS);
}
};
// ---------------------------------------------------------
// TOP LEVEL
// ---------------------------------------------------------
int sc_main(int argc, char* argv[]) {
Initiator init("init");
Target tgt("tgt");
// Bind the sockets (Notice how simple it is compared to port-by-port binding)
init.socket.bind(tgt.socket);
std::cout << "Starting TLM-2.0 Simulation...\n";
sc_core::sc_start();
return 0;
}Explanation of the Execution
When you run this simulation, the output will be:
Starting TLM-2.0 Simulation...
@ 0 s [Initiator] Sending WRITE to 0x1000.
-> [Target] Processing WRITE at 0x1000
@ 20 ns [Initiator] Transaction Complete. Target consumed delay.
Notice the power of the delay variable. The Target executes its C++ code instantly, but adds 20 ns to the delay reference. When the transport call returns, the Initiator calls wait(delay), advancing the simulation time to 20 ns. This temporal decoupling is the secret to Virtual Platform simulation speed.
Under the Hood: tlm_generic_payload and Memory Management
The TLM-2.0 tlm_generic_payload (GP) is designed for speed. In src/tlm_core/tlm_2/tlm_generic_payload/tlm_gp.h, you'll see it is a heavy class containing a standard set of attributes (command, address, data pointer, response status).
To prevent the immense overhead of allocating and deleting GP objects during millions of transactions, the standard mandates the use of a Memory Manager (tlm_mm_interface). The GP has a pointer m_mm. When a transaction completes and its reference count hits zero (release()), instead of calling delete, the m_mm->free() method is called, returning the GP to a pool for reuse.
Additionally, the GP contains an array of extension pointers (tlm_extension_base* m_extensions[]). This allows targets to attach custom metadata without modifying the GP structure or relying on slow dynamic_cast.
Comments and Corrections