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 3
Introduction
This third tutorial of the series builds on the example used in the first two tutorials, so it is a good idea to work through those first, particularly if you are new to TLM2. This time, we look at propagating transactions through an interconnect component representing a router placed between the initiator and several target memories. The router has to forward transport, DMI and debug transactions to the target, and it also has to manage the return path as the function calls unwind and transactions are returned to the initiator. We have to deal with two issues: how to model an interconnect component that has a single target socket but multiple initiator sockets, and how to handle the addresses in the transactions as they pass forward and backward through the router.
An Interconnect Component
The initiator and the target memory as very similar to the example in Tutorial 2, but now we add a router between them, modeled as a TLM2 interconnect component, that is, a component that forwards transactions from an incoming target socket to an outgoing initiator socket. In this case there are four outgoing initiator sockets connected to four instances of the memory.

Here is the top-level module, showing how the module hierarchy is connected up:
SC_MODULE(Top)
{
Initiator* initiator;
Router<4>* router;
Memory* memory[4];
SC_CTOR(Top)
{
initiator = new Initiator("initiator");
router = new Router<4>("router");
for (int i = 0; i < 4; i++)
{
char txt[20];
sprintf(txt, "memory_%d", i);
memory[i] = new Memory(txt);
}
initiator->socket.bind( router->target_socket );
for (int i = 0; i < 4; i++)
router->initiator_socket[i]->bind( memory[i]->socket );
}
};
You can see that the socket belonging to the initiator is bound to the single target socket of the router, and each of the four initiator sockets belonging to the router is bound to a socket belonging to a different target memory. Each initiator-to-target socket connection is point-to-point. You cannot bind one initiator socket to multiple target sockets or vice versa.
The idea is that each memory sits at a different location in the address space used by the initiator, so
the router must route transactions through to the appropriate memory depending on the address embedded in the transaction, translating the address to local address for each memory as it does so. This same principle of address translation will apply to transport, DMI and debug transactions. The router contains a single instance of the target socket for the blocking transport interface, and an array of initiator sockets, again to support the blocking transport interface:
templatestruct Router: sc_module, tlm::tlm_fw_b_transport_if<>, tlm::tlm_bw_b_transport_if { tlm::tlm_b_target_socket<> target_socket; tlm::tlm_b_initiator_socket<>* initiator_socket[N_TARGETS]; ... };
Notice that the number of targets is specified using a template argument. All of the sockets default to being 32 bits wide and to using the generic payload and generic DMI mode. This router is only able to route generic payload transactions, although those transactions could include extensions. Also, the router implements both the forward and backward transport interfaces as required by the sockets. This is not a general solution because it does not distinguish between incoming function calls through different sockets, but it is adequate for this example. Here is the router constructor:
SC_CTOR(Router)
: target_socket("target_socket")
{
target_socket.bind( *this );
for (int i = 0; i < N_TARGETS; i++)
{
char txt[20];
sprintf(txt, "socket_%d", i);
initiator_socket[i] = new tlm::tlm_b_initiator_socket<>(txt);
initiator_socket[i]->bind( *this );
}
}
Each of the sockets is bound to the router object itself, which implements both of the incoming interfaces. We will look in turn at the blocking transport interface, the direct memory interface, and the debug transaction interface.
Routing the b_transport method
The blocking transport method is only passed in the forward direction, so we do not have to worry about handling b_transport calls coming back from the target. Here is the complete implementation of b_transport in the router:
virtual void b_transport( tlm::tlm_generic_payload& trans )
{
sc_dt::uint64 address = trans.get_address();
sc_dt::uint64 masked_address;
unsigned int target_nr = decode_address( address, masked_address);
trans.set_address( masked_address );
( *initiator_socket[target_nr] )->b_transport( trans );
}
The router has to inspect the address attribute to determine which socket to send the transaction out through, which it puts into the variable target_nr. This example uses very simple built-in address decoding, as defined by the following functions:
inline unsigned int decode_address( sc_dt::uint64 address,
sc_dt::uint64& masked_address )
{
unsigned int target_nr = static_cast( (address >> 8) & 0x3 );
masked_address = address & 0xFF;
return target_nr;
}
inline sc_dt::uint64 compose_address( unsigned int target_nr,
sc_dt::uint64 address)
{
return (target_nr << 8) | (address & 0xFF);
}
The router overwrites the address attribute of the generic payload transaction with the masked address, that is, the local address within the target memory. The address attribute is one of the very few attributes that an interconnect component is permitted to modify, the others being the DMI hint and the extensions. The interconnect and the target are obliged to treat most generic payload attributes as readonly.
The final act of b_transport is to forward the transaction through the appropriate initiator socket. Note that the b_transport method of the target module will execute in the context of a thread process in the initiator module, and when that method returns, control gets unwound through the whole call chain back to the initiator.
Routing DMI and Debug Transactions
The important principle when routing DMI and debug transactions is to use exactly the same address transformations as for the transport interface, and to use those transformations in both forward and backward directions where necessary. We will see how this works, starting with the forward DMI interface:
virtual bool get_direct_mem_ptr(
const sc_dt::uint64& address,
tlm::tlm_dmi_mode& dmi_mode,
tlm::tlm_dmi& dmi_data)
{
sc_dt::uint64 masked_address;
unsigned int target_nr = decode_address( address, masked_address );
bool status = ( *initiator_socket[target_nr] )->
get_direct_mem_ptr( masked_address, dmi_mode, dmi_data );
So far, the code is similar to the implementation of b_transport above, except that the address is passed as an argument to the method rather than being embedded in the transaction. The DMI address being requested is translated into the address space of the target before being routed on to the appropriate target.
The target responds to the DMI request by returning a DMI data object containing an address range, which also needs to be translated back into the address space known by the initiator:
dmi_data.dmi_start_address =
compose_address( target_nr, dmi_data.dmi_start_address );
dmi_data.dmi_end_address =
compose_address( target_nr, dmi_data.dmi_end_address );
return status;
}
The start and end addresses are embedded within the DMI data object, and are overwritten. The critical point here is to use the inverse address transformation to that used on the forward path. Exactly the same would apply on the backward path when a target wishes to invalidate a DMI pointer, only in this example we can cheat and simply invalidate all DMI pointers:
virtual void invalidate_direct_mem_ptr(sc_dt::uint64 start_range,
sc_dt::uint64 end_range)
{
target_socket->invalidate_direct_mem_ptr(0, (sc_dt::uint64)-1);
}
There is a trick going on here. Remember that the router has multiple initiator sockets connected to multiple targets, but we only have a single implementation of the invalidate_direct_mem_ptr method which will therefore be called by all targets. Because we do not keep track of which target called the method, we simply invalidate the entire address space known to the initiator. We will show a more refined approach in a later tutorial.
Finally, let’s look at routing the debug transport method. This should seem very familiar by now, because the implementation is similar to the preceding methods:
virtual unsigned int transport_dbg(tlm::tlm_debug_payload& dbg)
{
unsigned int target_nr = decode_address( dbg.address, dbg.address );
return ( *initiator_socket[target_nr] )->transport_dbg( dbg );
}
This time, the address to be translated is buried in the debug payload, and is overwritten by the decode_address method through the second argument of the list. (decode_address reads its first arguments, and overwrites its second argument.) The target will execute the debug transaction by copying a block of data to or from the given address, and the return value from transport_dbg gives the number of bytes actually copied.
That concludes this tutorial. We have focussed on how transactions can be routed through an interconnect component, and in particular how the component performs address translation. In general a transaction can be routed through any number of interconnect components. We will show a more sophisticated bus model in a later tutorial.
You will find the source code for this example in file tlm2_getting_started_3.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 2
Next: Tutorial 4
Back to the full list of TLM2 Tutorials