Rational
This tutorial is an introduction to the use of C++ for describing hardware models.
Essential constructs for the creation of SystemC models are also introduced.
Note this is a deliberately simplified description of C++. Full details on the language,
coding styles; design guidelines can be found on the Essential C++ for SystemC class.
Introduction
C++ implements Object-Orientation on the C language. For most Hardware Engineers, the principles of Object-Orientation seem fairly remote from the creation of Hardware components. However, ironically, Object-Orientation was created from design techniques used in Hardware designs. Data abstraction is the central aspect of Object-Orientation which incidentally, is found in everyday hardware designs through the use of publicly visible “ports” and private “internal signals”. Furthermore, the principle of “composition” used in C++ for creating hierarchical design is almost identical to component instantiation found in hardware designs. The coming sections will introduce the basics of C++ by looking at the creation of a hardware component.
The Class
The class in C++ is called an Abstract Data Type (ADT). It defines both data members and access functions (also called methods). Both data members and access functions are said to be private by default. In other words, data members and access functions are not visible from the outside world. This ADT mechanism is analogous to a package and package body in VHDL. The designer is responsible for making publicly available the essential set of access functions for manipulating an ADT.
The semantics for a C++ class declaration is as follows:
class counter
{
int value;
public:
void do_reset() { value = 0 ; }
void do_count_up() { value++ ; }
int do_read() { return value; }
};
In this example we see the declaration of an ADT called counter
with a data member value and publicly available access functions:
do_reset, do_count_up and
do_read. Although this class declaration is complete,
a class declaration will commonly also contain specialized functions such as
constructors and a destructor.
When constructors are used, they provide initial values for the ADT’s data members.
This mechanism is the only allowed means for setting a default value to any data member.
A destructor is used to perform clean-up operations before an instance of the ADT becomes
out of scope. Pragmatically, the destructor is used for closing previously opened files or
de-allocating dynamically allocated memory.
An example of an ADT with constructors and a destructor is as follows:
class counter
{
int value;
public:
void do_reset() { value = 0 ; }
void do_count_up() { value++ ; }
int do_read() { return value; }
counter() {
cout << "Simple constructor" << endl;
value = 10;
}
counter(int arg): value(arg) {
cout << "Interesting constructor" << endl;
}
~counter() {
cout << "Destroying a counter object" << endl;
}
};
In these examples, all access functions are made visible to the outside world through the
use of the public keyword. However, not all functions have to be
made public; they can be held hidden from the rest of the world through the use of the
private keyword.
The Object
An object is an instance of an ADT. Any number of instances can be made of a given
ADT and each of these instances is initialized individually. An example of object
instantiation and message passing is as follows:-
void main() {
counter first_counter;
counter second_counter(55);
// Message passing
first_counter.do_reset();
for (int i=0; i < 10; i++) {
first_counter.do_count_up();
}
second_counter.do_count_up();
first_counter.do_reset();
second_counter.do_reset();
}
In this example two instances of the counter ADT are created.
The first_counter instance uses the default constructor
which does not require any arguments. The second_counter
instance uses a more sophisticated constructor form that requires an argument
(55).
Messages are sent to individual objects via the ‘dot’ notation which is similar to
what is used for struct or record data structures.
The building of more complex objects can be achieved through the composition mechanism.
This mechanism is akin to component instantiation in Hardware design.
The following example illustrates the principle of hierarchical design through composition.
class modulo_counter
{
counter internal_counter;
int end_count;
public:
void do_reset() { internal_counter.do_reset(); }
void do_count_up() {
if ( internal_counter.do_read() < end_count ) {
internal_counter.do_count_up();
} else { internal_counter.do_reset(); }
}
int do_read() { return internal_counter.do_read(); }
modulo_counter(int tc):
end_count(tc),
internal_counter(0) {
cout << "A new modulo_counter instance" << endl; }
};
In this example a new ADT modulo_counter is created from an
instance of a counter ADT called internal_counter.
An additional data member end_count was added to
provide the modulo value for this new ADT. The access function
do_count_up is customized to take in considerations
of the modulo counter specifications. As it can be observed, most actions are
delegated to the internal_counter instance via
message passing. It is interesting to notice how the constructor function
is used to initialize both the end_count value
and the instantiated object internal_counter.
The argument tc is used to set the value of
end_count and the value 0
is passed to the constructor of the internal_counter object.
Inheritance
Along with composition, C++ provides a sophisticated mechanism for reusing code called
inheritance. With inheritance designers are able to create new ADTs from existing ones
by accessing all public elements of a parent class into a child class.
This principle can be more appropriate than composition since it usually requires
less effort to achieve the same goal. However inheritance does not replace
composition but complements it.
The following example illustrates the creation of a modulo counter ADT from an
existing counter ADT whilst using inheritance.
class modulo_counter : public counter {
int end_count;
public:
void do_count_up() {
if ( do_read() < end_count ) {
counter::do_count_up(); }
else { do_reset(); }
}
modulo_counter(int tc): end_count(tc), counter(0) {
cout << "A new modulo_counter instance" << endl; }
};
In this example a new access function called do_count_up is created.
This overrides the inherited function from the parent class counter. The remaining public
access functions, (do_reset, do_read)
found in the parent class, are now available to the child
class modulo_counter.
The creation of the modulo_counter class is greatly simplified
through the use of inheritance. Nevertheless, the do_read
and counter::do_count_up functions had to be used in the
newly created do_count_up function to access the private data
members inside the parent class.
Although this is a valid solution, C++ offers a more pragmatic alternative, consisting in
using a protected encapsulation in place of a private one.
Unlike private members, protected members are inherited along with public ones when a
parent class is derived. However, protected data members remain hidden from the outside
world in the same way as private members do.
A new implementation of the counter and modulo_counter class using protected data members
is as follows:
class counter
{
protected:
int value;
public:
void do_reset() { value = 0 ; }
void do_count_up() { value++ ; }
int do_read() { return value; }
counter() {
cout << "Simple constructor" << endl;
value = 10;
}
counter(int arg): value(arg) {
cout << "Interesting constructor" << endl;
}
~counter() { cout << "Destroying a counter object"
<< endl;}
};
class modulo_counter : public counter {
protected:
int end_count;
public:
void do_count_up() {
if ( value < end_count ) { value++; }
else { value = 0; }
}
modulo_counter(int tc): end_count(tc), counter(0) {
cout << "A new modulo_counter instance" << endl; }
};
In this last example the data member value is directly
accessed from within the modulo_counter class. The use of the
protected encapsulation has simplified the
creation of the derived class by making data members from the parent class accessible.
The principle of single inheritance can be extended to allow a child class to be
built from multiple parents. This principle is referred as multiple inheritance.
Multiple inheritance enables designers to rapidly create new sophisticated classes
from a multitude of existing ADTs.
The use of multiple inheritance commonly requires careful planning since it can
create numerous undesirable side effects.
An example of multiple inheritance is as follows:
class reg {
protected:
int value;
public:
void do_reset() { value = 0; }
int do_read() { return value; }
void do_write(int arg) { value = arg; }
reg(): value(0) {}
};
class up_counter: virtual public reg {
public:
void do_count_up() { value++; }
};
class down_counter: virtual public reg {
public:
void do_count_down() { value--; }
};
class up_down_counter:
public up_counter, public down_counter { };
Here the up_down_counter ADT is created from two parent classes:
up_counter and down_counter.
The up_down_counter does not require any code since it inherits
all of its implementation from its parent classes.
It is important to point out that both the up_counter
and down_counter are inheriting virtually the register class.
The virtual inheritance is used here to prevent multiple declarations of the value,
do_reset, do_read,
do_write inside the up_down_counter
since this ADT inherits those attributes twice, though both the
up_counter and down_counter inheritance.
Template Class
The template mechanism is used for creating more versatile ADTs. Templates can be used for variable types or values. An example of a simple template class is as follows:
template class barrel_counter {
private:
int value;
public:
void do_reset() { value = min; }
void do_count_up() {
if (value< max ) { value++; }
else {value = min; }
}
int do_read() { return value; }
barrel_counter(): value(min) { }
};
int main(int argc, char *argv[])
{
barrel_counter<10, 50> first_counter;
barrel_counter<0, 10> second_counter;
for (int i=0; i < 60; i++) {
first_counter.do_count_up();
second_counter.do_count_up();
cout << first_counter.do_read() << endl;
cout << second_counter.do_read() << endl;
}
return 0;
}
This counter implementation defines two templates min and
max; these variables are then used inside the class to
define the boundaries of the barrel counter.
This example uses templates to set variable values; alternatively templates can be
used to set a variable type.
An example of templates used for variable types is as follows:
template
class adder {
private:
T result;
public:
T add( T a, T b ) {
result = a + b ;
return result ;
}
};
int main(int argc, char *argv[]) {
adder integer_adder;
adder float_adder;
int int_result = integer_adder.add(3, 5);
float float_result = float_adder.add(6.7, 10.2);
cout << int_result << endl;
cout <
This example uses a variable type T used throughout the
class to provide a versatile ADT implementation. At a later stage two objects of
type adder are declared, however both adders use
different types for their operations.
Conclusions
As we illustrated in this section, Hardware components can be modeled in C++ and
to some extent the mechanisms used are similar to those used in HDLs.
Additionally C++ provides inheritance as a way to complement the composition
mechanism and promotes design reuse.
Nevertheless, C++ does not provide for concurrency which is an essential aspect of
systems modeling. Furthermore, timing and propagation delays cannot easily expressed
in C++. The SystemC library provides additional mechanisms such as processes and
dedicated data types to tackle C++ modeling deficiencies.
In the next tutorial, Introduction to SystemC, we will discover how SystemC allows easier
and more efficient modeling of hardware by resolving some of the deficiencies of C++.
Back to top