An Introduction to Hierarchical Channels in SystemC

By John
June 16, 2022

In this post, we build on our previous discussion of SystemC primitive channels to show how we use hierarchical channels to create our own, custom channels. This includes a discussion of the sc_interface class, writing hierarchical channels and connecting to hierarchical channels.

SystemC is actually a set of classes and libraries which are built on top of the C++ programming language. We can download and install these libraries for free from the Accellera website.

As a result of this, the basic syntax of the SystemC syntax is taken directly from the C++ language.

However, in these tutorials we will only look at the SystemC extensions which we can use in FPGA design and verification.

Therefore, if you are not already familiar with the C++ language then it is a good idea to take a beginners C++ course before reading through these tutorials.

Hierarchical Channels in SystemC

In a previous post, we looked at the basic primitive channels in SystemC.

We use these channels to move basic data between modules or other channels in our SystemC code.

However, we often need to model more complex communication channels when we are modelling complete systems.

For example, we may want to model a complete TCP/IP connection as part of our system level model.

In SystemC, we can write our own channels in order to implement more complex communications protocols.

As these custom channels often feature multiple classes or modules, we refer to these as hierarchical channels.

A complete hierarchicial channel actually consists of at least 2 separate classes.

We use one class to define the interface to the channel. This class provides a way for other channels or modules to connect to our hierarchical channel. We can think of the interface class as being similar to ports in a module.

We use the the second class to implement the functionality of our hierarchical channel.

By adopting this approach, we can separate the implementation details from the physical interface to our channel.

This approach, which is also known as the bridge pattern, allows us to reuse an interface with different underlying functionality if we want to.

The block diagram below shows how modules and hierarchical channels are connected. In this case, each of the blocks in the diagram represent a different class.

A block diagram show how a hierarchical channel is made up of an interface class and a channel class

Let's take a closer look at the 2 different classes which we need to write a hierarchical channel.

The Interface Class

As we mentioned in the previous section, we use the interface class to define how another module (or channel) interacts with our hierarchical channel.

We use the interface to declare which methods can be used by external modules to access our channel.

For example, when we use the sc_fifo channel then the read, write, nb_write and nb_read methods are all defined as part of the interface.

We declare all of the methods inside of our interface class as virtual functions. As a result of this, all of the methods must be implemented in our channel class.

Whenever we write an interface class, we also have to make sure it inherits the sc_interface abstract class.

The code snippet below shows the general syntax we use to declare an interface class in SystemC.

class <name> : public sc_module {
  public:
    // Definition of our interface methods go here
};

Example

In order to better demonstrate how the interface class works, let's consider a basic example.

For this example, we will write an interface for a very basic memory module.

As we discussed earlier, we only need to define the methods which are required to interact with our channel. We actually implement these methods inside of our channel class.

Therefore, we will simply include a read and write virtual function definition in our interface class.

Our read function will take a single argument which will be the memory address and return the data associated with that address.

Our write function takes 2 arguments - one for the address and one for the data to be written to the memory.

The code snippet below shows the full code for the example interface class.

class memory_if : public sc_interface {
  public:
  	virtual int read(int addr) = 0;
 	virtual void write(int addr, int data) = 0;
};

The Channel Class

As we previously mentioned, we use the channel class to implement the methods defined in a given interface class.

As a result of this, we must inherit the methods from the interface class which we are implementing in the channel.

In addition to this, we also have to inherit from the sc_module class as hierarchical channels are implemented as modules in SystemC.

The code snippet below shows the general syntax that we use to declare a channel class in SystemC.

class <name> : public sc_module, public <interface> {
  // Code to implement
};

In the example above, we use the <name> field to give a name to our channel class.

We use the <interface> field to give the name of the interface class which we will implemented in our class.

Once we have declared our channel class, we have to implement the methods from the interface class as a minimum.

Apart from this, we can treat the channel class as if it were a normal SystemC module.

As a result of this, we can include any other functionality which we require in the class. We can even instantiate other modules or classes inside our channel class if required.

Example

In order to better demonstrate how the channel class works, let's consider a basic example.

For this example, we will write a channel which implements the basic memory interface class we previously wrote.

This means that we will have to implement both the read and write method in our channel class.

Finally, we will also declare a variable which we use to model our memory contents.

The code snippet below shows the full declaration for our channel class.

class memory_channel : public sc_module, public memory_if {
  public:
    // Constructor
    SC_CTOR(memory_channel);
    // Implementation of the interface functions
    int read(int addr);
    void write(int addr, int data);
  private:
    // Model of our 
    int mem [256];
};

In order to implement the read method, we simply return the relevant entry from the mem array.

However, we must firstly check that the addr argument is valid (i.e. between 0 and 255).

The code snippet below shows how we implement the read method in our channel class.

int memory_channel::read(int addr) {
   if (addr > 255 || addr < 0) {
     std::cout << "addr must be between 0 and 255" << std::endl;
     return -1;
   }
   else {
     return mem[addr];
   }
}

In order to implement our write method, we again have to check that the addr argument is valid.

If the address is valid, we then simply assign the value of the data argument to the relevant entry in the mem array.

The code snippet below shows how we implement the write method.

void memory_channel::write(int addr, int data) {
  if (addr > 255 || addr < 0) {
    std::cout << "addr must be between 0 and 255" << std::endl;
  }
  else {
    mem[addr] = data;
  }
}

Connecting to a Hierarchical Channel

After we have written a hierarchical channel, we need to connect it to the module that uses it.

There are actually 2 steps which we have to take in order to connect a module to our hierarchical channel.

Firstly, we have to declare a port in the module which we are connecting to our interface class.

As we discussed in the post on primitive channels, we use the general syntax below to declare a module port.

sc_port<<IF_TYPE>, <N>, <P>> <port_name>;

In this construct, we use the <IF_TYPE> parameter to define which interface class the port uses.

See the post on the sc_port class for more explanation of the <N> and <P> parameters in this construct.

After we have declared the module port, we then need to connect this port to an instance of our channel class.

To do this, we firstly instantiate our module and then create an instance channel class.

Once we have done this, we then connect the instance of our channel class to the relevant module port in the normal manner.

Example

To better demonstrate how we connect to a hierarchical channel, let's consider a basic example.

For this example, we will write a module which uses the basic memory interface which we have previously declared and implemented.

Our module will contain a single thread type process which simply writes random data to the memory and then reads it back to check that the data was written correctly.

We will make this process sensitive to the rising edge of the module clock signal.

In addition to this, we will also need to declare a port which uses the memory interface class.

The code snippet below shows how we declare this module in SystemC.

struct MemoryIFTest : public sc_module {
  // Ports
  sc_in<bool> clock;
  sc_port<memory_if> memory;
  // Processes
  void test_stim();
  // Constructor
  SC_CTOR(MemoryIFTest);
}; 

As we can see from this example, we use the sc_port class to declare a module port which uses our memory_if channel.

As we are connecting our port to exactly one channel, we can use the default settings for the rest of the parameters in the sc_port declaration.

We now need to implement a basic function which creates a random address and data.

Our function will then write this to our basic memory interface channel.

To generate the random data and address, we use the rand method from the C++ std library.

We then pass the random data and address to the write method of our memory interface channel.

Next, we have to read the data back from the channel using the read method which we declared in our interface class.

Finally, we check that the data we read back from the memory matches what we wrote in. If the data is incorrect, we print a message to the console saying there has been an error.

The code snippet below shows how this is implemented in SystemC.

void MemoryIFTest::test_stim() {
  int addr;
  int data;
  int m_data;

  while(1) {
    // Generate a random address and data
    addr = std::rand() % 255;
    data = std::rand();

    //Write the data into the memory
    std::cout <<  "Writing memory address = " << addr << " data = " << data << std::endl;
    memory->write(addr, data);

    // Read it back and check if it is correct
    m_data = memory->read(addr);
    std::cout <<  "Read memory address = " << addr << " data = " << m_data << std::endl;

    if (m_data != data) {
      std::cout << "Error = incorrect data read from memory" << std::endl;
    }

    wait();
  }
}

We can also simulate this example on EDA playground. When we run the simulation for 50ns, we get the results shown below.

Writing memory address = 163 data = 846930886
Read memory address = 163 data = 846930886
Writing memory address = 162 data = 1714636915
Read memory address = 162 data = 1714636915
Writing memory address = 83 data = 424238335
Read memory address = 83 data = 424238335
Writing memory address = 241 data = 1649760492
Read memory address = 241 data = 1649760492
Writing memory address = 249 data = 1189641421
Read memory address = 249 data = 1189641421
Writing memory address = 107 data = 1350490027
Read memory address = 107 data = 1350490027

Exercises

Hierarchical channels are made up of at least 2 classes - name them.

As a minimum, hierarchical channels consist of an interface class and a channel class.

What do we use interface classes for?

We use interface classes to define the methods which can be used to access our channel.

Write some code which implements an interface class for a basic stack type memory. The stack should store int type data and have methods for pushing and popping.

class basic_stack : public sc_interface {
  virtual void push(int data) = 0;
  virtual int pop() = 0;
};

What do we use the channel class for?

We use the channel class to implement the functionality of our channel, including implementing the methods defined in our interface class.

When we write a module we have to inherit 2 classes, name those classes.

We have to inherit from the interface class that we are going to implement as well as the sc_module class.

Enjoyed this post? Why not share it with others.

Leave a Reply

Your email address will not be published. Required fields are marked *

Subscribe

Join our mailing list and be the first to hear about our latest FPGA tutorials
Sign Up to our Mailing List
© 2024 FPGA Tutorial

Sign up free for exclusive content.

Don't Miss Out

We are about to launch exclusive video content. Sign up to hear about it first.

Close
The fpgatutorial.com site logo

Don't Miss Out

We are about to launch exclusive video content. Sign up to hear about it first.

Close