Processes and inter-process communication

Learning objectives
  • Start parallel threads with fork...join, join_any and join_none
  • Stop threads safely with disable fork
  • Synchronise threads with named events
  • Protect shared resources with semaphores
  • Pass transactions between components with mailboxes
1 Objective→ 2 Learn→ 3 Example→ 4 Practice→ 5 Verify→ 6 Continue
How to study this lessonUnderstand the idea first. Use the example to see the behaviour, practise where provided, then make sure you can explain the result before continuing.

Explanation

Testbenches are concurrent

A testbench runs several activities at once: a generator creates transactions, a driver applies them, a monitor samples the bus and a scoreboard checks results. Each activity is a thread. SystemVerilog provides fork/join blocks to start threads and three communication mechanisms to coordinate them.

fork variants

Construct Parent continues when Typical use
fork ... join All child threads finish Run independent stimulus streams and wait for all
fork ... join_any Any one child finishes Wait for a response or a timeout, whichever comes first
fork ... join_none Immediately Start long-running components such as a monitor
task automatic wait_with_timeout(event done_ev, int unsigned cycles);
  fork
    begin
      @(done_ev);
      $display("[%0t] done", $time);
    end
    begin
      repeat (cycles) @(posedge clk);
      $error("timeout after %0d cycles", cycles);
    end
  join_any
  disable fork;     // kill whichever branch is still running
endtask

disable fork kills every child thread started by the current thread, including threads started earlier in the same task. Wrap the fork in its own begin...end thread (or a fork begin ... end join guard) when other children must keep running.

Loops that fork threads

A thread started with join_none inside a loop runs after the loop finishes, so every thread may see the final loop variable. Copy it into an automatic variable first.

for (int i = 0; i < 4; i++) begin
  automatic int id = i;
  fork
    run_port(id);   // each thread gets its own id: 0, 1, 2, 3
  join_none
end
wait fork;          // wait for all child threads to finish

Named events

An event signals that something has happened. -> ev triggers it; @(ev) waits for the next trigger; wait (ev.triggered) also succeeds if the trigger happened earlier in the same time step, which avoids a race when the waiter and the trigger run at the same simulation time.

Semaphores

A semaphore holds a number of keys. A thread calls get() to take keys (blocking until available) and put() to return them. A semaphore with one key gives exclusive access, for example to a shared bus that two drivers want to use.

semaphore bus_lock = new(1);

task automatic do_transfer(int id);
  bus_lock.get(1);
  // ... drive the bus ...
  bus_lock.put(1);
endtask

try_get() returns immediately with 0 if no key is available.

Mailboxes

A mailbox is a FIFO between threads. put() adds an item, get() removes the oldest item and blocks if empty, peek() reads without removing, and try_get()/try_put() do not block. A bounded mailbox (new(1)) makes put() block when full, which keeps a generator from running far ahead of the driver. A parameterised mailbox checks the item type at compile time.

mailbox #(packet) gen2drv = new(1);

task automatic generator(int n);
  repeat (n) begin
    packet p = new();
    void'(p.randomize());
    gen2drv.put(p);       // blocks until the driver has taken the previous one
  end
endtask

task automatic driver();
  packet p;
  forever begin
    gen2drv.get(p);
    drive(p);
  end
endtask

Tool note: fork/join, events, semaphores and mailboxes run on Verilator 5.x with --timing. Icarus 12 supports basic fork/join but not mailboxes or semaphores.

Worked example

In wait_with_timeout, if done_ev fires after 20 cycles and the timeout is 100 cycles, join_any returns after the first branch. Without disable fork, the timeout branch keeps counting and reports a false error 80 cycles later. With disable fork, it is killed, but so is any other thread this task's caller forked earlier, which is why the guard block matters in larger testbenches.

Check yourself

What is the difference between join_any and join_none? Why does a forked thread in a loop need an automatic copy of the loop variable? When should you use wait (ev.triggered) instead of @(ev)? What does bounding a mailbox to one entry achieve?

Engineering practice

Practice: Processes and inter-process communication

Task→Run / reason→Check result→Explain why

Build a generator and driver connected by a bounded mailbox of size 1 and show, with $time prints, that the generator waits for the driver. Add a monitor started with join_none. Use a semaphore so that two drivers share one bus without overlapping transfers. Finally add a timeout with join_any and disable fork, and show both the normal and the timeout path.

Practice work is non-certified and is not stored by ASIC Campus.