This example demonstrates how testbench environments can evolve to become mixed component hierarchies, where OVM instantiates VMM that instantiates OVM and so on. Such hierarchies are referred as sandwiched or layered.
The following figure represents what is meant by mixed hierarchy
Such hierarchies are not typically designed from scratch like this. They typically evolve to something like this as block-level testing progresses to system-level, where integration of existing IP and legacy environments are commonly employed to maximize reuse.
When integrating OVM components as children of envs, subenvs, and xactors, care must be taken to ensure the hierarchical name of every OVM component is unique and deterministic. The OVM’s configuration and factory interfaces require this. It is not enough to simply pass a null handle to each OVM component’s parent constructor argument. Such components end up as children of ovm_top, the implicit top-level OVM component, and there can not be any two children by the same name in ovm_top.
Therefore, to meet this requirement, these guidelines should be followed to emulate hierarchical naming in an environment containing OVM components:
OVM instantiating VMM in build method: virtual function build(); env = new({get_full_name(),".env"},...); subenv = new({get_full_name(),".subenv"},...); xactor = new("typename",{get_full_name(),".xactor"},...);
VMM env instantiating OVM and VMM children in build method: virtual function build(); o_comp = new({log.get_name(),".o_comp"},this); xactor = new("Xactor Name","Xactor Inst",...); VMM subenv/xactor instantiating OVM and VMM children in constructor: function new(string name, string inst, ...); super.new(name,inst,...); o_comp = new({inst,".o_comp"},this); xactor = new("Xactor Name","Xactor Inst",...);
The instance names given any component containing any OVM component, directly or indirectly, should contain only alpha-numerics and underscores.
We now present the example in series of steps that might emulate how a mixed testbench environment evolves.
Mixed Hierarchy | |
This example demonstrates how testbench environments can evolve to become mixed component hierarchies, where OVM instantiates VMM that instantiates OVM and so on. | |
Step 1 - Simple block-level, mixed environment | We start with a simple mixed, block-level testbench that connects a VMM generator to on OVM driver via the avt_channel2tlm adapter. |
Step 2 - Define an OVM IP block | At some point, we may decide to encapsulate the block-level testbench in an OVM IP block for reuse at the system level. |
Step 3 - Define a VMM IP block - Part A | First encapsulate all OVM children with an OVM component wrapper that can make all port connections at the appropriate time. |
Step 3 - Define a VMM IP block - Part B | Then, define the VMM IP block, which instantiates the OVM wrapper and other VMM components. |
Step 4 - VMM top-level | Let us now consider a vmm_env that contains our vmm_ip block and, optionally, other instances of OVM IP and VMM IP. |
Step 5 - OVM sub-component level | We can easily encapsulate our tb_env above for reuse as an ovm_component by extending a avt_ovm_vmm_env specialization, avt_ovm_vmm_env #(tb_env). |
Step 6 - OVM top-level | Let us now consider an ovm_env that contains our ovm_subcomp block and, optionally, other instances of OVM IP and VMM IP. |
Step 7 - Module | Although we can continue the sandwiching of OVM and VMM components indefinitely, we stop at this point by instantiating the ovm_env as a top-level component. |
We start with a simple mixed, block-level testbench that connects a VMM generator to on OVM driver via the avt_channel2tlm adapter.
class block_env extends ovm_component; `ovm_component_utils(block_env) vmm_apb_rw_atomic_gen gen; apb_channel2tlm adapt; ovm_driver_req drv; apb_scoreboard compare; bit PASS = 0; function new (string name="block_env",ovm_component parent=null); super.new(name,parent); endfunction virtual function void build(); gen = new("gen", 0); adapt = new("adapt", this, gen.out_chan); drv = new("drv", this); void'(get_config_int("max_trans",drv.max_trans)); compare = new("comparator", this, gen.out_chan); gen.out_chan.tee_mode(1); endfunction virtual function void connect(); drv.seq_item_port.connect(adapt.seq_item_export); drv.ap.connect(compare.ovm_in); endfunction virtual task run(); gen.start_xactor(); gen.notify.wait_for(vmm_apb_rw_atomic_gen::DONE); ovm_top.stop_request(); endtask virtual function void check(); if(compare.m_matches >= 1 && compare.m_mismatches == 0) PASS = 1; endfunction // check virtual function void report(); if(PASS == 1) begin `OVM_REPORT_INFO("PASS","Test PASSED"); end else begin `OVM_REPORT_ERROR("FAIL","Test FAILED"); end endfunction // report endclass
At some point, we may decide to encapsulate the block-level testbench in an OVM IP block for reuse at the system level. Doing this does not take much effort, as we can simply extend the existing block-level env to change its role from top-level env to a reusable subcomponent as follows:
class ovm_ip extends block_env; `ovm_component_utils(ovm_ip) ovm_analysis_port #(ovm_apb_rw) ap; function new(string name, ovm_component parent=null); super.new(name,parent); endfunction function void build(); super.build(); ap = new ("ap",this); endfunction function void connect(); super.connect(); drv.ap.connect(ap); endfunction virtual task run(); gen.start_xactor(); endtask endclass
First encapsulate all OVM children with an OVM component wrapper that can make all port connections at the appropriate time.
As we define new environments from pieces of existing IP and block- level components, we may find ourselves wanting to integrate the ovm_ip with a VMM xactor and enclose that in a VMM subenv IP block. Such a pairing requires a connection from the ovm_ip’s analysis export to the analysis export of a <vmm_analysis_adapter>. The adapter and VMM consumer will then share a vmm_channel to complete the connection.
But there’s one small catch. The ovm_ip analysis port will not exist until after its build phase, which means we can not attempt an analysis port connection in the constructor of the VMM subenv.
The best approach is to enclose all sibling OVM components with a parent OVM component whose job is to build and connect the OVM components per the OVM use model.
The OVM wrapper, after making all OVM port connections between sibling components, would present only the vmm_channel connection to its VMM parent.
In this example, the wrapper will make the connection from the ovm_ip analysis port to the analysis2notify adapter’s analysis export during the connect phase, thus relieving the VMM parent from that responsibility. As with most VMM components, the wrapper can either be assigned a channel via its constructor or provide the channel via its out_chan property.
class ovm_private_wrapper extends ovm_component; `ovm_component_utils(ovm_private_wrapper) ovm_ip o_ip; apb_analysis_channel v_ap_adapter; vmm_channel_typed #(vmm_apb_rw) out_chan; function new(string name, ovm_component parent=null, vmm_channel_typed#(vmm_apb_rw) out_chan=null); super.new(name,parent); this.out_chan = out_chan; endfunction virtual function void build(); o_ip = new("o_ip",this); v_ap_adapter = new("v_ap_adapter", this, out_chan); if (out_chan == null) out_chan = v_ap_adapter.chan; endfunction virtual function void connect(); o_ip.ap.connect(v_ap_adapter.analysis_export); endfunction endclass
Then, define the VMM IP block, which instantiates the OVM wrapper and other VMM components.
In this example, we instantiate the VMM consumer and OVM wrapper. We connect them by making sure they share the same vmm_channel.
In compliance with the VMM use model, the subenv builds itself solely in its constructor. We also define the configure, start, stop, and cleanup methods, per methodology requirements.
When naming the VMM consumer and OVM wrapper, we include the instance name of the vmm_ip parent. This ensures that any OVM sub-components all receive a unique hierarchical name.
We also employ the OVM set_config interface to map the max_trans VMM config parameter from the vmm_ip_cfg object to an OVM configuration parameter used by the underlying OVM ip. Here, too, we prepend the vmm_ip’s instance name when making the call.
class vmm_ip_cfg; int max_trans=0; endclass class vmm_ip extends vmm_subenv; ovm_private_wrapper o_wrapper; vmm_consumer #(vmm_apb_rw) v_consumer; vmm_consensus end_vote; function new(string inst, vmm_ip_cfg cfg, vmm_consensus end_vote); super.new("vmm_ip", inst, end_vote); this.end_vote = end_vote; v_consumer = new({inst,".v_consumer"},0); o_wrapper = new({inst,".o_wrapper"},,v_consumer.in_chan); v_consumer.stop_after_n_insts = cfg.max_trans; set_config_int({inst,".o_wrapper.o_ip"},"max_trans",cfg.max_trans); end_vote.register_notification(v_consumer.notify,v_consumer.DONE); endfunction task configure(); super.configured(); endtask virtual task start(); super.start(); v_consumer.start_xactor(); endtask virtual task stop(); super.stop(); endtask virtual task cleanup(); super.cleanup(); endtask endclass
Let us now consider a vmm_env that contains our vmm_ip block and, optionally, other instances of OVM IP and VMM IP. If our env had OVM children needing port connections, we would wrap them as we did for the ovm_private_wrapper above.
When we instantiate the VMM IP in the build method, we provide an instance name that includes the env’s name, which is retrieved from its log object. We do this to ensure the embedded OVM IP gets unique name.
class tb_env extends vmm_env; vmm_ip_cfg cfg; vmm_ip v_ip; ovm_ip o_ip; //vmm_ip2 v_ip2; function new(string name=""); super.new(name==""?"mixed_tb_env":name); endfunction virtual function void gen_cfg(); super.gen_cfg(); cfg = new; // we could randomize, but we won't cfg.max_trans = 10; endfunction virtual function void build(); super.build(); v_ip = new({log.get_name(),".v_ip"},cfg,end_vote); o_ip = new({log.get_name(),".env_o_ip"}); endfunction virtual task cfg_dut(); super.cfg_dut(); v_ip.configure(); endtask virtual task start(); super.start(); v_ip.start(); endtask task wait_for_end(); super.wait_for_end(); fork end_vote.wait_for_consensus(); join global_stop_request(); endtask endclass
We can easily encapsulate our tb_env above for reuse as an ovm_component by extending a avt_ovm_vmm_env specialization, avt_ovm_vmm_env #(tb_env).
In this example, we wrap the tb_env to provide OVM users access to the env’s config object via the OVM configuration mechanism.
Our new ovm_subcomp can now be used like any other OVM component
class ovm_subcomp extends avt_ovm_vmm_env_named #(tb_env); `ovm_component_utils(ovm_subcomp) function new (string name="ovm_subcomp", ovm_component parent=null); super.new(name,parent); endfunction virtual function void vmm_gen_cfg(); ovm_object obj; super.vmm_gen_cfg(); if (get_config_object("cfg",obj,0)) begin ovm_container #(vmm_ip_cfg) v_cfg; assert($cast(v_cfg,obj)); env.cfg = v_cfg.obj; end endfunction endclass
Let us now consider an ovm_env that contains our ovm_subcomp block and, optionally, other instances of OVM IP and VMM IP. Because the parent (this) and child (subcomp) are both OVM components, we do not need to include the parent’s full name in the name we give the child.
class ovm_env extends ovm_component; `ovm_component_utils(ovm_env) ovm_subcomp subcomp; function new (string name="ovm_env", ovm_component parent=null); super.new(name,parent); endfunction virtual function void build(); super.build(); subcomp = new("subcomp",this); endfunction endclass
Although we can continue the sandwiching of OVM and VMM components indefinitely, we stop at this point by instantiating the ovm_env as a top-level component. Future integrations could reuse the ovm_env as a child or grandchild of larger system-level environment, but we have to stop somewhere.
As part of configuration of the ovm_env, we use the OVM’s configuration mechanism to set the configuration object that will be used by the underlying vmm_subenv. We also use it to set each generator’s max number of transactions.
module example_05_mixed_hierarchy; ovm_env top = new("top"); vmm_ip_cfg v_cfg = new; ovm_container #(vmm_ip_cfg) cfg = new(v_cfg); initial begin // configure the vmm consumer in the vmm_subenv instance // to finish after 2 transactions. v_cfg.max_trans = 2; set_config_object("top.subcomp","cfg",cfg,0); // configure the vmm generator in the ovm_ip instance // in top.subcomp to finish in 2 transactions. set_config_int("top.subcomp.v_ip.o_wrapper.o_ip","max_trans",2); // configure embedded generator in ovm ip to // finish in 3 transactions set_config_int("top.subcomp.env_o_ip","max_trans",3); run_test(); end endmodule
Use this class to connect a VMM producer to an OVM consumer.
class avt_channel2tlm #( type VMM_REQ = int, OVM_REQ = int, VMM2OVM_REQ = int, OVM_RSP = OVM_REQ, VMM_RSP = VMM_REQ, OVM2VMM_RSP = avt_converter #(OVM_RSP,VMM_RSP), OVM_MATCH_REQ_RSP = avt_match_ovm_id ) extends ovm_component
Use this class to wrap (contain) an existing VMM env whose constructor does not have a name argument.
class avt_ovm_vmm_env #( type ENV = vmm_env ) extends avt_ovm_vmm_env_base