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 5
Introduction
In the previous tutorial we introduced the non-blocking transport method, but the example was confined to calling nb_transport on the forward path. In this tutorial, we show how the target can defer the execution of the transaction and only return the response later by calling nb_transport on the backward path. As a result, the processing of the transaction will now be divided between two thread processes, one in the initiator calling nb_transport on the forward path, and a second in the target calling nb_transport on the backward path. This is more typical of how nb_transport would be used in practice to model detailed timing.
The point of this example is to show how to model latency in the target while using the non-blocking transport interface. By definition, nb_transport is non-blocking and so must return immediately. In the previous tutorial the latency was annotated as an argument to nb_transport and passed back to the initiator on return from the function call, but this assumes that the target is able to calculate the latency up-front and to execute the transaction right away. If the target is unable to calculate the latency up-front, perhaps because the latency depends on some external event, or if the target is unable to execute the transaction immediately, then the target must defer the response and call nb_transport on the backward path later.
Timing Annotation
This time the story starts with the delay argument passed to the nb_transport call by the initiator. In the previous tutorial, the target was constrained to execute the transaction immediately, and thus failed if the delay was non-zero. In this tutorial, the initiator does indeed annotate a non-zero delay, which for the sake of illustration it chooses at random:
tlm::tlm_generic_payload trans; tlm::tlm_phase phase = tlm::BEGIN_REQ; sc_time delay = (rand() % 4) * sc_time(10, SC_NS); ... tlm::tlm_sync_enum status; status = socket->nb_transport( trans, phase, delay );
The delay indicates the time at which the target is being asked to execute the transaction, expressed relative to the current simulation time sc_time_stamp(). This sort of coding style is used for temporal decoupling, where individual initiators are allowed to run ahead of simulation time in their own time warp. The purpose of temporal decoupling is to speed-up simulation by having initiator processes call wait less frequently. In SystemC, the context switching that accompanies each call to wait is a significant bottleneck for simulation speed.
When the target receives a transaction with a non-zero delay, it must do something with it. Assuming the author of the target memory model is still unwilling or unable to support temporal decoupling by predicting the future (!), the target must store the transaction somewhere until the delay has elapsed and the SystemC scheduler has caught up. An appropriate place to store the transaction is in a payload event queue (PEQ):
virtual tlm::tlm_sync_enum nb_transport(tlm::tlm_generic_payload& trans,
tlm::tlm_phase& phase, sc_time& delay)
{
if (delay > SC_ZERO_TIME)
{
m_peq.notify(trans, phase, delay);
return tlm::TLM_ACCEPTED;
}
else
{
// Execute the transaction as in Tutorial 4
...
return tlm::TLM_COMPLETED;
}
}
If the delay is zero, the transaction is executed immediately. Otherwise, the transaction is tucked away in a queue to be executed later. The return value of TLM_ACCEPTED indicates to the caller that the transaction has been accepted and will be processed later, the corollary being that neither the transaction nor the phase nor the delay argument has yet been changed.
The Payload Event Queue
There is a bit of a story behind the PEQ. The TLM 2.0-draft-2 kit contains two versions of the PEQ, both of which might be regarded as experimental. This tutorial provides a third version that we think offers some advantages. You may use any of these versions, or join the fun by creating your own. Hopefully the best version will make it into the final TLM 2.0 standard. Here is our PEQ:
namespace non_std {
template <typename TRANS = tlm::tlm_generic_payload,
typename PHASE = tlm::tlm_phase>
class peq : public sc_core::sc_object
{
public:
peq(const char* name = "") : sc_core::sc_object(name) {}
inline void notify(TRANS& trans, PHASE& phase, sc_core::sc_time& delay);
TRANS* get_transaction(PHASE& phase);
inline sc_core::sc_event& get_event();
};
} // namespace non_std
The basic idea of the PEQ is that you stuff transactions into the back of the queue with a delay, and those transaction emerge from the front of the queue after the delay has elapsed. non_std::peq is a class template which takes the transaction type and the phase type as template arguments. Class non_std::peq is derived from sc_object, so it can be instantiated dynamically as well as at elaboration time.
Transactions are put into the queue using the notify method, which takes the same set of arguments as nb_transport itself, namely the transaction, phase and delay. The get_event method returns an event that is notified whenever simulation time is advanced to the time of the next transaction in the queue. The get_transaction method returns any transactions that are ready to be removed from the queue, one-by-one. If there are no transactions ready, the method returns a null pointer. If a transaction is returned, the phase is also returned using a reference argument. The user of the PEQ is responsible for ensuring that something is listening for the event all the time, otherwise events may get missed. If a transaction is missed, it will be picked up next time get_transaction is called, and returned in chronological order.
Having taken a short diversion to look at the specification of the PEQ, let us now return to follow the life of the transaction through the target. The transaction was received by the target and inserted into a PEQ. The target also has a thread process which waits for transactions to emerge from the PEQ:
SC_CTOR(Memory) : ... m_peq("peq")
{
...
SC_THREAD(thread_process);
}
void thread_process()
{
for (;;) {
wait( m_peq.get_event() );
tlm::tlm_generic_payload* trans_ptr;
tlm::tlm_phase phase;
while ( trans_ptr = m_peq.get_transaction(phase) )
{
sc_time delay = SC_ZERO_TIME;
this->nb_transport( *trans_ptr, phase, delay );
phase = tlm::BEGIN_RESP;
socket->nb_transport( *trans_ptr, phase, delay );
}
}
}
non_std::peq<> m_peq;
The process makes itself sensitive to the event from the PEQ, then pulls transactions out of the PEQ as they become available. The execution of each transaction is completed by calling the nb_transport method of the target again, but this time with the delay set to zero. nb_transport will now execute the transaction at the current time, and the return of control from nb_transport will mark the second and final timing point of the transaction, that is, the transition to the BEGIN_RESP phase. This phase transition needs to be sent back to the initiator, and the mechanism to do that is for the target to make another call to nb_transport on the backward path. This final call to nb_transport effectively notifies the initiator that the target has now finished with the transaction.
After calling nb_transport, the target gets a final chance to read or update the transaction object. Once the currently executing process yields control back to the SystemC scheduler, the target must assume that the transaction object will be deleted by the initiator.
Implementing the Non-blocking Transport Method in the Initiator
There are now two possible routes by which the initiator can be notified that the transaction is complete: the return from nb_transport on the forward path, or a call to nb_transport on the backward path. We discussed the forward path at length in the previous tutorial. Now we will look at the implementation of the non-blocking transport interface on the backward path.
Well, it is very straightforward. First, the method extracts from the transaction the attributes it is interested in:
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();
Then it checks that the phase does indeed mark the second and final timing point, checks the response status buried in the transaction, and in this example prints out a diagnostic message before returning:
if (phase != tlm::BEGIN_RESP)
SC_REPORT_ERROR("TLM2",
"Invalid phase received from nb_transport by initiator");
if (trans.is_response_error() )
SC_REPORT_ERROR("TLM2", "Response error from nb_transport");
cout << "trans/bw = { " << (cmd ? 'W' : 'R') << ", " << hex << adr
<< " } , data = " << hex << data << " at time " << sc_time_stamp()
<< ", delay = " << delay << endl;
return tlm::TLM_COMPLETED;
}
The function should return the value TLM_COMPLETED, because it knows the transaction is done. After returning control from nb_transport, the initiator is free to delete or reuse the transaction object the next time one of its processes gets resumed. Memory management of the transaction object is always the responsibility of the initiator.
In summary, we have seen that TLM2 provides two ways in which a target can pass a response back to an initiator when using the non-blocking transport interface, either using the forward path or using the backward path.
You will find the source code for this example in file tlm2_getting_started_5.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 4
Next: Tutorial 6
Back to the full list of TLM2 Tutorials