Getting Started with TLM-2.0-draft-2


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


John Aynsley, Doulos, December 2007


Tutorial 1



Introduction

TLM 2.0-draft-2 is the latest transaction level modeling standard from the Open SystemC Initiative (OSCI), released on 30th November 2007 for a period of public review. The official release from OSCI includes comprehensive documentation and a growing set of examples, and can be obtained from the OSCI website, www.systemc.org.

The documentation in the official release kit is good; we know, because we wrote much of it! This tutorial is complementary to the documentation in the release, and will help get you started by providing a tutorial-style introduction to the basic features of TLM2.0 and how they fit together. This tutorial assumes that you know SystemC and that you know the basics of transaction-level modeling. A knowledge of the OSCI TLM 1.0 standard would be a great starting point, but is not essential.

This tutorial is accompanied by a set of source files that you can download. The examples can be run using SystemC 2.2.0 and TLM2.0-draft-2, both available from www.systemc.org. The examples can also be run using SystemC 2.1v1, but you would not want to use an out-of-date version, would you? The examples cannot be run with TLM2.0-draft-1, which was released a year ago and is incompatible with draft 2. The only other thing you need is a supported C++ compiler. Alternatively, you can use a dedicated SystemC simulator, although you will have to pay real money for one of those.

Modeling Concepts

Transaction level modeling in SystemC involves communication between SystemC processes using function calls. The focus of TLM is on the communication between the processes rather than the algorithms performed by the processes themselves, so the processes shown in this tutorial will be rather trivial. We assume that in a model of system behavior, some of the SystemC processes will produce data, others will consume data, some will initiate communication, others will passively respond to communication initiated by others.

The focus of OSCI TLM 2.0 in particular is the modeling of on-chip memory-mapped busses. This does not mean that TLM 2.0 is dedicated exclusively to memory-mapped busses, just that this is where most of the features are focussed. TLM 2.0 has a layered structure, with the lower layers being more flexible and general, and the upper layers being specific to bus modeling. In future, the standard may be re-oriented toward other styles of communication as they emerge, the obvious direction being network-on-chip (NoC) architectures.

As a starting point, we will assume you are familiar with the concepts of the module, port, process, channel, interface and event in SystemC. If not, you could start with the SystemC tutorial found right here. TLM 2.0 involves having processes embedded within separate modules communicating using interface method calls through ports and exports.

Initiators, Targets, and Sockets

In TLM 2.0, an initiator is a module that initiates new transactions, and a target is a module that responds to transactions initiated by other modules. A transaction is a data structure (a C++ object) passed between initiators and targets using function calls. The same module can act both as an initiator and as a target, and this would typically be the case for a model of an arbiter, a router, or a bus.

In order to pass transactions between initiators and targets, TLM 2.0 uses sockets. An initiator sends transactions out through an initiator socket, and a target receives incoming transactions through a target socket. A module that merely forwards transactions without modifying their content is known as an interconnect component. An interconnect component would have both a target socket and an initiator socket.

Now let us look at some SystemC code. A TLM 2.0 model needs to include the standard SystemC header, and also “tlm.h”. You need to point the include path of your C++ compiler (or makefile) to the ./include directory in the release.

#include "systemc"
using namespace sc_core;
using namespace sc_dt;
using namespace std;

#include "tlm.h"

Now we declare initiator and target modules, where the initiator module will generate transactions, and the target module will represent a simple memory. The transactions generated by the initiator will read from or write to the memory.

struct Initiator: sc_module, tlm::tlm_bw_b_transport_if
{...};

struct Memory: sc_module, tlm::tlm_fw_b_transport_if<>
{...};

Note that each module implements a different interface, tlm_bw_b_transport_if and tlm_fw_b_transport_if. The modules will communicate using the TLM 2.0 blocking transport interface. All TLM 2.0 declarations are in the C++ namespace tlm. Just to be clear, we will qualify all such names explicitly throughout the examples. bw and fw are abbreviations for backward and forward, which should seem reasonable enough; the initiator sends transactions forward to the target, and the target sends responses backward to the initiator. The target implements the forward interface, and the initiator implements the backward interface. We will expand the details of these modules below. Before that, let us see how to connect up the module hierarchy:

SC_MODULE(Top)
{
  Initiator *initiator;
  Memory    *memory;

  SC_CTOR(Top)
  {
    initiator = new Initiator("initiator");
    memory    = new Memory   ("memory");

    initiator->socket.bind( memory->socket );
  }
};

The top-level module of the hierarchy instantiates one initiator and one memory, and binds the initiator socket on the initiator to the target socket on the target memory. The sockets encapsulate everything you need for two-way communication between modules, including ports and exports for both directions of communication. One initiator socket is always bound to one target socket. (It is also possible to bind sockets hierarchically up and down a nested module hierarchy, but we will not worry about that right now.)

For completeness, when using the OSCI simulator, you will also need the following sc_main function:

int sc_main(int argc, char* argv[])
{
  Top top("top");
  sc_start();
  return 0;
}

The initiator and target sockets have to be declared and constructed explicitly, as follows:

struct Initiator: sc_module, tlm::tlm_bw_b_transport_if
{
  tlm::tlm_b_initiator_socket<> socket;

  SC_CTOR(Initiator) : socket("socket")
  {
    socket.bind( *this );
    ...
};

struct Memory: sc_module, tlm::tlm_fw_b_transport_if<>
{
  tlm::tlm_b_target_socket<> socket;

  SC_CTOR(Memory) : socket("socket")
  {
    socket.bind( *this );
    ...
};

Note the names of the socket types, tlm_b_initiator_socket<> and tlm_b_target_socket<>. The _b_ stands for blocking, because these particular sockets support the blocking transport interface. This is similar, but not identical, to the blocking transport interface in TLM 1.0. The <> show that we are using class templates, and in this example allowing the template arguments to take their default values. Amongst other things, the socket template arguments effectively allow us to specific the type of the transactions passed through the socket.

Note that each socket is bound to the module itself using socket.bind( *this ). This is because the modules themselves implement the methods of the interfaces named by the sockets. Well, you might think this statement confusing because the sockets in the example do not explicitly name any interfaces, but it has to do with the default values of the template arguments. An initiator socket is actually an sc_port that has an sc_export on the side, whereas a target socket is actually an sc_export that has an sc_port on the side. The bind operator of the socket class binds port-to-export in both directions with a single function call. This convenience is a feature of sockets.

The Generic Payload and Blocking Transport

The default transaction type for the socket classes, implied in the absence of any template arguments, is tlm_generic_payload. The generic payload is an important part of the TLM 2.0 standard because it is one of the keys to achieving interoperability between transaction level models. The generic payload serves two closely-related purposes. It can be used as a general-purpose transaction type for abstract memory-mapped bus modeling when you are not concerned with the exact details of any particular bus protocol, offering immediate interoperability between models off-the-shelf. Alternatively, the generic payload can be used as the basis for modeling a wide range of specific protocols at a more detailed level, the beauty of this approach being that it is relatively easy to bridge between different protocols when both are built on top of the same generic payload type.

Our initiator module has a thread process to generate a stream of generic payload transactions.

SC_CTOR(Initiator) : socket("socket")
{
  ...
  SC_THREAD(thread_process);
}

void thread_process()
{
  for (int i = 32; i < 96; i += 4)
  {
    tlm::tlm_generic_payload trans;
    ...
    socket->b_transport( trans );
    ...
  }
}

The transaction is sent through the socket using the b_transport method of the TLM 2.0 blocking transport interface, which passes its one-and-only argument by reference and has no return value. The initiator is responsible for allocating and deleting storage for the transaction.

tlm::tlm_generic_payload trans;

tlm::tlm_command cmd = static_cast(rand() % 2);
trans.set_command( cmd );
trans.set_address( i );
if (cmd == tlm::TLM_WRITE_COMMAND) data = 0xFF000000 | i;
trans.set_data_ptr( reinterpret_cast(&data) );
trans.set_data_length( 4 );

socket->b_transport( trans );

...
int data;

Each generic payload transaction has a standard set of bus attributes: command, address, data, data length, byte enables, streaming width, response status, and extensions. Each attribute has a default value, but at least some of the attributes should always be set explicitly to create a meaningful transaction. Here, the command is set to read or write at random, the address is set to the loop index, and the data length is set to 4 bytes. The transaction is given a pointer to a data area within the initiator. In the case of a write command, the data will be copied from the data area to the target, and in the case of a read command, copied from the target to the data area. In either case, the actual copy is executed at the target.

The blocking transport method is implemented in the target memory. First, the set of attributes that cannot be ignored are extracted from the generic payload transaction:

virtual void b_transport( tlm::tlm_generic_payload& trans )
{
  tlm::tlm_command cmd = trans.get_command();
  sc_dt::uint64    adr = trans.get_address() / 4;
  unsigned char*   ptr = trans.get_data_ptr();
  unsigned int     len = trans.get_data_length();
  bool*            byt = trans.get_byte_enable_ptr();
  unsigned int     wid = trans.get_streaming_width();

Next, the attributes are checked to ensure that the initiator is not trying to use features that the target cannot support. In this case, the target memory does not support byte enables, streaming width, or burst transfers. The following statement also checks that the address is not out-of-range. If the transaction cannot be executed, the SystemC report handler is called to generate an error.

  if (adr >= sc_dt::uint64(SIZE) || byt != 0 || wid != 0 || len > 4)
    SC_REPORT_ERROR("TLM2",
                "Target does not support given generic payload transaction");

Of course, if the target is unable to support certain features of the generic payload this will limit interoperability, but at least the set of features is well-defined and there are standard obligations on the target to check and report incompatibility.

The blocking transport method then models the latency of the target by suspending for a fixed delay. Calling wait is permitted in a blocking method:

  wait(LATENCY);

The target then implements the read and write command by copying data to or from the data area in the initiator. Regarding endianness, the rule is that the generic payload takes the same endianness as the host computer. As long as the target memory is also modeled using host endianness, the data copy can be done using memcpy:

  if ( cmd == tlm::TLM_READ_COMMAND )
    memcpy(ptr, &mem[adr], len);
  else if ( cmd == tlm::TLM_WRITE_COMMAND )
    memcpy(&mem[adr], ptr, len);

The final act of the blocking transport method is to set the response status attribute of the generic payload to indicate the successful completion of the transaction. If not set, the default response status would indicate to the initiator that the transaction is incomplete.

  trans.set_response_status( tlm::TLM_OK_RESPONSE );

After calling b_transport, the initiator checks the response status:

if (trans.is_response_error() )
  SC_REPORT_ERROR("TLM2", "Response error from b_transport");

Direct Memory and Debug Transaction Interfaces

For this first example, this is not quite the end of the story because the sockets provide a couple more interfaces we have not yet mentioned: the direct memory interface (DMI) and the debug transaction interface. DMI gives an initiator direct access to an area of memory in the target, bypassing the transport interface and thus speeding up simulation. The debug transaction interface gives an initiator delay-free, side-effect-free access to an area of memory in the target for debug purposes.

Both these interfaces must be implemented by any modules using the initiator and target sockets, whether or not the corresponding functionality is actually supported or used. Our example supports and uses neither, so the implementations are just empty function bodies. In the initiator module, we have:

virtual void invalidate_direct_mem_ptr(sc_dt::uint64 start_range,
                                       sc_dt::uint64 end_range)
{
  // DMI unused
}

This is the one-and-only method of the tlm_bw_direct_mem_if, a base class of the tlm_bw_b_transport_if. In the memory target module, we have:

virtual bool get_direct_mem_ptr(
  const sc_dt::uint64& address,
  tlm::tlm_dmi_mode& dmi_mode,
  tlm::tlm_dmi&  dmi_data)
{
  // DMI not supported
  return false;
}

virtual unsigned int transport_dbg(tlm::tlm_debug_payload& r)
{
  // Debug not supported
  return 0;
}

get_direct_mem_ptr is the one-and-only method of the tlm_fw_direct_mem_if and transport_dbg the one-and-only method of the tlm_transport_dbg_if, both base classes of the tlm_fw_b_transport_if. Every module must include a full set of methods as appropriate, but implementing the empty methods is not too onerous.

You will find the source code for this first example in file tlm2_getting_started_1.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.

Next:  Tutorial 2

Back to the full list of TLM2 Tutorials