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.

The trigger fires just before anyone is listening
01234567drain_done (trigger)monitordraintriggeridlescoreboardbusybusy, then @blockedreport printedtrigger, then @ in the same time step

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.

  1. Order 1: the waiter runs first

    The scoreboard reaches @(drain_done) and blocks. The monitor then triggers the event, and the scoreboard wakes up.

  2. Order 2: the trigger runs first

    The monitor triggers drain_done while 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.

MechanismWakes a waiter that arrivesGood for
@(e)before the trigger onlywaking processes that are already waiting
wait (e.triggered)before the trigger, or later in the same time stepremoving the same-time-step race
wait (flag) on a flag that stays setany time after the flag is setone-off facts: reset done, drain done
wait (count >= n) on a counterany time, and it counts repeatsfacts 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.

RUNVerilator · simulator service
Edit the code, then run it
Expected: 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 blocked

Check yourself

Predict the outputPredict the exact output.
module tb;
  event go;
  int woke;
  initial #5 -> go;
  initial begin #5; #0; @(go) woke++; end
  initial begin #5; #0; wait (go.triggered) woke++; end
  initial begin #6; wait (go.triggered) woke++; end
  initial #20 $display("woke=%0d", woke);
endmodule
Spot the bugA producer sets data, sets ready_flag and triggers ready_ev at t=10. The consumer reaches its wait later in the same time step. Each option is the consumer's wait. Which one leaves the consumer blocked?
ConceptAn event fired at t=100. At t=150 a process runs wait (e.triggered). What happens?
ConceptA monitor runs done = 1; and then @(posedge clk) result = sum;. A checker runs wait (done); check(result);. Which result does the checker see?
ConceptA producer triggers item_ev once for each of three items, at t=10, 20 and 30. The consumer loops on @(item_ev); take_item();, but it is busy until t=25. How many items does it take?

Remember

  • -> e wakes 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, but triggered is 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