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.
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).
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:
// 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
endclassdma_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.
// 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
endclassTo 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:
// 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
endclassThe 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.
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:
// 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)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.
Put in the object | Keep out of it |
|---|---|
Virtual interface handles for one agent | Handles to other components (use TLM connections) |
Mode switches: | 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 | Anything that must be different per instance but is shared by handle |
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:
// 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)With nothing published, dma_rand_test randomizes three jobs. Each [JOB] line prints the job it used (random values differ by simulator and seed):
+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 : 010 + 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.
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.
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.
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.
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.