The previous lesson covered the class call and its contract. Two lighter tools exist beside it, and picking the lightest one that can express the rule keeps benches fast and failures easy to read.
Class randomization suits transactions: reusable objects with intrinsic legality rules. Scope randomization suits local, one-off decisions where a class would be ceremony. Plain draws suit single bounded values.
// not-run: fragment (bus_txn is the class from the previous lesson; clk and drive() are not defined here)
// Typical test-level mix of all three tools:
task run_traffic(bus_txn t);
int n_txns, mode;
// joint local decision: std::randomize
assert (std::randomize(n_txns, mode) with {
n_txns inside {[10:50]};
mode inside {[0:2]};
(mode == 2) -> n_txns > 30; // stress mode needs volume
});
repeat (n_txns) begin
assert (t.randomize()); // transaction: class solve
drive(t);
repeat ($urandom_range(0, 5)) // simple gap: plain draw
@(posedge clk);
end
endtaskEach call sits at the right weight class. The std randomize jointly picks count and mode under the implication, while the class call runs the transaction rules.
$urandom_range draws a plain gap with no solver involved.
// not-run: fragment (clk and send_burst() are not defined here)
task drive_idle_gap();
int unsigned gap;
bit [3:0] burst;
// Solve gap and burst TOGETHER under shared constraints
if (!std::randomize(gap, burst) with {
gap inside {[1:100]};
burst inside {[1:8]};
gap > burst * 10; // relationship between the two
})
$error("std::randomize failed");
repeat (gap) @(posedge clk);
send_burst(burst);
endtaskThe with-clause is solved jointly: the solver picks a legal pair, not gap then burst. Variables from the surrounding scope that appear in the clause but not in the argument list are state variables, read and never written.
$urandom returns a 32-bit unsigned value and $urandom_range bounds it inclusively, tolerating swapped arguments. They are thread-stable system functions, not solver calls: no constraints, no failure mode, far cheaper than a solve.
// not-run: illustration with no self-check; Icarus also rejects a $urandom seed that is not a variable
module top;
bit [31:0] raw;
int unsigned d6;
initial begin
raw = $urandom; // full 32-bit draw
d6 = $urandom_range(6, 1); // inclusive [1:6]
d6 = $urandom_range(1, 6); // same: arguments may be swapped
raw = $urandom(42); // seed is an input expression: reseeds this
// thread's RNG (unlike $random's inout seed)
// What $urandom CANNOT do:
// pick (a,b) with a+b==100 -> needs std::randomize with
// honor class legality rules -> needs class randomize
// cyclic no-repeat values -> needs randc
end
endmodule$urandom_range | std::randomize | class randomize | |
|---|---|---|---|
Constraints | none | with-clause | class plus inline with |
Cross-variable | no | yes | yes |
Can fail | no | yes, returns 0 | yes, returns 0 |
Cost | trivial | solver call | solver call |
Random stream | calling thread | calling thread | the object |
Rule reuse | none | none, inline | high, inside the class |
Typical use | gaps, delays | local joint picks | transactions |
A joint std::randomize with an implication, and $urandom_range bounds. The class call was checked in the previous lesson.
module tb_tools;
int ok, n_txns, mode, gap;
initial begin
// 1. std::randomize solves local variables jointly.
repeat (100) begin
if (!std::randomize(n_txns, mode) with { n_txns inside {[10:50]}; mode inside {[0:2]}; (mode == 2) -> n_txns > 30; })
$fatal(1, "FAIL: std::randomize failed");
if (mode == 2 && n_txns <= 30) $fatal(1, "FAIL: stress mode with only %0d txns", n_txns);
end
// 2. $urandom_range: bounds inclusive, arguments in either order.
repeat (100) begin
gap = $urandom_range(5, 0);
if (gap < 0 || gap > 5) $fatal(1, "FAIL: gap %0d", gap);
end
$display("PASS: std::randomize and $urandom_range held their rules");
$finish;
end
endmodulePASS: std::randomize and $urandom_range held their rulesIndependent draws cannot express a relationship between variables. One std::randomize with a plus-equals constraint solves the pair jointly.
One way to write it:
typedef enum bit [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
// PacketFIFO transaction with rand fields: the solver now builds legal packets.
class pkt_txn;
rand kind_e kind;
rand bit [1:0] dest;
rand bit [3:0] len;
rand bit [7:0] payload[8]; // fixed size; only the first len bytes are sent
constraint c_len { len inside {[1:8]}; }
function bit [7:0] header();
return {kind, dest, len};
endfunction
function bit [7:0] check_byte();
bit [7:0] c = header();
for (int i = 0; i < len; i++) c ^= payload[i];
return c;
endfunction
endclass
module tb_pkt_rand;
pkt_txn p = new();
int lens[int];
int kinds[int];
initial begin
repeat (500) begin
if (!p.randomize()) $fatal(1, "FAIL: randomize failed"); // every call checked
if (p.len < 1 || p.len > 8) $fatal(1, "FAIL: illegal len %0d", p.len);
lens[int'(p.len)]++;
kinds[int'(p.kind)]++;
end
$display("lengths seen: %0d of 8, kinds seen: %0d of 3", lens.num(), kinds.num());
if (lens.num() != 8 || kinds.num() != 3) $fatal(1, "FAIL: some legal value never appeared");
$display("PASS: 500 checked solves gave legal packets covering every length and kind");
$finish;
end
endmodulelengths seen: 8 of 8, kinds seen: 3 of 3
PASS: 500 checked solves gave legal packets covering every length and kindRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown. Verilator prints a harmless WIDTHTRUNC warning for if (!obj.randomize()), because randomize() returns an int, and an UNSATCONSTR warning when a solve fails on purpose. Icarus 13 does not support constraints.