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 step | cfg.lane | port_a sees | port_b sees |
|---|---|---|---|
cfg.lane = 0, store for port_a | 0 | 0 | - |
cfg.lane = 1, store for port_b | 1 | 1 | 1 |
| Agents read their cfg in build | 1 | 1 | 1 |
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.
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
endclassAdding 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
set or one get; a get returns a handle and leaves the entry in place.- Test build_phase
The test fills
chip_env_cfgand 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
- data chip_env_cfg handle: chip_test → config db: "env"
- Env build_phase: get
The environment gets the parent object.
More context
getcopies 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
- data chip_env_cfg handle: config db: "env" → chip_env
- Env build_phase: set
It sets
cfg.port_afor"port_a"andcfg.port_bfor"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
- data cfg.port_a handle: chip_env → config db: "port_a", "port_b"
- data cfg.port_b handle: chip_env → config db: "port_a", "port_b"
- Agent build_phase
Each agent gets its own object, copies
is_activefrom it and builds its children.More context
The example prints
env.port_a lane=0 UVM_ACTIVE checks_on=1andenv.port_b lane=1 UVM_PASSIVE checks_on=1.Follow the calls, values and waits
- data cfg.port_a handle: config db: "port_a", "port_b" → port_a
- data cfg.port_b handle: config db: "port_a", "port_b" → port_b
- A run-time change5–10 ns (illustrative)
The test clears
checks_onin port_b's object at 5 ns. port_b reads 0 at 10 ns, with no newsetorget.More context
Nothing moves through the database: the agent's
cfgleads 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 bench | Inside a subsystem bench | |
|---|---|---|
Who fills chip_env_cfg | The test | The subsystem test, as part of its own object |
| Who sets it, for which name | The test, for "env" | The subsystem env, for its child's name |
port_b.is_active | Whatever this bench needs | UVM_PASSIVE where another block now drives the port |
chip_env and the agents | As written | Unchanged |
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.
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 by | x gets |
|---|---|---|
"x" = 1, then "*" = 4 | the test, both | 4: last one wins |
"*" = 4, then "x" = 3 | the test, both | 3: last one wins |
test "env.*" = 5, env "x" = 7 | test and env | 5: 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.
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 : 0Check yourself
Remember
- A configuration object bundles an agent's settings; the agent gets it once in build and stops if it is missing.
uvm_config_dbstores 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.