An Introduction to SystemC Primitive Channels

By John
June 2, 2022

In this post, we talk about the different types of primitive channels that we can use in SystemC. This includes a discussion, as well as examples, of the most popular primitive channels in SystemC - sc_fifo, sc_mutex and sc_semaphore.

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 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.

The sc_port Class

In a previous post in this series, we talked about how we use SystemC ports to declare the inputs and outputs of a module.

In this post, we looked at how we use the sc_in and sc_out classes to create basic ports. These classes allow us to move basic data between different modules.

However, we often require much more complex data structures in our SystemC test benches.

In this case, we can use the sc_port class to declare module inputs and outputs which use more complex data structures.

In fact, every port that we declare in SystemC must use the sc_port class. Even the sc_in and sc_out classes which we previously discussed are both inherited from this class.

We use the syntax below in order to declare a port using the sc_port class in SystemC.

sc_port<<IF_TYPE>, <N>, <P>> <port_name>;

The sc_port class is implemented as a generic C++ class. As a result of this, we need to pass some parameters to the sc_port class when we declare an instance of it.

In this construct, we use the <IF_TYPE> parameter to declare which type of interface our sc_port instance uses. We discuss this topic in more detail later in this post.

Defining Port Connections

We use the <N> and <P> parameters together to determine how many connections our port can have.

We use the <N> parameter to define the number of channels which can be connected to a port.

However, the way that the <N> parameter is interpreted is determined by the port policy.

We use the <P> parameter to define the port policy. This us an enumerated type that can take any of the values shown in the table below.

EnumerationEffect
SC_ONE_OR_MORE_BOUNDWe can connect the port to one or more channels.
SC_ZERO_OR_MORE_BOUNDWe can connect the port to zero or more channels.
SC_ALL_BOUNDWe can connect the port to exactly <N> channels.

When we set the port policy to either SC_ONE_OR_MORE_BOUND or SC_ZERO_OR_MORE_BOUND, the <N> parameter determines the maximum number of connections the port can have.

For example, if we set the value of <N> to 2 then we can connect the declared port to a maximum of 2 channels in our design.

When we set the port the port policy to SC_ALL_BOUND, we use the <N> parameter to determine the exact number of channels which can be connected to our port.

For example, if we set the values of <N> to 3 then we must connect exactly 3 other channels to the port.

By default, the <N> parameter is set to 1 and the <P> parameter is set to SC_ALL_BOUND. As a result, we have to connect the port to exactly one channel if we omit these parameters from our declaration.

In the next section, we look at some of the basic, pre-defined channels which we can use with the sc_port class.

In the next post in this series, we look at more complex hierarchical channels.

SystemC Primitive Channels

As we discussed in the previous section, when we create an instance of the sc_port class we have to associate an interface type with it.

To help with this, the SystemC libraries includes an abstract class called sc_prim_channel.

We can use this class to define an interface which can be associated with an instance of the sc_port class.

As this is an abstract class, we have to derive a class from it which we can then pass to the sc_port class.

However, we often use one of the predefined SystemC primitive channels rather than creating our own channel.

The SystemC libraries include three different channels which are derived from the sc_prim_channel class - sc_fifo, sc_mutex and sc_semaphore.

Let's take a closer look at each of these channels.

The sc_fifo Primitive Channel

The sc_fifo is one of the most basic types of primitive channels in SystemC. We use the sc_fifo channel to model basic a FIFO type interface .

The code snippet below shows the general syntax we use to declare an sc_fifo object in SystemC.

sc_fifo<<T>> <name> (<depth>);

As we can seee, the sc_fifo class is implemented as a generic C++ class. As a result of this, we need to pass a type to the sc_fifo class when we declare an instance of it.

In the example shown above, we use the <T> parameter to pass a type to the sc_fifo class.

In addition to this, we can also set the <depth> argument to declare the number of entries in the FIFO. By default, this value is set to 16.

When we want to use the sc_fifo in a port declaration, we use either the sc_fifo_out_if (for outputs) or the sc_fifo_in_if (for inputs).

The syntax we use to declare an instance of these classes is the same as the sc_fifo class, as shown in the code snippet below.

// General syntax to declare a sc_fifo_in_if 
sc_fifo_in_if<<T>> <name> (<depth>);

// General syntax to declare a sc_fifo_in_if 
sc_fifo_out_if<<T>> <name> (<depth>);

In order to work with the sc_fifo class, we use various inbuilt methods to access the contents of the FIFO.

We can use these methods to either read or write the FIFO data.

The table below summarizes the different methods which we can use with the sc_fifo class.

MethodDescription
read(x)We use this method to read data from the FIFO and assign to the variable x. If no data is in the FIFO, this method blocks until data is available.
nb_read(x)Non-blocking version of read. This method returns false when there is no data in the FIFO.
write(x)We use this method to write the variable c into the FIFO. If the FIFO is full, this method blocks until there is free space.
nb_write(x)Non-blocking version of read. This method returns false when the FIFO is full, otherwise true;

The code snippet below shows how we use each of the methods in practise.

// Create an instance of the sc_fifo class 
sc_fifo<int> fifo_example;

// Read data from the fifo and assign it to the variable x
fifo_example.read(x);

// Non-blocking read from FIFO
fifo_example.nb_read(x);

// Write the value of variable x into the FIFO
fifo_example.write(x);

// Non-blocking 
fifo_example.nb_write(x);

sc_fifo Example

To better demonstrate how the sc_fifo primitive channel works, let's consider a basic example.

For this example, we will write 2 different modules which are then connected to each other using the sc_fifo type primitive channel.

We will use one of the modules to generate a stream of data which it then transmits into the FIFO channel.

In the second module, we receive the data from the FIFO and then simply print it to the console screen.

The full code for this example is available on EDA playground where it can also be simulated.

Transmitter Module

In order to transmit packets, we declare that one of the outputs uses the type sc_fifo_out_if.

As we discussed in the previous section, we also have to pass a type to the sc_fifo_out_if class when we declare it. For this example, we will use char type data with the sc_fifo_out_if port.

The code snippet below shows how we declare this port in SystemC.

sc_port<sc_fifo_out_if<char> > fifo_out;

In order to transmit packets, we use a process inside of the transmitter module which is sensitive to the positive edge of the clock.

We then call the sc_fifo write method inside of this process to transmit data to the receiver module.

The code snippet below shows the full code for this process.

void Transmitter::transmit () {
  const char *ptr = "Sending Data";
  while (*ptr) {
    wait();
    fifo_out->write(*ptr++);
  }
}
Receiver Module

In the receiver module, we declare that one of the inputs uses the type sc_fifo_in_if. This allows us to receive data from the FIFO which we previously declared in the transmitter module.

As with the sc_fifo_out_if type, we also have to pass a type to the sc_fifo_in_if class when we declare it. Again, we will use char type data for this.

The code snippet below shows how we declare this port in SystemC.

sc_port<sc_fifo_in_if<char> > fifo_in;

We also include a single process in the receiver module which is sensitive to the rising edge of the clock.

We then call the sc_fifo nb_read method inside of our process to receive data from the transmitter module.

The code snippet below shows the full code for this process.

void Receiver::receive () {
  while (true) {
    wait();
    c = '?';
    fifo_in->nb_read(c);
    std::cout << c;
  }
}

The sc_mutex Primitive Channel

We use sc_mutex primitive channels to model basic mutually exclusive locks in SystemC.

The sc_mutex channel is intended to be used in situations where we have two concurrent processes that can access a shared resource.

In this case, a shared resource could be another process or a variable.

We use the sc_mutex channel in order to ensure that the shared resource can only be accessed by one process as a time. This helps us to prevent non-deterministic behavior in our code.

Typically, we use the sc_mutex primitive channel for communication between processes in a module. However, we can also use the sc_mutex_if class when we need to include a mutex channel in our port list.

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

// Create a sc_mutex object
sc_mutex <name>;

// Create a sc_mutex_if object
sc_mutex_if <name>;

In order to work with the sc_mutex class, we use various inbuilt methods to lock and unlock the mutex.

The table below summarizes the different methods which we can use with the sc_mutex class.

MethodDescription
lock(x)We use this method to lock the mutex. If the mutex is already locked by another process, this method blocks until it is free.
trylock(x)Non-blocking version of the lock method. This method returns false when the mutex is locked.
unlock(x)We use this method to unlock the mutex once we have finished with it.

The code snippet below shows how we use these methods in praticse

// Create an instance of the sc_mutex class
sc_mutex mutex_example;

// Lock the mutex
mutex_example.lock();

// Lock the mutex (non-blocking)
mutex_example.trylock();

// Unlock the mutex
mutex_example.unlock();

sc_mutex Example

To better demonstrate how the sc_mutex primitive channel works, let's consider a basic example.

For this example, we will write a single module which contains two processes. We uses these processes to write random data to a variable inside our module.

However, we will wait for a random amount of time before we write data to this variable.

By doing this, we can see how only one of the processes can lock the mutex at a time.

When we declare the module for this example, we create 2 thread type processes that are sensitive to the rising edge of the clock.

When the process is triggered, we firstly try to lock the mutex using the trylock method that we discussed in the previous section.

If we can't lock the mutex then our process will simply wait for the next rising clock edge.

However, if the process is able to lock the mutex, we can use the wait statement to pause execution of our thread for a random amount of time.

Once our thread starts executing again, we then update the value of the variable. Again, we will simply write random data to our variable.

Finally, we then release the mutex so that it can be used by other processes as required.

The code snippet below shows the full code for one of our processes. The code for the other process is identical to this except we shall give it the name write_data2.

void MutexExample::write_data1() { 
  while (true) {
    // Attempt to lock the mutex
    if ( var_lock.trylock() > -1 ) {
      std::cout << sc_time_stamp() << " : write_data1 locked mutex" << std::endl;

      // Wait some random time and then write data to the variable
      wait(sc_time(std::rand() % 10, SC_NS));
      update_var(); 

      // Release the mutex
      var_lock.unlock();
      std::cout << sc_time_stamp() << " : write_data1 released mutex" << std::endl;
    }
    
    // Wait until the next clock edge
    wait();
  }
}

We can also simulate this example on EDA playground which results in the output shown below.

0 s : write_data1 locked mutex
3 ns : var set to 846930886
3 ns : write_data1 released mutex
10 ns : write_data2 locked mutex
17 ns : var set to 1714636915
17 ns : write_data2 released mutex
20 ns : write_data2 locked mutex
23 ns : var set to 424238335
23 ns : write_data2 released mutex

As we can see from this example, when we use a mutex in this way only one of the processes is able to access the variable per clock cycle (10ns).

The sc_semaphore Primitive Channel

We use sc_semaphore primitive channels to model basic software type sempahores in SystemC.

As with the sc_mutex channel, we use the sc_sempahore channel to help us manage access to a shared resouce.

We use the sc_semaphore to keep a count of the number of processes which are currently using a shared resource.

Once the sempahore reaches a predefined value, it will prevent any other processes from accessing our shared resouce.

Typically, we use the sc_semaphore primitive channel for communication between processes in a module. However, we can also use the sc_semaphore_if class when we need to include a semaphore in our port list.

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

// Declare an instance of the sc_semaphore class
sc_semaphore <name>;

// Declare an instance of the sc_semaphore_if class
sc_semaphore_if <name>;

After we create an instance of the sc_semaphore class, we then have to assign it an initial value.

Each time we access the semaphore, the value is decremented by 1. Once the semaphore count reaches zero, the semaphore will prevent further access to the shared resources.

When a process finishes using the shared resource, the semaphore count is incremented by 1.

In order to work with the sc_semaphore class, we use various inbuilt methods to increment and decrement the semaphore count.

The table below summarizes the different methods which we can use with the sc_semaphore class.

MethodDescription
wait(x)We use this method to pause execution of a process until the semaphore count is greater than 1.
trywait(x)Non-blocking version of the wait method. Returns false if the semaphore count is 0.
postWe use this method to increment the semaphore count after we have finished with it.

The code snippet below shows how we use each of these methods in practise.

// Decrement the semaphore count by 1
semaphore_example.wait();

// Decrement the semaphore count by 1 (non-blocking)
semaphore_example.trywait();

// Increment the semaphore count by 1
semaphore_example.post();

sc_semaphore Example

To better demonstrate how the sc_semaphore primitive channel works, let's consider a basic example.

For this example, we will write a module that contains a total of 3 processes. We simply use these processes to decrement a semaphore count and then display its current value.

However, we will wait for a random amount of time before releasing the semaphore and incrementing the count.

We will initialize the sc_semaphore channel in our module to the value of 2.

By doing this, we can see how only one 2 of the processes can lock the semaphore object at any given time.

When we declare the module for this example, we create 3 thread type processes that are sensitive to the rising edge of the clock.

When the process is triggered, we firstly try to decrement the semaphore count using the trywait method that we discussed in the previous section.

If the semaphore count is already at 0 then we will simply wait for the next rising clock edge.

However, if the semaphore count is greater than 0, we display the current semaphore count.

We can then use the wait statement to pause execution of our thread for a random amount of time.

Once our thread starts executing again, we simply increment the semaphore count again using the post method we discussed above.

Finally, we then display the new semaphore count.

The code snippet below shows the full code for one of our process. The code for the other processes is identical to this except that they are named thread2 and thread3.

void SemaphoreExample::thread1() {
  while (true) {
    if (sem.trywait() > -1) {
      std::cout << sc_time_stamp() << " : thread1 got semaphore " << std::endl;
      std::cout << sc_time_stamp() << " : semaphore count = " << sem.get_value() << std::endl;
      
      // Wait for some random time
      wait(sc_time(std::rand() % 10, SC_NS));
      
      // Release the semaphore
      std::cout << sc_time_stamp() << " : thread1 released semaphore " << std::endl;
      sem.post();
      std::cout << sc_time_stamp() << " : semaphore count = " << sem.get_value() << std::endl;
    }

    wait();
  }
}

We can also simulate this example on EDA playground which results in the output shown below.

0 s : semaphore count = 1
0 s : thread2 got semaphore 
0 s : semaphore count = 0
3 ns : thread1 released semaphore 
3 ns : semaphore count = 1
6 ns : thread2 released semaphore 
6 ns : semaphore count = 2
10 ns : thread3 got semaphore 
10 ns : semaphore count = 1
10 ns : thread2 got semaphore 
10 ns : semaphore count = 0
15 ns : thread2 released semaphore 
15 ns : semaphore count = 1
17 ns : thread3 released semaphore 
17 ns : semaphore count = 2

This example shows how only 2 of our 3 processes can ever access the semaphore. It also demonstrates how we use the wait and post methods to decrement and increment the semaphore count respectively.

Exercises

How do we control the number of channels which can be connected to a port?

We use the port policy parameter together with the <N> parameter to control how many channels can be connected to a port.

By default, how many channels can we connect to a port?

We must connect exactly 1 channel to a port by default.

Name the three primitive channels that we can use in SystemC.

We can use the sc_fifo, sc_mutex and sc_semaphore primitve channels in SystemC.

What is the difference between the sc_fifo_if_in, sc_fifo_if_out and sc_fifo channels?

We use the sc_fifo for channels which are internal to a module whereas we use the sc_fifo_if_in and sc_fifo_if_out channels for module ports. We use the sc_fifo_if_in for input ports and the sc_fifo_if_out channel for output ports.

Which class do we use if we want to use the sc_mutex or sc_semaphore channels in a port?

We use the sc_mutex_if class when we want to use the sc_mutex in a port and the sc_semaphore_if class when we want to use the sc_semaphore in a port.

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