A semaphore is a bucket of keys. new(N) creates it with N keys, get(M) blocks until M keys are available and removes them atomically, put(M) returns keys, and try_get(M) probes without blocking.
When two processes call get(1) in one timestep, exactly one is granted. There is no torn state and no test-and-set race to write by hand.
Call | Blocks | Returns |
|---|---|---|
| Yes, until M keys free | Nothing, grants M keys |
| No | Nothing, adds M keys |
| No | 1 on success, 0 when short |
A semaphore has no ownership. Any process may put keys, even one that never called get, and putting more than the initial count is legal, so get-then-put pairing is a convention for review to enforce.
The standard testbench use is two bus drivers sharing one physical bus. Each takes the single key before driving and returns it after, so overlapping drives become impossible.
Blocked getters wake in FIFO order of arrival (IEEE 1800 15.3.3). The arrival order of processes in the same timestep is indeterminate, and the LRM does not promise fairness for a new get() whose keys are already free. Designs that need a policy must add explicit arbitration.
get(1) at time 0 on a one-key semaphore and hold the bus for 20 ns. When does each one get it?One at 0 ns and the other at 20 ns, when the first puts the key back. Which driver goes first is not defined; that only one holds the bus at a time is.
Two drivers share one bus through a one-key semaphore:
timeunit 1ns; // one time unit for the classes and the module
timeprecision 1ps;
class bus_driver_c;
string name;
semaphore bus_sem;
time granted_at;
function new(string n, semaphore s);
name = n;
bus_sem = s;
endfunction
task drive_one(int unsigned addr);
bus_sem.get(1);
granted_at = $time;
$display("[%0t] %s: GRANTED addr=%0h", $time, name, addr);
#20;
bus_sem.put(1);
endtask
endclass
module top_sem;
initial begin
semaphore sem; // declare first: initializers here would run at time 0
bus_driver_c a, b;
sem = new(1);
a = new("drvA", sem);
b = new("drvB", sem);
fork
a.drive_one('h10);
b.drive_one('h20);
join
if (a.granted_at == b.granted_at) $fatal(1, "FAIL: both drivers held the bus at %0d ns", a.granted_at);
$display("PASS: one key serialized the two drivers");
$finish;
end
endmodule[0] drvA: GRANTED addr=10
[20000] drvB: GRANTED addr=20
PASS: one key serialized the two drivers// snippet
// Fix: try_get with backoff. Never hold one lock while blocking on another.
task acquire_both(semaphore s_bus, semaphore s_mem);
forever begin
s_bus.get(1);
if (s_mem.try_get(1)) return;
s_bus.put(1);
#1;
end
endtaskWith two semaphores, process P can hold the bus lock while wanting memory, and Q can hold memory while wanting the bus. Neither proceeds and the run hangs at its last delay.
Three cures exist: one global lock order for every process, try_get with backoff as above, or one coarse lock for both resources. Ordering is cheapest, backoff keeps the most parallelism.
Exactly one wakes and the bucket is consistent. Blocked getters wake in FIFO order of arrival, but arrival order within one timestep is indeterminate, so order-sensitive code needs its own arbiter.
AB-BA deadlock. Fix with a global lock order, try_get with backoff, or one coarse lock for both.
The same exclusion with a plain token bit, for tools without semaphores. The busy count proves the drivers never overlap, and both complete. This hand-written test-and-set works because simulators do not suspend a process between wait (token == 1) and token = 1'b0; the LRM does not promise that, while get() is atomic by definition.
module tb_mutex_token;
timeunit 1ns;
timeprecision 1ps;
bit token = 1;
int busy = 0;
int done_count = 0;
int errors = 0;
task automatic drive_one;
wait (token == 1);
token = 1'b0;
busy++;
if (busy !== 1) errors++;
#20;
busy--;
token = 1'b1;
done_count++;
endtask
initial begin
fork
drive_one;
drive_one;
join
if (done_count !== 2) errors++;
if (busy !== 0) errors++;
if (errors == 0) $display("PASS: 2 drivers shared 1 bus with no overlap");
else $fatal(1, "FAIL: done=%0d busy=%0d", done_count, busy);
$finish;
end
endmodulePASS: 2 drivers shared 1 bus with no overlapRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.
Blocked getters wake in arrival order (IEEE 1800 15.3.3), but arrival order within one time step is not defined; the bench checks only that the drivers never overlap.