The check package is an assertion library for VHDL providing the more commonly used assertions as a number of check procedures and functions.
The check subprograms are mainly a number conditional log messages. If the condition represents a failing check then an error message is issued using the VUnit log package. A passing check has no effect other than updating the passing check statistics.
Every check you do is handled by a checker. There is a default checker that is used when none is specified but you can also create multiple custom checkers. For example
check(re = '1', "Expected active read enable at this point");
will use the default checker while
check(my_checker, re = '1', "Expected active read enable at this point");
will use the custom my_checker
. A checker is just a (shared) variable
variable my_checker : checker_t;
All procedures presented in this user guide are available for both the default checker and the custom checkers. The difference is the first checker_t
parameter which only exists for custom checker procedures. To make the user guide more compact we present this as an optional parameter using brackets. For example
procedure checker_init (
[variable checker : inout checker_t;]
constant default_level : in log_level_t := error;
variable logger : inout logger_t);
The full verbose API description can always be found in check_api.vhd.
There are also a number of functions in the check package. These are only available for the default checker and the reason is that checker_t
is defined as a protected type in the VHDL 2002 and 2008 implementations. Protected types cannot be input to a function so in these cases the check package will provide a procedure which will return the same result in an output parameter.
Since a check is a conditional log most of the checker initialization parameters are the same as you'll find for logger initialization. These parameters are described in the log user guide and are used to initialize the checker internal logger that outputs any error message. However, the default values for file_name
and display_format
are different and there is also a default_level
parameter unique to checker_init
. default_level
is used to specify the log level for the error message unless specified in the check subprogram as explained in the following chapters.
procedure checker_init (
[variable checker : inout checker_t;]
constant default_level : in log_level_t := error;
constant default_src : in string := "";
constant file_name : in string := "error.csv";
constant display_format : in log_format_t := level;
constant file_format : in log_format_t := off;
constant stop_level : in log_level_t := failure;
constant separator : in character := ',';
constant append : in boolean := false);
It is also possible to initialize a checker to use an already defined logger using the following procedure
procedure checker_init (
[variable checker : inout checker_t;]
constant default_level : in log_level_t := error;
variable logger : inout logger_t);
The check package provides a basic check
procedure which is similar to the VHDL assert
statement
check(re = '1', "Expected active read enable at this point");
The first parameter is the expression to check and the second parameter is the error message issued if the expression is false. Assuming this check fails and you've initialized the default checker with the default values the error message will be
ERROR: Expected active read enable at this point
If you wish to have another log level than the default one set by checker_init
you can override this for each check call. For example
check(re = '1', "Expected active read enable at this point", failure);
A failing check is always counted as failing check regardless of the level but the level affects whether or not the simulation stops as controlled by the stop_level
parameter to checker_init
.
Note that when using the VUnit Python test runner the default checker stop_level
is set to error
when calling test_runner_setup
. The reason is that the Python test runner has the ability to restart the simulation with the next test case so that all test cases are run despite of an error while guaranteeing that the error state of the failing test case doesn't propagate into the next. If you're not using the Python test runner and have stop_level
at failure
as a way to continue on error
you don't have this guarantee.
The check
procedure described in the previous section has two additional parameters, line_num
and file_name
. These are normally not set by the user but by the location preprocessor such that the location of a failing check is included in the error message. The location preprocessor is further described in the log user guide. Preprocessor parameters are always placed at the end of the parameter list and they have "good" default values such that the function behaves nicely even if the preprocessor isn't used.
procedure check(
[variable checker : inout checker_t;]
constant expr : in boolean;
constant msg : in string := "Check failed!";
constant level : in log_level_t := dflt;
constant line_num : in natural := 0;
constant file_name : in string := "");
The check
procedure described so far doesn't reveal whether the check passed or not. If you want that information to control the flow of your test and your testbench is setup to continue on a failing check you have a number of options. You can use this procedure where the pass
output is false
on a failing check
procedure check(
[variable checker : inout checker_t;]
[variable pass : out boolean;]
constant expr : in boolean;
constant msg : in string := "Check failed!";
constant level : in log_level_t := dflt;
constant line_num : in natural := 0;
constant file_name : in string := "");
or you can use this function which returns the same information
impure function check(
constant expr : in boolean;
constant msg : in string := "Check failed!";
constant level : in log_level_t := dflt;
constant line_num : in natural := 0;
constant file_name : in string := "")
return boolean;
or you can see if there has been any errors so far
procedure checker_found_errors (
[variable checker : inout checker_t;]
variable result : out boolean);
impure function checker_found_errors
return boolean;
or you can use any of the following subprograms to get more details.
procedure get_checker_stat (
[variable checker : inout checker_t;]
variable stat : out checker_stat_t);
impure function get_checker_stat
return checker_stat_t;
checker_stat_t
is a record containing pass/fail information.
type checker_stat_t is record
n_checks : natural;
n_failed : natural;
n_passed : natural;
end record;
A checker will continuously update its statistics counters as new check subprograms are called. If you want to collect the statistics for parts of your test you can make intermediate readouts using the get_checker_stat
subprograms and then reset the counters to zero using
procedure reset_checker_stat [(
variable checker : inout checker_t)];
Another way of collecting statistics for different parts is to use several separate checkers.
Variables of type checker_stat_t
can be added to or subtracted from each other using the normal -
and +
operators. There is also a to_string
function defined to allow for logging/reporting of statistics, for example
info(to_string(get_checker_stat));
In addition to the basic check subprograms the check package also provides a number of more specialized checks. These checks can be divided into four different types
- Point checks
- Relation checks
- Sequential checks
- Unconditional checks
These types and the checks belonging to each type are described in the following chapters.
Common to all point checks is that the condition for failure is evaluated at a single point in time, either when the subprogram is called as part of sequential code or synchronous to a clock in a clocked and usually concurrent procedure call. There are five unclocked versions of each point check and they correspond to the function and four procedures available for the basic check. The only difference to the parameter lists is that the boolean expr
parameter is replaced by one or more parameters specific to the point check.
The unclocked procedures have the following format. The four variants comes from the different combinations of using the two first optional parameters.
procedure check_<name>(
[variable checker : inout checker_t;]
[variable pass : out boolean;]
<specific parameters>
constant msg : in string := "Check failed!";
constant level : in log_level_t := dflt;
constant line_num : in natural := 0;
constant file_name : in string := "");
The function has the following format.
impure function check_<name>(
<specific parameters>
constant msg : in string := "Check failed!";
constant level : in log_level_t := dflt;
constant line_num : in natural := 0;
constant file_name : in string := "")
return boolean;
The clocked procedures come from the following format with and without the optional parameter.
procedure check_<name>(
[variable checker : inout checker_t;]
signal clock : in std_logic;
signal en : in std_logic;
<specific parameters>
constant msg : in string := "Check failed!";
constant level : in log_level_t := dflt;
constant active_clock_edge : in edge_t := rising_edge;
constant line_num : in natural := 0;
constant file_name : in string := "");
edge_t
is an enumerated type:
type edge_t is (rising_edge, falling_edge, both_edges);
The condition for failure is continuously evaluated on the clock edge(s) specified by active_clock_edge
as long as en = '1'
.
The figure below shows an example using the concurrent version of check_true
which evaluates a boolean expr
just like the basic check. The only difference is that check_true
provides the two concurrent procedure formats which the basic check does not.
expr
is evaluated on every rising clock edge except for edge 3 where en
is low. This means that the check doesn't fail despite the clock cycle where expr
is false.
Special Parameter | Type |
---|---|
expr | boolean or std_logic |
check_true
passes when expr
is true
/1
/H
.
Special Parameter | Type |
---|---|
expr | boolean or std_logic |
check_false
passes when expr
is false
/0
/L
.
Special Parameter | Type |
---|---|
antecedent_expr | boolean or std_logic |
consequent_expr | boolean or std_logic |
The unclocked subprograms use boolean
parameters while the clocked procedures use std_logic
.
check_implication
checks logical implication and passes unless antecedent_expr
is true
/1
/H
and consequent_expr
is false
/0
/L
.
Special Parameter | Type |
---|---|
expr | std_logic_vector or std_logic |
check_not_unknown
passes when expr
contains none of the metavalues U
, X
, Z
, W
, or -
.
Special Parameter | Type |
---|---|
expr | std_logic_vector |
check_zero_one_hot
passes when expr
contains none of the metavalues U
, X
, Z
, W
, or -
and there are zero or one bit equal to 1
or H
.
Special Parameter | Type |
---|---|
expr | std_logic_vector |
check_one_hot
passes when expr
contains none of the metavalues U
, X
, Z
, W
, or -
and there is exactly one bit equal to 1
or H
.
Relation checks are used to check whether or not a relation holds between two expressions, for example if (a + b) = c
. They support the following five unclocked formats.
procedure check_<name>(
[variable checker : inout checker_t;]
[variable pass : out boolean;]
<specific parameters>
constant msg : in string := "";
constant level : in log_level_t := dflt;
<preprocessor parameters>);
impure function check_<name>(
<specific parameters>
constant msg : in string := "";
constant level : in log_level_t := dflt;
<preprocessor parameters>)
return boolean;
Note the difference in default value for msg
when compared to the point checks. Point checks have Check failed!
as default while relation checks use an empty string. The reason is that relation checks generate error messages of their own that describes how the relation failed. Any msg
input provided by the user is added to that error message as additional information.
There's also a difference in the preprocessor parameters. check_equal
and check_match
have the line_num
and file_name
parameters just as the point checks but check_relation
also has a parameter called auto_msg
described later.
got | expected |
The got
and expected
parameters can have the following combination of types
got | expected |
---|---|
unsigned | unsigned |
natural | unsigned |
unsigned | natural |
std_logic_vector | std_logic_vector |
std_logic_vector | unsigned |
unsigned | std_logic_vector |
signed | signed |
integer | signed |
signed | integer |
integer | integer |
std_logic | std_logic |
boolean | std_logic |
std_logic | boolean |
boolean | boolean |
Preprocessor Parameter | Type | Default Value |
---|---|---|
line_num | natural | 0 |
file_name | string | "" |
check_equal
passes when got
equals expected
. When comparing std_logic
values with boolean
values 1
equals true
and 0
equals false
. Note that the std_logic
don't care (-
) only equals itself. If you want an equality like "0011" = "00--"
to pass you should use check_relation
with the matching equality operator (?=
) or check_match
instead.
If an check fails you will get an error message on the following format.
ERROR: Equality check failed! Got <got value>. Expected <expected value>. <msg input string if any>.
When you compare bit vectors, integer
and natural
type of values the error message will output the values on both formats. For example, here is an error message when a check_equal
between a signed
and an integer
value fails.
ERROR: Equality check failed! Got -256 (1_0000_0000). Expected 1010_0101 (-91).
Special Parameter | Type |
---|---|
expr | boolean, std_ulogic, or bit |
Preprocessor Parameter | Type | Default Value |
---|---|---|
auto_msg | string | "" |
line_num | natural | 0 |
file_name | string | "" |
expr
is intended to be a relational expression and three different types are supported. In case a matching relational operator is used the relation will return a std_ulogic
or bit
depending on the operands. All other relations will return a boolean.
check_relation
passes when expr
evaluates to true
in the boolean case and to 1
in the std_ulogic
and bit
cases. This means that the boolean
case behaves just like check_true
. The additional value of this check comes when you enable the check preprocessor in your VUnit run script.
ui = VUnit.from_argv()
ui.enable_check_preprocessing()
The check preprocessor scans your code for calls to check_relation
and then parses expr
as a VHDL relation. From that it will generate an error message describing how the relation failed. For example, the check
check_relation(real_time_clock <= timeout, "Response too late.");
will generate the following error message if it fails.
ERROR: Relation real_time_clock <= timeout failed! Left is 23:15:02. Right is 23:15:04. Response too late.
This works for any type of relation between any types as long as the operator and the to_string
function are defined for the types involved. In the example the operands are of a custom clock_t
for which both the <=
operator and the to_string
function have been defined.
In addition to the line_num
and file_name
preprocessor parameters all check_relation
subprograms have another preprocessor parameter auto_msg
which is set by the check preprocessor and forms the first sentence of the error message. auto_msg
is the empty string by default so without the check preprocessor the error message will be just the msg
provided by the user
The left and right hand sides of the relation are evaluated twice, once when the relation is evaluated and once to create the error message so if you have a call like this
check_relation(counter_to_verify = get_and_increment_reference_counter(increment_with => 3));
The reference counter will be incremented with 6 which is not what you expect by just looking at the code before the preprocessor has generated the auto_msg
which will be a string containing to_string(get_and_increment_reference_counter(increment_with => 3))
.
Conclusion: Do not use impure functions in your expression. If you have a case like this you can do something like
ref_cnt := get_and_increment_reference_counter(increment_with => 3);
check_relation(counter_to_verify = ref_cnt);
or since this is an equality relation, probably between standard countable types, use check_equal
instead. check_equal
has the left and right hand operands separated in the call itself so in that case there is no need for a second evaluation in order to create the error message.
The check preprocessor has a simplified parser to determine what the relation operator in the expression is and what the left and right hand operands are. For example, it knows that this is an inequality since that is the only relational operator on the "top-level".
check_relation((a = b) /= (c = d));
It also knows that this isn't a relation since there's no relational operator on the top-level.
check_relation((a = b) and c);
This will result in a syntax error from the check preprocessor
SyntaxError: Failed to find relation in check_relation((a = b) and c)
However, its knowledge about precedence is limited to parenthesis so it will not understand that this identical expression isn't a relation.
check_relation(a = b and c);
If this logical expression returns false the check will generate an error message claiming that a relation failed and that to_string(a)
was the left value and to_string(b and c)
was the right value.
Conclusion: Use check_relation
for relations as intended!
The check example testbench also contain another highly unlikely way to fool the parser.
It should also be noted that the parser can handle that there are relational operators within the check call but outside of the expr
parameter. For example, it won't be fooled by the relational operators appearing within strings and comments of this call.
check_relation(len("""Heart"" => <3") = -- The string contains <, so does
-- this comment
12, "Incorrect length of ""<3 string"".");
got | expected |
The got
and expected
parameters can have the following combination of types
got | expected |
---|---|
unsigned | unsigned |
std_logic_vector | std_logic_vector |
signed | signed |
std_logic | std_logic |
Preprocessor Parameter | Type | Default Value |
---|---|---|
line_num | natural | 0 |
file_name | string | "" |
check_match
passes when got
equals expected
but differs from check_equal
in that a don't care (-
) bit equals anything.
Sequence checks are checks that use several clock cycles to determine whether or not the desired property holds.
check_stable
supports four different clocked formats. The expr
parameter can be std_logic
or std_logic_vector
and the call can be made with or without the initial custom checker parameter.
procedure check_stable(
[variable checker : inout checker_t;]
signal clock : in std_logic;
signal en : in std_logic;
signal start_event : in std_logic;
signal end_event : in std_logic;
signal expr : in std_logic or std_logic_vector;
constant msg : in string := "Check failed!";
constant level : in log_level_t := dflt;
constant active_clock_edge : in edge_t := rising_edge;
constant line_num : in natural := 0;
constant file_name : in string := "");
check_stable
passes if the expr
parameter is stable in the window defined by the start_event
and end_event
parameters. The window starts at an active (according to active_clock_edge
) and enabled (en = '1'
) clock edge for which start_event = '1'
and it ends at the next active and enabled clock edge for which end_event = '1'
. expr
is sampled for a reference value at the start event and is considered stable if it keeps that reference value at all enabled active clock edges within the window, including the clock edge for the end event. Bits within expr
may change drive strength ('0'
<->'L'
or '1'
<->'H'
) and still be considered stable. Below is an example with two windows that will pass.
Here are two examples of failing checks. Note that any unknown value (U
, X
, Z
, W
, or -
) will cause the check to fail even if the unknown value is constant. The check will also fail if start_event
or end_event
in an active window has an unknown value.
check_stable
can handle one clock cycle windows and back-to-back windows. If a second window is started before the previous is completed the second start event will be ignored and the window will be completed by the next end event.
check_next
supports two different formats. One with and one without the initial custom checker parameter.
procedure check_next(
[variable checker : inout checker_t;]
signal clock : in std_logic;
signal en : in std_logic;
signal start_event : in std_logic;
signal expr : in std_logic;
constant msg : in string := "Check failed!";
constant num_cks : in positive := 1;
constant allow_overlapping : in boolean := true;
constant allow_missing_start : in boolean := true;
constant level : in log_level_t := dflt;
constant active_clock_edge : in edge_t := rising_edge;
constant line_num : in natural := 0;
constant file_name : in string := "");
check_next
passes if expr = '1'
num_cks
active (according to active_clock_edge
) and enabled (en = '1'
) clock edges after a start event. The start event is defined by an active and enabled clock edge for which start_event = '1'
. Below is an example of a passing check. The start event is sampled at clock edge two. expr
is expected to be high four enabled clock edges after that which is at clock edge seven due to en
being low at clock edge five.
When allow_overlapping
is true
check_next
will allow a new start event before the check based on the previous start event has been completed. Here is an example with two overlapping and passing sequences.
In case allow_overlapping
is false
check_next
will fail at the second start event
When allow_missing_start
is true
check_next
will allow expr = '1'
when there is no corresponding start event. When allow_missing_start
is false
such a situation will lead to a failure.
check_next
will handle the weak values L
and H
in the same way as 0
and 1
, respectively.
check_sequence
supports two different formats. One with and one without the initial custom checker parameter.
procedure check_sequence(
[variable checker : inout checker_t;]
signal clock : in std_logic;
signal en : in std_logic;
signal event_sequence : in std_logic_vector;
constant msg : in string := "Check failed!";
constant trigger_event : in trigger_event_t := penultimate;
constant level : in log_level_t := dflt;
constant active_clock_edge : in edge_t := rising_edge;
constant line_num : in natural := 0;
constant file_name : in string := "");
check_sequence
passes if a number of events, represented by the bits in the event_sequence
parameter, are activated (bit = '1'
or 'H'
) in sequence at consecutive active (according to active_clock_edge
) and enabled (en = '1'
) clock edges. check_sequence
supports three different modes of operation as controlled by the trigger_event
parameter:
first_pipe
- The sequence is started when the leftmost bit ofevent_sequence
is activated. This will also triggercheck_sequence
to verify that the remaining bits are activated at the following active and enabled clock edges.check_sequence
will also verify new sequences starting before the first is completed.
The figure below shows two overlapping sequences that pass.
In this example the sequence is started but not completed and the check fails.
first_no_pipe
- Same asfirst_pipe
with the exception that only one sequence is verified at a time. New sequences starting before the previous is verified will be ignored.
In this example we have two sequences, the first is completed while the second is interrupted. However, since only one sequence is handled at a time the second is ignored and the check pass.
penultimate
- The difference with the previous modes is thatcheck_sequence
only verifies the last event (the rightmost bit) when all the preceding events in the sequence have been activated. This means that a started sequence that is interrupted before the second to last bit is activated will pass.check_sequence
will also verify new sequences starting before the first is completed.
The figure below shows two overlapping sequences which pass and then an early interrupted sequence that doesn't cause a failure in this mode (which it did in the example for the first_pipe
mode.
In this example the sequence is interrupted after the second to last bit is activated and the check fails.
The check package has two unconditional checks, check_passed
and check_failed
, that contains no expression parameter to evaluate. They are used when the pass/fail status is already given by the program flow. For example,
if <some condition> then
<do something>
check_passed;
else
<do something else>
check_failed("This was not expected");
end if;
With no expr
parameter there are also fewer usable formats for these checkers.
procedure check_passed [(
variable checker : inout checker_t)];
procedure check_failed(
[variable checker : inout checker_t;]
constant msg : in string := "Check failed!";
constant level : in log_level_t := dflt;
constant line_num : in natural := 0;
constant file_name : in string := "");