Events and persistent state
Use an event to wake a process that is already waiting, and use a value that stays set when the waiter might arrive late.
- Lesson
- 3.4
- Time
- 25 min
- Runs on
- Verilator
- Check
- 5 questions
A scoreboard waits with @(drain_done) before it prints its final report. On most seeds the test ends cleanly, but on a few it hangs until the global timeout, and adding a $display makes the hang go away. The monitor triggers drain_done in the same time step in which the scoreboard reaches its @. When the trigger runs first, the scoreboard waits for a second trigger that never comes.
A trigger has no width: it is over before the scoreboard starts to wait.
An event marks a moment
A named event, declared as event drain_done;, carries no value. The statement -> drain_done triggers it, and every process that is blocked at @(drain_done) at that moment wakes up.
A process that reaches @(drain_done) afterwards waits for the next trigger, because the event keeps no record of the one that has passed. A trigger with nobody waiting does nothing at all.
That makes an event a good fit for "wake whoever is waiting now", such as telling an idle driver that a new item is ready. It is a poor fit for any fact a process might check later, such as "the drain has finished".
An event variable is a handle to the underlying event, so you can pass it to a task or store it in an object. A monitor and a scoreboard can then share one event without a global name, which is how class-based testbenches use them.
The lost wakeup
In the opening story, both processes act at the same simulation time. Within one time step, the standard lets a simulator run ready processes in any order, so two orders are legal.
Order 1: the waiter runs first
The scoreboard reaches
@(drain_done)and blocks. The monitor then triggers the event, and the scoreboard wakes up.Order 2: the trigger runs first
The monitor triggers
drain_donewhile nobody is waiting. The scoreboard then reaches@(drain_done)and blocks until a trigger that never comes.
Which order you get depends on details unrelated to what you meant. The order of code in a file, an added $display, a different seed or a different simulator can each flip it. That is why the hang appears and disappears. It is a race, and the cure is to remove it rather than to tune the order.
triggered lasts for the rest of the time step
Every event has a built-in method, triggered, that is 1 from the trigger until the end of the current time step. wait (drain_done.triggered) returns at once if the event has already fired in this time step; otherwise it waits for the trigger, just like @.
Edge: @
// monitor
-> drain_done;
// scoreboard, same time step
@(drain_done);
report();
Hangs whenever the trigger runs first.
State for this time step: triggered
// monitor
-> drain_done;
// scoreboard, same time step
wait (drain_done.triggered);
report();
Wakes in either order, because triggered is still 1 when the scoreboard looks.
triggered removes the race inside one time step, and nothing more. One time unit later it is 0 again, so a waiter that arrives a cycle late still blocks.
The idea behind the fix applies to every handshake between processes. Let the waiter look at a state that lasts, instead of trying to catch the instant when something changes. The rest of this lesson makes that state last longer than one time step.
When the waiter can arrive late, keep state
If the waiter may arrive cycles after the event, store the fact in a variable that stays set, such as bit drained, and wait on the value with wait (drained). A wait whose condition is already true returns at once, however long ago the condition became true.
| Mechanism | Wakes a waiter that arrives | Good for |
|---|---|---|
@(e) | before the trigger only | waking processes that are already waiting |
wait (e.triggered) | before the trigger, or later in the same time step | removing the same-time-step race |
wait (flag) on a flag that stays set | any time after the flag is set | one-off facts: reset done, drain done |
wait (count >= n) on a counter | any time, and it counts repeats | facts that happen many times: items sent |
Four ways to wait, and how late the waiter may arrive.
Publish in the right order. Write the data first, then set the flag, so a process that wakes on the flag finds the data already there. If the flag can be set again later, for example after a second reset, decide who clears it and when.
An event also cannot count. If a producer triggers item_ev three times while its consumer is busy, the consumer sees at most the next trigger. To pass items one by one, use the mailbox from lesson 3.5.
Try it
A #0 delay suspends a process until the other processes of the same time step have run, so here the bad order of the opening story happens every time. In a real testbench the order is left to chance, which is worse.
The @ waiter misses the trigger and never prints. The triggered waiter catches it in the same time step, but triggered is 0 again by t=40. Only the flag reaches the late waiter. Remove the two #0; delays and decide which lines can change.
t=30 wait(triggered) woke: total=12 t=40 triggered=0 t=40 wait(drained) woke: total=12 t=100 end: the @(drain_done) waiter is still blockedCheck yourself
Remember
-> ewakes only the processes already blocked at@(e); the event keeps no record of past triggers.- A trigger and a wait in the same time step race, because the standard leaves their order open.
wait (e.triggered)removes that race, buttriggeredis 1 only until the end of the time step.- For a fact a process may check later, set a flag or a counter that stays set, and wait on its value.
- Write the data before you set the flag that announces it.
Practice
- Persistent initialization flag Implement · Easy