Getting Started with TLM-2.0-draft-2


A Series of Tutorials based on a set of Simple, Complete Examples


John Aynsley, Doulos, January 2008


Tutorial 8



Introduction

In this tutorial we focus on extensions to the generic payload, including ignorable extensions and static extensions (also known as mandatory extensions). We also show how to define a new protocol types class to provide static checking when connecting sockets using different sets of static extensions. Finally, we show the use of the tlm_write_if non-negotiated interface.

This example makes use of the initiator, bus, bridge and memory modules from previous tutorials, together with a new initiator and a memory controller.

Ignorable Extensions

The generic payload class includes a mechanism to add any number of extensions to a transaction object. Each extension consists of an object of a distinct extension class, which is a class derived from tlm_extension. This extension mechanism is very flexible and open-ended, but needs to be used with care, the two main modes of usage being ignorable extensions and static extensions.

An ignorable extension is an extension that can safely be ignored by any interconnect component or target that receives the transaction. There is no obligation on any initiator to generate an ignorable extension (since it could get ignored anyway). Ignorable extensions are typically used to add auxiliary simulation information or meta-data to the standard generic payload. Ignorable extensions allow maximum interoperability when using the generic payload, because there is no obligation on any component to use any given extension. A good rule of thumb for an ignorable extension is an attribute that has a natural default value that can be assumed in the absence of the extension or by any component that ignores the extension.

A static extension is an extension that a target is obliged to act upon if it is present. Whether or not an initiator is obliged to generate a particular static extension will depend on the protocol in question, but a target cannot ignore a static extension. Static extensions are used when defining new protocols based on the generic payload. We will have more to say about static extensions below.

As an example of an ignorable extension, we define an extension class that carries a timestamp and a process handle identifying the time and place of origin of the transaction. Every extension class is derived from tlm_extension like so:

struct Origin_extension: tlm::tlm_extension<Origin_extension>
{
  Origin_extension() : m_timestamp(SC_ZERO_TIME) {}

  virtual tlm_extension_base* clone() const {
    Origin_extension* t = new Origin_extension;
    t->m_handle    = this->m_handle;
    t->m_timestamp = this->m_timestamp;
    return t;
  }

  sc_process_handle m_handle;
  sc_time           m_timestamp;
};

Notice that the base class is an instance of the tlm_extension class template, which must take the name of the extension class itself as an argument. Notice also that the extension class must override the clone method. An extension class may contain whatever attributes you please.

Provided that an extension is considered ignorable, an initiator can add the extension to any generic payload transaction. Initiator1 in this example is very similar to previous examples. It is a loosely-timed initiator generating random read and write commands with byte enables for the writes only, using a transaction pool, and receiving completed transactions along either the forward or backward paths.

Initiator1 uses the debug transaction interface to dump the contents of the target memory at the start and end of simulation. Initiator1 creates a new extension object for each transaction in the pool, and sets a pointer to the extension into the corresponding transaction:

tlm::tlm_generic_payload* trans_pool[POOL_SIZE];
for (unsigned int i = 0; i < POOL_SIZE; i++)
{
  trans_pool[i] = new tlm::tlm_generic_payload;
  trans_pool[i]->set_extension( new Origin_extension );
}
...

for (unsigned int i = 0; i < RUN_LENGTH; i++)
{
  trans = trans_pool[count = (count + 1) % POOL_SIZE];

  Origin_extension* origin;
  trans->get_extension( origin );
  origin->m_handle = sc_get_current_process_handle();
  origin->m_timestamp = sc_time_stamp();
  ...

  status = socket->nb_transport( *trans, phase, m_qk.get_local_time() );

The extension remains attached to the transaction throughout its lifetime, unless the pointer to the extension is explicitly cleared.

The transaction passes through a series of components on the way to the target. The topology of the model is shown in the diagram below.

Diagram

The generic payload transaction is passed first through the bus and then through the bridge before arriving at the target memory. The bus and bridge components are used off-the-shelf, and have no knowledge of any extensions. All that is required of a generic payload component is that it forwards any extensions along with the transaction, provided that the extensions are indeed ignorable. The bus is a TLM2 interconnect component, so simply forwards a reference to the original transaction. The bridge, on the other hand, can function as an initiator for a new transaction (depending on the endianness of the host computer), so it must ensure that pointers to any existing extensions are passed on to the outgoing transactions. The bridge achieves this by calling the copy constructor of the tlm_generic_payload class:

tlm::tlm_generic_payload* outgoing_trans;
outgoing_trans = new tlm::tlm_generic_payload( trans ); // Shallow copy

The copy constructor copies the array-of-pointers to the extensions from transaction to transaction, but does not copy the extension objects themselves. (Class tlm_generic_payload has a deep_copy method for that purpose.)

The target memory has been modified to interpret the Origin_extension.

virtual tlm::tlm_sync_enum nb_transport(tlm::tlm_generic_payload& trans,
                                        tlm::tlm_phase& phase, sc_time& delay)
{
  tlm::tlm_command cmd = trans.get_command();
  sc_dt::uint64    adr = trans.get_address();

  Origin_extension* origin;
  trans.get_extension( origin );
  if (origin) {
    cout << "Mem received " << ((cmd == 0) ? "R" : (cmd == 1) ? "W" : "I")
         << " addr " << adr
         << " from " << origin->m_handle.get_parent_object()->name()
         << " created at " << origin->m_timestamp.to_string().c_str() << "\n";
  }
  ...

get_extension is a function template that returns a pointer to an object of a given extension type, the argument being a pointer to the given type, or a null pointer if no such extension exists. Since this is an ignorable extension, the target must allow transactions with no Origin_extension, and there is no obligation on the target to inspect the Origin_extension at all. To restate the principles, by definition, ignorable extensions are not allowed to interfere with interoperability, and generic payload components should propagate any ignorable extensions along with the transaction.

Static Extensions

Static extensions (also known as mandatory extensions) use exactly the same extension mechanism as ignorable extensions, but operate under a different set of guiding principles. The point of static extensions is to permit specific protocols to be defined using the generic payload as a base, thus allowing user-defined protocols to benefit from a degree of interoperability by sharing the same underlying payload class.

Initiator and target sockets can only be bound together if they have the same bit width and use the same protocol types class. The default is tlm_generic_payload types:

struct tlm_generic_payload_types
{
  typedef tlm_generic_payload tlm_payload_type;
  typedef tlm_phase           tlm_phase_type;
  typedef tlm_dmi_mode        tlm_dmi_mode_type;
};

A default socket passes tlm_generic_payload transactions, possibly with ignorable extensions, and also uses the default classes for phase and DMI. This provides maximal interoperability.

The second option is to have a user-defined protocol types class, but still using the generic payload:

struct user-defined
{
  typedef tlm::tlm_generic_payload tlm_payload_type;
  typedef tlm::tlm_phase           tlm_phase_type;
  typedef tlm::tlm_dmi_mode        tlm_dmi_mode_type;
};

The transaction objects passed through the sockets are still of type tlm_generic_payload, possibly with extensions, but since the new user-defined protocol types class effectively gives us a distinct socket type for the purpose of checking connectivity, users can impose their own rules regarding extensions. Extensions can become a mandatory part of the definition of the protocol, if so desired.

The third option is to have user-defined protocol and transaction classes:

struct user-defined
{
  typedef user-defined  tlm_payload_type;
  typedef user-defined  tlm_phase_type;
  typedef user-defined  tlm_dmi_mode_type;
};

This third options allows users to build their own transaction classes and phase types from scratch, with no reliance on the generic payload and the associated extension mechanism. This gives uses maximum flexibility, but also throws away the interoperability benefits of using the generic payload, so we will not consider it further here.

In this example we define a new protocol types class cmd_ext_payload types together with an extension Command_extension. These new classes define an extension to the generic payload that includes two new commands in addition to read and write: load-link and store-conditional. The store-conditional command is similar to write, but will fail by default when overwriting stale data. A store-conditional transaction can only succeed when the most recent access to that same address (aside from readonly access) was a load-link command. Any other access that may have modified the contents of that address would invalidate any subsequent store-conditional. The load-link, store-conditional pair are used to implement read-modify-write type instructions that read from a particular address, modify the data, then write the modified data back to the same address in the knowledge that nothing else has modified that memory location in the meantime.

Here is the static extension class:

struct Command_extension: tlm::tlm_extension<Command_extension>
{
  Command_extension() : m_cmd(LOAD_LINK) {}

  virtual tlm_extension_base* clone() const {
    Command_extension* t = new Command_extension;
    t->m_cmd    = this->m_cmd;
    return t;
  }

  enum cmd_t {LOAD_LINK, STORE_CONDITIONAL};
  cmd_t m_cmd;
};

If you compare this code fragment with the Origin_extension class shown above, you will see that the structure of the extension classes are exactly the same. The difference between an ignorable and a static extension lies in how it will be used.

The new protocol types class used with the command extension is as follows:

struct cmd_ext_payload_types
{
  typedef tlm::tlm_generic_payload tlm_payload_type;
  typedef tlm::tlm_phase           tlm_phase_type;
  typedef tlm::tlm_dmi_mode        tlm_dmi_mode_type;
};

The new protocol is used by a second initiator that will generate read, write, load-link and store-conditional transactions:

struct Initiator2: sc_module, 
                   tlm::tlm_bw_nb_transport_if<cmd_ext_payload_types>
{
  tlm::tlm_nb_initiator_socket<64,cmd_ext_payload_types> socket;
  ..
};

Initiator2 generates either regular generic payload read and write commands or extended commands. It also generates the ignorable Origin_extension. Both the transactions and the extensions are pooled:

tlm::tlm_generic_payload* trans_pool[POOL_SIZE];
Command_extension*      cmd_ext_pool[POOL_SIZE];

for (unsigned int i = 0; i < POOL_SIZE; i++)
{
  trans_pool[i] = new tlm::tlm_generic_payload;
  trans_pool[i]->set_extension( new Origin_extension );
  cmd_ext_pool[i] = new Command_extension;
}

Command_extension* cmd_ext;
...

for (unsigned int i = 0; i < RUN_LENGTH; i++)
{
  trans = trans_pool[count = (count + 1) % POOL_SIZE];
  cmd_ext = cmd_ext_pool[count % POOL_SIZE];

  Origin_extension* origin;
  trans->get_extension( origin );
  origin->m_handle = sc_get_current_process_handle();
  origin->m_timestamp = sc_time_stamp();
  ...

  Command_extension::cmd_t x_cmd;
  x_cmd = static_cast<Command_extension::cmd_t>(rand() % 2);
  cmd_ext->m_cmd = x_cmd;

  if (...)
  {
    trans->set_command( tlm::TLM_READ_COMMAND );
    trans->clear_extension( cmd_ext );
  }
  else
  {
    trans->set_command( tlm::TLM_IGNORE_COMMAND );
    trans->set_extension( cmd_ext );
  }
  ...

For a regular command, Initiator2 calls the method clear_extension to clear the pointer to the given extension type from the generic payload. Note that this method only modifies the pointer, not the extension object itself. For an extended command, the initiator sets the generic payload command attribute to TLM_IGNORE_COMMAND (rather than read or write), and sets a pointer to the command extension.

The initiator socket of Initiator2 can only be bound to a target socket of identical width and type, so cannot be bound directly to the bus, bridge or memory. The memory controller component implements the load-link, store-conditional commands by caching valid links, and converts load-link to read commands, and store-conditional to write commands, respectively. The incoming socket on the memory controller uses type cmd_ext_payload_types, whereas the outgoing socket uses type tlm_generic_payload_types.

struct Mem_control
:      sc_module,
       non_std::tlm_tagged_fw_nb_transport_if<cmd_ext_payload_types>,
       non_std::tlm_tagged_bw_nb_transport_if<tlm::tlm_generic_payload_types>,
       tlm::tlm_write_if
{
  non_std::tlm_tagged_nb_target_socket
                        <64, cmd_ext_payload_types>          targ_socket;
  non_std::tlm_tagged_nb_initiator_socket
                        <64, tlm::tlm_generic_payload_types> init_socket;

  sc_export< tlm::tlm_write_if<sc_dt::uint64> > invalidate_export;
  ...
};

This allows the incoming target socket to be bound to Initiator2, and the outgoing initiator socket to be bound to the generic payload bus component. Here is the top-level module showing the instantiation and socket binding:

SC_MODULE(Top)
{
  typedef Bus<2,1>                    bus_t;
  typedef Bridge<tlm::TLM_BIG_ENDIAN> bridge_t;

  Initiator1*  init1;
  Initiator2*  init2;
  Mem_control* control;
  bus_t*       bus;
  bridge_t*    bridge;
  Memory*      memory;

  SC_CTOR(Top)
  {
    init1   = new Initiator1("init1");
    init2   = new Initiator2("init2");
    control = new Mem_control("control");
    bus     = new bus_t("bus");
    bridge  = new bridge_t("bridge");
    memory  = new Memory("memory");

    init1   -> socket           .bind( *(  bus -> targ_socket[0]) );
    init2   -> socket           .bind( control -> targ_socket );
    control -> init_socket      .bind( *(  bus -> targ_socket[1]) );
    ( *(bus -> init_socket[0]) ).bind(  bridge -> targ_socket );
    bridge  -> init_socket      .bind(  memory -> socket );

    bus->invalidate_port.bind( control->invalidate_export );
  }
};

Listed below is the heart of the code from the memory controller to intercept extended commands and replace them with regular generic payload commands:

Command_extension* cmd_ext;
trans.get_extension( cmd_ext );
if (cmd_ext)
{
  if (cmd_ext->m_cmd == Command_extension::STORE_CONDITIONAL)
    if ( !m_link_map[ address ] )
    {
      trans.set_response_status( tlm::TLM_COMMAND_ERROR_RESPONSE );
      return tlm::TLM_COMPLETED;
    }

The above conditional check will fail if the storage being written to is stale, that is, if there is no valid link stored in the link map, which is an associative array:

std::map  m_link_map;

The following code fragment creates a new generic payload transaction and copies across pointers to the data array, byte enable array and extensions into the new transaction:

  tlm::tlm_generic_payload* outgoing_trans;
  outgoing_trans = new tlm::tlm_generic_payload( trans ); // Shallow copy

The command extension is now removed from the outgoing transaction. This is not strictly necessary as the extension would be ignored anyway by any downstream components, but is good practice. The author of any component that copies generic payload transactions should consider carefully how extensions should be handled. Any ignorable extensions are left untouched, which in this example includes the Origin_extension:

  outgoing_trans->clear_extension( cmd_ext );
  ...

The outgoing command is now set, and for a load-link, the link is stored in the link map.

  if (cmd_ext->m_cmd == Command_extension::LOAD_LINK)
  {
    m_link_map[ address ] = true;
    outgoing_trans->set_command( tlm::TLM_READ_COMMAND );
  }
  else if (cmd_ext->m_cmd == Command_extension::STORE_CONDITIONAL)
  {
    outgoing_trans->set_command( tlm::TLM_WRITE_COMMAND );
  }

The link in the link map will be invalidated by a write command to the same address through the memory controller, but must also be invalidated whenever any other initiator in the system writes to that same memory address. For that purpose, the memory controller provides an additional interface:

struct Mem_control: ..., tlm::tlm_write_if<sc_dt::uint64>
{
  ...
  sc_export< tlm::tlm_write_if<sc_dt::uint64> > invalidate_export;

  ...

  virtual void write( const sc_dt::uint64& address )
  {
    m_link_map[ address ] = false;
  }

  std::map<sc_dt::uint64, bool> m_link_map;
};

This shows the use of tlm_write_if, which is another standard interface and is part of TLM2. tlm_write_if is a non-blocking, non-negotiated interface consisting of a single write method taking a const ref argument. Non-negotiated means that the target is always obliged to accept the transaction, and is not allowed to fail. The fact that the method is named write and is called whenever there is a write to a storage location is entirely co-incidental. In this example, the implementation of the write method invalidates the link. A corresponding port on the bus component is bound to the export, and write is called whenever a storage location is updated:

template<unsigned int N_INITIATORS, unsigned int N_TARGETS>
struct Bus: ...
{
  non_std::tlm_tagged_nb_target_socket<64>*    targ_socket[N_INITIATORS];
  non_std::tlm_tagged_nb_initiator_socket<64>* init_socket[N_TARGETS];

  sc_port< tlm::tlm_write_if<sc_dt::uint64>, 1, SC_ZERO_OR_MORE_BOUND >
                                                           invalidate_port;
  ...
  virtual tlm::tlm_sync_enum nb_transport(unsigned int id,
      tlm::tlm_generic_payload& trans, tlm::tlm_phase& phase, sc_time& delay)
  {
    ...
    if (trans.get_command() == tlm::TLM_WRITE_COMMAND)
      if (invalidate_port.get_interface())
        invalidate_port->write( address );
    ...
  }
  ...
};

The port policy SC_ZERO_OR_MORE_BOUND, a feature of IEEE 1666 and SystemC version 2.2, is used so that there is no obligation to bind the port. Whenever the bus intercepts a write command, it checks whether the invalidate port has been bound to a corresponding export. If so, it calls the write method, passing the address to be invalidated as an argument.

The final twist to this story is DMI, which could potentially bypass the memory controller by permitting direct access from an initiator to a memory. DMI must not be permitted to update a memory location without the knowledge of the memory controller, so the memory controller intercepts and disallows any request for DMI write access:

virtual bool get_direct_mem_ptr(unsigned int id,
                                const sc_dt::uint64& address,
                                tlm::tlm_dmi_mode& dmi_mode,
                                tlm::tlm_dmi&  dmi_data)
{
  if (dmi_mode.type & tlm::tlm_dmi_mode::WRITE)
    return false;
  else
    return init_socket->get_direct_mem_ptr( address, dmi_mode, dmi_data );
}

When you run the example, you should see the following:

  • Initiator1 dumps the contents of memory at the start and end of simulation using the debug transaction interface
  • Initiator2 generates a series of load-link, store-conditional transactions interleaved with read and write transactions to other addresses.
  • From time to time, Initiator1 generates a write transaction that modifies a memory location in the middle of a load-link, store-conditional pair, and Initiator2 detects that store-conditional has failed and generates an information report.

You will find the source code for this example in file tlm2_getting_started_8.cpp.

Click here to download both the source file for this example and this page in PDF format. In exchange, we will ask you to enter some personal details. To read about how we use your details, click here. On the registration form, you will be asked whether you want us to send you further information concerning other Doulos products and services in the subject area concerned.

Previous:  Tutorial 7

Back to the full list of TLM2 Tutorials