Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
    • UVM Factory: Registration, Overrides and Debug
    • config_db and Resources
      • The set/get Contract: Four Arguments That Must Agree
      • Instance Paths and Wildcards: Who Can See a Setting
      • config_db Resolution: Who Wins at Build and After
      • Config Objects: One Typed Handle Instead of Twenty Knobs
      • config_db Performance and Anti-Patterns
      • Auto-Config and Field Macros on Components
      • Configuring Sequences
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹config_db and Resources
  1. Topics
  2. UVM Verification
  3. Factory & Configuration
  4. config_db and Resources
  5. Config Objects: One Typed Handle Instead of Twenty Knobs
On this page

    Part 3/Factory & ConfigurationIntermediate

    Config Objects: One Typed Handle Instead of Twenty Knobs

    Why benches pass configuration as objects: dma_job_cfg and the get-or-randomize pattern in dma_rand_test, polymorphic config with one base type, and shared versus cloned handles.

    A random DMA test fails on seed 8812: the third job, len=3, ends with a mismatch. The designer wants to rerun that exact job, over and over, while adding debug prints to the RTL. Re-randomizing will not produce it again once anything in the testbench changes. Copying the test and hard-coding the job means one more class to maintain. The DMA bench's answer is already in dma_rand_test: it looks for a job description in config_db before inventing one.

    The idea: a sealed envelope instead of loose notes

    Passing configuration as separate knobs is like leaving twenty sticky notes on a desk: each one can fall off, be misread or be overwritten, and nobody can see at a glance whether the set is complete. A config object is a sealed envelope: one typed uvm_object that holds every field for one purpose, travels as one config_db entry, and can carry its own constraints, defaults, print method and checks.

    Because the object is a class, it can also be randomized. That is the trick in the bench: the same dma_job_cfg type serves as a directed setting (someone builds one and publishes it) and as a random one (the test randomizes its own when nobody did).

    The picture

    Flow: a derived test publishes a job config; the random test gets it if present, otherwise creates and randomizes one, then runs the job.
    The get-or-randomize pattern in dma_rand_test. A published job is used as is; with none published, the test randomizes its own. Either way the rest of the test sees one dma_job_cfg handle.

    Build it: the job config in the bench

    The object

    dma_job_cfg holds one job: the key, the length and whether the interrupt is enabled. The length is constrained to the legal range, and irq_en has a soft default that a directed test may override:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // snippet: from the course DMA bench
    // One DMA job as a randomizable object. A test can randomize it, or put one in
    // config_db so a test further down the hierarchy picks it up.
    class dma_job_cfg extends uvm_object;
      rand bit [31:0]   key;
      rand int unsigned len;
      rand bit          irq_en;
      constraint len_c { len inside {[1:16]}; }
      constraint irq_c { soft irq_en == 1; }
    
      `uvm_object_utils(dma_job_cfg)
      function new(string name = "dma_job_cfg");
        super.new(name);
      endfunction
    
      function string convert2string();
        return $sformatf("key=%08h len=%0d irq_en=%0b", key, len, irq_en);
      endfunction
    endclass

    The consumer: get, or randomize

    dma_rand_test asks config_db for a job at its own path before each job. On a miss it creates and randomizes one. The get happens in run_phase, which is fine: config_db is not restricted to build, as long as the set happened before the get.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // snippet: from the course DMA bench
    // Several random jobs. A job config placed in config_db by a derived test or the
    // command line wins; otherwise each job is randomized here.
    class dma_rand_test extends dma_base_test;
      `uvm_component_utils(dma_rand_test)
      int unsigned n_jobs = 3;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      task run_phase(uvm_phase phase);
        dma_job_cfg job;
        bit [31:0] status;
        phase.raise_objection(this);
        for (int j = 0; j < n_jobs; j++) begin
          if (!uvm_config_db#(dma_job_cfg)::get(this, "", "job", job)) begin
            job = dma_job_cfg::type_id::create($sformatf("job%0d", j));
            if (!job.randomize()) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
          end
          `uvm_info("JOB", job.convert2string(), UVM_LOW)
          run_job(job, job.len, status);
          if (status[2:1] !== 2'b01) `uvm_error("STATUS", $sformatf("job %0d: STATUS=%08h, expected DONE only", j, status))
        end
        #100ns;
        phase.drop_objection(this);
      endtask
    endclass

    The producer: a test that pins one job

    To replay the failing job, a small derived test publishes it. Setting with this and an empty instance name gives the scope uvm_test_top, which is exactly the key the base test's get uses:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // snippet: replaying one job without editing dma_rand_test
    class dma_replay_test extends dma_rand_test;
      `uvm_component_utils(dma_replay_test)
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      function void build_phase(uvm_phase phase);
        dma_job_cfg job = dma_job_cfg::type_id::create("job");
        super.build_phase(phase);
        job.key = 32'h5a17_93f4; job.len = 3; job.irq_en = 1;
        uvm_config_db#(dma_job_cfg)::set(this, "", "job", job);
      endfunction
    endclass

    The producer does not have to be a test. The top module could publish a job from null at uvm_test_top before run_test, which is how a regression script that generates SystemVerilog includes would inject one. Either way the consumer's code stays the same, which is the point of reading configuration by key instead of by handle.

    One subtlety: the base test gets the job once per loop iteration, and the same handle comes back every time, so all three jobs are the replayed one. For a replay that is what you want. A test that wants one directed job among random ones would publish a queue or a count, not a single object.

    Polymorphic config: set and get the base type

    config_db matches the exact type parameter. To publish a derived config (say dma_err_job_cfg extends dma_job_cfg, which adds an error-injection field), set it through the base type and let the consumer $cast if it needs the extra fields:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet: publish a derived object through the base type
    dma_err_job_cfg ejob = dma_err_job_cfg::type_id::create("job");
    uvm_config_db#(dma_job_cfg)::set(this, "", "job", ejob);       // T is the base type
    
    // consumer: get the base, then check for the derived type
    dma_job_cfg     job;
    dma_err_job_cfg ej;
    if (uvm_config_db#(dma_job_cfg)::get(this, "", "job", job) && $cast(ej, job))
      `uvm_info("JOB", "error-injecting job", UVM_LOW)

    Shared handles and clones

    A config object in config_db is a handle; every getter receives the same object. Treat received config as read-only. When several instances need slightly different settings, the producer clones the object per instance and sets each clone on its own path, for example cfg[i] = base_cfg.clone() per agent. Agent config objects (interface handle, is_active, protocol knobs) follow the same pattern; see Agent configuration.

    What belongs in a config object

    Put in the object

    Keep out of it

    Virtual interface handles for one agent

    Handles to other components (use TLM connections)

    Mode switches: is_active, coverage on/off, checks on/off

    Per-transaction state that changes every cycle

    Protocol knobs: idle caps, error rates, timeouts

    Values the test can set directly on a sequence it starts

    Job or scenario descriptions such as dma_job_cfg

    Anything that must be different per instance but is shared by handle

    Validate a config object where it is consumed

    Constraints protect randomized objects, but a directed producer can assign anything, including len = 0, which the DMA treats as an empty job. A small check method on the config class, called once by the consumer right after it receives the object, turns a confusing failure deep in the run into a clear one at the start:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet: a check on the config class, called by the consumer
    function bit is_valid(output string why);
      if (len == 0 || len > 16) begin why = $sformatf("len %0d outside 1..16", len); return 0; end
      return 1;
    endfunction
    
    // in the consumer, after a successful get:
    string why;
    if (!job.is_valid(why)) `uvm_fatal("BADCFG", why)

    Watch it run

    With nothing published, dma_rand_test randomizes three jobs. Each [JOB] line prints the job it used (random values differ by simulator and seed):

    Diagram
    +UVM_TESTNAME=dma_rand_test
    UVM_INFO dma_tb.sv(731) @ 0: uvm_test_top [JOB] key=90fdd8fd len=10 irq_en=1
    UVM_INFO dma_tb.sv(731) @ 645000: uvm_test_top [JOB] key=0e564b57 len=1 irq_en=1
    UVM_INFO dma_tb.sv(731) @ 885000: uvm_test_top [JOB] key=5a1793f4 len=3 irq_en=1
    UVM_INFO dma_tb.sv(411) @ 1305000: uvm_test_top.env.sb [SB] 14 words matched, 0 mismatched
    UVM_ERROR :    0

    10 + 1 + 3 = 14 words: the scoreboard's count agrees with the jobs. With dma_replay_test, all three [JOB] lines show key=5a1793f4 len=3 irq_en=1 and the scoreboard reports 9 words.

    What breaks: the directed job that silently became random

    A team wrote an error-job test by publishing uvm_config_db#(dma_err_job_cfg)::set(this, "", "job", ejob), with the derived class as the type parameter. The test passed. It also never ran an error job. dma_rand_test reads with uvm_config_db#(dma_job_cfg), a different type, so its get missed, and the optional-get fallback randomized normal jobs. The only clue was in the [JOB] lines, which nobody compared against the intent.

    Two fixes. Publish through the base type, as above, so the get matches. And make optional gets visible: when a fallback exists, log which branch ran (job from config_db or job randomized), so a missed directed setting shows up in the first ten lines of the log rather than never.

    "Why use a configuration object instead of individual config_db fields?"

    A weak answer: "It's cleaner and you only need one get."

    A strong answer: "One typed handle replaces many string-keyed lookups, so there are fewer silent misses and collisions. The object can carry constraints and soft defaults, so the same class serves directed and random use; our DMA test gets a job config if one was published and randomizes one otherwise. It prints as one unit for debug. I publish through the base type so derived configs still match, treat received handles as read-only, and clone per instance when agents need different values."

    Follow-ups: What happens if you set with the derived type and get with the base? (the get misses: config_db matches the exact T) and Why is a shared handle risky? (one consumer's change is seen by all).

    Check yourself dma_replay_test publishes one dma_job_cfg. How many different jobs does the test run, and why?

    One job, three times. dma_rand_test calls get once per iteration and receives the same handle each time, so every iteration uses the published key and length. Only a miss leads to a fresh, randomized object.

    Check yourself The env gets one shared agent config and hands it to two agents; agent A sets cfg.max_idle = 0 in its build_phase. What does agent B see?

    Also 0: both agents hold the same object. Received config should be read-only. If the agents need different values, the producer clones the object per agent and sets each clone on that agent's path.

    Check yourself Why is the job's irq_en constraint soft?

    So a directed test can override it in randomize() with { irq_en == 0; } without a constraint conflict, as dma_timeout_test does to create a job whose interrupt never fires. A hard constraint would make that randomize call fail.

    Next: config objects also make config cheaper. config_db performance and anti-patterns shows what a lookup really costs, and what not to do in a run loop.

    Related topics

    • Resolution order: who wins

    • config_db performance

    • Agent configuration

    • Plusargs and configuration

    Previous

    56 / 280

    Next: config_db Performance and Anti-Patterns

    Up next

    Up nextconfig_db Performance and Anti-Patterns

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The set/get Contract: Four Arguments That Must Agree
    • Instance Paths and Wildcards: Who Can See a Setting
    • Auto-Config and Field Macros on Components