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:
- a numeric value – this is of type uint64
- a time unit – this is of type sc_time_unit
The enumerated type sc_time_unit for the unit of time declares the following values:
- SC_FS – femtoseconds
- SC_PS – picoseconds
- SC_NS – nanoseconds
- SC_US – microseconds
- SC_MS – milliseconds
- SC_SEC – seconds
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:
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:
- notify() – immediate notification
- notify(const sc_time&) – delta notification (when time is 0) and timed notification (when time is not 0)
- notify(double v, sc_time_unit tu) – same as above
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.
- wait(const sc_event& e) – wait for an event
- wait(const sc_event_or_list& el) – wait for at least one of the events – example: wait(event1 | event2)
- wait(const sc_event_and_list& el) – wait for all events – example: wait(event1 & event2)
- wait(const sc_time& t) – wait for time
- wait(double v, sc_time_unit tu) – wait for time
- wait(const sc_time& t, const sc_event& e) – wait for event or timeout
- wait(double v, sc_time_unit tu, const sc_event& e) – wait for event or timeout
- wait(const sc_time& t, const sc_event_or_list& el) – wait for at least one of the events or timeout
- wait(double v, sc_time_unit tu, const sc_event_or_list& el) – wait for at least one of the events or timeout
- wait(const sc_time& t, const sc_event_and_list& el) – wait for all events or timeout
- wait(double v, sc_time_unit tu, const sc_event_and_list& el) – wait for all events or timeout
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; |
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); |
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:
- a RUNNING process moves to WAITING pool when it hits a call to wait() function
- 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
- if a RUNNING process executes an immediate notification all the WAITING processes watching for that event are moved to READY pool
- 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.
- next_trigger(const sc_event& e) – call method at event
- next_trigger(const sc_event_or_list& el) – call method at any of the events – example: next_trigger(event1 | event2)
- next_trigger(const sc_event_and_list& el) – call method when all events triggered – example: next_trigger(event1 & event2)
- next_trigger(const sc_time& t) – call method when when time passed
- next_trigger(double v, sc_time_unit tu) – call method when when time passed
- next_trigger(const sc_time& t, const sc_event& e) – call method at event or timeout
- next_trigger(double v, sc_time_unit tu, const sc_event& e) – call method at event or timeout
- next_trigger(const sc_time& t, const sc_event_or_list& el) – call method at at least one of the events or timeout
- next_trigger(double v, sc_time_unit tu, const sc_event_or_list& el) – call method at at least one of the events or timeout
- next_trigger(const sc_time& t, const sc_event_and_list& el) – call method at all events or timeout
- next_trigger(double v, sc_time_unit tu, const sc_event_and_list& el) – call method at all events or timeout
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
If I want my thread alpha to notify event beta every 10 ms. And thread beta to notify event beta every 5ms.
2
3
4
5
6
7
8
9
10
11
12
13
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:
2
3
4
5
6
for(;;){
wait(10,SC_MS);
A.notify();
}
}
What if I call wait method inside a sc_module method, but it’s neither sc_thread nor sc_method, is it undefined behaviour?