Processes and inter-process communication
- Start parallel threads with
fork...join,join_anyandjoin_none - Stop threads safely with
disable fork - Synchronise threads with named events
- Protect shared resources with semaphores
- Pass transactions between components with mailboxes
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?
Practice: Processes and inter-process communication
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.