Chapter 4: TLM and Platforms

Generic Payload Extensions

How to attach protocol-specific metadata to TLM transactions without replacing tlm_generic_payload.

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_generic_payload covers common transaction fields, but real platforms often need extra metadata: privilege level, cache attributes, security state, transaction ID, burst information, debug flags, or initiator identity.

TLM extensions let you attach that metadata without inventing a new payload class, adhering to the IEEE 1666 LRM extension mechanism.

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.

Extension Shape and Complete Example

An extension derives from tlm::tlm_extension<T>. The clone and copy functions matter because payloads can be reused, copied, or managed by memory managers.

Here is a complete, fully compilable example demonstrating how to declare, attach, and read a privilege extension across a blocking transport call.

#include <systemc>
#include <tlm>
#include <tlm_utils/simple_initiator_socket.h>
#include <tlm_utils/simple_target_socket.h>
 
using namespace sc_core;
 
// 1. Define the Extension
struct PrivilegeExtension : public tlm::tlm_extension<PrivilegeExtension> {
  bool privileged = false;
 
  tlm_extension_base* clone() const override {
    return new PrivilegeExtension(*this);
  }
 
  void copy_from(tlm_extension_base const& ext) override {
    privileged = static_cast<PrivilegeExtension const&>(ext).privileged;
  }
};
 
// 2. The Target reading the Extension
SC_MODULE(TargetDevice) {
  tlm_utils::simple_target_socket<TargetDevice> socket{"socket"};
 
  SC_CTOR(TargetDevice) {
    socket.register_b_transport(this, &TargetDevice::b_transport);
  }
 
  void b_transport(tlm::tlm_generic_payload& trans, sc_time& delay) {
    PrivilegeExtension* priv_ext = nullptr;
    trans.get_extension(priv_ext); // Attempt to retrieve the extension
 
    if (priv_ext && priv_ext->privileged) {
      std::cout << "[TARGET] Privileged access granted.\n";
      trans.set_response_status(tlm::TLM_OK_RESPONSE);
    } else {
      std::cout << "[TARGET] Error: Unprivileged access denied!\n";
      trans.set_response_status(tlm::TLM_COMMAND_ERROR_RESPONSE);
    }
  }
};
 
// 3. The Initiator attaching the Extension
SC_MODULE(CpuInitiator) {
  tlm_utils::simple_initiator_socket<CpuInitiator> socket{"socket"};
 
  SC_CTOR(CpuInitiator) { SC_THREAD(run); }
 
  void run() {
    tlm::tlm_generic_payload trans;
    sc_time delay = SC_ZERO_TIME;
    
    // Allocate and configure the extension
    PrivilegeExtension* priv_ext = new PrivilegeExtension();
    priv_ext->privileged = true;
    trans.set_extension(priv_ext); // Attach to transaction
    
    trans.set_command(tlm::TLM_READ_COMMAND);
    socket->b_transport(trans, delay);
    
    // Always clear extensions if managing memory manually, or if reusing payloads
    trans.clear_extension(priv_ext);
    delete priv_ext; 
    
    wait(delay);
  }
};
 
int sc_main(int argc, char* argv[]) {
  CpuInitiator cpu("cpu");
  TargetDevice tgt("tgt");
  cpu.socket.bind(tgt.socket);
  sc_start();
  return 0;
}

For long-lived transactions or non-blocking payloads, use an ownership strategy that matches the payload lifetime. Do not attach a pointer to a stack object if the transaction may outlive the call (as is the case with non-blocking PEQ logic).

Targets and Absent Extensions

Targets should treat absent extensions deliberately. Either define a default behavior (e.g. assume unprivileged) or report an error for protocols that require the metadata.

Extension Hygiene

Extensions are powerful, but they can make models implicit. Keep them documented:

  • Name every extension after the protocol concept it represents.
  • Define who creates it, who reads it, and who owns it (memory management is key).
  • State whether it is optional or required.
  • Include it in transaction logs when relevant.

Good extension design lets a generic payload stay generic while preserving protocol-specific meaning.

Exhaustive Deep Dive: IEEE 1666-2023 LRM and Accellera Source Implementation

To truly master TLM generic payload extensions, one must examine both the IEEE 1666-2023 Standard (Section 14) and the Accellera SystemC reference implementation located in src/tlm_core/tlm_2/tlm_generic_payload.

IEEE 1666-2023 LRM: Section 14.2 and 14.3 Semantics

The LRM explicitly defines the tlm_generic_payload as the standard interoperability mechanism for TLM-2.0 models. However, recognizing that no single payload can capture the intricacies of every bus protocol (e.g., AMBA AXI/AHB, PCIe, OCP), Section 14.3 (Extension Mechanism) introduces tlm_extension_base and the CRTP (Curiously Recurring Template Pattern) based tlm_extension<T>.

LRM Clause 14.3.1 (tlm_extension_base) mandates that extensions must provide:

  1. virtual tlm_extension_base* clone() const = 0;
  2. virtual void copy_from(tlm_extension_base const&) = 0;
  3. virtual void free() { delete this; }

The LRM strictly dictates that memory management of extensions is the responsibility of the creator, unless explicitly handed over to a memory manager. Section 14.2.3 defines the behavior of the payload's deep_copy_from() method, which calls copy_from() on all attached extensions. If an initiator forgets to implement clone() or copy_from() correctly, deep payload copies (often used in non-blocking PEQ queues) will corrupt memory or slice the extension object.

Accellera Source Code: tlm_extension.h and CRTP

If you open src/tlm_core/tlm_2/tlm_generic_payload/tlm_extension.h, you will find the implementation of the CRTP pattern used to assign unique IDs to extensions:

template <typename T>
class tlm_extension : public tlm_extension_base {
public:
    virtual tlm_extension_base* clone() const = 0;
    virtual void copy_from(tlm_extension_base const &ext) = 0;
    virtual ~tlm_extension() {}
    static const unsigned int ID;
};
 
template <typename T>
const unsigned int tlm_extension<T>::ID = register_extension(typeid(T).name());

This is a beautiful piece of C++ engineering. Because tlm_extension<T> is a template, T::ID is instantiated exactly once per unique extension type T across the entire simulation during static initialization. The register_extension function (defined in tlm_gp.cpp) returns a monotonically increasing integer.

Array Indexing for O(1) Access

Why go through the trouble of CRTP and static IDs? Performance. In src/tlm_core/tlm_2/tlm_generic_payload/tlm_gp.h, the extensions are stored inside the payload as a flat array of pointers:

tlm_extension_base* m_extensions[max_num_extensions];

When you call trans.set_extension(ext) or trans.get_extension<T>(), the Accellera kernel does not use std::map, string lookups, or slow dynamic_cast. It uses the statically assigned ID as an array index:

template <typename T>
void get_extension(T*& ext) const {
    ext = static_cast<T*>(m_extensions[T::ID]);
}

This ensures that attaching or reading an extension costs nothing more than an array indexing operation and a static_cast—executing in a couple of CPU cycles. This design choice is fundamental to why TLM-2.0 loosely-timed (LT) models can simulate at speeds of hundreds of millions of transactions per second.

Extension Memory Management and tlm_gp::clear_extension

A frequent source of memory leaks in TLM models occurs when extensions are attached but never deleted.

LRM Section 14.2.3.1 specifies clear_extension(const T*) and clear_extension<T>(). In the Accellera source, clear_extension simply sets m_extensions[T::ID] = 0. It does not delete the extension pointer. If an extension is attached to a generic payload that is managed by an sc_pool or a custom tlm_mm_interface, the extension will live forever unless you manually invoke delete or override the free() virtual method in conjunction with the memory manager.

When designing custom memory managers (LRM 14.2.4), developers must explicitly loop through the m_extensions array (or track attached extensions) and call free() on them when the payload's reference count reaches zero. The standard SystemC implementation provides tlm_generic_payload::reset(), which wipes the array but again, assumes the owner has cleaned up the actual memory.

Sticky Extensions and set_auto_extension

An advanced, lesser-known feature in tlm_gp.h is set_auto_extension. Normal extensions are cleared when reset() is called. However, "auto extensions" or sticky extensions are preserved across payload resets. This is heavily used when an initiator creates a pool of payloads, attaches an initiator-ID extension to all of them at initialization time, and wants that extension to persist across thousands of transaction cycles without reallocating it.

Understanding this dual layer—the LRM's strict behavioral contract and Accellera's highly optimized, template-driven O(1) array implementation—is what separates a casual SystemC user from an expert TLM architect.

Comments and Corrections