Functional Coverage Made Easy with VHDL’s OSVVM

Capturing functional coverage does not require a verification language. It does not need to be declarative. It simply requires a data structure. VHDL’s OSVVM makes capturing high fidelity (really detailed) functional coverage easy and concise.

In my previous post, “Why You Need Functional Coverage”, we looked at what is functional coverage and why you need it. We also noted functional coverage can be written using any code. CoveragePkg and language syntax are solely intended to simplify this effort.

In this post, we will first look at implementing functional coverage manually (without CoveragePkg). Then we will look at using CoveragePkg to capture item and cross coverage. We will see that with CoveragePkg, modeling functional coverage is simple, concise, and powerful.

1. Item (Point) Coverage done Manually

In this subsection we write item coverage using regular VHDL code. While for most problems this is the hard way to capture coverage, it provides a basis for understanding functional coverage and why we can implement it with a data structure.

In a packet based transfer (such as across an ethernet port), most interesting things happen when the transfer size is at or near either the minimum or maximum sized transfers. It is important that a number of medium sized transfers occur, but we do not need to see as many of them. For this example, lets assume that we are interested in tracking transfers that are either the following size or range: 1, 2, 3, 4 to 127, 128 to 252, 253, 254, or 255. The sizes we look for are specified by our test plan.

We also must decide when to capture (aka sample) the coverage. In the following code, we use the rising edge of clock where the flag TransactionDone is 1.

signal Bin : integer_vector(1 to 8) ;
 . . .
   wait until rising_edge(Clk) and TransactionDone = '1' ;
   case to_integer(unsigned(ActualData)) is
     when   1 =>          Bin(1) <= Bin(1) + 1 ;
     when   2 =>          Bin(2) <= Bin(2) + 1 ;
     when   3 =>          Bin(3) <= Bin(3) + 1 ;
     when   4 to 127 =>   Bin(4) <= Bin(4) + 1 ;
     when 128 to 252 =>   Bin(5) <= Bin(5) + 1 ;
     when 253 =>          Bin(6) <= Bin(6) + 1 ;
     when 254 =>          Bin(7) <= Bin(7) + 1 ;
     when 255 =>          Bin(8) <= Bin(8) + 1 ;
     when others =>
   end case ;
 end process ;

Any coverage can be written this way. However, this is too much work and too specific to the problem at hand. We could make a small improvement to this by capturing the code in a procedure. This would help with local reuse, but there are still no built-in operations to determine when testing is done, to print reports, or to save results and the data structure to a file.

2. Basic Item (Point) Coverage with CoveragePkg

In this subsection we use CoveragePkg to write the item coverage for the same packet based transfer sizes created in the previous section manually. Again, we are most interested in the smallest and largest transfers. Hence, for an interface that can transfer between 1 and 255 words we will track transfers of the following size or range: 1, 2, 3, 4 to 127, 128 to 252, 253, 254, and 255.
The basic steps to model functional coverage are declare the coverage object, create the coverage model, accumulate coverage, interact with the coverage data structure, and report the coverage.
Coverage is modeled using a data structure stored inside of a coverage object. The coverage object is created by declaring a shared variable of type CovPType, such as CovBin1 shown below.

architecture Test1 of tb is
  shared variable CovBin1 : CovPType ;

Internal to the data structure, each bin in an item coverage model is represented by a minimum and maximum value (effectively a range). Bins that have only a single value, such as 1 are represented by the pair 1, 1 (meaning 1 to 1). Internally, the minimum and maximum values are stored in a record with other bin information.
The coverage model is constructed by using the method AddBins and the function GenBin. The function GenBin transforms a bin descriptor into a set of bins. The method AddBins inserts these bins into the data structure internal to the protected type. Note that when calling a method of a protected type, such as AddBins shown below, the method name is prefixed by the protected type variable name, CovBin1. The version of GenBin shown below has three parameters: min value, max value, and number of bins. The call, GenBin(1,3,3), breaks the range 1 to 3 into the 3 separate bins with ranges 1 to 1, 2 to 2, 3 to 3.

TestProc : process
  --                    min, max, #bins
  CovBin1.AddBins(GenBin(1,   3,   3)); -- bins 1 to 1, 2 to 2, 3 to 3
  . . .

Additional calls to AddBins appends additional bins to the data structure. As a result, the call, GenBin(4, 252, 2), appends two bins with the ranges 4 to 127 and 128 to 252 respectively to the coverage model.

CovBin1.AddBins(GenBin(  4, 252, 2)) ; -- bins 4 to 127 and 128 to 252

Since creating one bin for each value within a range is common, there is also a version of GenBin that has two parameters: min value and max value which creates one bin per value. As a result, the call GenBin(253, 255) appends three bins with the ranges 253 to 253, 254 to 254, and 255 to 255.

CovBin1.AddBins(GenBin(253, 255)) ; -- bins 253, 254, 255

Coverage is accumulated using the method ICover. Since coverage is collected using sequential code, either clock based sampling (shown below) or transaction based sampling (by calling ICover after a transaction completes – shown in later examples) can be used.

-- Accumulating coverage using clock based sampling
  wait until rising_edge(Clk) and nReset = '1' ;
  CovBin1.ICover(to_integer(unsigned(RxData_slv))) ; 
end loop ;
end process ;

A test is done when functional coverage reaches 100%. The method IsCovered returns true when all the count bins in the coverage data structure have reached their goal. The following code shows the previous loop modified so that it exits when coverage reaches 100%.

-- capture coverage until coverage is 100%
while not CovBin1.IsCovered loop 
    wait until rising_edge(Clk) and nReset = '1' ;
    CovBin1.ICover(to_integer(RxData_slv)) ; 
end loop ;

Finally, when the test is done, the method WriteBin is used to print the coverage results to OUTPUT (the transcript window when running interactively).

-- Print Results
CovBin1.WriteBin ;

Putting the entire example together, we end up with the following.

architecture Test1 of tb is
  shared variable CovBin1 : CovPType ;  -- Coverage Object
  TestProc : process
    -- Model the coverage
    CovBin1.AddBins(GenBin(  1,   3     ));  
    CovBin1.AddBins(GenBin(  4, 252, 2)) ;
    CovBin1.AddBins(GenBin(253, 255   )) ; 

    -- Accumulating Coverage 
    -- clock based sampling
    while not CovBin1.IsCovered loop 
      wait until rising_edge(Clk) and nReset = '1' ;
      CovBin1.ICover(to_integer(RxData_slv)) ; 
    end loop ;

    -- Print Results
    CovBin1.WriteBin ;
    wait ; 
  end process ;

Note that when modeling coverage, we primarily work with integer values. All of the inputs to GenBin and ICover are integers, WriteBin reports results in terms of integers. This is similar to what other verification languages do.

3. Cross Coverage with CoveragePkg

Cross coverage examines the relationships between different objects, such as making sure that each register source has been used with an ALU. The hardware we are working with is as shown below. Note that the test plan will also be concerned about what values are applied to the adder. We are not intending to address that part of the test here.


Cross coverage for SRC1 crossed SRC2 with can be visualized as a matrix of 8 x 8 bins.


The steps for modeling cross coverage are the same steps used for item coverage: declare, model, accumulate, interact, and report. Collecting cross coverage only differs in the model and accumulate steps.

Cross coverage is modeled using the method AddCross and two or more calls to function GenBin. AddCross creates the cross product of the set of bins (created by GenBin) on its inputs. The code below shows the call to create the 8 x 8 cross. Each call to GenBin(0,7) creates the 8 bins: 0, 1, 2, 3, 4, 5, 6, 7. The AddCross creates the 64 bins cross product of these bins. This can be visualized as the matrix shown previously.

ACov.AddCross( GenBin(0,7), GenBin(0,7) );

AddCross supports crossing of up to 20 items. Internal to the data structure there is a record that holds minimum and maximum values for each item in the cross. Hence for the first bin, the record contains SRC1 minimum 0, SRC1 maximum 0, SRC2 minimum 0, and SRC2 maximum 0. The record also contains other bin information (such as coverage goal, current count, bin type (count, illegal, ignore), and weight)
The accumulate step now requires a value for SRC1 and SRC2. The overloaded ICover method for cross coverage uses an integer_vector input. This allows it to accept a value for each item in the cross. The extra set of parentheses around Src1 and Src2 in the call to ICover below designate that it is a integer_vector.

ACov.ICover( (Src1, Src2) ) ;

The code below shows the entire example. The shared variable, ACov, declares the coverage object. AddCross creates the cross coverage model. IsCovered is used to determine when all items in the coverage model have been covered. Each register is selected using uniform randomization (RandInt). The transaction procedure, DoAluOp, applies the stimulus. ICover accumulates the coverage. WriteBin reports the coverage.

architecture Test2 of tb is
  shared variable ACov : CovPType ;  -- Declare 
  TestProc : process 
    variable RV : RandomPType ;
    variable Src1, Src2 : integer ;
    -- create coverage model
    ACov.AddCross( GenBin(0,7), GenBin(0,7) );  -- Model

    while not ACov.IsCovered loop    -- Done?
      Src1 := RV.RandInt(0, 7) ;     -- Uniform Randomization 
      Src2 := RV.RandInt(0, 7) ; 

      DoAluOp(TRec, Src1, Src2) ;    -- Transaction
      ACov.ICover( (Src1, Src2) ) ;  -- Accumulate
    end loop ;

    ACov.WriteBin ;  -- Report 
    EndStatus(. . . ) ;   
  end process ;

4. Summary

OSVVM’s sequential approach to modeling allows creating functional coverage models as concisely as language syntax. In fact, for cross coverage it is more concise since we skip the step of first creating item (point) coverage before creating the cross coverage.

In future blog posts we will explore areas where OSVVM surpasses SystemVerilog capability: the Intelligent Coverage approach to randomization and high fidelity functional coverage modeling. We will see that while VHDL’s OSVVM makes easy work of these tasks, the declarative syntax of other verification languages makes it hard or impossible in these languages.

An easy way to stay tuned is to get the RSS feed.