The VUnit communication library (
com) provides a high-level communication mechanism
based on the actor model.
The actor model is a mathematical model of computation in which
concurrent actors perform all computation. The only way actors can
communicate is by sending messages to each other and the message passing
is based on two important principles:
The sending actor only knows the name of the receiving actor. It doesn’t know the location of the receiver or how a message gets there.
Communication is asynchronous. The sender doesn’t know when the receiver will read the message.
By extending the basic communication provided by the actor model
com can also provide
synchronous communication and some more advanced communication patterns.
Message passing is not a core functionality of unit testing so
is provided as an optional add-on to VUnit. It is compiled to the
vunit_lib library with the
add_com method in your Python script
prj = VUnit.from_argv() prj.add_com()
The VHDL functionality is provided to your testbench with the
library vunit_lib; context vunit_lib.vunit_context; context vunit_lib.com_context;
Basic Message Passing¶
Sending and Receiving¶
To send a message we must first create an actor for the receiver. The actor
below has a name passed to the
new_actor function but
com also allows
constant my_receiver : actor_t := new_actor("my receiver");
To send a message to the receiver the sender must have access to the value of the
If the receiver made
my_receiver publically available, for example with a package, it can be accessed
directly. If not, it can be found with the
find function providing it has a name.
constant found_receiver : actor_t := find("my receiver");
The next step is to create a message to send and we start by creating an empty message
msg := new_msg;
msg is a variable of type
msg_t. Information is added to the message by pushing objects of
different types into it.
push_string(msg, "10101010"); push(msg, my_integer);
com supports pushing of all native and standardized IEEE types. In case there is no ambiguity you can just
push, otherwise you have to use the more specific alias
push_<type> as exemplified above.
To send the created message to the receiver you call the
send(net, my_receiver, msg);
send is asynchronous and takes no simulation time, only delta cycles. Messages will be stored in the receiver inbox
until the receiver is ready to receive.
net is a network connecting actors and it is used to signal that an event has occurred, for example that a message
has been sent. The event notifies all connected actors that something has happened which they may be interested in.
For example, the event created when sending a message will wake up all receivers such that they can see if they are the
receiver for the message.
An actor waiting for a message uses the receive procedure
receive(net, my_receiver, msg);
This procedure returns immediately if there are pending message(s) in the receiver’s inbox or blocks until the first
message arrives. The returned message contains the oldest incoming message and its information can be retrieved using
pop functions. The code below will verify that the message has the expected content using the VUnit
check_equal(pop_string(msg), "10101010"); my_integer := pop(msg); check_equal(my_integer, 17);
push there are both
pop functions and more verbose aliases on the form
Objects are always popped from the message in the same order they were pushed into the message and once all objects have been popped the message is empty. If you want to keep a message for later you can make a copy before popping.
msg_copy := copy(original_msg);
In the example above the sender and the receiver exchanged one type of message (a string followed by an integer) but the normal use case is that a receiver can handle several types of messages. For example, if the receiver is a bus functional model (BFM) connected to a memory bus it would be able to handle both read and write messages.
Rather than using a regular type as the message type, for example the string
"write" for a write message,
provides a special message type.
constant write_msg : msg_type_t := new_msg_type("write");
"write" is just a description of the message type and not a unique identifier. Even if we have two independently
created BFMs, both providing the constant above in their own packages, they would be given different values by the
new_msg_type function. This means that we can safely create the different types of write messages without any risk
of mistaking one for the other.
msg := new_msg(memory_bfm_pkg.write_msg); push(msg, my_unsigned_address); push(msg, my_std_logic_vector_data); send(net, memory_bfm_pkg.actor, msg);
The receiver starts by looking at the message type and then handles the message types it recognizes.
message_handler: process is variable request_msg : msg_t; variable msg_type : msg_type_t; variable address : unsigned(7 downto 0); variable data : std_logic_vector(7 downto 0); variable memory : integer_vector(0 to 255) := (others => 0); begin receive(net, actor, request_msg); msg_type := message_type(request_msg); if msg_type = write_msg then address := pop(request_msg); data := pop(request_msg); memory(to_integer(address)) := to_integer(data); end if; end process;
Normally a BFM would never be exposed to a write message aimed for another BFM but under certain cases it can happen. For example when using the publisher/subscriber pattern described later. A typical BFM would also provide a write transaction procedure which hides the message passing details (creating message, pushing data, and sending). That gives an extra level of type safety (and readability).
memory_bfm_pkg.write(net, my_unsigned_address, my_std_logic_vector_data);
If you do not expect the receiver to receive massages of a type it can’t handle you can add this else statement
else unexpected_msg_type(msg_type); end if;
which will cause an unrecognize message to fail the testbench.
The sender of a message is the owner of that message while it’s being created. As soon as the
send procedure is
called that ownership is handed over to the receiver and the message passed to the
send call can no longer be used
to retrieve the information you pushed into it. If you need to keep the message information you can make a copy
Since memory is allocated whenever you push to a message its important that the receiver side deallocates that memory to avoid memory leaks. This can be done explicitly by deleting the message.
However, the typical receiver is a looping process that calls the
receive procedure as soon as the previous message
has been handled. To simplify the design of the such a receiver the
delete procedure is called first in the
receive procedure to delete the message from the previous loop iteration.
Replying to a Message¶
Replying to a message is done with the
reply procedure. Below is the previous message handler process which has
been updated to also handle read request messages. Every such message results in a reply message targeting the
message_handler: process is variable request_msg, reply_msg : msg_t; variable msg_type : msg_type_t; variable address : unsigned(7 downto 0); variable data : std_logic_vector(7 downto 0); variable memory : integer_vector(0 to 255) := (others => 0); begin receive(net, actor, request_msg); msg_type := message_type(request_msg); if msg_type = write_msg then address := pop(request_msg); data := pop(request_msg); memory(to_integer(address)) := to_integer(data); elsif msg_type = read_msg then address := pop(request_msg); data := to_std_logic_vector(memory(to_integer(address)), 8); reply_msg := new_msg(read_reply_msg); push(reply_msg, data); reply(net, request_msg, reply_msg); else unexpected_msg_type(msg_type); end if; end process;
Just like the
reply will hand message ownership to the receiver.
Receiving a Reply¶
If you want to await a specific message like the reply to a request message you can use the
procedure. Below is a read procedure for our memory BFM.
procedure read( signal net : inout event_t; constant address : in unsigned(7 downto 0); variable data : out std_logic_vector(7 downto 0)) is variable request_msg : msg_t := new_msg(read_msg); variable reply_msg : msg_t; variable msg_type : msg_type_t; begin push(request_msg, address); send(net, actor, request_msg); receive_reply(net, request_msg, reply_msg); msg_type := message_type(reply_msg); data := pop(reply_msg); delete(reply_msg); end;
receive_reply will block until the specified message is received. All other incoming messages will be ignored but
can be retrieved later. Note that we didn’t need a message type for the reply messages, the read procedure just
throws it away. However, we will see later that including it can be helpful when debugging a communication system.
Sending a request and directly receiving the reply is a common sequence of calls so it has been given a dedicated
request procedure. The two lines above can be replaced by
request(net, actor, request_msg, reply_msg);
Another approach to the read procedure is to think of it as two steps. The first step sends the the non-blocking read request and the second waits to get the requested data. The link between the two is the request message. This message is sometimes called a future since it represents the requested data that will be available in the future. Splitting blocking procedures like this allow you to initiate several concurrent transactions on different DUT interfaces or perform other tasks while waiting for the replies.
memory_bfm_pkg.non_blocking_read(net, address => x"80", future => future1); some_other_bfm_pkg.non_blocking_transaction(net, some_input_parameters, future2); <Do other things> memory_bfm_pkg.get(net, future1, data); some_other_bfm_pkg.get(net, future2, requested_information);
So far all request messages have been anonymous, I’ve only created an actor for the receiving part. In these situations
reply call can’t send a reply back to the sender so the reply message is placed in the receiver
receive_reply procedure called by the sender knows that the request message was anonymous and waits
for the reply to appear in the receiver outbox instead of its own inbox.
Some communication patterns, for example the publisher/subscriber pattern, requires that all messages are signed. To sign a message you can provide the sending actor when the message is created.
msg := new_msg(sender => sending_actor);
Sending/Receiving to/from Multiple Actors¶
message_handler process presented above had a single actor. However, the actor model is not limited to have one
actor for each concurrently running process. A process may have several actors, each representing some other object
like a channel. A typical receiver in such a design needs to act on messages from several actors and to support that
you can call
receive with an array of actors rather than a single actor. If several actors have messages the
procedure will return the oldest message from the leftmost actor with a non-empty inbox.
receive(net, actor_vec_t'(channel_1, channel_2), msg);
It’s also possible to send a message to multiple receiving actors. Just call
send with an array of receivers.
send(net, actor_vec_t'(receiver_1, receiver_2), msg);
There is no shared ownership of
msg once it’s sent. The sender loses ownership and each receiver get its own
Message passing based on the actor model is inherently asynchronous in nature. Sending a message takes no time which
means that the sender can send any number of messages before the receiver starts processing the first one. Transactions
requesting a reply, like the read transaction presented before, will naturally break this flow of unprocessed messages
by blocking while waiting for a reply. Sometimes it’s also useful to synchronize the sender and receiver on
transactions which initiate an action without expecting a reply, a write transaction for example. To do that we can
create a reply message with a positive or negative acknowledge to signal the completion of the transaction or the
failure to complete the request. Rather than doing that explicitly you can use one of the convenience procedures that
Instead of using the
reply procedure with a reply message the receiver can use
acknowledge with a
positive/negative response in the form of true/false boolean as the third parameter
acknowledge(net, request_msg, positive_ack);
On the sender side there is a matching
receive_reply procedure that will return that boolean.
receive_reply(net, msg, positive_ack);
There is also a
request procedure to be used in conjunction with
request(net, actor, msg, positive_ack);
Another approach to synchronization is to limit the number of unprocessed messages that a
receiver can have in its inbox. If the limit is reached, a new send to that receiver will block.
The default inbox size is
integer'high but it can be set to some other value when the actor is created.
constant my_actor : actor_t := new_actor("my actor", inbox_size => 1);
It’s also possible to resize the inbox of an already created actor.
resize(my_actor, new_size => 2);
Reducing the size below the number of messages in the inbox will cause a run-time failure.
A third way to synchronize actors is to have a dedicated message for that purpose but without any information exchange. The message exchange will just be an indication that the receiver is idling waiting for new messages.
request_msg := new_msg(wait_until_idle_msg); request(net, actor_to_synchronize, request_msg, reply_msg);
The sender will block on the
request call until the actor to synchronize has replied and the two actors becomes
synchronized. Since there is no information exchange there is no need to pop the reply message.
The actor to synchronize will have to add an if statement branch to handle the new message type. Below I’ve extended the message handling of the previous BFM example.
receive(net, actor, request_msg); msg_type := message_type(request_msg); if msg_type = wait_until_idle_msg then reply_msg := new_msg; reply(net, request_msg, reply_msg); elsif msg_type = write_msg then ... else unexpected_msg_type(msg_type); end if;
Note that no information is pushed to the reply message in this example but you may want to have a message type for debugging purposes.
Message Handlers and Verification Component Interfaces (VCI)¶
The synchronization based on
wait_until_idle_msg is something that can be used by many actors. We’ve seen before
how we can create transaction procedures like
write and we can also create such a procedure for this
message. To synchronize with the memory BFM actor we would just do
We can also create a reusable procedure for the message handling.
procedure handle_wait_until_idle( signal net : inout event_t; variable msg_type : inout msg_type_t; variable request_msg : inout msg_t) is variable reply_msg : msg_t; begin if msg_type = wait_until_idle_msg then handle_message(msg_type); reply_msg := new_msg; reply(net, request_msg, reply_msg); end if; end;
This is the same code I showed before to handle the wait until idle message with one addition - the call to the
handle_message is in itself a message handler, the simplest message handler possible.
The only thing it does is to set
msg_type to a special value
message_handled. To understand why we can look at
the updated BFM.
receive(net, actor, request_msg); msg_type := message_type(request_msg); handle_wait_until_idle(net, msg_type, request_msg); if msg_type = write_msg then ... else unexpected_msg_type(msg_type); end if;
msg_type has the value
message_handled and no more message handling
takes place in the following if statement. The
unexpected_msg_type procedure of the else branch will be called but
that procedure takes no action when the message type is
By putting the
wait_until_idle_msg message type and the
procedures in a package we can create a reusable verification component interface (VCI) that can be added to actors.
An actor can call several message handlers, that is add several interfaces, and you can create message handlers that
call other message handlers to bundle interfaces. The interface I just presented is actually already provided as a part
of VUnit’s synchronization VCI.
Receive and send procedures which may block on empty or full inboxes have an optional timeout parameter. For example
receive(net, actor, msg, timeout => 10 ns);
Reaching the timeout limit is an error that will fail the testbench. If you need to timeout a receive call without
failing you can use the
get_message subprograms. The
wait_for_message procedure below will be
ok if a message is received before the timeout and
if the timeout limit is reached.
wait_for_message(net, my_actor, status, timeout => 10 ns);
You can also see if an actor has at least one message in its inbox.
When there are messages in the inbox you can get the oldest with
get_message(net, my_actor, msg);
It’s also possible to wait for a reply with a timeout.
wait_for_reply(net, request_msg, status, timeout => 10 ns); if status = ok then get_reply(net, request_msg, reply_msg); end if;
Deferred Actor Creation¶
When finding an actor using the
find function there is a potential race condition. What if the actor hasn’t been
created yet? The default VUnit solution is that the
find function creates a temporary actor with limited
functionality and defer proper actor creation until the
new_actor function is called. The process calling
can send messages to this actor and can’t tell the difference. However, it’s not possible to call receive type of
procedures on such an actor. Full actor capabilities are acquired when the receiver process has created the actor with
The danger with this approach is if the actor “found” by the sender is never created, maybe as a result of a misspelled
name. In that case the sender will send messages that are never received but it will block on the second send since the
temporary actor has an inbox of size one. The safest way to avoid this is to not use
find but rather make the actor
constant available to the sender. It’s also possible to to disable the deferred creation by adding an extra parameter
find("actor name", enable_deferred_creation => false);
If the actor isn’t found the function returns
null_actor so to make this work you must make sure that the
find function is called after
new_actor, for example by adding an initial delay before making the call.
Another approach is to make sure that there are no deferred creations pending a short delay into the simulation, before
the actual testing starts. You can find out by calling the
A common message pattern is the publisher/subscriber pattern where a publisher actor publishes a messages rather than sending it. Actors interested in these messages subscribe to the publisher and the published messages are received just like messages sent directly to the subscribers. The purpose of this pattern is to decouple the publisher from the subscribers, it doesn’t have to know who the subscribers are and there is no need to update the publisher when subscribers are added or removed.
An example for how this pattern can be used is when you have a verification component monitoring an interface of the
DUT. Let’s say we have a simple adder with streaming input and output interfaces. The input interface consists of two
unsigned operands and a data valid signal while the output consists of an unsigned
sum and a data valid. The input
interface is controlled by a driver BFM which receives
add transactions as well as
wait_for_time to insert idle
cycles in the input stream.
wait_for_time is a standard VCI provided by the sync_pkg. The output
interface has a monitor process which creates sum messages from valid output sums. Just like the input driver doesn’t
know or care who’s sending the add transactions, the monitor doesn’t have to know who’s consuming the sum messages. To
achieve that it will publish the sum messages and just provide the publishing actor (
monitor_process : process is variable msg : msg_t; begin wait until rising_edge(clk) and (dv_out = '1'); msg := new_msg(sum_msg); push(msg, to_integer(sum)); publish(net, monitor, msg); end process;
In addition to the driver and the monitor there is a scoreboard process to verify the adder functionality. The
scoreboard subscribes to the sum messages published by the monitor using the
subscribe procedure. Rather than
having a single actor the scoreboard has several actors called channels and the
slave_channel is setup to subscribe
to messages published by the
The next step is to make sure that the scoreboard also receives the add transactions on the input interface. There are
several ways to do this. One is to build another monitor for the input interface and another is to let the driver
publish the add transactions. However, in order to demonstrate
com functionality this scoreboard will use a third
approach and let the scoreboard subscribe to inbound traffic to the driver. This can be done by adding a third
parameter to the
subscribe(master_channel, driver, inbound);
The default value used before is
published and it is also possible to subscribe to
outbound traffic is every output message from an actor regardless if that message is the result of a
With the two subscriptions at hand we can create a scoreboard process. The main flow of the code below is to wait for
add_msg on the
wait_for_time is ignored) and when it’s received wait for the associated
sum_msg on the
slave_channel. Once both these messages are available the scoreboard will use its reference
model to verify that the output data matches the input.
scoreboard_process : process is variable master_msg, slave_msg : msg_t; variable msg_type : msg_type_t; procedure do_model_check(indata, outdata : msg_t) is variable op_a, op_b, sum : natural; begin op_a := pop(indata); op_b := pop(indata); sum := pop(outdata); check_equal(sum, op_a + op_b); end; begin subscribe(master_channel, driver, inbound); subscribe(slave_channel, monitor); loop receive(net, master_channel, master_msg); msg_type := message_type(master_msg); handle_wait_until_idle(net, msg_type, master_msg); if msg_type = add_msg then receive(net, slave_channel, slave_msg); if message_type(slave_msg) = sum_msg then do_model_check(master_msg, slave_msg); end if; end if; end loop; end process;
In order for the test sequencer to know when the verification is complete it will send a
after all add transactions. That transaction is handled by the
handle_wait_until_idle message handler on the
scoreboard side. The example test sequencer below just sends 10 random add messages separated by a random delay
(not good for functional coverage but good enough for this example).
for i in 1 to 10 loop msg := new_msg(add_msg); push(msg, rnd.RandInt(0, 255)); push(msg, rnd.RandInt(0, 255)); send(net, driver, msg); wait_for_time(net, driver, rnd.RandTime(0 ns, 10 * clk_period)); end loop; wait_until_idle(net, master_channel);
Subscribing to messages actively being published is the classic form of the publisher/subscriber communication pattern while subscriptions on inbound or outbound traffic is more like eavesdropping. This has implications that you need to be aware of:
When receiving a message that has been published, a call to
receiveron that message will return the publisher and subscriber actors respectively. However, when receiving a message resulting from an inbound or outbound subscription the two functions will return the sender and the receiver for the original message transaction.
The subscriber of inbound and outbound traffic will receive all messages, not only those that would have been published if the decision was more active. For example, if someone sends a
wait_for_idletransaction to the driver it will also be sent to the subscriber which will act upon it “thinking” it was from the test sequencer. This wouldn’t be a problem if we had a monitor for the input interface only publishing add messages. It’s still possible to fix though, for example by only handling
wait_for_idletransactions aimed at the master channel.
if receiver(master_msg) = master_channel then handle_wait_until_idle(net, msg_type, master_msg); end if;
Since you can subscribe on inbound traffic you can also subscribe to the inbound traffic of a subscriber. This may not have great practical value but can, if misused, create an infinite loop of subscriptions which will hang the simulation.
A subscription on the outbound traffic of an actor won’t pick up messages sent anonymously.
A subscription on the inbound traffic of an actor won’t pick up replies to an anonymously request.
Although the intent of the publisher/subscriber pattern is to decouple the publisher from the subscribers it can still be affected if a subscriber inbox is full. A message transaction will be blocked until all of its subscribers and any regular receiver have available space in their inboxes.
An actor can unsubscribe from a subscription at any time by calling
unsubscribe with the same parameters used when
Message passing provides a communication mechanism an abstraction level above the normal signalling in VHDL.
This also means that there is a need for an equally elevated level of debugging. To support that
a number of built-in features specially targeting debugging.
One way of debugging is to inspect the messages that flow through the system, for example by subscribing to actor traffic. You can use previously presented functions to find out sender, receiver and message content but you can also convert a message to a string such that it can be logged.
The resulting string may look something like this
3:2 memory BFM -> test sequencer (read reply)
The first number (
3) is the message ID which is unique to this message. The second number (
2) is present
in reply messages and denotes the message ID for the request message. After that we have the sender
memory BFM) which sent the message to (
->) the receiver (
test sequencer). Finally, the value in
read reply) is the message type. All communicated messages have a message ID but not all messages
are replies, sender and receivers may be anonymous and not all messages have a message type. Fields missing a
value will be replaced with
com has limited knowledge of the contents of a message. All data pushed into a message is encoded
and is basically handled as a sequence of bytes without any overhead for type information.
know if four bytes represents an integer, four characters or something else. The interpretation of
these bytes takes place when the user pops data using a type specific pop function. The exception is the message
type for which the type overhead is included to provide better debugging. Higher levels of debug information,
for example that a message represents a read request to a specific address is something that the verification
Rather than manually logging messages you can quickly see all messages by showing the trace logs.
com_logger, and you enable the trace logs by showing log entries on the
trace log level.
show(com_logger, display_handler, trace);
Ignoring the initial part introduced by the logging framework (everything up to and including
TRACE -) we
still see a difference when compared to the string presented above.
30000 ps - vunit_lib:com - TRACE - test sequencer inbox => [3:2 memory BFM -> test sequencer (read reply)]
First is an actor mailbox (
test sequencer inbox), than an arrow (
=>) followed by the message string
enclosed in square brackets. This means that the message was removed from the mailbox, for example as a result
com also logs when a message is put into a mailbox. In this
example that event is logged 10 ns earlier and is the result of a
20000 ps - vunit_lib:com - TRACE - [3:2 memory BFM -> test sequencer (read reply)] => test sequencer inbox
Now that we have all these transactions available it becomes possible to follow sequences of events. For example, at time 0 ps we have the message with ID = 2 which is the request message for the reply above.
0 ps - vunit_lib:com - TRACE - [2:- test sequencer -> memory BFM (read)] => memory BFM inbox
Again, if you want higher level of debug information you can add debug logging to your BFM which may result in something like this.
0 ps - vunit_lib:com - TRACE - [2:- test sequencer -> memory BFM (read)] => memory BFM inbox 10000 ps - vunit_lib:com - TRACE - memory BFM inbox => [2:- test sequencer -> memory BFM (read)] 20000 ps - memory BFM - DEBUG - Reading x"21" from address x"80" 20000 ps - vunit_lib:com - TRACE - [3:2 memory BFM -> test sequencer (read reply)] => test sequencer inbox 30000 ps - vunit_lib:com - TRACE - test sequencer inbox => [3:2 memory BFM -> test sequencer (read reply)]
In addition to tracing messages you can also examine the state of the communication system. By calling
get_mailbox_state you can take a snapshot and examine all messages in an actor mailbox.
mailbox_state := get_mailbox_state(memory_bfm_pkg.actor, inbox);
mailbox_state is a record that you can expand and examine in your simulator. Be aware that this gives you a
glimpse of internal representations of data which we may change. It’s suitable for browsing but not something you
should act upon programmatically.
You can also create a string representation of the mailbox state by calling
The result is something like this
Mailbox: inbox Size: 2147483647 Messages: 0. 5:- - -> memory BFM (write) 1. 6:- - -> memory BFM (read)
The size is the maximum number of messages that the mailbox can contain (this is dynamically allocated) while the
list at the bottom shows the actual messages in the mailbox. Message 0 is the oldest message and the first one
to be returned when you call
You can also get an actor’s state as well as the string representation for that state
actor_state := get_actor_state(driver); debug(get_actor_state_string(driver));
The string representation contains information about both mailboxes, subscriptions and subscribers and if the actor’s creation is deferred. For example,
Name: driver Is deferred: no Mailbox: inbox Size: 2147483647 Messages: 0. 8:- - -> driver (add) 1. 9:- - -> driver (add) 2. 10:- - -> driver (add) Mailbox: outbox Size: 2147483647 Messages: Subscriptions: Subscribers: driver channel subscribes to inbound traffic
In this case the outbox is empty and driver doesn’t subscribe to anything. However, the
actor subscribes to inbound traffic to
Finally, you can get the state for the messenger which is the manager of the communication system. That state contains two lists - one with the states of all active actors (those not deferred) and one with the states of all deferred actors.
messenger_state := get_messenger_state; debug(get_messenger_state_string);
com maintains a number of deprecated APIs for better backward compatibility. Using these APIs will result in
a runtime error unless enabled by calling the
Earlier releases of
com would not cause a runtime error on timeout. This behavior can be enabled with the
deprecated APIs by calling
allow_timeout. If not, a timeout will result in an error with the exception of the
wait_for_reply procedures which return a status.
The deprecated APIs will be removed in the future so it’s recommended to replace these with contemporary APIs.