Skip to content

Communication between modules through ports

pavelkryukov edited this page Mar 26, 2016 · 8 revisions

Introduction

Motivation

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.

Functions

Ports have the two main functions in MIPT-MIPS:

  • To pass information from one module to another.
  • To model delays within a hardware structure.
Between modules

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.

Within a module

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.

Port paradigms

Bandwidth and latency

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.

Port connection

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.

Clocking

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.


Using details

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.

Port Basics

Setup

The following steps should be done to setup the port (all other connection details happen automatically):

  1. 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.
  1. Initialize them with the same unique string.

  2. 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

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.

Error checking

Dublicating of keys

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.
No initialization error

If you try to write or to read from uninitialized ports, you'll get message:

   <name> <type>Port was not initializated

and critical error.

Errors during initialization

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

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
Finding lost tokens

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.


A rather big example

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:

  1. 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).

  2. In the next cycle b receives the data, processes it and sends back to a.

  3. 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:

Port usage example

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.


Exercise 1

Add a separate class C and move DATA_LIMIT control into it.

The port connection scheme is presented in the next picture:

Ports usage - exercise 1

Clone this wiki locally