When designing complex designs, I have come to use assertions more and more often. I've seen their benefits, and in my own design I have tried to get the most out of them by extending their traditional use. I thought I'd share my experiences and thoughts on these matters.
At first some short general notes on assertions. Assertions in VHDL is a means of producing an error at run-time if some condition is not met 1. An assertion for the signal
err looks like this:
An assertion at run-time results in the simulator printing a message about it, or halting the simulation depending on its settings. Assertions can be clocked (put within
if rising_edge(clk) statements) or combinatorial.
Assertions is primarily a simulation/behavioral construct, but at least XST supports static assertions 2.
Benefits of designing with assertions
Using assertions in the HDL code for catching unexpected behavior has some clear benefits, like
- Catching bugs close to their origin
- Once implemented, they "stay there" and will continue to be activated for all future simulations
- Much faster to code than two other common means of catching bugs:
- writing testbenches, studying the designs internal signals in the time domain
- checking functionality through input/expected output
- Will catch bugs earlier in the design process compared to 1. and 2. above
- Because of their low-level nature, they are more likely to catch corner case bugs than more high-level tests (like 2. above)
I like to view the use of assertions as a sort of of built-in unit tests, using Software Testing terminology. Although assertions might only be able to test some limited features of the units, many of the benefits that unit testing has to offer will be achieved.
It is also worth mentioning that assertions generally come to a low cost in terms of increased simulation time 3.
Traditional use of assertions
Traditional use of assertions include
- checking of valid signal values of input interfaces
- example: for a Set-Reset-Flipflop, checking that set and reset aren't driven high on the same time
- asserting for existing error signals
- example: Overflow or Underflow signals of FIFOs
- checking that unexpected combinations of internal signals don't occur
- example: for a Flipflop providing both the ordinary output
Qand a negated output
nQ, checking that
(Q xor nQ)=1
- example: for a Flipflop providing both the ordinary output
These traditional usages of assertions are important and in my opinion they should be used as much as possible.
Taking the use of assertions up one level
One of the benefits of using assertions as described above is that they are implemented on a low level. However, extending the use to a more "functional" level will give all the other benefits.
Consider an example unit 'sparser', with some implemented functionality, through which data flows with ready/write enable-interfaces on both sides. In my case, the unit was supposed to "sparse out" data so equal data words don't happen on consecutive clock flanks.
What I typically do for such units is to add a monitorer unit, 'monitorer', only intended for simulation, that listens to both the left and the right interface of the unit:
The unit can check for a number of things that the unit should or should not do, e.g.:
- making sure all data written to the unit is then read from the unit (the unit doesn't lose any data), if that should be the case
- checking that no equal data on consecutive clock flanks is allowed to be read out (which was the functionality the unit was supposed to implement in my case)
The unit can be written only for simulation which makes it easier to code and faster to simulate.
...and one more level
After having started to use the assertion strategies described in the section above - and seeing the big benefits of it - I soon got the idea of extending the strategy one more level: running a full-fledged behavioral model of the unit parallel to the actual unit, comparing input and output and demanding 1:1 correspondance, asserting for any discrepancy.
This will work for units where a behavioral model can easily be written. In addition to that, it is of use only, of course, if the behavioral model differs in its implementation from the synthesizable model 4.
One typical use for this strategy would be for a unit implementing a hardware integer multiplier:
The multiplier might be implemented through the Booth Algorithm, or maybe by using FPGA silicon resources such as the DSP48 blocks of Xilinx's FPGAs, or in any other way.
Such a hardware multiplier has three properties which in this example will make it perfectly suitable for this assertion strategy:
- the multiplication product will be bit-precise
- the unit has a fixed, known delay
- a behavioral model can be written very easily
The behavioral model in this case will cast the input integers to VHDL's REAL datatype, perform the multiplication and cast back to integer 5:
variable factor1, factor2, product: integer;
-- in some sequential environment:
product := integer( real(factor1) * real(factor2) );
product is then delayed appropriately and compared to the output of the unit.
The increased simulation time for adding such a unit will be negligible. Performing a floating-point multiplication is nothing compared to simulating a large number of signal interactions in the RTL multiplier.
...a lesson learned about the dangers of delegating bug-hunting:
- Assertions in Verilog can be achieved through SystemVerilog/OVL. ↩
- that is, where the assertion condition is made up of generics or constants so it can be evaluated at compile-time. If such an assertion fires, depending on the severity level, XST will halt synthesis with an error message. For non-static assert conditions, you don't need to enclose them within '--synthesis translate_off/on' clauses, XST will simply ignore them. ↩
- This is mainly due to their usually simple nature and also them being coded with non-synthesizable/behavioral code (variables rather than signals) - however, this generally also holds when more complex behavioral models are being executed according to my "extended usages" suggested below. ↩
- typically, it should. The unit itself describes and implementation, whereas the behavioral model should describe a behaviour. ↩
- casting a real to an integer in VHDL will, if the simulation tool follows the VHDL specification, perform a round-to-nearest of the resulting REAL, taking care of possible floating-point errors. This method will of course only work up to a certain number of bits for the multiplication product - for an unsigned, approximately 53 bits on a 64 bit system (eps(2^53-1) = 1). ↩