Coverage Extensibility in SystemVerilog-2023
Quite a while back I wrote a series of posts about coverage extensibility in SystemVerilog. In the first post we looked at how to use policy classes to implement extensible coverage collectors, where ignore bins can be tweaked via class parameters. The second post explored a dynamic flavor of this approach, using constructor arguments instead. Finally, the third post tied everything together by looking at how these approaches interact with UVM and the factory.
All of that was necessary because SystemVerilog didn’t have a way to extend coverage groups natively. In contrast, the e language makes it possible to modify coverage definitions from anywhere in the verification environment. Getting anything similar in SystemVerilog required quite a bit of pre-planning and infrastructure code.
With the release of IEEE Std 1800-2023, SystemVerilog finally got a new language feature in this area. Section 19.4.1 Embedded covergroup inheritance describes that it’s now possible for a covergroup to extend another covergroup. This is great news, if it means we can express what we need without all of the scaffolding we had to put in place before.
In this post, let’s have a look at what covergroup extends brings to the table and see how much of what I wrote back then has become obsolete.
The Original ExamplePermalink
Let’s start by revisiting the example from the original post series.
We had a simple RISC CPU that supported four operations (ADD, SUB, MUL and DIV) on eight registers (R0 to R7).
Here’s what our initial coverage looked like:
class coverage_collector;
covergroup cg with function sample(operation_e operation, register_e op1, register_e op2, register_e dest);
coverpoint operation;
coverpoint op1;
coverpoint op2;
coverpoint dest;
operation_vs_op1 : cross operation, op1;
operation_vs_op2 : cross operation, op2;
operation_vs_dest : cross operation, dest;
same_reg_both_ops : coverpoint (op1 == op2);
same_reg_op1_and_dest : coverpoint (op1 == dest);
same_reg_op2_and_dest : coverpoint (op2 == dest);
same_reg_both_ops_and_dest : coverpoint (op1 == dest && op2 == dest);
option.per_instance = 1;
endgroup
// ...
endclass
Notice that we’re now using the with function sample(...) construct.
This allows us to avoid polluting our class with member variables that don’t actually represent the object’s state,
but are just used for “argument passing” to the coverpoint expressions.
Variant with Fewer OperationsPermalink
The original posts introduced a variant of the CPU which dropped the MUL and DIV instructions.
They showed how to customize the coverage model for it,
by using hooks to control the contents of the operation coverpoint’s ignore bins.
With covergroup extends, this becomes really straightforward:
class no_mul_div_coverage_collector extends coverage_collector;
covergroup extends cg;
coverpoint operation {
ignore_bins mul_div = { MUL, DIV };
}
endgroup
endclass
We can now customize the ignore bins without having to set up any infrastructure for future extensions.
Notice that we don’t need to specify the arguments for sample(...) again.
The extended covergroup inherits the signature from the base, so no need for boilerplate code.
But here’s where things get tricky.
If we compile and run this code, we’ll notice something interesting: the crosses still include all the multiplication and division bins.
This is because the cross definitions in the base class still use the base class’s definition of the operation coverpoint, not our refined version.
Initially, I thought that this was a bug in the tool,
but actually things are working as specified in the standard:
Even if a coverage point in the base covergroup does not contribute to the computation, a cross […] in the base covergroup that includes that base coverpoint still contributes to the computation unless there is a cross with the same name in the derived covergroup.
To fix this, we need to redeclare the crosses:
class no_mul_div_coverage_collector extends coverage_collector;
covergroup extends cg;
// ...
operation_vs_op1 : cross operation, op1;
operation_vs_op2 : cross operation, op2;
operation_vs_dest : cross operation, dest;
endgroup
endclass
This feels a bit unsatisfying.
From the point of view of class inheritance, coverpoints behave like non‑virtual functions:
the derived coverpoint hides the original operation definition in the derived class’s scope rather than overriding it for the base class.
Since the base class’s crosses still reference the original operation,
we have to hide those too by redeclaring them.
In e, when extending a coverage item, the crosses that reference that item use the updated definition. The folks on the IEEE 1800 committee could have used the e language as a reference when designing embedded covergroup inheritance, but, for reasons I don’t know, they decided to implement different semantics. I can’t think of any use case where keeping base crosses bound to base coverpoints is actually helpful. The example above shows the opposite. If you know of a situation where this behavior is desirable, please tell me in a comment.
Variant with Fewer Registers: More of the SamePermalink
Let’s look at another variant from the original post series,
where our CPU only implements registers R0 through R3.
We have to adapt our coverage to ignore the higher-numbered registers.
Following the same pattern as before, we extend the covergroup and add ignore bins:
class fewer_regs_coverage_collector extends coverage_collector;
covergroup extends cg;
coverpoint op1 {
ignore_bins r4_to_r7 = { R4, R5, R6, R7 };
}
coverpoint op2 {
ignore_bins r4_to_r7 = { R4, R5, R6, R7 };
}
coverpoint dest {
ignore_bins r4_to_r7 = { R4, R5, R6, R7 };
}
operation_vs_op1 : cross operation, op1;
operation_vs_op2 : cross operation, op2;
operation_vs_dest : cross operation, dest;
endgroup
endclass
Again, we need to redeclare all the crosses that involve the modified coverpoints.
The whole point of using covergroup extends was to avoid having to write too much code.
Granted, this is boilerplate code,
so it’s simpler than having to build the infrastructure for extensibility,
but forgetting to redeclare crosses is easy to overlook
and can potentially create work for us in the future.
Combining Variants: Where Things Break DownPermalink
Now here’s where we hit a wall. In the original post series, we looked at a final CPU variant that had both limitations: only four registers and only addition and subtraction. With single inheritance, we can’t cleanly combine both extensions.
We could create a new class that extends one of them and adds the other ignores. For example:
class fewer_regs_no_mul_div_coverage_collector extends no_mul_div_coverage_collector;
covergroup extends cg;
coverpoint op1 {
ignore_bins r4_to_r7 = { R4, R5, R6, R7 };
}
coverpoint op2 {
ignore_bins r4_to_r7 = { R4, R5, R6, R7 };
}
coverpoint dest {
ignore_bins r4_to_r7 = { R4, R5, R6, R7 };
}
operation_vs_op1 : cross operation, op1;
operation_vs_op2 : cross operation, op2;
operation_vs_dest : cross operation, dest;
endgroup
endclass
But this just means duplicating code.
We’ve essentially reimplemented the functionality in fewer_regs_coverage_collector.
Of course, in this case we could switch where we start the inheritance tree, by creating a no_mul_div_fewer_regs_coverage_collector which extends fewer_regs_coverage_collector,
because this way we have less code to duplicate.
This is still a compromise, though.
The problem isn’t only the amount of duplicated code,
but also the fact that we have to remember to look for it if we ever need to change it.
We could try to use the mixin pattern (which I’ve written about before) to work around this, but that comes with its own set of problems and added complexity.
The dynamic variant with policy classes that I described in the second post of the original series handled this more elegantly. It allowed us to pass multiple policy objects and compose their filtering behavior. This is a classic example of how inheritance can’t beat composition.
When Requirements Change: Redeclaring Means Rewriting From ScratchPermalink
The problems don’t stop at crosses. There’s another subtlety that makes modifying existing coverpoints more painful than it looks.
Let’s say that during development, the requirements for our CPU change such that R0 is now a hardwired zero register.
This means it can never be the destination of an instruction.
We have to update the base coverage model to capture this:
class coverage_collector;
covergroup cg with function sample(operation_e operation, register_e op1, register_e op2, register_e dest);
coverpoint dest {
ignore_bins r0_cannot_be_dest = { R0 };
}
// ...
endgroup
// ...
endclass
For the fewer_regs_coverage_collector class,
if we were to check the bins of the dest coverpoint,
we would unexpectedly find that R0 is still included.
This is because we’re redeclaring the coverpoint:
we’re not extending what’s already there, we’re replacing it entirely.
There’s no way to refer to the existing bins from the base coverpoint and just add to them.
We have to write it all out again:
class fewer_regs_coverage_collector extends coverage_collector;
covergroup extends cg;
coverpoint dest {
ignore_bins r0_cannot_be_dest = { R0 };
ignore_bins r4_to_r7 = { R4, R5, R6, R7 };
}
// ...
endgroup
endclass
In e, the prev keyword lets us reference the previous (base) definition of a cover item,
so we can include its existing bin expressions without repeating them.
This is another area where the folks on the IEEE 1800 committee could have taken a page from the e language book,
but SystemVerilog has no equivalent construct.
When we redeclare a coverpoint,
we start from a blank slate.
For the case where we have both fewer registers and no MUL and DIV operations,
since we copied and pasted code from fewer_regs_coverage_collector,
we have to fix it there as well:
class fewer_regs_no_mul_div_coverage_collector extends no_mul_div_coverage_collector;
covergroup extends cg;
coverpoint dest {
ignore_bins r0_cannot_be_dest = { R0 };
ignore_bins r4_to_r7 = { R4, R5, R6, R7 };
}
// ...
endgroup
endclass
Because the change was fresh, it was easy to remember to update this code. If a lot of time had passed from when we originally wrote it, we might not have been so lucky.
The same issue applies to crosses.
If any of the crosses that we were redeclaring had its own ignore bins,
we’d have to duplicate those too.
The need to redeclare coverage group members is actually more problematic than we initially thought.
(I would have liked to show an example of this by having some ignore bins defined in a cross,
but the tool which supports covergroup extends doesn’t have good support for defining cross bins.)
Adding New Coverage: Where covergroup extends ShinesPermalink
So far we’ve been looking at the rough edges.
Let me end on a more positive note,
because covergroup extends is genuinely well-suited for one thing:
adding coverage items that are missing from the base class entirely.
Let’s assume that the original coverage model is part of a reusable library for CPU verification,
which we can’t modify.
The same_reg_* coverpoints only track whether two registers happen to be the same,
but they’re never crossed against the operation type and the register value.
That’s potentially interesting coverage, for example
“Does a SUB instruction ever write back into R1 after it read the first operand also from R1?”.
We can layer these crosses on top without touching the base class at all:
class enhanced_coverage_collector extends coverage_collector;
covergroup extends cg;
operation_vs_same_op1_and_op2 : cross operation, op1, same_reg_both_ops;
operation_vs_same_dest_and_op1 : cross operation, dest, same_reg_op1_and_dest;
operation_vs_same_dest_and_op2 : cross operation, dest, same_reg_op2_and_dest;
operation_vs_same_dest_and_both_ops : cross operation, dest, same_reg_both_ops_and_dest;
endgroup
endclass
Without covergroup extends, we would have had to rewrite the entire covergroup:
all its coverpoint definitions, all their bins, and all its crosses.
(You might notice that the crosses look a bit unusual.
Ideally we would cross op1 directly with op2, or dest with op1,
and filter to keep only the bins where they are equal.
I tried doing exactly that,
using the with syntax from section 19.6.1.2 Cross bin with covergroup expressions.
When this didn’t work, I tried to instead cross with the boolean same_reg_* coverpoints
and to use the older binsof ... intersect syntax from section 19.6.1 Defining cross coverage bins.
This also didn’t work,
so I stopped trying to filter out the cases where the registers are not the same.
The result is not as clean,
but it’s good enough for the purpose of demonstrating covergroup extends.)
ConclusionPermalink
So how much of what I wrote back in 2015 has become obsolete? Pretty much none of it, as it turns out.
Embedded covergroup inheritance is a welcome addition to SystemVerilog. It’s a good feature, but some questionable design decisions stop it from being great.
For adding new coverpoints and crosses on top of a base model, it works cleanly, with minimal boilerplate, and no need to touch the original class. This use case alone makes it worth knowing about.
For modifying existing coverage, however, the story is less rosy. Redeclaring a coverpoint replaces it entirely and there is no way to build on what the base already defined. Also, since crosses don’t automatically track changes to the coverpoints they are composed of, any cross that involves a redeclared coverpoint must itself be redeclared.
For composable, modification-heavy scenarios, the policy class approach from the original post series still holds its ground. The two techniques are best treated as complements rather than replacements for each other.
One practical caveat: simulator support for covergroup extends is still far from universal.
If you want to experiment with the examples in this post,
they might not work in your tool of choice.
If that’s the case,
I’d encourage you to ask your EDA vendor to implement this feature.
The more teams make the request, the higher the chances it gets worked on.
As always, you can find the code for this post on GitHub. Thanks for reading and see you next time!
Comments