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 4
Introduction
In this the fourth tutorial of the series, we turn from the blocking- to the non-blocking transport interface. Previous tutorials have used the blocking transport interface, which is fine for untimed modeling. When used with the generic payload in particular, the blocking transport interface serves to pass a request from an initiator to a target, and then pass back a response from target to initiator on return from the function call. The entire transaction must be processed in a single call to the b_transport method, which may block until the target is able to complete any necessary processing. While the blocking transport interface does permit the function to be suspended for a delay, there is no explicit provision for passing delays as arguments to the function call or for having transactions with multiple phases, that is, having a single transaction span multiple transport function calls. These requirements are addressed by the non-blocking transport interface.
Non-blocking Transport Sockets
In previous tutorials we used the socket types tlm_b_initiator_socket and tlm_b_target_socket. Now we will introduce two new sockets for used with the non-blocking transport interface, tlm_nb_initiator_socket and tlm_nb_target_socket. Here is the overall structure of the example using the non-blocking interfaces:
struct Initiator: sc_module, tlm::tlm_bw_nb_transport_if<>
{
tlm::tlm_nb_initiator_socket<> socket;
...
};
struct Memory: sc_module, tlm::tlm_fw_nb_transport_if<>
{
tlm::tlm_nb_target_socket<> socket;
...
};
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 is unchanged from example 1, but all the blocking _b_ interfaces and sockets have been replaced by non-blocking _nb_ interfaces and sockets. By now you should be familiar with the individual interfaces from previous tutorials, that is, the transport, direct memory interface and debug transaction interface. Compared to the blocking transport sockets, the forward and backward non-blocking transport interfaces used by these sockets support one additional interface, namely the non-blocking transport interface on the backward path. So in the forward direction the initiator can call the following methods:
nb_transport get_direct_mem_ptr transport_dbg
and in the backward direction the target can call the following methods:
nb_transport invalidate_direct_mem_ptr
Calling the Non-blocking Transport Method
The non-blocking transport method is broadly similar to blocking transport in that it passes a reference to a transaction object, but there the similarity ends. Non-blocking transport also carries information related to timing, transaction phases, and synchronization. Here is the call from the initiator:
tlm::tlm_generic_payload trans; tlm::tlm_phase phase = tlm::BEGIN_REQ; sc_time delay = SC_ZERO_TIME; ... tlm::tlm_sync_enum status; status = socket->nb_transport( trans, phase, delay );
The generic payload transaction argument is identical to the blocking transport case discussed in previous tutorials.
The phase argument indicates the phase of the transaction, where a single transaction object can be passed back and forth in either direction through multiple calls to nb_transport. In the simplest case of the loosely-timed coding style, a transaction has two timing points that effectively mark the start and the end of the transaction. The corresponding values for the phase argument are BEGIN_REQ and BEGIN_RESP. BEGIN_REQ is sent from the initiator to the target, and BEGIN_RESP from the target back to the initiator. The interesting thing about it is that BEGIN_RESP can be sent back in one of two ways: either with the return from the nb_transport function call, or using a separate nb_transport function call on the backward path. This simple example only uses the forward path, so the initiator expects the target to execute the transaction right away and return the completed transaction.
This tutorial mentions both phases and timing points. Timing points mark the transitions between adjacent phases. It is like the distinction between a row of fence panels separated by posts and a row of fence posts separated by panels, with the phases being the fence panels and the timing points the fence posts; the two concepts are complementary. Only the phases appear explicitly in the TLM2 interfaces.
The delay argument is used to annotate timing onto the function call. Timing can be annotated by the initiator or by the target. In this example the target annotates a delay indicating the latency of the memory, and this delay is acted on by the initiator after the call.
The value returned from the nb_transport function is an enumeration with four possible values: TLM_REJECTED, TLM_ACCEPTED, TLM_UPDATED and TLM_COMPLETED. We will have a lot more to say about using these values in later tutorials.
switch (status)
{
case tlm::TLM_REJECTED:
case tlm::TLM_ACCEPTED:
SC_REPORT_FATAL("TLM2",
"nb_transport return value not supported by initiator");
break;
case tlm::TLM_UPDATED:
case tlm::TLM_COMPLETED:
if ( trans.is_response_error() )
SC_REPORT_ERROR("TLM2", "Response error from nb_transport");
cout << "trans/fw = { " << (cmd ? 'W' : 'R') << ", " << hex << i
<< " } , data = " << hex << data << " at time " << sc_time_stamp()
<< ", delay = " << delay << endl;
}
In this example we are effectively just directly substituting non-blocking transport in place of blocking transport. This simple initiator only expects two timing points marking the start and end of the transaction. Moreover, this initiator requires that the target executes and completes the transaction immediately and returns the updated transaction object, so it aborts simulation if it gets an unexpected response. Clearly this is not recommended practice for interoperability, but it will suffice to get us started. We will show better practice in later tutorials.
If the target returns an updated or completed transaction, which should amount to the same thing given there are only two timing points, the initiator is obliged check the response status in the generic payload transaction. In the case that the transaction is complete the initiator should not check the phase argument because the target is not obliged to update it.
The final responsibility of the initiator with respect to this transaction is to act on the delay annotated onto the function call by the target. The delay indicates that the initiator should behave as if the BEGIN_RESP timing point occurred after the given delay, and the simplest way to do this (although not the most efficient) is to suspend for the given delay:
wait(delay);
A more sophisticated approach used by the loosely-timed coding style is to keep a tally of any such delays in a local variable, and make a single call to wait later in the thread process. This will be shown in a subsequent tutorial.
Implementing the Non-blocking Transport Method
The non-blocking transport method is implemented in the target memory, where it has a similar behavior to the blocking transport method in previous tutorials.
virtual tlm::tlm_sync_enum nb_transport(tlm::tlm_generic_payload& trans,
tlm::tlm_phase& phase, sc_time& delay)
{
if (delay > SC_ZERO_TIME)
SC_REPORT_FATAL("TLM2", "Temporal decoupling not supported by target");
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();
if (adr >= sc_dt::uint64(SIZE) || byt != 0 || wid != 0 || len > 4)
SC_REPORT_ERROR("TLM2",
"Target does not support given generic payload transaction");
...
Let us pause to consider the delay argument. If the delay argument is non-zero, this would oblige the target to behave as if it had received the incoming transaction after the given delay, that is, as if the transaction were received at simulation time sc_time_stamp()+delay. Running ahead of simulation time in this way is known as temporal decoupling, and characterizes the loosely-timed coding style. The simple target in this example does not support temporal decoupling, so it reacts to a non-zero delay by aborting simulation. This is a crude but acceptable reaction, acceptable in the sense that it is safe. At least the user is informed that there is something wrong, and can do something to fix it.
After extracting and checking the generic payload attributes, the target checks the phase argument. Only the value BEGIN_REQ is acceptable:
switch (phase) {
case tlm::BEGIN_REQ:
if ( cmd == tlm::TLM_READ_COMMAND )
memcpy(ptr, &mem[adr], len);
else if ( cmd == tlm::TLM_WRITE_COMMAND )
memcpy(&mem[adr], ptr, len);
break;
case tlm::END_REQ:
case tlm::BEGIN_RESP:
case tlm::END_RESP:
SC_REPORT_ERROR("TLM2", "Unexpected transaction phase received by target");
}
The function then returns with the appropriate status flags:
trans.set_response_status( tlm::TLM_OK_RESPONSE ); delay = LATENCY; return tlm::TLM_COMPLETED;
Since this is a non-blocking method it cannot implement the latency by waiting, so instead it passes the latency back to the initiator using the delay argument. Having executed either the read or the write command through to completion, the nb_transport method returns with the value TLM_COMPLETED. In this situation it is not obliged to update the phase argument, even though the appropriate phase is now BEGIN_RESP.
There is one more loose end to tie off. As described above, the non-blocking transport interface must also be implemented on the backward path (even though it is not called in this example). Since the initiator in this example does not expect or indeed allow nb_transport to be called, it provides the following dummy implementation:
virtual tlm::tlm_sync_enum nb_transport(tlm::tlm_generic_payload& trans,
tlm::tlm_phase& phase, sc_time& delay)
{
SC_REPORT_FATAL("TLM2",
"nb_transport backward path not supported by initiator");
return tlm::TLM_REJECTED;
}
In summary, the non-blocking transport interface should be used instead of the blocking transport interface whenever you need to model transactions with timing or with multiple phases. The example in this tutorial uses nb_transport more-or-less as a direct replacement for b_transport, which is not really very satisfactory. For simple untimed models, the blocking transport interface is the better choice because of its simplicity.
You will find the source code for this example in file tlm2_getting_started_4.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 3
Next: Tutorial 5
Back to the full list of TLM2 Tutorials