Functional Coverage is one of the main means to measure the quality and progress of the verification project. We define coverage models, run semi-random tests, and every once in a while analyze the coverage report to decide “are we there yet?”.
When talking about “coverage closure”, people tend to talk about generation, how we create the data that will fill the coverage. In this blog, we discuss the definition of the coverage model – how we define a model that will give us the exact information that we need.
To misquote Lewis Carroll – “if you don’t know where you are going, how will you know that you got there?”. If the model is not well defined, the collected data will not give you the information that you need, and you will not have the required confidence in the verification status.
Coverage model can de described as built of three layers.
The top layer is the coverage group. In this layer you define the foundations of the coverage model:
- Where – where is it best to collect the coverage? In which structs and units?
- When – when to sample the values?
- How – are there values that can be ignored? Conditions in which the whole cover group should not be sampled?
Under the group, there are the coverageitems. With the items you specify the “what”:
- What – what are the items that have to be covered?
- Which of the fields?
- Are there important crosses?
- Should the transition be covered?
In this blog, we focus on the third layer – the coverage buckets. With the buckets we perform the fine tuning of the coverage.
- What values are legal?
- What values do not add to the coverage hence should be ignored?
- Are there values of more importance?
- What granularity is required?
To answer the last question, the granularity, we use what’s called “to bucketize”, grouping the values into buckets.
For example, let’s assume a data item having a 32 bits field named address. Surely we cannot collect nor analyze a bucler for each possible value (0, 1, …0xffffffff), so we define buckets. The naïve immediate approach to define the address item’s bucket is to define sub ranges all of identical size, using the ranges option. Like this -
extend monitor {
cover ended using per_unit_instance is {
item address : uint(bits : 32) = cur_packet.address using
radix=HEX,
ranges = {
range([0..0xfffffffe], "", 0xf00000, 10);
};
};
};
};
A small side note – in the above code example, we use the coverage option per_unit_instance. This option is recommended when there are multiple instances of the monitor unit, as it allows viewing the coverage data for each instance separately. For sake of reusability, it is a good habit to always use this option, even if in the current project there is only one instance of this unit.
This definition of the item ranges works, but it does not give enough information of how close we are to our verification goals.
For getting the information that we need, the buckets should capture the coverage goals as good as possible. Typically, the verification plan describes which addresses are more important to cover. For example – the first address and the last in the memory space are meaningful values that must be executed multiple times. In addition, from the verification plan we can derive which values are not meaningful and can be ignore altogether.
The following code defines a coverage model that captures these verification goals:
- The addressees that are meaningful in our project are in the range 0x1488..0x4488
- Packets addressed to the first (0x1488) or last (0x4488) address are of special importance
By creating different buckets to the edge values – 0x1488 and 0x4488 – we define a coverage model that is tuned to the goals. We also define that all the packets with address out of the range as ignorable – having such packets is not part of the verification goal and viewing the in the coverage report will just waste our time.
extend monitor {
cover ended using per_unit_instance is {
item address : uint(bits : 32) = cur_packet.address using
ignore = address < 0x1488 or address > 0x4488,
radix=HEX,
ranges = {
range([0x1488], "first address", UNDEF, 100) ;
range([0x1489..0x4487], "", 0x200, 10);
range([0x4488], "last address", UNDEF, 100);
};
};
};
The code example shown above is fine tuned to a very specific verification project, this model will help us know exactly when we reached the goals of this project. What will happen when we reuse this code in a new project? Let’s assume that one of the differences between the two projects is that in the new project the address space starts with address 0xff00000 and not 0x1488. Should we rewrite the coverage definition? Extend and override?
This effort of rewrite could be avoided, if we make use of non constants in the coverage model (available starting Specman 15.2). That is – use configurable fields, rather than constants.
To make the above code more reusable, we replace “0x1488” and “0x4488” with references to fields. We define fields in a unit or a struct that are accessible from sys, and refer to these fields from the coverage model. For example:
struct cover_config {
min_address : uint (bits : 32);
max_address : uint (bits : 32);
every_count : uint;
keep soft min_address == 0x1488;
keep soft max_address == 0x4488;
keep soft every_count == 0x200;
};
extend sys {
cover_config : cover_config;
};
extend monitor {
cover ended using per_unit_instance is {
item address : uint(bits : 32) = cur_packet.address using
ignore = address < sys.cover_config.min_address
or address > sys.cover_config.max_address,
radix=HEX,
ranges = {
range([sys.cover_config.min_legal_address],
"first address", UNDEF, 100);
range([sys.my_config.min_address+1..
sys.my_config.max_address-1], "",
sys.my_config.every_count, 10);
range([sys.cover_config.max_legal_address],
"last address", UNDEF, 100);
};
};
};
Using configurable fields, the coverage model is now much more reusable – it supports fine tuning for multiple configurations.
And here comes the next challenge – what if we want to reuse the same coverage model in multiple instances in one project? For one instance of the sub-system we want to get fine tuned coverage of the space 0x1488..0x4488, and in another instance – the area that needs to be focused on is 0xff00000.. 0xff0b000.
This is the next step of the per instance discussed above. With per_unit_instance we can view each unit instance independently, and what we ask for now is not only separate view, but also having a different model definition for each instance.
To achieve this requirement, we use instance_ranges. With this option, instead of using a configuration struct that is used by all instances we can define different configurations for different instances. To do so, the configuration fields are not under sys, but rather under the unit that we cover. These fields can be accessed with inst which is a reference to current instance.
For example:
extend monitor {
config : cover_config;
cover ended using per_unit_instance is {
item address : uint(bits : 32) = cur_packet.address using
instance_ignore = address > inst.config.max_address
or address < inst.config.min_address,
radix=HEX,
instance_ranges = {
range([inst.config.min_address],
"first address", UNDEF, 100);
range([inst.config.min_address+1..
inst.config.max_address-1], "",
inst.config.every_count, 10);
range([inst.config.max_address],
"last address", UNDEF, 100);
};
};
};
With this code, we defined the coverage model once, but created two different models, configured per each instance of the agents. The following are screenshots of one run, each showing the coverage of one of the monitor instances.
Defining the coverage model is one of the most important tasks in functional verification. The more accurate it, the best it captures your exact goals – it increases the chances that you will get there.
We encourage you to try out the new features – usage of non constant and instance_ranges, so that you get more from coverage models, and improve the overall efficiency of your verification project.
Rodion Melnikov, Efrat Shneydor
Team Specman