Give each instance its own configuration

Pass each copy of an agent its own configuration object, and predict which setting an agent receives when several scopes match it.

Lesson
U11.2
Time
25 min
Runs on
Verilator with UVM
Check
5 questions

The chip bench reuses one configuration object for both incrementer agents. The test sets cfg.lane = 0, stores it for port_a, sets cfg.lane = 1 and stores it for port_b. Every scoreboard message then blames lane 1, even for port A's traffic. The database stored the same handle twice, so both agents read one object whose lane is 1.

Test stepcfg.laneport_a seesport_b sees
cfg.lane = 0, store for port_a00-
cfg.lane = 1, store for port_b111
Agents read their cfg in build111

One object, two database entries. Changing the object changes what both entries lead to.

One object instead of many settings

An agent needs several settings: its virtual interface, whether it is active, a lane number for messages, a switch for its checks. Storing each one separately in uvm_config_db means one set and one get per setting, each with its own type and string name to get wrong.

A configuration object bundles them. It is a plain uvm_object with one field per setting. The test fills one object per agent and stores it once. The agent gets it once in build_phase and stops with a fatal error if it is missing, so a forgotten setting fails at time 0.

The incrementer agent's configuration object. Defaults live in the class.
class incr_agent_cfg extends uvm_object;
  `uvm_object_utils(incr_agent_cfg)
  virtual incr_if         vif;
  uvm_active_passive_enum is_active = UVM_ACTIVE;
  int                     lane;
  bit                     checks_on = 1;
  function new(string name = "incr_agent_cfg"); super.new(name); endfunction
endclass

Adding a setting now means adding a field. The test sets it where needed, and every other test keeps the default without changing a line.

The database stores a handle

A class variable holds a handle, a reference to an object, as U2.1 showed. uvm_config_db #(incr_agent_cfg)::set stores that handle. It does not copy the object, so the entry and the test's variable lead to the same object.

That is why the hook went wrong. Both entries held one handle, and the second assignment changed the only object. The fix is one object per agent: call create (or new) once for each, before filling it.

One object, stored twice

cfg = incr_agent_cfg::type_id::create("cfg");
cfg.lane = 0;
uvm_config_db #(incr_agent_cfg)::set(this, "env.port_a", "cfg", cfg);
cfg.lane = 1;
uvm_config_db #(incr_agent_cfg)::set(this, "env.port_b", "cfg", cfg);

Both agents read lane 1. The second assignment changed the object the first entry points to.

One object per agent

cfg_a = incr_agent_cfg::type_id::create("cfg_a");
cfg_b = incr_agent_cfg::type_id::create("cfg_b");
cfg_a.lane = 0;
cfg_b.lane = 1;
uvm_config_db #(incr_agent_cfg)::set(this, "env.port_a", "cfg", cfg_a);
uvm_config_db #(incr_agent_cfg)::set(this, "env.port_b", "cfg", cfg_b);

Each entry leads to its own object, so each agent reads its own lane.

Sharing is sometimes what you want. Agents that should all see one error counter, or one switch that the test flips during the run, can share one object. Do it on purpose, and name it so a reader knows it is shared.

Configuration objects for the environment

One handle in, two handles outThe example's configuration path. Each arrow is one set or one get; a get returns a handle and leaves the entry in place.
One handle in, two handles outThe example's configuration path. Each arrow is one `set` or one `get`; a `get` returns a handle and leaves the entry in place. test chip_test Createschip_env_cfg and oneincr_agent_cfg perport config store config db: "env" Entry cfg =chip_env_cfg handle env chip_env Knows only its ownchildren config store config db:"port_a","port_b" Entries cfg.port_aand cfg.port_b agent port_a lane 0, UVM_ACTIVE agent port_b lane 1,UVM_PASSIVE set for "env"get returns handleset, relative namesgetget
  1. Test build_phase

    The test fills chip_env_cfg and its two agent objects, then sets the parent object once, for "env".

    More context

    The entry stores a handle, not a copy of the object. The test keeps its own handle to the same objects.

    Follow the calls, values and waits
    1. data chip_env_cfg handle: chip_test → config db: "env"
  2. Env build_phase: get

    The environment gets the parent object.

    More context

    get copies the handle out and leaves the entry in place. Test, entry and environment now all lead to one object.

    Follow the calls, values and waits
    1. data chip_env_cfg handle: config db: "env" → chip_env
  3. Env build_phase: set

    It sets cfg.port_a for "port_a" and cfg.port_b for "port_b", names relative to itself.

    More context

    The environment names only its own children. It passes on handles it already holds; nothing is cloned.

    Follow the calls, values and waits
    1. data cfg.port_a handle: chip_env → config db: "port_a", "port_b"
    2. data cfg.port_b handle: chip_env → config db: "port_a", "port_b"
  4. Agent build_phase

    Each agent gets its own object, copies is_active from it and builds its children.

    More context

    The example prints env.port_a lane=0 UVM_ACTIVE checks_on=1 and env.port_b lane=1 UVM_PASSIVE checks_on=1.

    Follow the calls, values and waits
    1. data cfg.port_a handle: config db: "port_a", "port_b" → port_a
    2. data cfg.port_b handle: config db: "port_a", "port_b" → port_b
  5. A run-time change5–10 ns (illustrative)

    The test clears checks_on in port_b's object at 5 ns. port_b reads 0 at 10 ns, with no new set or get.

    More context

    Nothing moves through the database: the agent's cfg leads to the very object the test changed. port_a's object is separate and still reads 1.

Read the diagram as text
  • chip_test: Creates chip_env_cfg and one incr_agent_cfg per port
  • config db: "env": Entry cfg = chip_env_cfg handle
  • chip_env: Knows only its own children
  • config db: "port_a", "port_b": Entries cfg.port_a and cfg.port_b
  • port_a: lane 0, UVM_ACTIVE
  • port_b: lane 1, UVM_PASSIVE
  • chip_test → config db: "env": set for "env"
  • config db: "env" → chip_env: get returns handle
  • chip_env → config db: "port_a", "port_b": set, relative names
  • config db: "port_a", "port_b" → port_a: get
  • config db: "port_a", "port_b" → port_b: get

No layer names a grandchild. Renaming port_b changes only the environment.

In a bigger bench the test should not know every agent's path. Instead, the environment has its own configuration object, which holds one object per agent. The test fills that object and stores it once for env. The environment gets it and passes each agent its part, using a path relative to itself.

Each layer only knows the names of its own children. Rename port_b in the environment and only the environment changes. The same environment can then sit inside a larger one, which passes it its configuration in the same way.

One level up: the same objects in a subsystem

Nesting pays off at the next level of integration. A subsystem bench puts chip_env inside a larger environment, and chip_env_cfg becomes a member of the subsystem's own configuration object. The subsystem environment hands it to its child exactly as the test did here.

This benchInside a subsystem bench
Who fills chip_env_cfgThe testThe subsystem test, as part of its own object
Who sets it, for which nameThe test, for "env"The subsystem env, for its child's name
port_b.is_activeWhatever this bench needsUVM_PASSIVE where another block now drives the port
chip_env and the agentsAs writtenUnchanged

Only the layer above changes. U12.2 builds such a subsystem and measures what breaks when an agent stays active.

The set calls inside chip_env keep working because they name "port_a" and "port_b" relative to chip_env itself, wherever it sits.

The agent keeps the handle

The agent's cfg variable leads to the very object the test filled. A change the test makes during the run is therefore visible to the agent at once. In the example, the test clears port_b's checks_on at 5 ns, and the agent reads 0 at 10 ns.

checks_on in each object during the run
0123time (ns)051015port_a cfg.checks_onport_b cfg.checks_ontest clears port_b onlyboth agents read

Two objects, so the change reaches port_b and nothing else.

Use this for settings meant to change during a run, such as turning checks off around a planned error. Settings that decide the structure, such as is_active, are read once in build and must not change after.

Checking the object before use

A configuration object can arrive incomplete. The test may forget the virtual interface, or set a lane number that the scoreboard does not know. The agent is the right place to catch that, in build_phase, right after the get.

Check the fields the agent cannot work without. A null interface handle means the driver and monitor would fail on their first pin access, deep in the run and with a confusing message. Stopping at time 0 with "port_b: cfg.vif is null" points straight at the cause.

Give every object a name that says whose it is, such as "port_b_cfg". The name appears in messages and in printed objects, so a shared or misplaced object is easy to spot. It also helps when two objects are compared in a debugger and look identical.

Keep defaults in the class, as the example does with is_active = UVM_ACTIVE and checks_on = 1. A test then sets only what differs from the usual bench. Adding a new field with a sensible default leaves every existing test working unchanged.

Which setting wins

Several entries can match one agent: a wildcard from the test, an exact path from the environment, an older entry from the same test. During build, UVM ranks them in two steps.

First, a setting made by a component higher in the hierarchy beats one made lower down. Each entry made in build gets a precedence that drops by one per level, so the test outranks its environment. A default the environment sets for its agents therefore loses to anything the test says, which is what you want.

Second, among settings made by the same component, the last one wins. Being more specific does not help: if the test stores lane for "x" and then for "*", agent x gets the wildcard value. After build, all new settings share one precedence, so the last setting wins everywhere.

Settings for agent x (in order)Made byx gets
"x" = 1, then "*" = 4the test, both4: last one wins
"*" = 4, then "x" = 3the test, both3: last one wins
test "env.*" = 5, env "x" = 7test and env5: the higher component wins

Measured on uvm-core 2020.3.1 during the build phase.

So store each agent's object for its own exact path, and avoid wildcards for anything that should differ between agents.

Try it

The first two lines come from end_of_elaboration_phase: each agent has its own lane, and port B became passive from its object's is_active. The last two show the run-time change reaching port B only.

Replace the second create in the test with cfg.port_b = cfg.port_a; and run again: both agents report lane 1 and passive, as in the opening story.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: uvm_test_top.env.port_a lane=0 UVM_ACTIVE checks_on=1 uvm_test_top.env.port_b lane=1 UVM_PASSIVE checks_on=1 uvm_test_top.env.port_a at 10 ns checks_on=1 uvm_test_top.env.port_b at 10 ns checks_on=0 UVM_ERROR : 0

Check yourself

Predict the outputAgents x and y sit under the test; a and b under env. What line does the $display print? UVM's own messages are not part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
class port_agent extends uvm_agent;
  `uvm_component_utils(port_agent)
  int lane = -1;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    void'(uvm_config_db #(int)::get(this, "", "lane", lane));
  endfunction
endclass
class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  port_agent a, b;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    uvm_config_db #(int)::set(this, "b", "lane", 7);
    a = port_agent::type_id::create("a", this);
    b = port_agent::type_id::create("b", this);
  endfunction
endclass
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  my_env env;
  port_agent x, y;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    uvm_config_db #(int)::set(this, "x", "lane", 1);
    uvm_config_db #(int)::set(this, "*", "lane", 2);
    uvm_config_db #(int)::set(this, "*", "lane", 4);
    uvm_config_db #(int)::set(this, "y", "lane", 3);
    uvm_config_db #(int)::set(this, "env.*", "lane", 5);
    env = my_env::type_id::create("env", this);
    x = port_agent::type_id::create("x", this);
    y = port_agent::type_id::create("y", this);
  endfunction
  function void end_of_elaboration_phase(uvm_phase phase);
    $display("x=%0d y=%0d a=%0d b=%0d", x.lane, y.lane, env.a.lane, env.b.lane);
  endfunction
endclass
module tb;
  initial run_test("my_test");
endmodule
Spot the bugPort A should read lane 0 and port B lane 1. Each option fills cfg_a and cfg_b in the test's build_phase and stores them. Which one gives both agents the same lane?
ConceptWhy does an agent usually stop with uvm_fatal when uvm_config_db::get finds no configuration object?
ConceptAn environment stores a default checks_on = 1 for "*" in its build_phase. The test stored checks_on = 0 for "env.port_b" in its own build_phase, earlier. What does port_b read?
ConceptThe example's chip_env moves inside a subsystem environment, one level deeper. The subsystem env gets chip_env_cfg from its own object and sets it for its child "chip". Which line inside chip_env must change?

Remember

  • A configuration object bundles an agent's settings; the agent gets it once in build and stops if it is missing.
  • uvm_config_db stores the handle, not a copy, so give each agent its own object.
  • The agent keeps the same object, so run-time changes the test makes reach it at once.
  • During build, settings from higher components win; among one component's settings, the last one wins, however specific the earlier one was.
  • Configuration objects nest one level per environment; each layer sets its children's part for names relative to itself, so the same environment works inside a subsystem.