-
Notifications
You must be signed in to change notification settings - Fork 139
Communication between modules through ports
Current trends in semiconductor scaling indicate that wire delay will play a critical role in processor performance. Thus, time and, more specifically, delays between and in hardware structures must become first-class abstractions in the processor design representation. For this reason, and to keep the interfaces between modules cleaner and better defined, we created MIPT-MIPS's ports communication paradigm.
Ports define interfaces between modules to facilitate coding of reusable modules. A well-defined interface disallows implicit side effects, which might hinder the fidelity, reusability, and portability of model code. Traditionally, performance models use method calls and global variables to pass information between hardware structures. Unfortunately, these techniques permit an unrestricted information flow that allows unexpected side effects to occur.
Ports have the two main functions in MIPT-MIPS:
- To pass information from one module to another.
- To model delays within a hardware structure.
Modules that represent hardware structures exchange information through ports. A real processor or system typically does not have any global information instantaneously accessible anywhere in the chip. Instead, a processor’s state is distributed among several hardware components that might take several cycles to communicate. Components that need to access the state of other components must do so through explicit communication. Ports represent communication channels that let modules representing hardware components communicate such state information with one another.
To pass information from one module to another, the sending module must write the relevant information to the port. The receiving module on the other side of the port’s connection reads the information from the port.
MIPT-MIPS also uses ports to model delays within a hardware structure. It creates a clear separation between a hardware algorithm, independent of time, and the timing component itself.
Like real hardware, ports have a fixed latency and a maximum bandwidth. The information that a module sends through a port does not appear in the receiving module before the port’s fixed latency elapses. Similarly, a module cannot send more information than the port’s bandwidth allows. Thus, MIPT-MIPS can accurately model wire delays and bandwidth between modules.
MIPT-MIPS implements ports through first-in, first-out (FIFO) queues. Each module declares one end of the port. The sending module declares an output port with an identifier string, and the receiving module declares the corresponding input port with the same identifier string. MIPT-MIPS’s utility code handles the actual connection between output and input ports. The utility code runs at initialization and connects output and input ports using the identifier strings.
In fact, there is no clock method for ports. Instead, the number of the current cycle is necessary when writing or reading from the ports. If the write clock and the port's latency are know it is easy to calculate the read clock, i.e. the moment when data can be read from the port.
The Port class is implemented as a templated C++ class. This means that a Port can be used to communicate any type of data. This part contains detailed information about how to use MIPT-MIPS ports.
The following steps should be done to setup the port (all other connection details happen automatically):
- Declare the endpoints.
Note that a port consists of two parts: write (or input) and read (or output). Be careful, usually they are also called "ports", but only their combination is a real port.
-
Initialize them with the same unique string.
-
Set bandwidth and latency.
Only the input knows the real bandwidth and only the output knows the latency. It means that the sending module defines the bandwidth and the receiving module defines the latency (i.e. different received modules can provide different latencies without changing of the sending module).
Let's look at an example:
- Declare the write port, and give it a type (
CpuInstr
is used for all our examples, but any type or class will work):
WritePort<CpuInstr> var1_wport( Key, Bandwidth, Fanout);
The string is used to connect the ports. Bandwidth is the desired bandwidth for this connection. Fanout is number of connected read parts of ports.
- Declare the read port with the same type as the write port it will get connected to:
ReadPort<CpuInstr> var2_rport( Key, Latency);
Again, the string is used to connect the ports. The other parameter, latency is the desired latency for this connection.
Note that the SAME unique string is used for each port. This is how the ports are connected, so these MUST be the same exact string for each connection. |
---|
- After declaring all the ports, you should init them with command
Port<CpuInstr>::init();
or
Port<CpuInstr>::portMap->init();
Using ports is pretty simple. Carrying on with the previous example:
- To write instr1 to a port:
CpuInstr instr1;
... // fulfill instr1
var1_wport.write( instr1, cycle); // Where cycle is a current CPU cycle number.
- To read data from a port:
CpuInstr instr2;
var2_rport.read( instr2, cycle); // Where instr2 is given as a reference parameter,
// i.e. the read function fulfills it.
// Cycle is a current CPU cycle number.
You can create two writing ports with the same key, but younger one will replace older one in map of ports. You'll got message with warning:
Reusing of '<name>' key for WritePort. Last WritePort will be used.
If you try to write or to read from uninitialized ports, you'll get message:
<name> <type>Port was not initializated
and critical error.
When you call initialization of ports, you may get some messages:
No WritePort for '<name>' key
It means that there's no writing port with this key, but there are reading ports with that. It's a critical error and execution will be stopped.
No ReadPorts for '<name>' key
Similar, but about reading ports.
<name> WritePort is overloaded by fanout
Your writing port is connected to greater number of reading ports, than mentioned in fanout. It's a critical error.
<name> WritePort is underloaded by fanout
Your writing port is connected to lesser number of reading ports, than mentioned in fanout. It's NOT a critical error, it's just a warning because that situation can happen during some debug works.
Overloading by bandwidth is a critical error, you'll be notified about it (on calling .write( )
method) with message:
Port '<name>' is overloaded by bandwidth
Commands cpp
Port<type>::lost( cycle);
or
Port<type>::portMap->lost( cycle);
will show you is there any 'lost tokens' in ports, which were added and haven't been got yet.
In this example ports are using to connect two object and also to start and to stop the calculation.
In more details it works in the following order:
-
The object of A class (a) is received an initialization signal. It processes the data and sends it to the object of B class (b).
-
In the next cycle b receives the data, processes it and sends back to a.
-
In the next cycle a receives the data and processes it. If the data is larger than the DATA_LIMIT value then a sends a stop signal and the calculation is finished. Otherwise it just sends the data to b and all repeats from the 2nd step.
The next picture presents the ports connection scheme:
Note if the DATA_LIMIT value isn't exceeded for the certain number of cycles (CLOCK_LIMIT) the calculation is finished. It allows to avoid an endless loop in case of an error.
The code below is main.cpp
file. It contains the whole example code you need, but you have to change the relative path to port.h
.
/**
* A simple example of using of ports
* @author Alexander Titov
* Copyright 2011 MDSP team
*/
#include <iostream>
// Do not move port.h just set the right relative path.
// You MUST change it to yours!!!
#include "../trunk/sources/perfsim/ports.h"
using namespace std;
#define PORT_LATENCY 1
#define PORT_FANOUT 1
#define PORT_BW 1
#define DATA_LIMIT 5 // exit when this data value is exceeded
#define CLOCK_LIMIT 10 // how many cycles to execute
class A
{
WritePort<int>* _to_B;
ReadPort<int>* _from_B;
ReadPort<int>* _init;
WritePort<bool>* _stop;
// process recieved data
void processData ( int data, int cycle);
public:
A ();
~A ();
/* "Clock" A object, i.e. execute all actions
* that it performs for a cycle: read ports,
* process data and so on.
*/
void clock ( int cycle);
};
class B
{
WritePort<int>* _to_A;
ReadPort<int>* _from_A;
// process recieved data
void processData ( int data, int cycle);
public:
B ();
~B ();
/* "Clock" B object, i.e. execute all actions
* that it performs for a cycle: read ports,
* process data and so on.
*/
void clock ( int cycle);
};
int main()
{
A _a;
B _b;
WritePort<int> _init( "Init_A", PORT_BW, PORT_FANOUT);
ReadPort<bool> _stop( "Stop", PORT_LATENCY);
// Connect all the ports.
// MUST be after declaration of all ports!
// MUST be for each port type!
Port<int>::init();
Port<bool>::init();
// Init A object by 0 value
_init.write(0, 0);
for ( int cycle = 0; cycle < CLOCK_LIMIT; cycle++)
{
cout << "\n--- cycle " << cycle << "----\n";
// check the stop port from A object
bool tmp;
if ( _stop.read( &tmp, cycle))
{
cout << "-------------------------------\n\n"
<< "A stop signal is recieved.\n"
<< "Calculation is COMPLETED in cycle " << cycle << ".\n\n";
return 0;
}
// execute each module
_a.clock( cycle);
_b.clock( cycle);
}
cout << "-------------------------------\n\n"
<< "Calculation is FINISHED by CLOCK_LIMIT (=" << CLOCK_LIMIT << ").\n\n";
return 0;
}
//=================================================================
// Implementation of A class
//=================================================================
A::A()
{
_to_B = new WritePort<int> ( "A_to_B", PORT_BW, PORT_FANOUT);
_from_B = new ReadPort<int> ( "B_to_A", PORT_LATENCY);
_init = new ReadPort<int> ( "Init_A", PORT_LATENCY);
_stop = new WritePort<bool> ( "Stop", PORT_BW, PORT_FANOUT);
}
A::~A ()
{
delete _to_B;
delete _from_B;
delete _init;
delete _stop;
}
void A::processData ( int data, int cycle)
{
// perform calculation
++data;
cout << "\t\tProcess data: new value = " << data << "\n";
// If data limit is exceeded
// then send a stop signal
if ( data > DATA_LIMIT)
{
cout << "\t\t\t data limit is exceeded => "
<< "send a stop signal\n";
_stop->write( true, cycle);
return;
}
cout << "\t\t\tsend it to B\n";
_to_B->write( data, cycle);
}
void A::clock ( int cycle)
{
cout << "Clock of A:\n";
int data;
// Read all the port in order
// and break the loop if there is no message to read.
while( true)
{
if( _init->read( &data, cycle))
{
cout << "\tread the init port: data = " << data << "\n";
} else if ( _from_B->read( &data, cycle))
{
cout << "\tread the port from B: data = " << data << "\n";
} else
{
cout << "\tnothing to read\n";
break;
}
this->processData( data, cycle);
}
}
//=================================================================
// Implementation of B class
//=================================================================
B::B ()
{
_to_A = new WritePort<int> ( "B_to_A", PORT_BW, PORT_FANOUT);
_from_A = new ReadPort<int> ( "A_to_B", PORT_LATENCY);
}
B::~B ()
{
delete _to_A;
delete _from_A;
}
void B::processData ( int data, int cycle)
{
// perform calculation
++data;
cout << "\t\tProcess data: new value = "
<< data << "\n" << "\t\t\tsend it to A\n";
_to_A->write( data, cycle);
}
void B::clock ( int cycle)
{
cout << "Clock of B:\n";
int data;
if ( _from_A->read( &data, cycle))
{
cout << "\tread the port from A: data = " << data << "\n";
this->processData( data, cycle);
} else
{
cout << "\tnothing to read\n";
}
}
If everything is right the calculation should be completed in cycle 8. You can change DATA_LIMIT and CLOCK_LIMIT and investigate a behavior of the program to understand ports more carefully.
Add a separate class C
and move DATA_LIMIT control into it.
The port connection scheme is presented in the next picture:
MIPT-V / MIPT-MIPS — Cycle-accurate pre-silicon simulation.