An Introduction to SystemC Processes

By John
January 27, 2022

In this post, we introduce the three basic SystemC processes - methods, threads and clocked thread. We also show how we use the SC_METHOD, SC_THREAD and SC_CTHREAD macros to register a SystemC process. Finally, we give an example for each of these different processes to show how we use them in practice.

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.

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

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.

SystemC Processes

SystemC processes are roughly equivalent to methods or functions in C++ except that they run concurrently.

The reason for this is that we need to model the underlying behavior of our FPGA which can have many different circuits running in parallel.

Therefore, a traditional program that executes sequentially can not be used to fully model the behavior of an FPGA.

In SystemC, we can use 3 different macros to declare a process - SC_METHOD, SC_THREAD and SC_CTHREAD. These correspond to methods, threads and clocked threads respectively.

We use these macros to register processes in the module constructor. The code snippet below shows the general syntax we use to register a process in SystemC.

// Registering a method 
SC_METHOD(<process_name>);
sensitive << <signal1> << <signal2>;

// Registering a thread
SC_THREAD(<process_name>);
sensitive << <signal1> << <signal2>;

// Registering a clocked thread
SC_CTHREAD(<process_name>, <event_name>);

We use the <process_name> to declare which C++ method we are registering as a process.

This method must return a void type and take no arguments, as shown in the example code below.

// Declaring a method 
void example_method();

We use the sensitive keyword to declare a sensitivity list for threads and methods. This is important as it determines when our process executes.

When we declare a clocked thread, we use the <event_name> field to declare the sensitivity list of our process. Although we can only include a single event in this list, the functionality is otherwise equivalent to the sensitive keyword.

Let's take a closer look at the functionality of the sensitivity list in SystemC.

Senitivity List

Any code which we write in a normal C++ method is sequentially executed once whenever the method is called.

This means that all of the statements within the method body execute in sequence until we reach the last line.

Once the last line has been executed, our method is complete and all of the memory associated with it is deallocated.

However, this behavior is not representative of a real circuit which will remain in a steady state until one of the input signals changes state.

In order to more closely match the behavior of real hardware circuits, we use the sensitivity list to emulate this behavior.

To do this, we use the sensitive keyword to list all of the signals which can trigger our process.

We can also use 4 different built-in methods to ensure that our process only executes on either the rising or falling edge of a given signal.

This is useful when we need to model sequential circuits, such as flip-flops, which are only sensitive to one clock edge.

We use either the pos() or posedge() methods to declare that a process is sensitive to a rising edge of a signal.

If we want to declare that a process is sensitive to a falling edge, we use the neg() or negedge() methods.

The code snippet below shows how we use all of these methods in a sensitivity list.

// using the rising edge methods 
sensitive << clk.pos();
sensitive << clk.posedge();

// using the falling edge methods
sensitive << clk.negedge();
sensitive << clk.neg();

SystemC Method Process

We use the SC_METHOD macro to register a method type process in our SystemC design.

Although the name may suggest otherwise, SystemC methods are not quite the same as methods in C++.

When we write a method in SystemC, all of the code inside the method body executes from start to finish.

As a result of this, our simulator hands control of our program to the method when it is invoked.

Once the method has finished executing all of its code, control of the program is then handed back to the main program.

This means that we can not include infinite loops inside of a method as it will cause the entire simulation to block.

We use the sensitivity list to determine when a given process is executed.

Whenever a relevant event is detected on one of the signals in this list, all of the code in our method block is executed.

Method Example

To better demonstrate how we use the method type process in SystemC, let's consider a basic example.

For this example, we will model a simple clocked and gate circuit as shown below.

A circuit diagram showing a two input and gate with the output of the and gate being an input to a D type flip flop

First of all, we would have to declare a SystemC module that contains this circuit. As we can see from the circuit diagram above, our circuit has a total of 3 inputs and 1 output.

We will also need to declare a single method type process in our module. We will use this method to model the behavior of the circuit we discussed above.

Finally, we will also need to use the SC_CTOR macro to declare the constructor for this method.

The SystemC code below shows how we would declare this module.

struct Example : public sc_module {
  // Circuit inputs and outputs
  sc_in<bool> clk;
  sc_in<bool> a;
  sc_in<bool> b;
  sc_out<bool> q;
  // Declaration of our method
  void clocked_and();
  // Constructor declaration
  SC_CTOR(Example);
};

After declaring our module, we then need to register the clocked_and method as a process. We do this in the module constructor.

As our circuit is sensitive to the rising edge of the clock signal, we need to include this signal in the sensitivity list for the method.

The code snippet below shows how we would register this method in SystemC.

Example::Example (sc_module_name name) : sc_module(name) {
  // Register the method
  SC_METHOD(clocked_and);
  sensitive << clk.pos();
}

Finally, we can now implement our circuit inside of the clocked_and method. The code snippet below shows how we would do this.

void Example::clocked_and () {
  q.write( a.read() && b.read() );
}

We can also simulate this code on EDA playground.

This example is fairly simple to understand as we use a common C++ logical operator to implement the functionality of the and gate.

In this example, we use the read() and write() methods when we are reading or writing our module ports.

However, we don't necessarily need to use these methods as we could simply treat a, b and q as normal C++ variables. If we were to do this, we would implement the clocked_and method as shown in the example below.

void Example::clocked_and () {
  q = a && b;
}

Despite this, it is good practice to use the read() and write() methods as these methods help to ensure that our code compiles correctly.

SystemC Thread Process

We use the SC_THREAD macro to register a thread type process in our SystemC design.

When we write a thread in SystemC, all of the code executes from start to finish. In this sense, we can see that threads are similar to SystemC methods.

However, we can also pause the execution of threads using the wait() method. This is in contrast to methods that always execute from start to finish without interruption.

When we call the wait method, the execution of our thread is temporarily suspended. When one of the events in our sensitivity list occurs, our thread starts executing again.

As a result of this, we can include infinite loops inside of threads as long as they include the wait() method. This feature makes them ideal for use in SystemC testbenches.

Typically, we don't use threads inside of synthesizable code as methods behave more similarly to real hardware.

Thread Example

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

For this example, we will write a basic test for the circuit shown below. This is the same circuit that we modeled using a SystemC method in the previous section.

A circuit diagram showing a two input and gate with the output of the and gate being an input to a D type flip flop

The first thing we have to do is declare a module that will contain our test code. In order to connect to the circuit which we are testing, our module will have 2 outputs and one input as well as a clock signal.

We will also need to declare a single thread type process in our module which we will use to implement our test.

Finally, we will also need to use the SC_CTOR macro to declare the constructor for this method.

The SystemC code below shows how we would declare this module.

struct ExampleStim : public sc_module {
  // Module IO
  sc_in<bool> clk;
  sc_out<bool> a;
  sc_out<bool> b;
  sc_in<bool> q; 
  // Declaration of our thread process
  void stim ();
  // Constructor declaration
  SC_CTOR(ExampleStim);
};

After declaring our SystemC module, we then need to register the stim method as a thread. We do this in the module constructor.

As we are testing a circuit that has a rising edge triggered flip flop, we will update the inputs on every falling edge of the clock. As a result of this, we will need to declare that our thread is sensitive to the falling edge of the clock.

The code snippet below shows how we would register this thread in SystemC.

ExampleStim::ExampleStim (sc_module_name name) : sc_module(name) {
  // Register a thread in SystemC
  SC_THREAD(stim);
  sensitive << clk.neg();
} 

We can now implement the test inside of the stim thread.

In order to test this circuit, we generate each of the 4 possible inputs combinations to the and gate. We then wait for one clock period to ensure that the outputs of the circuit are correct.

The code snippet below shows how we would implement this in SystemC.

void stim () {   
  a.write(false);
  b.write(false);
  wait();
  a.write(true);
  wait();
  a.write(false);
  b.write(true);
  wait();
  a.write(true);
  wait();
};

We can also simulate this code on EDA playground.

We can see from this example how we use the wait method inside of threads. Whenever we call this method, our thread is suspended.

When one of the events in our sensitivity list occurs, our thread starts executing again.

In this case, we only have one signal in our sensitivity list so our thread is suspended until there is another falling edge on the clock signal.

SystemC Clocked Thread Process

We use the SC_CTHREAD macro to register a clocked thread type process in our SystemC design.

When we write a clocked thread in SystemC, all of the code executes from start to finish. In addition to this, we can also pause the execution of clocked threads by calling the wait method().

As we can see from this, clocked threads are very similar to normal threads in SystemC.

In fact, we can think of clocked threads as being a special case of threads. The only difference is that clocked threads are sensitive to a single event whereas threads can be sensitive to multiple events.

Although we can still use clocked threads, they are deprecated in later versions (2.0 or later) of SystemC. As a result of this, we should generally use threads instead of clocked threads.

We can include infinite loops inside of clocked threads as long as they include the wait() method. This feature makes them ideal for use in SystemC testbenches.

Typically, we don't use clocked threads inside of synthesizable code as methods behave more similarly to real hardware.

Clocked Thread Example

To better demonstrate how we use clocked threads in SystemC, let's consider a simple example.

For this example, we will write a clocked thread that monitors the inputs and outputs of the circuit below. We typically use processes such as this in SystemC testbenches.

A circuit diagram showing a two input and gate with the output of the and gate being an input to a D type flip flop

The first step which we take is to declare a module that will contain our clocked thread. In order to connect to the circuit we are testing, our module has a total of 2 outputs and 2 inputs.

We will also need to declare a single clocked thread type process in our module.

Finally, we will also need to use the SC_CTOR macro to declare the constructor for this method.

The SystemC code below shows how we would declare this module.

struct ExampleStim : public sc_module {
  // Module IO
  sc_in<bool> clk;
  sc_out<bool> a;
  sc_out<bool> b;
  sc_in<bool> q; 
  // Declaration of our thread process
  void monitor ();
  // Constructor declaration
  SC_CTOR(ExampleStim);
};

After declaring our SystemC module, we then need to register the monitor method as a clocked thread. We do this in the module constructor.

As we are testing a circuit that has a rising edge triggered flip flop, we will monitor the state of the circuit on the falling edge of the clock. By doing this, we allow some time for the circuit to settle into a steady state following a change of inputs.

As a result of this, we will need to declare that our clocked thread is sensitive to the falling edge of the clock.

The code snippet below shows how we would register this thread in SystemC.

ExampleStim::ExampleStim (sc_module_name name) : sc_module(name) {
  // Register a clocked thread in SystemC
  SC_CTHREAD(monitor, clk.neg());
}   

From this example, we can see how the syntax for declaring a clocked thread is much more succinct than a normal thread. We simply add an extra argument to the macro when we register the process.

However, we can only use clocked threads when we are writing a process that is sensitive to a single signal.

Now that we have registered our clocked thread, we can write the code for our monitor function. This code is actually very simple.

Firstly, we use a standard C++ command to print the state of the module's inputs and outputs.

After this, we call the wait() method which suspends the execution of the thread until a negative clock edge is detected.

We use an infinite loop to ensure that this thread executes continuously whilst our simulation is running.

The code snippet below shows how we would implement this using SystemC.

  void monitor() {
    while (true) {
      std::cout << "a = " << a.read();
      std::cout << " b = " << b.read();
      std::cout << " q = " << q.read() << " ";
      std::cout << sc_time_stamp() << std::endl;
      wait();
    }
  }

We can also simulate this example on EDA playground.

Exercises

What is the difference between a C++ method and a SystemC process?

SystemC processes execute concurrently whereas methods execute sequentially.

What do we use sensitivity lists for in SystemC processes?

We use sensitivity lists to define a list of signals that a process will wait on before resuming the execution of code.

Name the three macros we use to declare a method, a thread and a clocked thread.

We use the SC_METHOD macro to declare a method, the SC_THREAD macro to declare a thread and the SC_CTHREAD macro to declare a clocked thread.

What is the difference between a thread and a clocked thread?

Which process(es) do we typically use in synthesizable SystemC code? Which process(es) do we typically use in SystemC testbenches?

We normally use method types processes to write synthesizable SystemC code and threads or clocked threads to write SystemC testbenches.

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