A code review comment on apb_rw_seq: "local:: is noise here, the names are obvious." The author deletes it from addr == local::addr. It compiles cleanly. That night every DMA test times out, and the APB monitor shows writes to addresses nobody asked for.
This lesson explains the three layers of constraints a sequence item lives under, how a sequence aims one call at a corner, and why that deleted word turned a register write into a random access.
A house is shaped by three kinds of rules. The building code applies to every house and cannot be waived: that is a class constraint, such as word_aligned_c. The architect's default applies unless someone asks otherwise: that is a soft constraint. The client's request applies to this one house: that is an in-line constraint, the block you pass to randomize() with { ... }. (The language standard reserves the word in-line for that block; constraints declared in the class are class constraints, or constraint blocks.)
A request may override a default but never the code. If it tries, randomize() fails and returns 0, and the item's random fields keep their previous values. That is why every randomize call in the bench is checked.
Layer | Bench example | Applies to | Who can override it |
|---|---|---|---|
Class constraint (hard) |
| Every randomize call | Nobody, short of |
Class constraint with a distribution |
| Every call; shapes the odds | An in-line constraint that pins |
Soft constraint |
| Every call, unless contradicted | Any hard constraint, including an in-line one |
In-line constraint |
| This call only | Nothing: it is hard for this call |
apb_item forbids misaligned addresses. axis_item goes further and shapes how often each idle count appears: idle dist { 0 := 6, [1:3] := 3, [4:8] := 1 }. The operator matters, and it is a favourite interview question. := gives the weight to each value in a range; :/ splits the weight across the range. So the bench's [1:3] := 3 gives each of 1, 2 and 3 a weight of 3, a total of 9, and [4:8] := 1 totals 5. With 6 for zero, the odds are 30 / 45 / 25 percent, not the 60 / 30 / 10 the numbers might suggest. This model runs both versions:
// Same ranges, two weight operators: := weights each value, :/ splits the weight across the range.
class beat_per_value; // the bench's axis_item idle constraint
rand int unsigned idle;
constraint idle_c { idle dist { 0 := 6, [1:3] := 3, [4:8] := 1 }; }
endclass
class beat_per_range;
rand int unsigned idle;
constraint idle_c { idle dist { 0 :/ 6, [1:3] :/ 3, [4:8] :/ 1 }; }
endclass
module tb_dist;
beat_per_value a = new();
beat_per_range b = new();
int a0, a13, a48, b0, b13, b48;
initial begin
for (int i = 0; i < 2000; i++) begin
if (!a.randomize() || !b.randomize()) $fatal(1, "randomize failed");
if (a.idle == 0) a0++; else if (a.idle <= 3) a13++; else a48++;
if (b.idle == 0) b0++; else if (b.idle <= 3) b13++; else b48++;
end
$display(":= idle=0 %0d%% 1..3 %0d%% 4..8 %0d%%", a0/20, a13/20, a48/20);
$display(":/ idle=0 %0d%% 1..3 %0d%% 4..8 %0d%%", b0/20, b13/20, b48/20);
// Expected: := gives 6 : 3*3 : 5*1 = 30/45/25; :/ gives 6:3:1 = 60/30/10.
if (a0/20 inside {[27:33]} && a13/20 inside {[42:48]} && b0/20 inside {[57:63]} && b48/20 inside {[7:13]})
$display("PASS: := weights each value, :/ splits the weight across the range");
else
$display("FAIL: unexpected distribution");
$finish;
end
endmodule:= idle=0 29% 1..3 46% 4..8 24%
:/ idle=0 61% 1..3 29% 4..8 9%
PASS: := weights each value, :/ splits the weight across the rangeNeither is wrong; they are different stimulus. The bench wants mostly back-to-back beats with a fair number of short gaps, and := gives exactly that. If someone "tidies" it to :/, long gaps nearly vanish and the backpressure corner gets a tenth of its traffic.
// 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
endclassRandom jobs should normally enable the interrupt, because the tests wait for it. But one test needs the opposite: dma_timeout_test wants a job whose interrupt never fires. It does not edit the class; it overrides the default in one call:
// snippet: from dma_timeout_test::run_phase
if (!job.randomize() with { len == 4; irq_en == 0; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")A soft constraint is dropped whenever it conflicts with a hard one, and the in-line irq_en == 0 is hard. Had irq_c been a hard constraint, this call would fail and the fatal would fire. When two soft constraints conflict, the higher-priority one wins: an in-line soft constraint beats a class one, and a derived class's soft constraint beats its parent's.
// snippet: from the course DMA bench
// One APB access. The test or a parent sequence fills addr/write/data.
class apb_rw_seq extends uvm_sequence #(apb_item);
`uvm_object_utils(apb_rw_seq)
rand bit [7:0] addr;
rand bit write;
rand bit [31:0] data;
bit [31:0] rdata;
bit slverr;
function new(string name = "apb_rw_seq");
super.new(name);
endfunction
task body();
apb_item it = apb_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { addr == local::addr; write == local::write; data == local::data; })
`uvm_fatal("RAND", "apb_item randomize failed")
finish_item(it); // returns after the driver's item_done
rdata = it.rdata;
slverr = it.slverr;
endtask
endclassapb_rw_seq has fields called addr, write and data, and so does the item it randomizes. Inside with { ... }, a bare name is looked up in the object being randomized first, and only then in the calling scope. So addr == addr compares the item's addr with itself: it is always true and constrains nothing. local::addr tells the compiler to resolve the name in the caller's scope, which here is the sequence. This model shows the difference:
// Why apb_rw_seq writes local::addr inside randomize() with.
class item; // stands in for apb_item
rand bit [7:0] addr;
constraint word_c { addr[1:0] == 2'b00; }
endclass
class rw_seq; // stands in for apb_rw_seq: it has a field with the same name
bit [7:0] addr = 8'h0C; // the register the caller asked for (KEY)
int hit_plain, hit_local;
function void run(int n);
item it = new();
repeat (n) begin
// Inside with { }, a bare name resolves to the object being randomized first:
// this compares it.addr with itself, so it constrains nothing.
if (!it.randomize() with { addr == addr; }) $fatal(1, "randomize failed");
if (it.addr == 8'h0C) hit_plain++;
// local:: makes the name resolve in the caller's scope: the sequence's addr.
if (!it.randomize() with { addr == local::addr; }) $fatal(1, "randomize failed");
if (it.addr == 8'h0C) hit_local++;
end
endfunction
endclass
module tb_local;
rw_seq s = new();
initial begin
s.run(200);
$display("addr == addr hit KEY %0d of 200 times", s.hit_plain);
$display("addr == local::addr hit KEY %0d of 200 times", s.hit_local);
if (s.hit_local == 200 && s.hit_plain < 50)
$display("PASS: only local::addr pins the item to the requested register");
else
$display("FAIL: plain=%0d local=%0d", s.hit_plain, s.hit_local);
$finish;
end
endmoduleaddr == addr hit KEY 6 of 200 times
addr == local::addr hit KEY 200 of 200 times
PASS: only local::addr pins the item to the requested registerSix hits in 200 is chance: there are 64 aligned addresses. Now compare axis_pkt_seq, which writes last == (i == n - 1) with no local::. That is correct only because axis_item has no field called i or n, so the names fall through to the sequence. If someone later adds an n field to the item, the constraint silently changes meaning. Writing local:: for every caller variable costs nothing and removes the trap.
The bench randomizes after start_item returns, that is, after the sequencer has granted the sequence its turn. This is called late randomization, and it means the values are chosen at the last moment, when the sequence can still look at the current state of the test. Every call is wrapped in if (!...randomize()) uvm_fatal(...). Never write assert(it.randomize()): if someone disables assertions to silence a noisy check, the whole assert statement is skipped, including the call to randomize(), and every item goes out with stale values.
Tool | What it does | Watch out for |
|---|---|---|
| Stops randomizing it; you assign it directly | It stays off until |
| Turns a named class constraint off, for example | Also persistent; turn it back on |
| Changes the odds of a combination, never which values are legal | Not a fix for a failing randomize |
| Hooks around the solver; |
|
dma_rand_test randomizes three jobs with no in-line constraint on irq_en, so the soft default holds every time (values differ by simulator and seed):
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=1dma_timeout_test overrides the default with irq_en == 0. The interrupt never fires, the test waits forever for it, and the built-in timeout ends the run, which is exactly what that test is for:
UVM_FATAL uvm_phase_hopper.svh(598) @ 20000000: reporter [PH_TIMEOUT] Explicit timeout of 20000000 hit, indicating a probable testbench issueWith local:: removed, apb_rw_seq still compiled and still ran, but every reg_write became a write to a random aligned address, right only about one time in 64. The write of GO to CTRL almost never landed, so no job started, no interrupt came, and every test sat in wait (irq_vif.irq === 1'b1) until the timeout. The writes that fell into the holes came back with PSLVERR, which nobody checked. The scoreboard said nothing at all: there was no data to compare, and a PH_TIMEOUT fatal ends the run before check_phase can report that nothing was compared.
The fix was to restore the three local:: prefixes and add one line to the team's review checklist: in a `with` block, every name that belongs to the caller gets `local::`. A cheaper safety net would have caught it on the first run: the sequence could check slverr after finish_item, or a register-aware scoreboard could flag writes to unmapped addresses.
it.randomize() with { addr == 8'h0D; } on an apb_item. What happens?word_aligned_c requires addr[1:0] == 0, and 0x0D has its low bits set, so the constraints conflict and randomize() returns 0. The bench's check fires uvm_fatal("RAND", ...). Without the check, the item would keep its previous field values and the driver would send the previous access again.
axis_item gains a new rand int unsigned n field for packet metadata. What happens to axis_pkt_seq's constraint last == (i == n - 1)?n now resolves to the item's own new field, not the sequence's packet length, because names in a with block are looked up in the randomized object first. last becomes tied to a random value, and packets end at the wrong beat. Writing local::n (and local::i) restores the meaning and protects against this kind of change in future.
job.irq_en = 0; job.randomize();. Test B writes job.randomize() with { irq_en == 0; }. Which one gets a job with the interrupt disabled?Only test B. In test A, irq_en is still a random variable, so randomize() overwrites the assigned 0 and the soft default makes it 1. To assign it by hand you would need job.irq_en.rand_mode(0) first, and remember that it stays non-random afterwards.
Next: some tests need a different kind of item, not just a different value. Derived items narrows apb_item to the DMA's register map, and to its unmapped holes, without touching the agent.