Chapter 4: TLM and Platforms

TLM-2.0, Generic Payloads, and Sockets

The practical model for fast transaction-level platforms using initiator and target sockets.

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.

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

This 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.

Comments and Corrections