Learning Outcome
By the end of this post we’ll understand how to use queues to store and share data across processes in our test testbenches, and we’ll have several examples of working code to help us get started in our next project.
What is a Queue?
A queue is a data structure into which data are written on one side (push) and read on the other side (pop). Queues operate on a first-in first-out basis, and like a hardware FIFO, we usually cannot access the elements inside the queue.
At a minimum, a queue must support pushing data to it and popping data from it. In a ‘push’ operation the data is added to the back of the queue. Any existing elements inside the queue are shifted to make room for the new data.
A ‘pop’ operation works in the opposite direction: data is removed from the from the front of the queue. Any remaining elements stay in their position as the queue shrinks, until eventually it is empty again.
Queues are very useful for storing and sharing data between processes in a test bench. A typical use case is to save the inputs to a design under test (DUT) in an input queue and the outputs of the DUT in an output queue. The test bench can then run a reference model that reads the data from the input queue, calculates a set of expected outputs, and compares them with the actual output values stored in the output queue.
Using Queues in VUnit
Unlike SystemVerilog, VHDL does not support queues natively. Luckily, the VUnit verification framework includes a queue data type and API to interact with it, so we don’t have to implement our own.
The function calls that we’ll use most frequently when working with queues in VUnit are: new, push, pop, is_empty, length, and flush. Let’s discuss each one with a code example.
We start by setting up a minimal VUnit testbench.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
-- File: vunit_queues.vhd
library vunit_lib;
context vunit_lib.vunit_context;
entity tb_example is
generic (runner_cfg : string);
end entity;
architecture tb of tb_example is
begin
main : process
begin
test_runner_setup(runner, runner_cfg);
test_runner_cleanup(runner);
end process;
end architecture;
|
Then we can add our ’test_queue’ signal, of type ‘queue_t’.
1
2
3
4
5
|
-- File: vunit_queues.vhd
architecture tb of tb_example is
signal test_queue : queue_t;
begin
|
We’ll also declare several variables in our ‘main’ process to exchange data with the queue API.
1
2
3
4
5
6
7
8
9
10
11
12
|
-- File: vunit_queues.vhd
main : process
variable bit_push_v : std_logic := '1';
variable vector_16_push_v : std_logic_vector(15 downto 0) := x"0123";
variable vector_32_push_v : std_logic_vector(31 downto 0) := x"456789AB";
variable integer_push_v : integer := 2147483647;
variable bit_pop_v : std_logic
variable vector_16_pop_v : std_logic_vector(15 downto 0);
variable vector_32_pop_v : std_logic_vector(31 downto 0);
variable integer_pop_v : integer;
begin
|
Now we are ready to describe the logic in our ‘main’ process.
New
The ’new’ function creates the underlying queue object that we will use. It must be called before performing any other operations on the queue.
1
2
3
4
5
|
-- File: vunit_queues.vhd
info("***** Creating the queue *****");
test_queue <= new_queue;
wait for 0 ns; -- Removing this will cause an error
|
One important thing to note is that the queue operations do not consume any simulation time, so in some cases we’ll need to add a delay to make sure that a function call is executed. In this example, if we remove the ‘wait for 0 ns’ statement, the next operation (push) would produce an error, because we’d be trying to manipulate a null queue.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# Surrounding code from 'see' command
# 86 : value : character
# 87 : ) is
# 88 : variable tail : integer;
# 89 : variable head : integer;
# 90 : begin
# ->91 : assert queue /= null_queue report "Push to null queue";
# 92 : tail := get(queue.p_meta, tail_idx);
# 93 : head := get(queue.p_meta, head_idx);
# 94 : if length(queue.data) < tail + 1 then
# 95 : -- Allocate more new data, double data to avoid
#
fail (P=0 S=0 F=1 T=1) lib.vubit_queue_tb.all (1.6 seconds)
|
Push
The ‘push’ function stores a new element into the queue. Unlike SystemVerilog queues, a single VUnit queue can hold elements of different data types at the same time.
1
2
3
4
5
6
7
|
-- File: vunit_queues.vhd
info("Pushing elements of different types to the queue");
push(test_queue, bit_v);
push(test_queue, vector_16_v);
push(test_queue, vector_32_v);
push(test_queue, integer_v);
|
Pop
The ‘pop’ function removes the oldest element in the queue and returns it so that we can use or store it.
1
2
3
4
5
6
7
8
9
10
|
-- File: vunit_queues.vhd
info("***** Popping elements of different types from the queue *****");
bit_pop_v := pop(test_queue);
vector_16_pop_v := pop(test_queue);
vector_32_pop_v := pop(test_queue);
integer_pop_v := pop(test_queue);
info("Popped a 16-bit vector: " & to_hstring(vector_16_pop_v));
info("Popped a 32-bit vector: " & to_hstring(vector_32_pop_v));
info("Popped an integer: " & integer'image(integer_pop_v));
|
1
2
3
4
5
6
|
-- Simulation output
# 0 ps - default - INFO - ***** Popping elements of different types from the queue *****
# 0 ps - default - INFO - Popped a 16-bit vector: 0123
# 0 ps - default - INFO - Popped a 32-bit vector: 456789AB
# 0 ps - default - INFO - Popped an integer: 2147483647
|
The VUnit API also includes verbose versions of the ‘push’ and ‘pop’ functions, which we can use to make the type of the argument or return type explicit.
1
2
3
4
5
6
7
|
-- File: vunit_queues.vhd
info("***** Using verbose funtion calls to resolve data types *****");
push(test_queue, vector_32_push_v);
info("Popping a logic vector: " & to_string(pop_std_ulogic_vector(test_queue)));
push(test_queue, vector_32_push_v);
-- info("Popping a logic vector: " & to_string(pop(test_queue))); -- Error!
|
Uncommenting the last line of the code section above produces an error.
1
2
3
4
5
|
-- Simulation output
# 0 ps - default - INFO - ***** Using verbose funtion calls to resolve data types *****
# 0 ps - default - INFO - Popping a logic vector: 01000101011001111000100110101011
# ** Error: Got queue element of type std_ulogic_vector, expected string_ptr_t.
|
We can comment the last line again to get the testbench running. This also leaves the second 32-bit vector still in the queue.
Is Empty
The ‘is_empty’ function returns ‘true’ if the queue is empty, and ‘false’ if it has at least one element.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
-- File: vunit_queues.vhd
info("Checking if the queue is empty");
if is_empty(test_queue) then
info("The queue is empty");
else
info("The queue is not empty");
end if;
push(test_queue, bit_v);
if is_empty(test_queue) then
info("The queue is empty");
else
info("The queue is not empty");
end if;
|
1
2
3
4
5
|
-- Simulation output
# 0 ps - default - INFO - ***** Checking if the queue is empty *****
# 0 ps - default - INFO - The queue is not empty
# 0 ps - default - INFO - The queue is empty
|
Flush
The ‘flush’ function removes all elements from the queue, leaving it empty.
1
2
3
4
5
6
7
8
9
10
|
-- File: vunit_queues.vhd
info("***** Flushing the queue *****");
push(test_queue, bit_push_v);
push(test_queue, vector_16_push_v);
push(test_queue, vector_32_push_v);
push(test_queue, integer_push_v);
info("Queue length before flushing:" & natural'image(length(test_queue)));
flush(test_queue);
info("Queue length after flushing:" & natural'image(length(test_queue)));
|
1
2
3
4
5
|
-- Simulation output
# 0 ps - default - INFO - ***** Flushing the queue *****
# 0 ps - default - INFO - Queue length before flushing: 59
# 0 ps - default - INFO - Queue length after flushing: 0
|
Length
The ’length’ function returns the length of the queue, in bytes. It’s important to note that this length refers to the space that the simulator allocates to each element in the queue, and not the size of the element that we use in our test bench. For example, calling the ‘length’ function on a queue with one integer returns 5 bytes, not the 4 bytes that we might expect given that an integer in VHDL is by default 32-bits wide. The same applies to logic vectors: the length of a 1-bit vector is 2 bytes, the length of a 32-bit vector is 30 bytes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
-- File: vunit_queues.vhd
info("***** Getting the queue's length *****");
push(test_queue, bit_push_v);
info("Queue length after pushing a single bit: " & natural'image(length(test_queue)));
flush(test_queue);
push(test_queue, vector_8_push_v);
info("Queue length after pushing a 8-bit vector: " & natural'image(length(test_queue)));
flush(test_queue);
push(test_queue, vector_16_push_v);
info("Queue length after pushing a 16-bit vector: " & natural'image(length(test_queue)));
flush(test_queue);
push(test_queue, vector_32_push_v);
info("Queue length after pushing a 32-bit vector: " & natural'image(length(test_queue)));
flush(test_queue);
push(test_queue, integer_push_v);
info("Queue length after pushing an integer: " & natural'image(length(test_queue)));
flush(test_queue);
|
1
2
3
4
5
6
7
8
|
-- Simulation output
# 0 ps - default - INFO - ***** Getting the queue's length *****
# 0 ps - default - INFO - Queue length after pushing a single bit:2
# 0 ps - default - INFO - Queue length after pushing a 8-bit vector:18
# 0 ps - default - INFO - Queue length after pushing a 16-bit vector:22
# 0 ps - default - INFO - Queue length after pushing a 32-bit vector:30
# 0 ps - default - INFO - Queue length after pushing an integer:5
|
Summary
In this post we discussed queues, a FIFO-like data structure that we can use to transfer data between processes in a testbench. Because of VHDL’s lack of native support for queues, we looked into the VUnit’s queue library, and ran code examples for the most common operations in the VUnit queue API: new, push, pop, is_empty, length, and flush.
Cheers,
Isaac