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.