System Verilog Mod 4
System Verilog Mod 4
System Verilog
(MECV115A)
Course Handling Faculty: Keshava A
Module - 4
2
DEPARTMENT OF ELECTRONICS AND COMMUNICATION ENGINEERING
BANGALORE INSTITUTE OF TECHNOLOGY
Introduction
1. What are Threads?
• Threads allow parallel execution of multiple processes in a testbench.
• In real hardware, sequential logic updates on clock edges, while combinational logic updates
when inputs change.
• SystemVerilog simulates parallel activities using:
initial and always blocks
fork...join constructs
Testbench components like generators, drivers, monitors, and checkers
Introduction
2. Role of Threads in a Testbench
• Each testbench component runs in its own execution thread.
• The SystemVerilog Scheduler decides which thread runs next.
• The testbench environment consists of multiple blocks running in parallel.
Introduction
3. Testbench Environment Blocks
• Components in a layered testbench:
Generator → Creates stimulus for the DUT.
Agent → Processes and modifies the stimulus.
Driver → Sends the stimulus to the DUT.
Monitor → Captures responses from the DUT.
Checker → Verifies if DUT output matches expected results.
Scoreboard → Keeps track of verification results.
Introduction
4. What is Interprocess Communication (IPC)?
• IPC enables threads to exchange data and synchronize execution.
• Needed because different testbench components must communicate.
• IPC ensures:
Data Transfer – Between testbench components.
Synchronization – Ensuring one thread waits for another.
Resource Management – Prevents multiple threads from using the same resource
simultaneously.
Introduction
5. SystemVerilog IPC Mechanisms
Introduction
6. Mathematical Representation of Parallel Execution
• Execution time of a thread Ti:
Introduction
7. Example SystemVerilog Code for Thread Execution
initial begin
fork
#10 $display("Thread 1 Executing");
#20 $display("Thread 2 Executing");
join
$display("Both threads completed");
end
Introduction
8. Key Takeaways
Threads enable parallel execution of testbench components.
IPC allows communication and synchronization between threads.
SystemVerilog constructs include events, semaphores, and mailboxes.
Testbench architecture relies on multiple threads for efficient verification.
10
11
12
22
stop all child threads. // Create a thread to limit the scope of disable
fork
• Be careful: It stops all threads spawned begin
wait_for_tr(tr1); // Spawn thread 1
from the current fork, which may be too fork // Spawn another forked thread (Thread 2)
wait_for_tr(tr2);
many! join
#500 disable fork; // Stops threads 1 & 2, but leaves thread 0
• Solution: Use a labeled block to control end
join
which threads are disabled. end
24
25
begin : threads_1_2
wait_for_tr(tr1); // Spawn thread 1
wait_for_tr(tr2); // Spawn thread 2
end
Instead of stopping all threads, only the labeled group (threads_1_2) is stopped.
Thread 0 keeps running. 26
wait_for_tr(tr1); // Spawn thread 1 All spawned threads (tr1, tr2, tr3) must complete
wait_for_tr(tr2); // Spawn thread 2 before continuing.
wait_for_tr(tr3); // Spawn thread 3 wait fork; ensures no unfinished thread is left behind.
... // Do some other work
28
30
31
32
38
39
Events
1. Introduction to Events
• Events in SystemVerilog are used for thread synchronization.
• They allow one thread to trigger an event while another thread waits for it.
• Similar to a phone call where:
One person waits for a call (@event_name;).
Another person triggers the call (-> event_name;).
40
Events
2. Blocking on the Edge of an Event
• How does a thread wait for an event?
Use @event_name; → Blocks execution until the event occurs.
• How does a thread trigger an event?
Use -> event_name; → Triggers the event and unblocks any waiting thread.
• Potential Issue:
If an event is triggered before a thread starts waiting, the waiting thread misses the event and
gets stuck.
41
Events
2. Blocking on the Edge of an Event
Example Code: Blocking on an Event
event e1, e2;
initial begin
$display("@%0d: 1: before trigger", $time);
-> e1; // Trigger event e1
@e2; // Wait for event e2 Problem:
$display("@%0d: 1: after trigger", $time); • If e2 is triggered before @e2 starts waiting, the
end waiting thread misses the event and hangs.
initial begin
$display("@%0d: 2: before trigger", $time); Solution:
-> e2; // Trigger event e2 • Use triggered function (explained next).
@e1; // Wait for event e1
$display("@%0d: 2: after trigger", $time);
end
42
Events
Example Code: Waiting for an Event with triggered
3. Waiting for an Event Trigger event e1, e2;
• Problem with @event:
initial begin
If an event occurs before a thread waits $display("\n@%0d: 1: before trigger", $time);
-> e1;
for it, the waiting thread misses it. wait (e2.triggered); // Non-blocking wait for event e2
$display("@%0d: 1: after trigger", $time);
• Solution: Use triggered function end
45
initial begin
foreach (gen[i]) begin
gen[i] = new; // Create N generators
gen[i].run; // Start them running
end Ensures:
• Each thread waits for its corresponding
// Wait for all generators to finish
foreach (gen[i]) event to be triggered.
fork • The testbench proceeds only after all
automatic int k = i; events have occurred.
wait (done[k].triggered);
join_none
Events
6. Counting Event Triggers Instead of Waiting
• Instead of waiting on individual events, we can count the number of events triggered.
• Useful when we don’t care about the order of event completion.
Example Code: Counting Completed Events begin
event done[N_GENERATORS]; wait (done[k].triggered);
done_count++;
initial begin end
foreach (gen[i]) begin join_none
gen[i] = new;
gen[i].run; wait fork; // Ensures all the triggers complete
end end
Events
7. Alternative: Using a Static Thread Counter
• Another way to track completion is using a static variable in a class.
• Instead of events, we count active threads.
Example Code: Using a Static Counter Instead of Events Generator gen[N_GENERATORS];
class Generator;
static int thread_count = 0; initial begin
// Create N generators
task run; foreach (gen[i])
thread_count++; // Start another thread gen[i] = new;
fork
begin // Start them running
... // Perform thread work foreach (gen[i])
thread_count--; // Decrement thread count when done gen[i].run;
end
join_none // Wait for the generators to complete
endtask wait (gen[0].thread_count == 0);
endclass end 48
Events
7. Alternative: Using a Static Thread Counter
Example Code: Using a Static Counter Instead of Events Generator gen[N_GENERATORS];
class Generator;
static int thread_count = 0; initial begin
// Create N generators
task run; foreach (gen[i])
thread_count++; // Start another thread gen[i] = new;
fork
begin // Start them running
... // Perform thread work foreach (gen[i])
thread_count--; // Decrement thread count when done gen[i].run;
end
join_none // Wait for the generators to complete
endtask wait (gen[0].thread_count == 0);
endclass end
Key Benefit:
• No need for separate events for each thread.
• The testbench just waits until thread_count == 0. 49
Events
8. Summary of Event-Based Synchronization
50
Events
9. Key Takeaways
Events synchronize threads in testbenches.
@event blocks execution, while ->event triggers it.
Use triggered function to prevent missed events.
For multiple threads, use wait fork; or count triggers manually.
51
Semaphores
1. Introduction to Semaphores
• A semaphore is a synchronization mechanism used to control access to a shared resource.
• Works like a key system, where a process must obtain a key before using a resource.
• Example:
Imagine you and your spouse share one car. Only the person who has the key can drive it.
The key acts as a semaphore, ensuring only one person accesses the car at a time.
• Why Semaphores?
In a testbench, multiple threads may need exclusive access to a shared resource (e.g., a bus).
Ensures mutual exclusion so only one thread at a time accesses the resource.
52
Semaphores
2. How Semaphores Work in SystemVerilog
• Semaphores are objects in SystemVerilog.
• Operations:
new(N): Creates a semaphore with N keys (permits).
get(K): Acquires K keys (blocks if unavailable).
put(K): Releases K keys, making them available.
try_get(K): Tries to acquire K keys (non-blocking).
53
Semaphores
3. Basic Semaphore Operations
• Semaphores are declared as objects and must be instantiated using new().
• If a thread requests a key when none are available, it blocks.
Example Code: Controlling Access to a Bus task sequencer;
program automatic test; repeat($urandom%10) // Random wait, 0-9 cycles
semaphore sem; // Declare semaphore @bus.cb;
initial begin sendTrans; // Execute the transaction
sem = new(1); // Allocate with 1 key endtask
fork
sequencer; // Spawn two threads that both task sendTrans;
sequencer; // perform bus transactions sem.get(1); // Get the key to the bus
join @bus.cb; // Drive signals onto the bus
end bus.cb.addr <= t.addr;
...
sem.put(1); // Release the key when done
endtask
endprogram 54
Semaphores
3. Basic Semaphore Operations
Example Code: Controlling Access to a Bus task sequencer;
program automatic test; repeat($urandom%10) // Random wait, 0-9 cycles
semaphore sem; // Declare semaphore @bus.cb;
initial begin sendTrans; // Execute the transaction
sem = new(1); // Allocate with 1 key endtask
fork
sequencer; // Spawn two threads that both task sendTrans;
sequencer; // perform bus transactions sem.get(1); // Get the key to the bus
join @bus.cb; // Drive signals onto the bus
end bus.cb.addr <= t.addr;
...
sem.put(1); // Release the key when done
Execution Flow: endtask
1. Each thread tries to access the bus. endprogram
2. The first thread acquires the semaphore key and
starts the transaction.
3. The second thread blocks until the key is released. 55
Semaphores
4. Using Semaphores with Multiple Keys
• A semaphore can have multiple keys to allow multiple threads to access a resource.
• Example Scenario:
A testbench simulates a system with multiple buses.
Each bus requires a key, and multiple buses can be used at once.
56
Semaphores
5. Potential Problems with Semaphores
Issue 1: Returning More Keys Than Taken
• If a thread puts back more keys than it took, the system loses control over resource access.
Example Mistake:
sem.get(1);
sem.put(2); // ERROR: More keys returned than taken!
Semaphores
5. Potential Problems with Semaphores
Solution:
• Modify the algorithm to prioritize smaller requests over large ones.
• Custom logic can manage key allocation dynamically.
58
Semaphores
6. Alternative Approach: Custom Synchronization
• If FIFO blocking is an issue, a custom resource manager can be built.
• Example:
A restaurant seating system prioritizes smaller parties if a small table is available.
Similar logic can be implemented in a testbench for shared resource management.
59
Semaphores
7. Summary of Semaphore Operations
Operation Description
60
Semaphores
8. Key Takeaways
Semaphores ensure exclusive access to shared resources.
get(K) blocks execution if insufficient keys are available.
Returning extra keys leads to resource mismanagement.
FIFO order can cause blocking issues if larger requests take priority.
Custom solutions can prioritize smaller requests when needed.
61
Mailboxes
1. Introduction to Mailboxes
• Mailboxes in SystemVerilog provide a way for threads to communicate by passing data
asynchronously.
• Concept:
A producer thread puts data into a mailbox.
A consumer thread retrieves data from the mailbox.
• Why Use Mailboxes?
Avoids direct function calls between objects, improving testbench modularity.
Allows asynchronous execution (the sender and receiver operate independently).
62
Mailboxes
2. How Mailboxes Work in SystemVerilog
• Mailboxes are objects and must be instantiated using new().
• Basic Operations:
put(value): Adds data to the mailbox (blocks if full).
get(variable): Retrieves data from the mailbox (blocks if empty).
peek(variable): Reads data without removing it.
try_get(variable): Tries to retrieve data but does not block if empty.
63
Mailboxes
3. Mailbox Example in a Testbench
• Scenario: A generator produces transactions and passes them to a driver using a mailbox.
Example Code: Generator-Driver Communication Using a Mailbox
program mailbox_example(bus_if.TB bus, ...); class Driver; mailbox mbx; // Declare mailbox
Transaction tr; Generator gen;
class Generator; mailbox mbx; Driver drv;
Transaction tr;
mailbox mbx; function new(mailbox mbx); initial begin
this.mbx = mbx; mbx = new;
function new(mailbox mbx); endfunction gen = new(mbx);
this.mbx = mbx; drv = new(mbx);
endfunction task run; fork
repeat (10) begin gen.run(); // Start generator
task run; mbx.get(tr); // Fetch transaction from drv.run(); // Start driver
repeat (10) begin mailbox join
tr = new; @(posedge busif.cb.ack); end
assert(tr.randomize); bus.cb.kind <= tr.kind; endprogram
mbx.put(tr); // Send transaction to ...
mailbox end
end endtask
endtask endclass
endclass 64
Mailboxes
3. Mailbox Example in a Testbench
Example Code: Generator-Driver Communication Using a Mailbox
program mailbox_example(bus_if.TB bus, ...); class Driver; mailbox mbx; // Declare mailbox
Transaction tr; Generator gen;
class Generator; mailbox mbx; Driver drv;
Transaction tr;
mailbox mbx; function new(mailbox mbx); initial begin
this.mbx = mbx; mbx = new;
function new(mailbox mbx); endfunction gen = new(mbx);
this.mbx = mbx; drv = new(mbx);
endfunction task run; fork
repeat (10) begin gen.run(); // Start generator
task run; mbx.get(tr); // Fetch transaction from mailbox drv.run(); // Start driver
repeat (10) begin @(posedge busif.cb.ack); join
tr = new; bus.cb.kind <= tr.kind; end
assert(tr.randomize); ... endprogram
mbx.put(tr); // Send transaction to end
mailbox endtask How It Works:
end endclass
• The generator creates and sends transactions to
endtask
endclass the mailbox.
• The driver retrieves transactions from the
mailbox and applies them to the DUT. 65
Mailboxes
4. Bounded vs. Unbounded Mailboxes
• Unbounded Mailbox: Can hold unlimited messages.
Default behavior when no size is specified (new()).
• Bounded Mailbox: Has a fixed size.
If full, put() blocks until space is available.
Ensures the producer doesn’t generate data faster than the consumer can process it.
66
Mailboxes
4. Bounded vs. Unbounded Mailboxes
Example Code: Bounded Mailbox
Mailboxes
5. Synchronization Issues with Unsynchronized Threads
• If producer and consumer run at different speeds, the producer may get ahead of the consumer.
• Problem: The producer fills the mailbox before the consumer can process data.
Mailboxes
5. Synchronization Issues with Unsynchronized Threads
Example Code: Unsynchronized Producer-Consumer
program automatic unsynchronized; class Consumer; mailbox mbx;
task run; Producer p;
class Producer; int i; Consumer c;
task run; repeat (3) begin initial begin
for (int i=1; i<4; i++) begin mbx.get(i); mbx = new;
$display("Producer: before put(%0d)", $display("Consumer: after get(%0d)", i); p = new;
i); end c = new;
mbx.put(i); endtask fork
end endclass p.run;
endtask c.run;
endclass join
Issue: end
endprogram
• The producer fills the mailbox quickly, while the consumer retrieves data slowly.
• The mailbox may overflow, causing unexpected blocking. 69
Mailboxes
6. Using Events for Producer-Consumer Synchronization
• Solution: Use events to signal when the consumer has finished processing a transaction.
Example Code: Synchronizing Producer and Consumer with an Event
program automatic mbx_evt; class Consumer;
event handshake; task run;
int i;
class Producer; repeat (3) begin
task run; mbx.get(i);
for (int i=1; i<4; i++) begin $display("Consumer: after get(%0d)",
$display("Producer: before put(%0d)", i);
i); ->handshake; // Signal producer
mbx.put(i); end
@handshake; // Wait for consumer endtask
before next put endclass
$display("Producer: after put(%0d)", i); endprogram
end
endtask 70
endclass
DEPARTMENT OF ELECTRONICS AND COMMUNICATION ENGINEERING
BANGALORE INSTITUTE OF TECHNOLOGY
Mailboxes
6. Using Events for Producer-Consumer Synchronization
Example Code: Synchronizing Producer and Consumer with an Event
program automatic mbx_evt; class Consumer;
event handshake; task run; How It Works:
int i; • The producer waits for an
class Producer; repeat (3) begin
task run; mbx.get(i); event before putting
for (int i=1; i<4; i++) begin $display("Consumer: after get(%0d)", another transaction.
$display("Producer: before put(%0d)", i);
i); ->handshake; // Signal producer • The consumer triggers the
mbx.put(i); end event after retrieving each
@handshake; // Wait for consumer endtask
before next put endclass transaction.
$display("Producer: after put(%0d)", i); endprogram • Result: The producer
end
endtask doesn’t get ahead of the
endclass consumer.
71
Mailboxes
7. Alternative Synchronization Methods
8. Using a Second Mailbox:
• Instead of using an event, the consumer sends acknowledgment messages via a second
mailbox.
2. Using a Semaphore:
• The producer gets a semaphore key before putting data, and the consumer releases the key
after consuming it.
72
Mailboxes
8. Summary of Mailbox-Based Communication
Feature Description
Unbounded Mailbox Holds unlimited transactions.
Bounded Mailbox Has a fixed size (blocks producer if full).
put(value) Adds a value to the mailbox (blocks if full).
get(variable) Retrieves a value from the mailbox (blocks if empty).
Synchronization Issue Producer may generate data faster than the consumer can process it.
Solution 1 Use events to coordinate producer and consumer execution.
Solution 2 Use a second mailbox to send acknowledgments.
73
Mailboxes
9. Key Takeaways
Mailboxes enable asynchronous communication between threads.
Bounded mailboxes prevent unbounded memory usage but can block producers.
Synchronization issues arise if producers run faster than consumers.
Use events or a second mailbox to ensure correct execution order.
74
76
77
endclass
78
task Environment::run;
fork
gen.run(run_for_n_trans);
How It Works:
agt.run;
drv.run; • All testbench components run in parallel.
mon.run;
• fork...join ensures that the test completes only after all components finish
chk.run;
scb.run(run_for_n_trans); execution.
join
endtask
81
82
Step Description
Create Instantiates all testbench components.
Environment
Initialize IPC Creates mailboxes for communication.
Build Testbench Instantiates and connects components.
Run Test Uses fork...join to execute threads.
Wrap Up Ensures all components terminate correctly.
84
85
Conclusion
1. Summary of Threads and IPC in SystemVerilog
• SystemVerilog provides advanced multi-threading capabilities for testbenches.
• Threads (Processes) allow concurrent execution of different tasks.
• Interprocess Communication (IPC) enables efficient data exchange and synchronization
between threads.
86
Conclusion
2. Key Topics Covered in the Chapter
Concept Description
Threads Enable parallel execution of processes.
fork...join Runs multiple threads in parallel.
fork...join_none Starts threads but does not wait for completion.
fork...join_any Waits for any one thread to complete.
Disabling Threads Stops one or more running threads dynamically.
Waiting for Threads Ensures all spawned threads complete execution.
Events Synchronize threads using triggers.
Semaphores Ensure mutual exclusion for shared resources.
Mailboxes Facilitate data exchange between processes.
87
Conclusion
3. Importance of Using Threads in Verification
• Parallel Execution:
Allows different testbench components (generator, driver, monitor, checker) to operate
simultaneously.
• Synchronization:
Ensures that different parts of the testbench communicate effectively without race conditions.
• Scalability:
Enables complex, large-scale verification environments where multiple testbench elements
need to run concurrently.
88
Conclusion
4. Choosing the Right IPC Mechanism
• Use fork...join when all threads must complete before continuing.
• Use fork...join_none when threads should run in the background.
• Use fork...join_any when execution should proceed after any thread completes.
• Use Events (@event, triggered) for thread synchronization.
• Use Semaphores for resource access control in testbenches.
• Use Mailboxes to pass data asynchronously between testbench components.
89
Conclusion
5. Final Takeaways
SystemVerilog’s multi-threading capabilities allow for highly efficient and flexible testbenches.
IPC mechanisms ensure testbench components operate synchronously without conflicts.
Choosing the right technique (forking, events, semaphores, mailboxes) depends on the specific
testbench requirement.
Proper thread management leads to faster, more scalable, and modular testbenches.
90