In parts one and two of this series we looked at how to use policy classes to implement an extendable coverage model, where ignore bins can be tweaked. The first post looked at how to use these policies as parameters for a parameterizable coverage collector (the so called static flavor), while the second post focused on using them as constructor arguments for the collector (the so called dynamic flavor).
Since SystemVerilog and UVM have become almost synonymous terms, let's look at how these two approaches for implementing coverage extendability interact with UVM features such as the factory.
Typically, coverage collectors are UVM subscribers that are connected to monitors. Let's start as before with the static implementation, that relies on a parameterizable class:
class cov_collector #(type POLICY = cg_ignore_bins_policy) extends uvm_subscriber #(instruction); `uvm_component_param_utils(cov_collector #(POLICY)) covergroup cg; // ... endclass
We need to instantiate this component inside our environment:
cov_collector #(cg_ignore_bins_policy) cov; virtual function void build_phase(uvm_phase phase); cov = cov_collector #(cg_ignore_bins_policy)::type_id::create("cov", this); // ... endfunction
We had the foresight to create our coverage collector using the factory. Now, when we want to verify the CPU variant without a multiplier, we just need to make sure that we create an instance of the appropriate type by setting a factory override:
virtual function void build_phase(uvm_phase phase); cov_collector #(cg_ignore_bins_policy)::type_id::set_type_override( cov_collector #(no_mul_cg_ignore_bins_policy)::get_type()); super.build_phase(phase); endfunction
While this code would compile, it will fail at run time. The reason is because even though no_mul_cg_ignore_bins_policy is a sub-class of cg_ignore_bins_policy, this relationship doesn't trickle down through the parameterization of cov_collector. The two different variants of the coverage collector are totally unrelated to each other from an OOP point of view and cannot be swapped for each other.
The classic solution to this problem is to create a common base class from which the parameterizations will inherit:
class cov_collector_base extends uvm_subscriber #(instruction); `uvm_component_utils(cov_collector_base) // ... endclass class cov_collector #(type POLICY = cg_ignore_bins_policy) extends cov_collector_base; `uvm_component_param_utils(cov_collector #(POLICY)) // ... endclass
When declaring the coverage collector, we need to declare it as an object of type cov_collector_base and immediately set an override on it:
cov_collector_base cov; virtual function void build_phase(uvm_phase phase); cov_collector_base::type_id::set_type_override( cov_collector #(cg_ignore_bins_policy)::get_type()); cov = cov_collector_base::type_id::create("cov", this); // ... endfunction
For the restricted CPU we can just replace the previous override:
virtual function void build_phase(uvm_phase phase); cov_collector_base::type_id::set_type_override( cov_collector #(no_mul_cg_ignore_bins_policy)::get_type()); super.build_phase(phase); endfunction
This will work, but it's not terribly elegant in my opinion. We needed to create the extra level of class hierarchy just so we could get the compiler to do our bidding. I'm also not particularly thrilled with directly starting out with a factory override in the base environment.
The dynamic version of the code is much more straightforward. This time we need to make sure we can switch out policy class instances for each other, so we'll need to make them UVM objects:
class cg_ignore_bins_policy extends uvm_object; `uvm_object_utils(cg_ignore_bins_policy) // ... endclass
In the vanilla SystemVerilog example from the previous post we passed an instance of the policy class as the coverage collector's constructor argument. This isn't possible in UVM because a component's constructor has a fixed signature, taking only its name and its parent. However, there isn't anything stopping us from creating a policy object internally:
class cov_collector extends uvm_subscriber #(instruction); `uvm_component_utils(cov_collector) protected cg_ignore_bins_policy policy; function new(string name, uvm_component parent); super.new(name, parent); policy = cg_ignore_bins_policy::type_id::create("policy", this); // ... endfunction // ... endclass
You may have noticed something slightly off about the previous code snippet. Even though policy is a uvm_object at heart, we pass a uvm_component parent to the create(...) call. What that does is create the policy object inside the coverage collector's scope. This makes it possible to override the policy class type using the following instance override:
cov_collector no_mul_cov; virtual function void build_phase(uvm_phase phase); cg_ignore_bins_policy::type_id::set_inst_override( no_mul_cg_ignore_bins_policy::get_type(), "no_mul_cov.*", this); no_mul_cov = cov_collector::type_id::create("no_mul_cov", this); // ... endfunction
A type override would have also worked, but this is a nifty little trick to know. The dynamic variant ended up being far shorter than the static one. I also like this one more because it feels closer to the Gang of Four's "favor object composition over class inheritance" principle. The main disadvantage is that, as we saw last time, it might suffer from more simulator limitations.
As always, you can download the code for this and the previous two posts from SourceForge.
I hope this series of posts convinced you that a certain degree of coverage extendability is also possible in SystemVerilog. As with all other things, as opposed to e, this requires pre-planning. Because of the overhead required, this approach might not catch traction for many testbenches, but it is ideal when developing UVCs. There are still some kinks that need ironing: the syntax for coverpoints isn't fully described and simulator support might be limited for now. While this approach currently resembles a dangerous trek through uncharted waters of the LRM, once the seas of SystemVerilog vendor interoperability calm down it's sure to really start to shine.