Learning SystemC: #003 Time, Events and Processes

Learning SystemC: #003 Time, Events and Processes

Posted by

In this post I will talk about how time and events are modeled in SystemC and how these two elements are used in SystemC processes.
Here is a list of content if you want to jump to a particular subject:

1. Time Class – sc_time
2. Event Class – sc_event
   2.1. How to Trigger Events?
      2.1.1. Immediate Notification
      2.1.2. Delta Notification
      2.1.3. Timed Notification
   2.2. How to Wait for Events and Time?
3. Processes
   3.1. Thread – SC_THREAD
      3.1.1. Dynamic Sensitivity of Threads
      3.1.2. Static Sensitivity of Threads
   3.2. Method- SC_METHOD
      3.2.1. Dynamic Sensitivity of Methods
      3.2.2. Static Sensitivity of Methods



1. Time Class – sc_time

In order to model the time SystemC has a class called sc_time. To represent a time value we need two elements:

The enumerated type sc_time_unit for the unit of time declares the following values:

Declaring a time variable and initialing it is pretty easy:

1
sc_time clock_length(10, SC_NS);

 
 FYI: Even though we specify the unit of time at initialization, looking at the constructor implementation reveals that the actual unit value is only used to transform the numerical value into femtoseconds.
 
There are several operator overloads including stream operator so you can do basic arithmetics, comparisons and printing with sc_time class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sc_time clock_1_length(10, SC_NS);
 
//stream operator is overloaded so you will get nice prints
cout << "clock #1 length: " << clock_1_length << endl;
 
//you can perform multiplications on the time
sc_time clock_2_length = clock_1_length * 2;
 
cout << "clock #2 length: " << clock_2_length << endl;

//you can perform comparisons
if(clock_2_length > clock_1_length) {
  cout << "clock #2 is slower than clock #1" << endl;
}

And the output of this example is:

1
2
3
clock #1 length: 10 ns
clock #2 length: 20 ns
clock #2 is slower than clock #1

 

2. Event Class – sc_event

One of the big advantage that SystemC brings in the C++ world is the possibility to run discrete event simulations. This imply that your SystemC program/model will advance from one state to another only when some particular events “happen”. Some examples of events from digital electronics are the rising edge of the clock signal, push and pop actions from a FIFO and so on.

In SystemC an event is modeled with sc_event class.
Declaration of an event is straightforward:

1
2
//declare an event for notifying an "increment" action
sc_event inc_ev;

There are only two actions that you can do with an event:

  • you can trigger an event via different implementations of a notify() function
  • you can wait for an event via different implementations of a wait() function

One important aspect of sc_event class is that it contains no history, current value or any other information that might help you determine that an event happened or is happening right now.
 This means that if you want to detect an event you must begin waiting for it before it happen.

2.1. How to Trigger Events?

sc_event class implements several variants of notify() function in order to trigger events:

2.1.1. Immediate Notification

Triggering an immediate notification makes a process which was waiting for that event to advance immediately.
This immediately is a relative term and it has to do with how processes are scheduled in a discrete event simulator like SystemC.

1
2
//trigger an immediate notification
my_event_ev.notify();

2.1.2. Delta Notification

Triggering a delta notification makes a process which was waiting for that event to advance once all currently running processes reached a wait() function or finished.

1
2
//trigger a delta notification
my_event_ev.notify(SC_ZERO_TIME);

2.1.3. Timed Notification

Triggering a timed notification makes a process which was waiting for that event to advance after the specified simulation time passed.

1
2
//trigger a timed notification
my_event_ev.notify(5, SC_NS);

One very important aspect regarding timed notifications:

You can only schedule one timed notification. If more than one timed notifications are scheduled only the nearest will be executed and the rest are ignored.

In the example below we have three statements one after the other. Each of those want to trigger event my_event_ev with different delays.

1
2
3
4
5
my_event_ev.notify(5, SC_NS);

my_event_ev.notify(2, SC_NS);

my_event_ev.notify(3, SC_NS);

In the example from above event my_event_ev will be triggered only once after 2ns as this is the nearest scheduled timed notification. This also apply to delta notifications as these are only timed notifications with time 0.
One option that you have in this case is to call cancel() function of the event which will cancel any previous scheduled timed notification.

2.2. How to Wait for Events and Time?

In sc_module class there are quite a few implementations of the wait() function in which you can specify to wait for some time or for some events.

These are called dynamic sensitivity for SC_THREADs and SC_CTHREADs because in these type of processes you can call these wait() functions to wait for an event or time independent of the sensitivity list of a process. We will discuss more on the subject in chapter 3.1.1. Dynamic Sensitivity of Threads.

In the example below there are two processes, one called watcher_thread() which waits for events or time and another process called trigger_thread() which generates events.

1
2
3
4
5
6
7
8
9
10
11
wait(my_event_1_ev);
cout << "[" << sc_time_stamp() << "] detected wait(my_event_1_ev)" << endl;
 
wait(my_event_2_ev | my_event_3_ev);
cout << "[" << sc_time_stamp() << "] detected wait(my_event_2_ev | my_event_3_ev)" << endl;
 
wait(my_event_4_ev & my_event_5_ev);
cout << "[" << sc_time_stamp() << "] detected wait(my_event_4_ev & my_event_5_ev)" << endl;
 
wait(sc_time(5, SC_NS), my_event_6_ev);
cout << "[" << sc_time_stamp() << "] detected wait(sc_time(5, SC_NS), my_event_6_ev)" << endl;

Process watcher_thread()

In the above code there are calls to different implementation of the wait() function. The sc_time_stamp() is an utility function which returns the current time of the simulation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//this is required in order to allow the process watcher_thread() to start
//waiting for my_event_1_ev event
wait(SC_ZERO_TIME);
   
//trigger an immediate notification
my_event_1_ev.notify();
cout << "[" << sc_time_stamp() << "] called my_event_1_ev.notify()" << endl;
 
//trigger a timed notification
my_event_2_ev.notify(5, SC_NS);
cout << "[" << sc_time_stamp() << "] called my_event_2_ev.notify(5, SC_NS)" << endl;
 
//wait some time so that event my_event_2_ev is triggered
wait(10, SC_NS);
 
//trigger both events in the same time so satisfy the wait from the watcher_thread() which waits for both
my_event_4_ev.notify();
cout << "[" << sc_time_stamp() << "] called my_event_4_ev.notify()" << endl;
my_event_5_ev.notify();
cout << "[" << sc_time_stamp() << "] called my_event_5_ev.notify()" << endl;
 
//wait some time so that the timeout from watcher_thread() will expire
wait(20, SC_NS);

Process trigger_thread()

The output of this code is:

1
2
3
4
5
6
7
8
[0 s] called my_event_1_ev.notify()
[0 s] called my_event_2_ev.notify(5, SC_NS)
[0 s] detected wait(my_event_1_ev)
[5 ns] detected wait(my_event_2_ev | my_event_3_ev)
[10 ns] called my_event_4_ev.notify()
[10 ns] called my_event_5_ev.notify()
[10 ns] detected wait(my_event_4_ev & my_event_5_ev)
[15 ns] detected wait(sc_time(5, SC_NS), my_event_6_ev)

There are a few important notes to be remembered here:

  • the initial delta delay from trigger_thread()  process is necessary in order to allow watcher_thread()  to start waiting for my_event_1_ev
  • event my_event_2_ev  was triggered with a timed notification at time 0ns and was detected only after the 5ns passed
  • even if my_event_3_ev  was never triggered the wait() function which was watching for my_event_2_ev  OR my_event_3_ev  was passed due to its OR statement
  • even if my_event_6_ev  was never triggered the wait() function which was watching for it was passed due to the 5ns timeout

You can also run this example on EDA Playground

3. Processes

Unlike a regular C++ program where you have one execution thread, in SystemC you can easily create multiple threads which run in parallel. This parallelism is accomplished with SystemC processes, which are no more than regular C++ functions but handled in the background by the SystemC kernel. The parallelism is in fact an illusion as at any given moment in time there is only one line of code being executed but SystemC does a pretty good job in mimicking parallelism.

Here is a very simplified version of what SystemC is doing.
Imagine that there are three pools in which to store at some point the processes which are executing in your SystemC program:

  • READY pool – for storing processes which are ready to execute lines of their code
  • RUNNING pool – for storing the currently executing process
  • WAITING pool – for storing processes which are waiting for some events to be triggered or some time to pass

The rules for moving processes between these pools are the following:

  1. a RUNNING process moves to WAITING pool when it hits a call to wait() function
  2. when the RUNNING pool is empty and there are still processes in READY pool SystemC “randomly” chooses a READY process to move it to RUNNING pool
  3. if a RUNNING process executes an immediate notification all the WAITING processes watching for that event are moved to READY pool
  4. if there are no more processes in READY or RUNNING pools all the WAITING processes which were waiting for events triggered in current simulation time or waiting for a timeout which just expired are moved to READY pool

Here is a short animation showing these rules in action:
[vc_video link=”https://youtu.be/a7axM9FhUAc”]
Now that we have a better understanding on how the processes are handled we can move on and see the types of processes available in SystemC.

3.1. Thread – SC_THREAD

A thread is a process which is started only once by the kernel at the beginning of the simulation. Once it is finished it is never called again so in most of the cases you probably want to make it an infinite loop.

Registering a function as being a thread is done via SC_THREAD macro in the constructor of the enclosing module.

1
2
3
4
5
6
7
8
9
SC_MODULE(event_example) {

   //thread which will trigger the events
   void trigger_thread();
     
   SC_CTOR(event_example) {
      SC_THREAD(trigger_thread);
   };
};

Most likely you will have in your SystemC program multiple threads which need to run in parallel. This means that each thread must suspend itself to allow others to enter in RUNNING pool.
This is accomplished by a call to wait() function, obviously to wait for some event(s) and/or time.
There are a lot of implementations of wait() function but these implementations can be split in two groups which we will discuss in the next two sections.

3.1.1. Dynamic Sensitivity of Threads

Threads can move to WAITING pool when you call a wait() function with some arguments in which you specify event(s) and/or time. A full list of these functions is listed in chapter 2.2. How to Wait for Events and Time?.

This represents the dynamic sensitivity of a thread because you dynamically specify what to wait for.

Throughout this tutorial there were a lot of examples on the usage of these wait() functions so there is no point in going through them again now.

3.1.2. Static Sensitivity of Threads

An other option for moving a thread to the WAITING pool is to call wait() function with no arguments. This has the effect of moving the thread from WAITING to READY pool when events from the thread static sensitivity list are triggered.

Each thread can have a list of events called static sensitivity. This is specified during elaboration when the thread is registered and it can not be changed afterwards. This is why it is called static.

This static sensitivity list of events is easily declared using the sensitive member of type sc_sensitive from sc_module class.
The syntax is this:

1
sensitive << event1 [<< event2];

When you add multiple events a call to wait() will be satisfied when at least one event is triggered (OR function between them).

Let’s see a very simple example on the usage of static sensitivity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SC_MODULE(event_example) {
     
   sc_event my_event_ev;
     
   //thread which will trigger the event
   void trigger_thread();
     
   //thread which will watch for the event
   void watcher_thread();
     
   SC_CTOR(event_example) {
      SC_THREAD(watcher_thread);

      //assign the static sensitivity for the last process declared
      sensitive << my_event_ev;
       
      SC_THREAD(trigger_thread);
   };
};

The implementation of those threads is quite simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void event_example::trigger_thread() {
  wait(SC_ZERO_TIME);
   
  my_event_ev.notify();
  cout << "[" << sc_time_stamp() << "] called my_event_ev.notify()" << endl;
 
  wait(10, SC_NS);
 
  my_event_ev.notify();
  cout << "[" << sc_time_stamp() << "] called my_event_ev.notify()" << endl;
 
  wait(10, SC_NS);
 
};
   
void event_example::watcher_thread() {
  cout << "[" << sc_time_stamp() << "] watcher_thread(): start" << endl;
 
  for(;;) {
    cout << "[" << sc_time_stamp() << "] watcher_thread(): before wait()" << endl;
    wait();
    cout << "[" << sc_time_stamp() << "] watcher_thread(): after wait()" << endl;
  }
};

The output of this example is this one:

1
2
3
4
5
6
7
8
[0 s] watcher_thread(): start
[0 s] watcher_thread(): before wait()
[0 s] trigger_thread(): my_event_ev.notify()
[0 s] watcher_thread(): after wait()
[0 s] watcher_thread(): before wait()
[10 ns] trigger_thread(): my_event_ev.notify()
[10 ns] watcher_thread(): after wait()
[10 ns] watcher_thread(): before wait()

Notice that all messages from watcher_thread() thread printed after the wait() are displayed after the event is triggered.

3.2. Method – SC_METHOD

SystemC offers another type of process called method. The SystemC method is a regular C++ function but, as with threads, the SystemC kernel is responsible for calling it.
The SystemC method has several properties which differentiate it from the SystemC thread. These properties are:

  • a SystemC method can not suspend itself by calling wait() function – doing this will result in a runtime error
  • a SystemC method is called by the kernel every time an event from its static or dynamic sensitivity list is triggered

Registering a function as being a SystemC method is done using SC_METHOD macro:

1
2
3
4
5
6
7
8
9
SC_MODULE(example) {

   //method which will watch for the event
   void watcher_method();
     
   SC_CTOR(example) {
      SC_METHOD(watcher_method);
   };
};
3.2.1. Dynamic Sensitivity of Methods

As I stated in the previous section, a SystemC method can not invoke wait() function as you can do in a SystemC thread to configure the next event on which the method is called.
However, it is still possible to dynamically control what is the next event on which a SystemC method is called. This is accomplished by using different implementations of next_trigger() function. These implementations are very similar with the wait() implementations.

Let’s change a little bit the example from chapter 3.1.2. Static Sensitivity of Threads in order to implement the watcher as a SystemC method:

1
2
3
4
5
void example::watcher_method() {
  cout << "[" << sc_time_stamp() << "] watcher_method(): called" << endl;
 
  next_trigger(my_event_ev);
};

You can view and run the full example on EDA Playground.

This looks much simpler than watcher_thread() implementation, right?
The output of the program is this:

1
2
3
4
5
[0 s] watcher_method(): called
[0 s] trigger_thread(): my_event_ev.notify()
[0 s] watcher_method(): called
[10 ns] trigger_thread(): my_event_ev.notify()
[10 ns] watcher_method(): called
3.2.2. Static Sensitivity of Methods

Specifying a static sensitivity list for a SystemC method is done in the same way as for threads.

1
2
3
4
5
6
7
8
9
10
SC_MODULE(example) {
     
   //method which will watch for the event
   void watcher_method();
     
   SC_CTOR(example) {
      SC_METHOD(watcher_method);
      sensitive << my_event_ev;
    };
};

Now, in the declaration of the watcher_method() method we do not need to specify what is the next trigger:

1
2
3
void example::watcher_method() {
  cout << "[" << sc_time_stamp() << "] watcher_method(): called" << endl;
};

The output of this example is exactly as in the previous section:

1
2
3
4
5
[0 s] watcher_method(): called
[0 s] trigger_thread(): my_event_ev.notify()
[0 s] watcher_method(): called
[10 ns] trigger_thread(): my_event_ev.notify()
[10 ns] watcher_method(): called

One very important aspect to be observed in this output is the fact that the output message from watcher_method() is printed three times even if my_event_ev is triggered only two times. How can this be?
The explication is very simple: SystemC calls all its methods on the events from their sensitivity list but also at the beginning of the simulation so the user will get a chance to initialize them (at least their dynamic sensitivity list).
You can suppress this behavior by calling dont_initialize() function. Please take note that this makes sense only when you specify a static sensitivity list (otherwise the method will never be called).

1
2
3
4
5
6
7
8
SC_MODULE(example) {
   
   SC_CTOR(example) {
       SC_METHOD(watcher_method);
       sensitive << my_event_ev;
       dont_initialize();
    };
};

One last important thing to mention. You can temporarily override the static sensitivity list by calling any next_trigger() function presented in 3.2.1. Dynamic Sensitivity of Methods. In such a case, if you want to return to the static sensitivity list use next_trigger() function with no arguments.

Hope you found this lesson useful 🙂

Next Lesson: Learning SystemC: #004 Primitive Channels
Previous Lesson: Learning SystemC: #002 Module – sc_module



Cristian Slav

3 Comments

  • pagels@gmail.com' ElektroKalle says:

    If I want my thread alpha to notify event beta every 10 ms. And thread beta to notify event beta every 5ms.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void Modulename::Alpha_thread(){
       for(;;){
          wait(SC_ZERO_TIME);
          A.notify(10,SC_MS);
       }
    }

    void Modulename::Beta_thread(){
       for(;;){
         wait(SC_ZERO_TIME);
         B.notify(8,SC_MS);
       }
    }

    Why wont it work? I got a method and “next_trigger(Alpha);” and I would expect it to trigger every 10 ms. Is that wrong?

    • Hi,

      the problem is that your “for” loop is an infinite loop at time 0 – you wait 0 time and schedule the event notification later. This means that in your simulation time will not advance.
      You should change your code like this:

      1
      2
      3
      4
      5
      6
      void Modulename::Alpha_thread(){
         for(;;){
            wait(10,SC_MS);
            A.notify();
         }
      }
  • sreerajchundail@gmail.com' Sreeraj Chundayil says:

    What if I call wait method inside a sc_module method, but it’s neither sc_thread nor sc_method, is it undefined behaviour?

Leave a Reply

Your email address will not be published.