Free Online Training Events
Free Technical Resources
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:
Writing hierarchical channels is a big topic, so this chapter will just show a basic example, a model of a stack.
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.
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.
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.
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.
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...).
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: