[Edit of Image1]
Hey it's a me again @drifter1!
Today we continue with the Logic Design series on SystemVerilog in order to cover Events, which are one of the three main interprocess communication mechanisms of SystemVerilog. The other two will be covered next time.
So, without further ado, let's get straight into it!
Processes often exchange data with each other, which is what is known as interprocess communication. SystemVerilog provides three main mechanisms for that purpose, that allow us to manage the control flow in processes. This is done through specific interactions between dynamic processes, that are basically synchronization techniques, but on a hardware level. They are of course only meant to be used in Testbenches for validation and verification purposes.
The mechanisms are the following:
In Verilog, events are static objects which are triggered via the -> operator and waited on via the @ operator. So, the @posedge(clk), @negedge(clk) and @(sensitivity_list) in general that we put in always blocks are also part of the event mechanism!
SystemVerilog enhances those events allowing them to be triggered for a longer duration. In Verilog, these events have no duration, whilst in SystemVerilog the trigger state perisists throughout the whole time step the event is triggered in. Additionaly, in SystemVerilog such event objects can also be used like synchronization queues, by being passed as arguments to tasks and functions.
Event objects are defined using the event keyword, and can be assigned to and compared to other event variables. When assigned to another event, both events / variables then point to the same synchronization object. It's also possible to assign a null value, which basically specifies no synchronization object.
event e1; // a new event with identifier "e1"
event e2 = e1; // "e2" and "e1" refer to the same event
event e3 = null; // "e3" has no synchronization object
Events can be triggered using the -> and ->> operators. Both of them unblock all processes which are currently waiting for the event to occur. The difference between them is about how the unblocking is executed on the calling process. The first one is of blocking nature, whilst the second one is non-blocking, which basically means that the trigger statement executes without blocking the execution of the statements after it.
Triggering event e1 can thus be done with either of the following two statements:
->e1;
->>e1;
Blocking a process until an event is triggered can be done using the @ operator, as well as a wait on the event's triggered state, which can be accessed using .triggered. The second one should be used if there is a race condition between triggering and waiting for the event, as the triggered state persists throughout the simulation time step. So, using the @ operator the specific order in which waiting and triggering happens matters, whilst using .triggered the process will unblock independent of that order.
Waiting for event e1 is thus done using either of the following statements:
@(e1);
wait(e1.triggered);
Of course, it's possible to make a process wait on multiple events. Waiting on events in a specific order can be done using wait_order(), which fails if the events are triggered out-of-order (from left-to-right in the arguments) and is basically a conditional statement.
So, if the events e1, e2 and e3 should occur in the order e1 -> e2 -> e3 for the process to unblock, the corresponding conditional statement would be:
wait_order(e1, e2, e3)
// events triggered in-order
else
// events triggered out-of-order
When an event variable is assigned to another, then all processes waiting on the first event will basically wait until the second event triggers.
For example the following definition:
event e1;
event e2 = e1;
would make any process waiting on e2 wait on e1 instead.
In that case, the following statements are basically all equal:
@(e2);
@(e1);
wait(e2.triggered);
wait(e1.triggered);
Of course, such an assignment can occur anywhere, and is not limited to the definition / declaration section, which allows for some very advanced synchronization!
Events can also be passed to functions, tasks or queues as arguments. This basically makes the task or function point to the corresponding synchronization object, allowing it to be used (trigger or wait) within the task or function.
Block diagrams and other visualizations were made using draw.io
And this is actually it for today's post!
Next time we will cover Semaphores and Mailboxes...
See Ya!

Keep on drifting!