Component control through layers of components/environments

l have several components that are instantiated in several environments and components. For the sake of the discussion let’s take a scoreboard and a reference model that are encapsulated in what some call “module UVC” (which is a fancy name for a uvm_component that can be reused wherever the dut module is used).

I have some control knobs on both the scoreboard and the reference model, e g. enable flags, status flags, configuration fields. A common way to pass all these knobs around is through configuration objects, which are stored into the config_db by the upper component and retrieved by those who need it, so we end up with a config for the scoreboard, one for the reference model and one for the module UVC which is creating all the config objects and possibly initialize their values with some defaults.

Now the environment instantiate the module UVC together with it’s configuration… which is contained in the environment configuration object… It seems to me a lot of configuration objects that need to be recursively included in the upper layer configuration. Aside from all the typing, at some point the test may want to tune a knob in the scoreboard, but it would be bad practice to access it hierarchically thorough the class hierarchy, e.g. env.mod_uvc.scb_cfg.enable = 0, so we start to write accessors (setters and getters) that now need to be propagated up through the hierarchy in order for the test to call cfg.set_scoreboard_enable(0), where cfg is the top level config.

Ideally the test should have handles on those configs and be able to call their method directly but certainly not accessing them hierarchically as it will hamper severely the test reusability.

Is there any design pattern or a generic recommendation for such a problem? Also, is there any recommendation on the api design when it comes to providing access to control knobs and status flags in those components?

Thanks a lot for any suggestion/comment.

1 Like

That’s actually a very nice question/discussion. What you described is what I have seen mostly, but that in itself is a structural design pattern called composite, where a top configuration object composes objects into a tree structure to represent part/whole hierarchies, so I think it’s a natural fit to hierarchical configurations. However, we can complement it with a behavior design pattern like the Policy pattern for better decoupling and allowing tests to cleanly define different configuration policies.
You might be familiar with this nice paper Configuration Conundrum
which dives deeper into this approach. To illustrate the idea, I’ve created a minimal example (EDA_link) :

  • A top-level UVM environment with two sub-environments.
  • Each sub-environment has its own configuration object (env_a_config, env_b_config).
  • The top environment’s configuration (cfg_top) aggregates these sub-configurations, while policies dynamically set their values based on test requirements.
import uvm_pkg::*;

//---------------------------------------------------------//
// Configuration Classes
class env_a_config;
    int param1;
    int param2;

    function void display();
        $display("env_a_config: param1 = %0d, param2 = %0d", param1, param2);
    endfunction
endclass

class env_b_config;
    int param1;
    int param2;

    function void display();
        $display("env_b_config: param1 = %0d, param2 = %0d", param1, param2);
    endfunction
endclass

//---------------------------------------------------------//
// Sub-Environments
class env_a extends uvm_env;
    `uvm_component_utils(env_a)
    env_a_config cfg;
	
    function new(string name, uvm_component parent);
    	super.new(name, parent);
    endfunction
  
    function void build_phase(uvm_phase phase);
        if (!uvm_config_db#(env_a_config)::get(this, "", "cfg", cfg)) begin
            `uvm_fatal("CFG", "env_a_config not found")
        end
    endfunction
endclass

class env_b extends uvm_env;
    `uvm_component_utils(env_b)
    env_b_config cfg;
	
    function new(string name, uvm_component parent);
    	super.new(name, parent);
    endfunction
  
    function void build_phase(uvm_phase phase);
        if (!uvm_config_db#(env_b_config)::get(this, "", "cfg", cfg)) begin
            `uvm_fatal("CFG", "env_b_config not found")
        end
    endfunction
endclass

// Generic Policy Interface
virtual class config_policy#(type T);
    pure virtual function void apply(T cfg);
endclass
      
//---------------------------------------------------------//
// Policy Classes
class env_a_policy extends config_policy#(env_a_config);
    function void apply(env_a_config cfg);
        cfg.param1 = 0;
        cfg.param2 = 1;
        $display("Applying Policy for env_a");
    endfunction
endclass

class env_b_policy extends config_policy#(env_b_config);
    function void apply(env_b_config cfg);
        cfg.param1 = 2;
        cfg.param2 = 3;
        $display("Applying Policy for env_b");
    endfunction
endclass

//---------------------------------------------------------//
// Top Configuration Class
class cfg_top;
    env_a_config env_a_cfg;
    env_b_config env_b_cfg;
endclass

//---------------------------------------------------------//
// Top Environment Class
class env_top extends uvm_env;
    `uvm_component_utils(env_top)
    cfg_top cfg;
    env_a env_a_inst;
    env_b env_b_inst;
    config_policy#(env_a_config) env_a_policy;
    config_policy#(env_b_config) env_b_policy;
  	
    function new(string name, uvm_component parent);
    	super.new(name, parent);
    endfunction
  
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        // Configure sub-environments via config_db
        uvm_config_db#(env_a_config)::set(this, "env_a_inst", "cfg", cfg.env_a_cfg);
        uvm_config_db#(env_b_config)::set(this, "env_b_inst", "cfg", cfg.env_b_cfg);
        env_a_inst = env_a::type_id::create("env_a_inst", this);
        env_b_inst = env_b::type_id::create("env_b_inst", this);
    endfunction

    function void end_of_elaboration_phase(uvm_phase phase);
        if (env_a_policy != null) env_a_policy.apply(cfg.env_a_cfg);
        if (env_b_policy != null) env_b_policy.apply(cfg.env_b_cfg);
    endfunction
endclass

//---------------------------------------------------------//
// Test Class
class my_test extends uvm_test;
    `uvm_component_utils(my_test)
    cfg_top cfg;
    env_top top_env;
    env_a_policy env_a_policy_inst;
    env_b_policy env_b_policy_inst;
  	
    function new(string name, uvm_component parent);
    	super.new(name, parent);
    endfunction
  
    function void build_phase(uvm_phase phase);
        cfg = new();
        cfg.env_a_cfg = new();
        cfg.env_b_cfg = new();
        top_env = env_top::type_id::create("top_env", this);
        top_env.cfg = cfg;
        env_a_policy_inst = new();
        env_b_policy_inst = new();
        top_env.env_a_policy = env_a_policy_inst;
        top_env.env_b_policy = env_b_policy_inst;
    endfunction

    function void end_of_elaboration_phase(uvm_phase phase);
        cfg.env_a_cfg.display();
        cfg.env_b_cfg.display();
    endfunction
endclass

module test;
    initial run_test("my_test");
endmodule

Hope others can weigh in too :)

Hello @Ahmed_Kelany , thanks for the proposal. I know that article inside out as it was developed in the context of a project at my firm :grin:.

We extensively use the policy patterns for constraints on sequences as well as for controlling analog behavioral models in DMS environments and it’s very effective as the user can pick and choose a combination of policies for the scenario at hand.

I guess trying to apply the same pattern here is doable, but more often than not those policies are meant to be “statically” defined, you add them once and they stay as is, you wouldn’t want to come up with different policies for every combination of your list of params and then apply them throughout your test.

My goal would rather be to give the test the opportunity to access the config objects api regardless of where they are in the hierarchy so I can avoid propagating that api across, as in this example:

class test extends uvm_test;
  // List of configs we need
  config_a cfga;
  config_b cfga;
  //... Usual stuff
  virtual task run_phase(uvm_phase phase);
    super.run_phase(phase);
    cfga = get_config_obj("context_a", config_a::get_type());
    cfgb = get_config_obj("context_b", config_b::get_type());

    // Now we can access the config api directly
    cfga.set_enable(1);
    cfgb.clear_status();

    // Start sequences ... Do stuff!

    if (cfga.get_status())
      `uvm_info(...)
    else
      `uvm_error(...)

  endtask
    

The config objects can have a common interface (through abstract classes) and could work as a control panel to their associated components (a bit like proxies). Protecting their internal implementation could greatly improve their reuse, as common knobs (enable/disable/status etc ) could be part of a base class.

Now the question remains in how the get_confi_obj returns them. There’s been a paper at this year DVCon on the notion of a sequencer pool and sequencer aggregator from Glasser and Cummings which might come to the rescue. Through a similar approach environments and components alike could get an extra function to store their config objects, while building, in a global structure (like a singleton) only to be retrieved by the test at a later time.

The shortcoming in this situation would be that in an environment with many config objects you need to have a way to indicate which one you need (hence the “context” parameter) but maintaining that list might be tedious.