SystemVerilog queues are dynamic data structures that allow efficient insertion and deletion operations. Passing queues to tasks and functions enables modular testbench design, scoreboarding, and transaction processing. This article provides a detailed, line-by-line breakdown of how queues are passed as arguments in SystemVerilog.
1. Basic Queue Declaration and Initialization
Before passing a queue to a function or task, we must first declare and populate it.
Example: Declaring and Initializing a Queue
module queue_example;
// Declare a queue of integers
int int_queue[$] = '{10, 20, 30, 40}; // Initialize with values 10,20,30,40
initial begin
$display("Initial Queue: %p", int_queue); // Prints: Initial Queue: '{10, 20, 30, 40}
end
endmodule
Explanation:
- int int_queue[$]: Declares a dynamic queue of integers ($ denotes an unbounded queue).
- = '{10, 20, 30, 40}: Initializes the queue with four integer values.
- $display("%p", int_queue): Prints the entire queue using the %p format specifier.
2. Passing a Queue to a Function
Functions can take queues as arguments, either by value (copy) or by reference (ref).
Example: Passing a Queue to a Function
module queue_function_example;
int numbers[$] = '{1, 2, 3, 4};
// Function to calculate the sum of a queue (passed by reference)
function automatic int sum_queue(ref int q[$]);
int total = 0;
foreach (q[i]) begin
total += q[i]; // Accumulate sum
end
return total;
endfunction
initial begin
int result = sum_queue(numbers); // Pass the queue by reference
$display("Sum of queue: %0d", result); // Output: Sum of queue: 10
end
endmodule
Explanation:
- function automatic int sum_queue(ref int q[$]):
- automatic: Ensures the function uses dynamic memory (important for recursion and re-entrancy).
- ref int q[$]: The queue is passed by reference, meaning modifications inside the function affect the original queue.
- foreach (q[i]): Iterates through each element in the queue.
- sum_queue(numbers): The queue numbers is passed to the function.
3. Passing a Queue to a Task
Tasks can also take queues as arguments, useful for modifying queues in procedural blocks.
Example: Passing a Queue to a Task
module queue_task_example;
string names[$] = '{"Alice", "Bob"};
// Task to add a name to the queue (passed by reference)
task automatic add_name(ref string q[$], input string name);
q.push_back(name); // Modifies the original queue
endtask
initial begin
$display("Before: %p", names); // Output: Before: '{"Alice", "Bob"}
add_name(names, "Charlie"); // Pass queue by reference
$display("After: %p", names); // Output: After: '{"Alice", "Bob", "Charlie"}
end
endmodule
Explanation:
- task automatic add_name(ref string q[$], input string name):
- ref string q[$]: The queue is passed by reference, allowing modification.
- input string name: A new string is passed as input.
- q.push_back(name): Appends "Charlie" to the original queue.
- add_name(names, "Charlie"): The queue names is modified inside the task.
4. Passing Queues by Value vs. by Reference
Key Differences:
| Method | Syntax | Effect | Performance |
|---|---|---|---|
| By Value (Copy) | function int func(int q[$]) | Changes inside the function do not affect the original queue | Slower (copies all elements) |
| By Reference (ref) | function int func(ref int q[$]) | Changes inside the function affect the original queue | Faster (no copy) |
| Read-Only (const ref) | function int func(const ref int q[$]) | Prevents modification inside the function | Fastest (no copy, read-only) |
Example: Comparing ref vs. const ref
module ref_vs_const_ref;
int data[$] = '{100, 200, 300};
// Modifies the original queue (ref)
task automatic modify_queue(ref int q[$]);
q.pop_front(); // Removes first element (100)
endtask
// Only reads the queue (const ref)
function automatic int get_size(const ref int q[$]);
return q.size(); // Returns current size
endfunction
initial begin
$display("Original size: %0d", get_size(data)); // Output: 3
modify_queue(data); // Removes 100
$display("New size: %0d", get_size(data)); // Output: 2
end
endmodule
Explanation:
- modify_queue(ref int q[$]): Modifies the original queue (data loses 100).
- get_size(const ref int q[$]): Only reads the queue (cannot modify it).
5. Using Queues with Mailboxes (Inter-Process Communication)
Mailboxes are often used with queues for passing transactions between processes.
Example: Passing a Mailbox (Queue-like) to a Task
module mailbox_example;
mailbox #(int) int_mbox = new(); // Mailbox behaves like a queue
// Producer task: Sends data to mailbox
task automatic producer(ref mailbox #(int) mbox);
for (int i = 1; i <= 5; i++) begin
mbox.put(i); // Sends 1,2,3,4,5 into the mailbox
end
endtask
// Consumer task: Receives data from mailbox
task automatic consumer(ref mailbox #(int) mbox);
int received;
repeat (5) begin
mbox.get(received); // Retrieves values 1,2,3,4,5
$display("Received: %0d", received);
end
endtask
initial begin
fork
producer(int_mbox); // Pass mailbox by reference
consumer(int_mbox); // Pass mailbox by reference
join
end
endmodule
Explanation:
- mailbox #(int) int_mbox = new(): Creates a mailbox for integer data.
- producer(ref mailbox #(int) mbox): Puts data into the mailbox.
- consumer(ref mailbox #(int) mbox): Retrieves data from the mailbox.
6. Best Practices for Passing Queues
- Use automatic for tasks/functions that handle queues (avoids static storage issues).
- Prefer ref for large queues (avoids unnecessary copying).
- Use const ref when the queue should not be modified.
- Avoid modifying queues while iterating (can cause unexpected behavior).
- Initialize queues before passing them to prevent null reference errors.
Conclusion
Passing queues in System Verilog enables efficient data sharing between functions and tasks. Key takeaways:
- ref allows modifying the original queue.
- const ref is best for read-only access.
- Mailboxes (a type of queue) are useful for inter-process communication.