Global training solutions for engineers creating the world's electronics

Hierarchical Channels

Hierarchical channels form the basis of the system-level modelling capabilities of SystemC. They are based on the idea that a channel may contain quite complex behaviour - for instance, it could be a complete on-chip bus.

Primitive channels on the other hand cannot contain internal structure, and so are normally simpler (for instance, you can think of sc_signal as behaving a bit like a piece of wire).

To construct complex system level models, SystemC uses the idea of defining a channel as a thing that implements an interface. An interface is a declaration of the available methods for accessing a given channel. In the case of sc_signal, for instance, the interfaces are declared by two classes sc_signal_in_if and sc_signal_out_if, and these declare the access methods (for instance read() and write()).

By distinguishing the declaration of an interface from the implementation of its methods, SystemC promotes a coding style in which communication is separated from behaviour, a key feature to promote refinement from one level of abstraction to another.

One additional feature of SystemC is that if you want modules to communicate via channels, you must use ports on the modules to gain access to those channels. A port acts as an agent that forwards method calls up to the channel on behalf of the calling module.

Hierarchical channels are implemented as modules in SystemC, in fact, they are derived from sc_module. Primitive channels have their own base class, sc_prim_channel.

To summarise:

 

  • Separating communication from behaviour eases refinement
  • An interface declares a set of methods for accessing a channel
  • A hierarchical channel is a module that implements the interface methods
  • A port allows a module to call the methods declared in an interface

Writing hierarchical channels is a big topic, so this chapter will just show a basic example, a model of a stack.

Define the interface

The stack will provide a read method and a write method. These methods are non-blocking (they return straight away without any waits). To make this usable, each method returns a bool value saying if it succeeded. For instance, if the stack is empty when read, nb_read() (the read method) returns false.

These methods are declared in two separate interfaces, a write interface stack_write_if, and a read interface, stack_read_if. Here is the file containing the declarations, stack_if.h.

CODE TO GO HERE BUT DOESN'T WORK?

 

The interfaces are derived from sc_interface, the base class for all interfaces. They should be derived using the keyword virtual as shown above.

The methods nb_write, nb_read, and reset are declared pure virtual - this means that it is mandatory that they be implemented in any derived class.

Stack Channel

The stack channel overrides the pure virtual methods declared in the stack interface. Here is the code

#include "systemc.h"
#include "stack_if.h"

// this class implements the virtual functions
// in the interfaces
class stack
: public sc_module,
  public stack_write_if, public stack_read_if
{
private:
  char data[20];
  int top;                 // pointer to top of stack

public:
  // constructor
  stack(sc_module_name nm) : sc_module(nm), top(0)
  {
  }

  bool stack::nb_write(char c)
  {
    if (top < 20)
    {
      data[top++] = c;
      return true;
    }
    return false;
  }

  void stack::reset()
  {
    top = 0;
  }

  bool stack::nb_read(char& c)
  {
    if (top > 0)
    {
      c = data[--top];
      return true;
    }
    return false;
  }

  void stack::register_port(sc_port_base& port_,
                            const char* if_typename_)
  {
    cout << "binding    " << port_.name() << " to "
         << "interface: " << if_typename_ << endl;
  }
};

 

There is some local (private) data to store the stack, and an integer indicating the current location in the stack array.

The nb_write and nb_read functions are defined here. Also there is a reset() function which simply sets the stack pointer to 0.

Finally, you will notice a function called register_port - where does this come from?

It is defined in sc_interface itself, and may be overridden in a channel. Typically, it is used to do checking when ports and interfaces are bound together. For instance, the primitive channel sc_fifo uses register_port to check that a maximum of 1 interface can be connected to the FIFO read or write ports. This example just prints out some information as binding proceeds.

Creating A Port

To use the stack, it must be instanced. In this example, there are two modules, a producer and a consumer. Here is the producer module

#include "systemc.h"
#include "stack_if.h"

class producer : public sc_module
{
public:

  sc_port<stack_write_if> out;
  sc_in<bool> Clock;

  void do_writes()
  {
    int i = 0;
    char* TestString = "Hallo,     This Will Be Reversed";
    while (true)
    {
      wait();             // for clock edge
      if (out->nb_write(TestString[i]))
        cout << "W " << TestString[i] << " at "
             << sc_time_stamp() << endl;
      i = (i+1) % 32;
    }
  }

  SC_CTOR(producer)
  {
    SC_THREAD(do_writes);
      sensitive << Clock.pos();
  }
};

 

The producer module declares a port that interfaces to the stack. This is done with the line:

sc_port<stack_write_if> out;

 

which declares a port that can be bound to a stack_write_if, and has a name out.

You can bind more than one copy of an interface to a port, and you can specify the maximum number of interfaces that may be bound. For instance, if we wanted to allow 10 interfaces to be bound, we could declare the port as

sc_port<stack_write_if,10> out;

 

If you leave out the number, the default is 1.

To actually write to the stack, call the method nb_write via the port:

out->nb_write('A')

 

This calls nb_write('A') via the stack_write_if. You must use the pointer notation -> when doing this.

The consumer module that reads from the stack looks very similar, except it declares a read port

sc_port<stack_read_if> in;

 

and calls nb_read, e.g.

in->nb_read(c);

 

where c is of type char.

Note that nb_read and nb_write both return the value true if they succeed.

Perhaps the most interesting thing about this is that the functions nb_write and nb_read execute in the context of the caller. In other words, they execute as part of the SC_THREADs declared in the producer and consumer modules.

Binding Ports and Interfaces

Here is the code for sc_main, the top level of the design

#include "systemc.h"
#include "producer.h"
#include "consumer.h"
#include "stack.h"

int sc_main(int argc, char* argv[])
{
  sc_clock ClkFast("ClkFast", 100, SC_NS);
  sc_clock ClkSlow("ClkSlow", 50, SC_NS);

  stack Stack1("S1");

  producer P1("P1");
  P1.out(Stack1);
  P1.Clock(ClkFast);

  consumer C1("C1");
  C1.in(Stack1);
  C1.Clock(ClkSlow);

  sc_start(5000, SC_NS);

  return 0;
}

 

Note how the write interface of the stack is implicitly bound in the line

P1.out(Stack1);

 

This example is a bit odd (but perfectly legal!) as the stack itself does not have ports. It is simply causing the first stack_write_if to be bound in this line. If there were more than one stack_write_if, they would be bound in sequence.

This is the result of running the code:

binding    C1.port_0 to interface: 13stack_read_if
binding    P1.port_0 to interface: 14stack_write_if
W H at 0 s
R H at 0 s
W a at 100 ns
R a at 150 ns
W l at 200 ns
// and so on for 5000 ns

 

The messages about binding are coming from the register_port function in stack.h. The names of the ports (C1.port_0...) are fabricated by the SystemC kernel, as are the interface names (13stack_read_if...).

Conclusion

The example shown above is quite simple, and yet there is a lot to learn and understand. It gives you just a quick tour through the process of defining and writing your own hierarchical channels. The key points to remember are:

    • Hierarchical channels allow complex bus models
    • Communication and behaviour are separated using interfaces
    • Interfaces may be accessed from outside a module using ports
    • When you call a method in a channel via an interface, you do not have to know how it is implemented - you only need to know the declaration of the interface call
    • When methods defined in the channel are executed, they are executed in the context of the caller

 

Prev   Next