How to Write a Basic SystemC Module

By John
January 10, 2022

This post is the first in a series that looks at how we can use SystemC in the design and verification of FPGAs. We begin by discussing how we structure our designs in SystemC. This includes a discussion of the SystemC module, ports and instantiation.

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.

Structuring SystemC Designs

When we design FPGAs, it is important to remember one fundamental principle – we are designing hardware and not writing a computer program.

Therefore, we must describe the behavior of a number of different components which we then connect together. This expectation is reflected in the way that we structure our SystemC design files.

As with every electronic component, we need to know the external interface to our component. This information allows us to connect it to other components in our system.

We also need to know how the component behaves so that we can use it in our system.

In SystemC, we use a construct called a module to define this information. The SystemC module is roughly equivalent to the entity architecture pair in VHDL and the module in verilog.

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

SC_MODULE(<name>) {
  // We declare ports and processes here
  // We must also define a constructor here
private:
  //We declare all internal signals as private
}

In this construct, we use the <name> field to give a unique name to the module which we are designing.

Although we can declare several modules in a single file, it is good practice to have one file corresponding to one module.

It is also generally recommended to separate the declaration and implementation of classes and similar structures (like modules) in C++. As a result, we will typically declare a module in a header file and implement it in a separate .cpp file.

In addition to this, it is also good practice to keep the name of the file(s) and the module the same. This makes it simpler to manage large designs with many components. 

In the previous code example, we use the SC_MODULE macro to declare a SystemC module.

However, as this macro is very simple we can also omit it and directly declare the module as a struct. When we use this method, our struct must be inherited from the sc_module class. This class is a part of the SystemC library.

The code snippet below shows the general syntax we use to declare a module as a struct in SystemC.

struct <name> : public sc_module {
  // We declare ports and processes here
  // We must also define a constructor here
private:
  //We declare all internal signals as private
} 

SystemC Module Ports

In SystemC, we use ports to declare the inputs and outputs to a module.

We can think of module ports as being equivalent to pins in a traditional electronic component.

We can declare ports as being either inputs, outputs or inout (bidirectional).

The code snippet below shows the general syntax which we use to declare ports in SystemC.

// Syntax to declare a module input
sc_in<<type>> <port_name>;

// Syntax to declare a module output
sc_out<<type>> <port_name>;

// Syntax to declare a bidirectional port
sc_inout<<type>> <port_name>;

Module ports are implemented as generic C++ classes in SystemC. As a result of this, we need to provide a data type to a port when we declare it. In the above construct, we use the <type> field to do this.

We can use any C++ data type in the <type> field with bool and int types being the most common.

However, we can also use several data types which are built into the SystemC library. We discuss the SystemC data types in more depth in a separate post.

Finally, we use the <port_name> field to give a unique name to each of the ports in our module.

SystemC Module Processes

In SystemC, we use processes to define the behavior of our modules. SystemC processes are similar to methods in normal C++ classes.

However, processes in SystemC are slightly more complex than methods in C++. The reason for this is that they have to be able to more accurately model the behavior of hardware circuits.

For example, we can have processes that run concurrently in SystemC whereas this is not possible in C++. We can also write processes that suspend their operation until some external event forces them to start executing again.

As a result of this, we use one of the 3 inbuilt processes rather than normal methods when we write SystemC code.

The three SystemC processes which we can use are known as methods, threads and clocked threads.

As this is such a large topic, we discuss the three SystemC processes in-depth in a later post.

SystemC Module Constructor

When we write a SystemC module, we are effectively writing a new C++ class.

As a result of this, we need to declare a constructor inside of our module. We use constructors to initialize the state of our module when we instantiate it.

In SystemC, we use the SC_CTOR macro to declare a constructor in our SystemC module.

The code snippet below shows the general syntax we use to declare our constructor in this way.

struct <name> : public sc_module {
  // Constructor declaration
  SC_CTOR(<name>);
}

We use the <name> field in the above construct to declare the name of our module. Our constructor should have the same name as the module.

In SystemC, we typically use the constructor to register any processes which are in our module.

We talk about SystemC processes in more detail in the next post. As a part of this discussion, we also look at several examples of SystemC constructors.

SystemC Signals

In SystemC, we use signals to define connections that are internal to our module.

As an example, if we were to write a module that implements the circuit below then we could declare the green wires as signals.

A simple 2-bit twisted ring counter circuit with the inputs to both flip flops highlighted in red.

As we can only use signals inside modules, we declare them as being private.

The code snippet below shows the basic syntax we use to declare a signal in SystemC.

//Declare a signal in SystemC
sc_signal<<type>> <signal_name>;

As we can see, signals are quite similar to ports in SystemC.

Signals are also implemented as generic C++ classes. As a result of this, we have to provide a data type when we declare a signal. In the construct above, we would use the <type> field to do this.

As with ports, we can pass any valid C++ data type to the <type> field. Alternatively, we can also use one of the data types in the SystemC library. We discuss the different SystemC data types in more depth in a separate post.

Finally, we use the <port_name> field to give a unique name to each of the signals in our module.

Module Instantiation

We can invoke a SystemC module that we have already written in another part of our design. This process of invoking modules in SystemC is commonly referred to as instantiation.

Each time we instantiate a module, we create a unique object which has its own name, parameters and IO connections.

We can think of module instantiation in SystemC as being equivalent to placing a component in a traditional electronic circuit.

Once we have created all of the instances we require in our design, we must interconnect them to create a complete system. This is the same as wiring components together in a traditional electronic system.

SystemC provides us with two methods we can use for module instantiation - named instantiation and positional instantiation.

Positional Instantiation

When using the positional instantiation approach in SystemC, we use an ordered list to connect the module ports. The order of the list we use must match the order in which the ports were declared in our module.

As an example, if we declare the clock first, followed by a reset then we must connect the clock signal to the module IO first.

The SystemC code snippet below shows the general syntax for positional module instantiation.

// Create an instance of the module
<module_name> <instance_name>(<name_argument>);

// Connect to the module ports
<instance_name> (
  // Connection to the module ports
  <signal_name>, // this connects to the first port
  <signal_name>  // this connects to the second port
);

The <module_name> field must match the name we gave the module when we declared it.

We use the <instance_name> field to give a unique name to an instantiated module in our design.

We use the <name_argument> field to pass a string to the module constructor which acts as an identifier.

This method can be difficult to maintain as the order of our ports may change as our design evolves.

Positional Instantiation Example

To better demonstrate how we use positional instantiation in SystemC, let's consider a basic example.

For this example, we will create an instance of the simple circuit shown below.

A logic circuit with the function (a and b) or c.

When we use positional instantiation, the order of the ports in the module declaration is important. The code snippet below shows how we would declare a module for this circuit.

struct and_or : public sc_module {
  // Module IO
  sc_in<bool> a;
  sc_in<bool> b;
  sc_in<bool> c;
  sc_out<bool> logic_out;
  // Rest of declaration omitted for brevity 
}

For brevity, we only show the module ports in the declaration above. In reality, we would also have to declare signals, processes and a constructor.

The code snippet below shows how we would instantiate this module in another part of our design.

// Create an instance of our module
and_or and_or_example("example");

// Connect the IO ports
example_and_or(sig_a, sig_b, sig_c, sig_logic_out);

Named Instantiation

When we use named module instantiation in SystemC, we explicitly define the name of the port we are connecting our signal to. Unlike positional instantiation, the order in which we declare the ports is not important.

This method is generally preferable to positional instantiation as it produces code that is easier to read and understand.

It is also easier to maintain as we can modify port declarations without having to worry about the order in which we declare them.

The SystemC code snippet below shows the general syntax for named module instantiation.

// Create an instance of the module
<module_name> <instance_name>(<name_argument>);

// Connect to the module ports
<instance_name>.<port_name>(<signal_name>);
<instance_name>.<port_name>(<signal_name>);

The <module_name> field must match the name we gave the module when we declared it.

We use the <instance_name> field to give a unique name to an instantiated module in our design.

We use the <name_argument> field to pass a string to the module constructor which acts as an identifier.

After we have created an instance of our module, we have to connect to each of the ports on a separate line.

Named Instantiation Example

To better demonstrate how named instantiation works, let's consider a basic example.

For this example, we will create an instance of the simple circuit shown below. This is the same circuit we previously used in the positional instantiation example.

A logic circuit with the function (a and b) or c.

The code snippet below shows how we would declare a module for this circuit.

struct and_or : public sc_module {
  // Module IO
  sc_in<bool> a;
  sc_in<bool> b;
  sc_in<bool> c;
  sc_out<bool> logic_out;
  // Rest of declaration omitted for brevity 
}

The SystemC code below shows how we would instantiate this module in another part of our design using named instantiation.

// Create an instance of our module
and_or and_or_example("example");

// Connect the IO ports to a signal
example_and_or.a(sig_a);
example_and_or.b(sig_b);
example_and_or.c(sig_c);
example_and_or.logic_out(sig_logic_out);

Exercises

What do we use a module for in SystemC?

We use modules to define the behavior of a component in SystemC.

Name the three directions that ports can have.

We can declare ports as either inputs, outputs or bidirectional (inout).

What do we use signals for in SystemC modules?

We use signals to declare connections that are internal to our module.

What is the difference between named and positional instantiation? Which one is easier to maintain and why?

We use an ordered list to connect ports when using positional instantiation. We have to explicitly define the port we are connecting to when we use named instantiation. Named instantiation is easier to maintain as the code is not affected if we change the order of the ports in the module declaration.

Write a module declaration for the circuit shown below. This module should only include the declaration of the IO and the constructor.

A circuit diagram showing a two input multiplexor and a d type flip flop. The output of the multiplexor is the input to the flip flop.
struct MuxFF : public sc_module {
  // Circuit inputs and outputs
  sc_in<bool> clock;
  sc_in<bool> a;
  sc_in<bool> b;
  sc_in<bool> addr;
  sc_out<bool> q;

  // Processes would be declared here
  
  // Constructor declaration
  SC_CTOR(MuxFF);
};
Enjoyed this post? Why not share it with others.

Leave a Reply

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

four × three =

Follow Us

Follow us on social media for all of the latest news.

Subscribe

Join our mailing list and be the first to hear about our latest FPGA tutorials
Sign Up to our Mailing List
© 2021 FPGA Tutorial
Designed in partnership with ceotutorial.com

ENJOYING THIS ARTICLE?

Why not join our mailing list and be the first to hear about our latest FPGA tutorials

Close
The fpgatutorial.com site logo

Sign Up to our Mailing List

Join our mailing list and be the first to hear about our latest FPGA themed articles and tutorials .

Close
linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram