L
Lars Asplund
Guest
One of the previous posts (https://groups.google.com/forum/#!topic/comp.lang.vhdl/QYoZ_ufMP3g) went off topic to discuss the case for high-level modelling in VHDL using message passing. This is a post to bring the discussion on topic again by presenting the VUnit solution to message passing. Are there things you would like to see that is not covered by message passing in its purest form? For example, using message passing to model communication in hardware which takes time and has capacity limitations.
/Lars
Summary
======
When your testbench grows to include several concurrent statements like processes and components surrounding the DUT it becomes important to coordinate the verification effort between these statements. This can be as simple as using synchronizing events implemented with boolean signals but when communication involves significant information exchange and many modes like asynchronous, synchronous, broadcasting, and two-way interaction it becomes much more challenging. VUnit, the open source unit testing framework for VHDL, includes a high-level message passing mechanism to handle this
complexity (https://github.com/LarsAsplund/vunit/blob/master/vhdl/com/user_guide.md)
Concept
======
In real life we use different modes of communication all the time when making phone calls, sending emails, and posting status updates on Facebook. We do this with ease because the services we use let us focus on the information to exchange and with whom. We don't have to know where our counterparts are located and we don't have to care about infrastructure details like putting messages into FIFOs, routing them to the correct destination and so on.. The VUnit message passing mechanism, a.k.a. com, takes this approach to high-level testbench communication and let you communicate using intuitive subprogram calls like send, receive, publish, and request.
The next section provides a few examples on how com is used. The complete tutorial for com can be found at https://github.com/LarsAsplund/vunit/blob/master/vhdl/com/user_guide.md and a testbench example is found at https://github.com/LarsAsplund/vunit/tree/master/examples/com. Com was developed in the open by a number of community members. If you want to see the discussions that led to this solution have a look at this thread https://github.com/LarsAsplund/vunit/issues/23
Examples
=======
Inspired by the actor model (https://en.wikipedia.org/?title=Actor_model) a statement like a process that wants to communicate creates an actor for itself
variable test_sequencer : actor_t := create("test sequencer");
To communicate with another statement you must find its actor, e.g.
variable driver : actor_t := find("driver");
A message can now be sent over the net(work). Any communication problems are reported in the returned send receipt
send(net,test_sequencer,driver,"Hello!",receipt);
Messages are received, in this case by the driver, with the blocking receive procedure. The received payload can then be processed. Here I'm just printing it.
receive(net,driver,message);
report message.payload.all;
Note that the locations of the actors are hidden, they can be in different processes in the same file, in different files, or even in the same process.. You don't have to change the communication if you refactor the code such that actors are moved. No details about the transport of messages are exposed other than that there is some sort of net(work) involved. Message passing is, in this case, asynchronous, you can send thousands of messages before the receive procedure is called. The send call takes no time, only delta cycles.
In this case I'm sending a string message and string is the only datatype com natively handles. Other datatypes have to be encoded/decoded to/from string. For example, you can send an integer as encode(my_integer) and receive it with
my_integer := decode(message.payload.all);
Encode and decode functions for standard VHDL and IEEE datatypes are provided by com, but more importantly, com will generate these functions for your custom datatypes as well. For example, if you have a write_mem transaction defined by a record like this
type addr_data_msg_t is record
msg_type : addr_data_msg_type_t;
addr : integer;
data : std_logic_vector(7 downto 0);
end record addr_data_msg_t;
where msg_type is a custom enumeration with values representing the bus transactions for which this record is used, e.g. write_mem, then com will provide you with an encode function so you can do
send(net,test_sequencer,driver,encode((write_mem, 17, X"A5")),receipt);
but there is also a named encode function which makes the code more readable
send(net,test_sequencer,driver,write_mem(17, X"A5"),receipt);
which you can wrap in a local procedure to get something even more readable
write_mem(17, X"A5");
On the receiving side you will have support for parsing messages that arrive. They can have different msg_type and can be based on different records
receive(net, driver, message);
case get_msg_type(message.payload.all) is
when write_mem =>
-- Process write mem transaction
when read_mem =>
-- Process read mem transaction
...
So you don't have to do anything other than defining your message types to get the necessary support functions.
If you want to broadcast a message to anyone interested you do publish instead of send and remove the receiver parameter
publish(net,test_sequencer,"Hello!",status);
Actors interested in these publications have to subscribe to the publishing actor and then receive messages in the normal way using the receive procedure.
subscribe(driver,find("test sequencer"),status);
You don't have to change anything to the publisher or existing subscribers if a new actor becomes interested in what's being published and subscribes.
Two-way interaction can be handled in a number or ways but the simplest way to send a request and wait for a reply is
request(net,test_sequencer,driver,request_msg,reply_msg);
This will return the reply matching the request even if other messages arrive while waiting for the reply or requests are handled out of order at the receiving side. A request can be the basis for a synchronous transaction like read_mem, i.e. the call will return when the data is available.
These were a few examples of what you can do. For more examples and details jump to
https://github.com/LarsAsplund/vunit/blob/master/vhdl/com/user_guide.md
/Lars
Summary
======
When your testbench grows to include several concurrent statements like processes and components surrounding the DUT it becomes important to coordinate the verification effort between these statements. This can be as simple as using synchronizing events implemented with boolean signals but when communication involves significant information exchange and many modes like asynchronous, synchronous, broadcasting, and two-way interaction it becomes much more challenging. VUnit, the open source unit testing framework for VHDL, includes a high-level message passing mechanism to handle this
complexity (https://github.com/LarsAsplund/vunit/blob/master/vhdl/com/user_guide.md)
Concept
======
In real life we use different modes of communication all the time when making phone calls, sending emails, and posting status updates on Facebook. We do this with ease because the services we use let us focus on the information to exchange and with whom. We don't have to know where our counterparts are located and we don't have to care about infrastructure details like putting messages into FIFOs, routing them to the correct destination and so on.. The VUnit message passing mechanism, a.k.a. com, takes this approach to high-level testbench communication and let you communicate using intuitive subprogram calls like send, receive, publish, and request.
The next section provides a few examples on how com is used. The complete tutorial for com can be found at https://github.com/LarsAsplund/vunit/blob/master/vhdl/com/user_guide.md and a testbench example is found at https://github.com/LarsAsplund/vunit/tree/master/examples/com. Com was developed in the open by a number of community members. If you want to see the discussions that led to this solution have a look at this thread https://github.com/LarsAsplund/vunit/issues/23
Examples
=======
Inspired by the actor model (https://en.wikipedia.org/?title=Actor_model) a statement like a process that wants to communicate creates an actor for itself
variable test_sequencer : actor_t := create("test sequencer");
To communicate with another statement you must find its actor, e.g.
variable driver : actor_t := find("driver");
A message can now be sent over the net(work). Any communication problems are reported in the returned send receipt
send(net,test_sequencer,driver,"Hello!",receipt);
Messages are received, in this case by the driver, with the blocking receive procedure. The received payload can then be processed. Here I'm just printing it.
receive(net,driver,message);
report message.payload.all;
Note that the locations of the actors are hidden, they can be in different processes in the same file, in different files, or even in the same process.. You don't have to change the communication if you refactor the code such that actors are moved. No details about the transport of messages are exposed other than that there is some sort of net(work) involved. Message passing is, in this case, asynchronous, you can send thousands of messages before the receive procedure is called. The send call takes no time, only delta cycles.
In this case I'm sending a string message and string is the only datatype com natively handles. Other datatypes have to be encoded/decoded to/from string. For example, you can send an integer as encode(my_integer) and receive it with
my_integer := decode(message.payload.all);
Encode and decode functions for standard VHDL and IEEE datatypes are provided by com, but more importantly, com will generate these functions for your custom datatypes as well. For example, if you have a write_mem transaction defined by a record like this
type addr_data_msg_t is record
msg_type : addr_data_msg_type_t;
addr : integer;
data : std_logic_vector(7 downto 0);
end record addr_data_msg_t;
where msg_type is a custom enumeration with values representing the bus transactions for which this record is used, e.g. write_mem, then com will provide you with an encode function so you can do
send(net,test_sequencer,driver,encode((write_mem, 17, X"A5")),receipt);
but there is also a named encode function which makes the code more readable
send(net,test_sequencer,driver,write_mem(17, X"A5"),receipt);
which you can wrap in a local procedure to get something even more readable
write_mem(17, X"A5");
On the receiving side you will have support for parsing messages that arrive. They can have different msg_type and can be based on different records
receive(net, driver, message);
case get_msg_type(message.payload.all) is
when write_mem =>
-- Process write mem transaction
when read_mem =>
-- Process read mem transaction
...
So you don't have to do anything other than defining your message types to get the necessary support functions.
If you want to broadcast a message to anyone interested you do publish instead of send and remove the receiver parameter
publish(net,test_sequencer,"Hello!",status);
Actors interested in these publications have to subscribe to the publishing actor and then receive messages in the normal way using the receive procedure.
subscribe(driver,find("test sequencer"),status);
You don't have to change anything to the publisher or existing subscribers if a new actor becomes interested in what's being published and subscribes.
Two-way interaction can be handled in a number or ways but the simplest way to send a request and wait for a reply is
request(net,test_sequencer,driver,request_msg,reply_msg);
This will return the reply matching the request even if other messages arrive while waiting for the reply or requests are handled out of order at the receiving side. A request can be the basis for a synchronous transaction like read_mem, i.e. the call will return when the data is available.
These were a few examples of what you can do. For more examples and details jump to
https://github.com/LarsAsplund/vunit/blob/master/vhdl/com/user_guide.md