Learning SystemC: #006 Module Hierarchy And Connectivity

Learning SystemC: #006 Module Hierarchy And Connectivity

Posted by

In this post I will talk about how to create a complex structure of SystemC modules and how to connect them so that information can be passed between them safely.

Here is a list of content if you want to jump to a particular subject:

1. Module Hierarchy
   1.1. Example
2. Module Connectivity
   2.1. Interface Proper
      2.1.1 Build Connectivity From Scratch
      2.1.2 SystemC Interface Proper
   2.2. Port
      2.2.1 Build A Port From Scratch
      2.2.2 SystemC Port



1. Module Hierarchy

In SystemC, as in any other object oriented programming language, the best way of implementing a complex project is to split it in smaller, more manageable peaces. In a OOP language you split your software into classes, in SystemC you split it in modules.

There are numerous options of creating a hierarchy of modules in SystemC. In the book SystemC: From the Ground Up the authors present six ways of declaring a module hierarchy but basically you have to choose an option in which:
[ww-shortcode-fancy-list style=”1″ dividers=”true”]

  • you declare the sub-modules as pointers or as a straight forward instances
  • you declare the constructor of the module in the header file (.h) or in the implementation file (.cpp)

[/ww-shortcode-fancy-list]

Let’s see an example in which we declare the sub-module as pointers and the constructor of the module is declared in the implementation file.

1.1 Example

Let’s try to implement a model of a very basic network in which we have three computers connected only to a printer.

Network with three computers and a printer

Network with three computers and a printer


In the header file of the network module we need to declare pointers to each of the four sub-modules of the network:

1
2
3
4
5
6
7
8
9
10
11
12
13
SC_MODULE(network) {
 
  computer* comp0Inst;
  computer* comp1Inst;
  computer* comp2Inst;
 
  printer* printerInst;
 
  SC_HAS_PROCESS(network);
 
  network(sc_module_name moduleName);

};

network.h

The big advantage of declaring the members of the network module as pointers is that you can dynamically create the instances based, for example, on some given configuration. Or think about what it takes to have a configurable number of computers in this network.

Next, in the implementation file network.cpp we declare the constructor and create the actual instances:

1
2
3
4
5
6
network::network(sc_module_name moduleName) : sc_module(moduleName) {
  comp0Inst = new computer("comp0Inst");
  comp1Inst = new computer("comp1Inst");
  comp2Inst = new computer("comp2Inst");
  printerInst = new printer("printerInst");
}

network.cpp

You can play with this code on your own on EDA Playground.

I’ve created four examples on EDA playground in which I used different methods of creating the module hierarchy for this network usecase. Take a look at these approaches and decide for yourself which one you like:
[ww-shortcode-fancy-list style=”1″ dividers=”true”]

[/ww-shortcode-fancy-list]
I invite you to leave a comment to let me know which is your preferred method of creating a module hierarchy and why.

2. Module Connectivity

In SystemC information should be passed from one module to the other indirectly via some entities called channels. We already form a basic idea about what a channel is in lesson Learning SystemC: #004 Primitive Channels in which we discussed how channels make communication between modules easier and safer.

Let’s get back to our network example and try to figure out the meaning of two SystemC connectivity concepts: interface proper and port.

These two connectivity concepts will help make our SystemC code much more flexible to future changes.

Let’s imagine that we have to connect the computers with the printer using a router made by ACME. The class of this router is called acmeRouter and it has some functions to send and receive simple text.

The header file that we would get from ACME would look like this:

1
2
3
4
5
6
7
class acmeRouter {
  //function for sending some text to the printer
  void write(string text);

  //function for getting some text to be printed
  string read();
}

acmeRouter.h

Our network now looks like this:
Network with three computers connected to a printer via ACME router

Network with three computers connected to a printer via ACME router


Now the header of our network class looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
SC_MODULE(network) {
 
  computer* comp0Inst;
  computer* comp1Inst;
  computer* comp2Inst;
 
  printer* printerInst;
 
  //pointer to the router
  acmeRouter* routerInst;
  ...
};

network.h

Unfortunately, we need to change our model of the network quite a lot because each computer needs to have a pointer to the acmeRouter in order to access the write() function. Also the printer needs a pointer to acmeRouter in order to access the read() function.
Add the pointer in computer header:

1
2
3
4
SC_MODULE(computer) {
  //pointer to the rounter in order to have access to the write() function
  acmeRouter* routerInst;
};

computer.h

Add the pointer in printer header:

1
2
3
4
SC_MODULE(printer) {
  //pointer to the rounter in order to have access to the read() function
  acmeRouter* routerInst;
};

printer.h

Make the connections:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
network::network(sc_module_name moduleName) : sc_module(moduleName) {
  comp0Inst = new computer("comp0Inst");
  comp1Inst = new computer("comp1Inst");
  comp2Inst = new computer("comp2Inst");
  printerInst = new printer("printerInst");

  //create the instance of the router
  routerInst = new acmeRouter();

  //connect the pointers
  comp0Inst-> routerInst = routerInst;
  comp1Inst-> routerInst = routerInst;
  comp2Inst-> routerInst = routerInst;
  printerInst-> routerInst = routerInst;
}

network.cpp

You can run this example on EDA Playground.

But now the big question:

What would it take for you to change the provided router from ACME with one from Stark Industries?

It is pretty obvious that such a change requires a lot of coding on our part.

So in order to understand some connectivity concepts from SystemC let’s try to figure out what standards we need to impose to our fictional router industry in order to be able to make such a change in only a few lines of code.

2.1 Interface Proper

In order to understand better what an interface proper is, this chapter is split in two sections.
In the first section we will see what would it take for us to build a proper code structure similar to what SystemC offers via coding guidelines and implemented classes.
In the second section we will implement the same thing using SystemC.

2.1.1 Build Connectivity From Scratch

In our network example so far we put a pointer of acmeRouter inside computer and printer in order to access its write() and read() functions.
However we can easily see that:
[ww-shortcode-fancy-list style=”1″ dividers=”true”]

  • computer class only needs to access write()
  • printer class only needs to access read()

[/ww-shortcode-fancy-list]
So it would be very useful for us if we managed to split the router in some way in which we can separate the functionality to match our needs.

Luckily, C++ language comes with a concept called interface class:

An interface class is a class that has no member variables, and where all of the functions are pure virtual.

The above definition is taken from a great C++ tutorial – learncpp.com

We can create two interface classes simpleInIntf and simpleOutIntf and we must make them standard for our fictional router industry to use them in their design:

1
2
3
4
class simpleInIntf {
  public:
    virtual void write(string text) = 0;
};
1
2
3
4
class simpleOutIntf {
  public:
    virtual string read() = 0;
};
simpleInIntf.h
simpleOutIntf.h

 
The header files of routers that we will get from either ACME or Stark Industries will look like this:

1
2
3
class acmeRouter : public simpleInIntf, public simpleOutIntf {
  ...
}
1
2
3
class starkRouter : public simpleInIntf, public simpleOutIntf {
  ...
}
acmeRouter.h
starkRouter.h

 
We can use these two interfaces to make our computer and printer immune to any change of the router provider:

1
2
3
4
5
SC_MODULE(computer) {
  //pointer to the router input interface in
  //order to have access to the write() function
  simpleInIntf* routerInst;
};
1
2
3
4
5
SC_MODULE(printer) {
  //pointer to the rounter output interface in
  //order to have access to the read() function
  simpleOutIntf* routerInst;
};
computer.h
printer.h

 
With all these changes in place we can now easily switch between ACME or Stark Industries routers just by changing two lines of code in our network class:

1
2
3
4
5
6
7
8
9
SC_MODULE(network) {
  ...
  //pointer to the STARK router
  starkRouter* routerInst;

  //pointer to the ACME router
  //acmeRouter* routerInst;
  ...
};
1
2
3
4
5
6
7
8
network::network(sc_module_name moduleName) :
      sc_module(moduleName) {
  ...
  //create the instance of the router
  routerInst = new starkRouter();
  //routerInst = new acmeRouter();
  ...
}
network.h
network.cpp

 
You can run this example for yourself in EDA Playground. I encourage you to switch between the two routers and check out the output messages.

2.1.2 SystemC Interface Proper

In SystemC, the analog of the C++ interface class is a concept called interface proper.
The Language Reference Manual defines the concept of an interface proper. Unfortunately this definition makes very difficult to grasp the purpose of this concept:

An interface proper is an abstract class derived from class sc_interface, but not derived from class sc_object. An interface proper contains a set of pure virtual functions that shall be defined in one or more channels derived from that interface proper.

The reason behind this SystemC interface proper is exactly the same as in our router example: to give standard access to some custom modules developed independently by somebody else.

The need to derived this interface proper from sc_interface class has to do with connectivity and ports in SystemC – it is under the hood logic for which we do not really need to know to much details. Now we only need to know several rules stated by the LRM which must be obeyed by an interface proper:
[ww-shortcode-fancy-list style=”1″ dividers=”true”]

  • Shall be publicly derived directly or indirectly from class sc_interface
  • If directly derived from class sc_interface, shall use the virtual specifier
  • Shall not be derived directly or indirectly from class sc_object
  • Should contain one or more pure virtual functions
  • Should not be derived from any other class that is not itself an interface proper
  • Should not contain any function declarations or function definitions
  • Should not contain any data members

[/ww-shortcode-fancy-list]
With all these rules now clearly stated let’s try to adapt out example to use SystemC interface proper instead of C++ interface class.
Making the switch is very simple, we just have to inherit our router interfaces from sc_interface:

1
2
3
4
class simpleInIntf : virtual public sc_interface {
  public:
    virtual void write(string text) = 0;
};
1
2
3
4
class simpleOutIntf : virtual public sc_interface {
  public:
    virtual string read() = 0;
};
simpleInIntf.h
simpleOutIntf.h

 
You can run the code on your own on EDA Playground.

 
So the important thing to remember here is that to respect the SystemC standard we had to do just a small change in our code. The advantages will become more obvious in the the next chapter.

2.2. Port

So far, with the help of the interface proper, we managed to protect our models of computer and printer from any change of the router provider.
To understand the concept of port we need to answer an other question:

What if we want to connect one of the computers, in the same time, to the printer and also to a video projector?

Obliviously we need to somehow multiply the interface pointers from our computer.
A good approach would be to handle such multiplication in a separate class. This is the point there the port concept comes into the scene.

As in the previous chapter, let’s try to implement from scratch our own model of a port. Once we get a feeling of its function and complexity we can switch to native SystemC port to better see what are the advantages offered by the SystemC library.

In this task we will try to change our previous example to have one computer connected in the same time to a video projector and to a printer like in the image bellow:

Network with three computers, a printer and a video projector

Network with three computers, a printer and a video projector

2.2.1 Build A Port From Scratch

The first thing that we need to do is to create a basic video projector:

1
2
3
4
5
6
7
8
9
10
11
struct projector : ::sc_core::sc_module, simpleInIntf {
   
public:
 
  void write(string text);
 
  SC_CTOR(projector) {
   
  };

};
1
2
3
4
void projector::write(string text) {
  cout << "[" << sc_time_stamp() << "]" << name()
     << ": " << text << endl;    
};
projector.h
projector.cpp

The important thing to notice here is the fact that we no longer used SC_MODULE macro. This is needed in order to be able to extend our projector module also from our custom simpleInIntf interface proper.

Next we must implement the most tricky part of our change.

We need to implement our port class for the computer in such a way that it will work regardless of the number of connections that we need to use:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template <class I> class computerPort {
 
private:
 
  //pointers to the interfaces
  deque<I*> intfs;
 
public:
 
  I* getIntf(int i) {
    if(intfs.size() <= i) {
      cout << "Trying to access an interface index out of range" << endl;
      return NULL;
    }
    else {
       return intfs[i];
    }
  }
 
  int size() {
    return intfs.size();
  }
 
  void connect(I* i) {
    intfs.push_back(i);
  }
 
};

computerPort.h

 
As you can see the computerPort is a template class with a deque for holding all the pointers to our interfaces. We have a basic API to determine the number of interfaces – size(), to retrieve a pointer to a particular interface – getInterface() and to connect a new interface – connect().

 Having our port ready we can now update the computer to use it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SC_MODULE(computer) {
 
public:
 
  //pointer to the port
  computerPort<simpleInIntf>* port;
 
   //thread which will send some text to be processed (e.g. printed, displayed)
   void generateText();
     
  void sendText(string text);
       
public:
 
  SC_CTOR(computer) {
    SC_THREAD(generateText);
    port = new computerPort<simpleInIntf>();
  };

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void computer::generateText() {
  wait(rand() % 100, SC_NS);

  cout << "[" << sc_time_stamp() << "]" <<
    name() << ": preparing to generate some text..." << endl;  
 
  string text = name();
  text = "I am " + text;

  sendText(text);
};
   
void computer::sendText(string text) {
  for(int i = 0; i < port->size(); i++) {
    port->getIntf(i)->write(text);
  }
};
computer.h
computer.cpp

Notice that now our computer will work regardless of how many interfaces are connected to it!

The final step is to change our network class. We have to add the projector and redo the connections:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SC_MODULE(network) {
 
  computer* comp0Inst;
  computer* comp1Inst;
  computer* comp2Inst;
 
  printer* printerInst;
 
  projector* projectorInst;
 
  //pointer to the STARK router
  starkRouter* routerInst;

  //pointer to the ACME router
  //acmeRouter* routerInst;
 
  SC_HAS_PROCESS(network);
 
  network(sc_module_name moduleName);

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
network::network(sc_module_name moduleName) : sc_module(moduleName) {
  comp0Inst = new computer("comp0Inst");
  comp1Inst = new computer("comp1Inst");
  comp2Inst = new computer("comp2Inst");
  printerInst = new printer("printerInst");
  projectorInst =  new projector("projectorInst");

  //create the instance of the router
  routerInst = new starkRouter();
  //routerInst = new acmeRouter();

  //connect the pointers
  printerInst-> routerInst = routerInst;
 
  //connect computer #0 to router AND projector
  comp0Inst->port->connect(routerInst);
  comp0Inst->port->connect(projectorInst);
 
  comp1Inst->port->connect(routerInst);
  comp2Inst->port->connect(routerInst);
 
}
network.h
network.cpp

When we run this example we get this nice output in our terminal:

1
2
3
4
5
6
7
8
9
10
11
12
13
[77 ns]networkInst.comp2Inst: preparing to generate some text...
starkRouter::write(I am networkInst.comp2Inst)
starkRouter::read() -> I am networkInst.comp2Inst
[77 ns]networkInst.printerInst: I am networkInst.comp2Inst
[83 ns]networkInst.comp0Inst: preparing to generate some text...
starkRouter::write(I am networkInst.comp0Inst)
[83 ns]networkInst.projectorInst: I am networkInst.comp0Inst
starkRouter::read() -> I am networkInst.comp0Inst
[86 ns]networkInst.printerInst: I am networkInst.comp0Inst
[86 ns]networkInst.comp1Inst: preparing to generate some text...
starkRouter::write(I am networkInst.comp1Inst)
starkRouter::read() -> I am networkInst.comp1Inst
[88 ns]networkInst.printerInst: I am networkInst.comp1Inst

The important think to notice here is that the message from computer #0 goes to both the video projector and the printer while the messages from computers #1 and #2 goes only to the printer.
Re-configuring the network in any way we want is now a much simpler task.

You can run this example on your own on EDA Playground.

2.2.2 SystemC Port

As in the case of the interface proper, the LRM comes with a port definition which is very difficult to understand:

A port is the means by which a module can be written such that it is independent of the context in which it is instantiated. A port forwards interface method calls to the channel to which the port is bound. A port defines a set of services (as identified by the type of the port) that are required by the module containing the port.

I don’t know about you but for me this definition was very difficult to understand.
A more simplistic definition which should be enough for us at this point in time is the following:

A SystemC port is a class which allows you to make easily a one-to-many connection.

DISCLAIMER: I am aware that the above definition is very simplistic and there is more to ports than that but in later lessons we will learn together more about ports and most likely we will refine this definition.

The implementation of this port concept in SystemC is done by a template class called sc_port.
The parameters of the template class sc_port are the following:
[ww-shortcode-fancy-list style=”1″ dividers=”true”]

  • class IF – the type of the interface proper
  • int N = 1 – the maximum number of channels that can be bound to this port. N <= 0 means no maximum.
  • sc_port_policy P=SC_ONE_OR_MORE_BOUND – the policy of the port. possible values are: SC_ONE_OR_MORE_BOUND, SC_ZERO_OR_MORE_BOUND, SC_ALL_BOUND

[/ww-shortcode-fancy-list]

At this point it becomes obvious that in order to use a port we must also use an interfaces proper.

But let’s try to adapt our previous example to use native SystemC ports.

First we modify our computer class to use the native SystemC port:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SC_MODULE(computer) {
 
public:
 
  //port through which the computer will
  //send some simple information
  sc_port<simpleInIntf, 2> port;
 
  //thread which will generate some text to be processed (e.g. printed, displayed)
  void generateText();
 
  void sendText(string text);
     
public:
 
  SC_CTOR(computer) {
    SC_THREAD(generateText);
  };

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void computer::generateText() {
  wait(rand() % 100, SC_NS);

  cout << "[" << sc_time_stamp() << "]" << name() << ": preparing to generate some text..." << endl;  
 
  string text = name();
  text = "I am " + text;

  sendText(text);
};
 
 void computer::sendText(string text) {
   for(int i = 0; i < this->port.size(); i++) {
     this->port[i]->write(text);    
   }
}
computer.h
computer.cpp

There are several important remarks we must be aware here.

First, notice that when we declared the port we used N = 2. This means that we accept maximum two connections to our port. In sc_port class there is a protection somewhere which will trigger an error if you connect to a port more than N entities.

Second thing to notice is in the implementation of sendText() function.
The sc_port class does a nice thing for us: it overloads the [] operator so we can access a particular interface proper just as an array.

Next, we only need to do the connections in our network class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
network::network(sc_module_name moduleName) : sc_module(moduleName) {
  comp0Inst = new computer("comp0Inst");
  comp1Inst = new computer("comp1Inst");
  comp2Inst = new computer("comp2Inst");
  printerInst = new printer("printerInst");
  projectorInst =  new projector("projectorInst");

  //create the instance of the router
  routerInst = new starkRouter();
  //routerInst = new acmeRouter();

  //connect the pointers
  printerInst-> routerInst = routerInst;
 
  comp0Inst->port(*routerInst);
  comp0Inst->port(*projectorInst);
 
  comp1Inst->port(*routerInst);
  comp2Inst->port(*routerInst);
 
}

network.cpp


Notice here the overloading of () operator to allow an easier binding.

You can run this example for yourself on EDA Playgound.

That’s it!
Hope this was helpful to you! 🙂

PS: because this lesson contains a lot of EDA Playground examples I’ve listed bellow all of them for an easy reference:
[ww-shortcode-fancy-list style=”1″ dividers=”true”]

[/ww-shortcode-fancy-list]

Previous Lesson: Learning SystemC: #005 Signal Channels



Cristian Slav

One Comment

Leave a Reply

Your email address will not be published.