Directed tests are slow to write and miss edge cases. Constrained random verification (CRV) lets the solver generate thousands of legal stimuli automatically — you write the rules once, the solver does the rest. Master rand, randc, constraint blocks, distributions, implications, soft constraints, solve‑before, and inline overrides in one deep session.
A directed test hard-codes every stimulus field. An engineer who knows the bug can write the perfect reproducer in minutes. But for exploring a 64-bit address space combined with a 256-value burst-length field, the number of combinations is astronomically large — writing one directed test per corner case is not feasible.
Constrained random verification (CRV) inverts the process: you describe the legal space of stimuli as a set of constraints, and the simulator's built-in constraint solver generates a new legal stimulus every time you call randomize(). Run the same test 10 000 times with different seeds and you cover the entire legal space probabilistically.
| Aspect | Directed Testing | Constrained Random |
|---|---|---|
| Setup cost | Low per bug, high for full coverage | Higher upfront (constraint authoring) |
| Coverage growth | Linear — one test, one scenario | Exponential — one run, many scenarios |
| Corner-case reach | Only what engineer anticipates | Solver finds unexpected combinations |
| Reproducibility | Deterministic by definition | Seed-controlled: same seed = same run |
| Maintenance | Fragile — changes in DUT require new tests | Constraints adapt naturally to spec changes |
| Typical use | Bug regression, protocol smoke | Regression campaigns, coverage closure |
SystemVerilog provides two randomisation qualifiers for class members. Both require the rand or randc keyword in the class declaration and become active when randomize() is called on the object.
| Qualifier | Distribution behaviour | Repetition | Typical use |
|---|---|---|---|
rand | Uniform random over legal domain, independent each call | Any value may repeat immediately | Most stimulus fields |
randc | Cyclic — cycles through all legal values before repeating | No repeats within one cycle | Opcodes, transaction IDs, small enums where full coverage is needed |
class Packet; // rand — each randomize() picks independently from the legal domain rand logic [31:0] addr; rand logic [7:0] burst_len; // randc — cycles through all 4 values before repeating randc bit [1:0] cmd; // values 0,1,2,3 appear once per cycle endclass module tb; Packet pkt; initial begin pkt = new(); repeat(8) begin void'(pkt.randomize()); // cmd will visit 0,1,2,3 then 0,1,2,3 — never 0,0,1,... $display("addr=%0h cmd=%0d len=%0d", pkt.addr, pkt.cmd, pkt.burst_len); end end endmodule
randc only works on integral types with a small domain. The solver must enumerate all legal values to build the cycle. Large bit-widths (e.g., 32-bit randc) are illegal and will cause a compile error. Keep randc to enums or small integer fields.A constraint block is a named block inside a class containing one or more constraint expressions. The solver AND's every active constraint in the object together before solving. All constraints must be simultaneously satisfiable.
class BusTxn; rand logic [31:0] addr; rand logic [7:0] len; rand bit write; // Constraint block 1 — address alignment constraint c_addr_align { addr[1:0] == 2'b00; // always 4-byte aligned addr < 32'h1000_0000; // within mapped region } // Constraint block 2 — burst length constraint c_len { len inside {1, 2, 4, 8, 16}; // only valid AXI burst lengths } // Constraint block 3 — write transactions are short constraint c_write_short { write -> len <= 8; // implication: if write then len ≤ 8 } endclass
All three blocks are active simultaneously. The solver must find addr, len, and write that satisfy every expression at once. Individual constraints within a block are separated by semicolons; you may also use multiple expressions in a single block — they are treated identically to expressions across multiple named blocks.
Call constraint_mode(0) to deactivate a block at runtime. This is useful in tests that need a special stimulus outside the normal constraint envelope, without permanently modifying the class.
BusTxn txn = new(); // Temporarily disable the length constraint txn.c_len.constraint_mode(0); void'(txn.randomize()); // len is now fully unconstrained (0–255) txn.c_len.constraint_mode(1); // re-enable
By default the solver assigns uniform probability across the legal domain. dist lets you bias that distribution by attaching weights to individual values or ranges. Two operators control how weights apply:
| Operator | Meaning | Example weight behaviour |
|---|---|---|
:= | Each item in range gets the stated weight individually | [0:3] := 10 — values 0,1,2,3 each have weight 10 (total 40) |
:/ | Weight is divided equally among all items in range | [0:3] :/ 10 — values 0,1,2,3 each have weight 2.5 (total 10) |
class PktType; rand logic [7:0] opcode; rand logic [7:0] priority_lvl; // opcode distribution: READ 70%, WRITE 20%, NOP 10% constraint c_opcode_dist { opcode dist { 8'h01 := 70, // READ 8'h02 := 20, // WRITE 8'h00 := 10 // NOP }; } // priority: low (0–63) gets half the weight, high (192–255) gets other half constraint c_priority_dist { priority_lvl dist { [0:63] :/ 50, // 50 total weight shared among 64 values [192:255] :/ 50 // 50 total weight shared among 64 values }; } endclass
dist weights to mirror real traffic profiles — e.g., reads are typically 3x more frequent than writes on most buses. This ensures coverage points accumulate at the same rate as real use.The inside operator is the cleanest way to constrain a variable to a set or range of values. It expands to a chain of equality checks under the hood, but reads much more naturally than a long chain of || conditions.
class MemAccess; rand logic [31:0] addr; rand logic [3:0] be; // byte-enable rand bit secure; // addr must fall within one of the two valid memory windows constraint c_addr_range { addr inside { [32'h0000_0000 : 32'h0FFF_FFFF], // ROM window [32'h2000_0000 : 32'h3FFF_FFFF] // DRAM window }; } // byte enables: only single-byte or full-word valid constraint c_be { be inside {4'b0001, 4'b0010, 4'b0100, 4'b1000, 4'b1111}; } // secure access only allowed in ROM region constraint c_secure_range { secure -> addr inside {[32'h0000_0000 : 32'h0FFF_FFFF]}; } endclass
You can negate inside with the ! operator: !(addr inside {forbidden_range}) excludes that range entirely from the solution space.
-> OperatorImplication (a -> b) means "if a is true, then b must also be true." When a is false, the constraint places no restriction on b. This is the go-to tool for modelling conditional protocol rules without hard-coding separate constraint blocks for each mode.
class AXITxn; rand bit write; rand logic [7:0] burst_len; rand bit [1:0] burst_type; // 0=FIXED 1=INCR 2=WRAP rand bit [3:0] id; rand bit lock; // WRAP bursts require power-of-2 length: 2,4,8,16 constraint c_wrap_len { (burst_type == 2'b10) -> burst_len inside {1, 3, 7, 15}; // AWLEN is len-1 encoding so beat counts 2,4,8,16 → values 1,3,7,15 } // Locked transactions must be INCR and single beat constraint c_lock { lock -> (burst_type == 2'b01 && burst_len == 0); } // Write-only IDs are in upper nibble range constraint c_id { write -> id inside {[8:15]}; } endclass
Under the hood, a -> b is equivalent to (!a || b). The solver satisfies this by either making a false (no restriction on b) or making both a and b true simultaneously. Both paths are legal and will appear in the random solution space.
A soft constraint expresses a preference, not a requirement. If a hard constraint or a higher-priority soft constraint contradicts it, the soft constraint is silently dropped rather than causing an infeasibility. This makes base-class constraints much more reusable because sub-classes can override them without causing solve failures.
class BaseTxn; rand logic [7:0] len; rand bit write; // Default preference: short bursts, reads constraint c_soft_defaults { soft len inside {[1:16]}; soft write == 1'b0; } endclass // Sub-class scenario: long write bursts — overrides both soft defaults class LongWriteTxn extends BaseTxn; constraint c_long_write { len inside {[64:255]}; // hard — overrides soft len write == 1'b1; // hard — overrides soft write } endclass // Inline override via randomize() with {} — also overrides soft defaults BaseTxn txn = new(); void'(txn.randomize() with { write == 1'b1; len inside {[32:64]}; });
with{} blocks > hard constraints in the class > soft constraints in with{} blocks > soft constraints in the class. When two soft constraints at the same priority level conflict, the solver picks one arbitrarily.The constraint solver is free to pick the order in which it assigns values to variables. In most cases this makes no difference, but it becomes critical when one variable controls the legal domain of another and you want a specific statistical distribution rather than letting the solver collapse probabilities.
Suppose mode is a 1-bit variable (50% chance of 0 or 1) and data is an 8-bit variable that is unconstrained when mode == 1 but must be 0 when mode == 0. Without solve-before, the solver sees 257 total solutions — 1 for mode==0 and 256 for mode==1. It picks uniformly, giving mode==0 only a 1/257 ≈ 0.4% probability instead of 50%.
class BiasedPkt; rand bit mode; rand logic [7:0] data; // mode=0 → data must be 0; mode=1 → data can be anything constraint c_data { !mode -> data == 8'h00; } // Without this: mode==0 has probability 1/257 ≈ 0.4% // With this: mode first (50/50), then data conditioned on mode constraint c_order { solve mode before data; } endclass
With solve mode before data, the solver first picks mode with a fair 50/50 distribution, then independently solves for data given the chosen mode value. The result is the expected 50% probability on each mode.
solve-before forces the solver into a two-phase approach and can be slower on complex constraint graphs. Use it only when you observe statistical skew and need to correct it.Inline constraints let a test scenario impose additional restrictions on a single randomize() call without modifying the class. They are hard constraints, they are AND'd with all class constraints, and they are discarded after the call returns.
BusTxn txn = new(); // Scenario A: force a write transaction with a specific address window if (!txn.randomize() with { write == 1'b1; addr inside {[32'h8000_0000 : 32'h8FFF_FFFF]}; len inside {4, 8}; }) `uvm_fatal("RAND", "Randomize failed") // Scenario B: read from page-boundary addresses only if (!txn.randomize() with { write == 1'b0; addr[11:0] == 12'h000; // 4 KB page boundary }) `uvm_fatal("RAND", "Randomize failed") // Note: inline constraints use the calling scope for variable references logic [31:0] base_addr = 32'hC000_0000; void'(txn.randomize() with { addr == base_addr; });
The body of the with{} block runs in a mixed scope — names refer first to the randomized object's variables, then fall back to the calling scope. This lets you pass parameters from the test environment directly into the inline constraint.
The following class integrates all the features covered above into a realistic AXI4 transaction. It demonstrates how constraint blocks compose, how soft defaults interact with sub-class overrides, and how dist weights model real traffic.
class AXI4Transaction; // ── Randomizable fields ───────────────────────────────────── rand logic [31:0] addr; rand logic [7:0] burst_len; // AWLEN/ARLEN encoding (beats - 1) rand bit [2:0] burst_size; // bytes per beat: 2^burst_size rand bit [1:0] burst_type; // FIXED=0 INCR=1 WRAP=2 rand bit write; rand bit [3:0] id; rand bit lock; // exclusive access rand bit [3:0] prot; randc bit [1:0] cache; // cyclic: covers all 4 values evenly // ── 1. Address: aligned, within mapped windows ─────────────── constraint c_addr { addr inside { [32'h0000_0000 : 32'h0FFF_FFFF], // ROM [32'h2000_0000 : 32'h3FFF_FFFF], // SRAM [32'h8000_0000 : 32'hBFFF_FFFF] // DRAM }; addr[1:0] == 2'b00; // always 4-byte aligned } // ── 2. Traffic distribution — reads are 3× more common ────── constraint c_rw_dist { write dist { 1'b1 := 25, 1'b0 := 75 }; } // ── 3. Burst length: weighted toward short transfers ───────── constraint c_len { burst_len dist { 8'd0 := 30, // 1 beat [8'd1:8'd3] := 40, // 2–4 beats [8'd4:8'd15] := 20, // 5–16 beats [8'd16:8'd255] := 10 // 17–256 beats }; } // ── 4. Burst size: 4B or 8B transfers most common ─────────── constraint c_size { burst_size inside {3'b010, 3'b011}; // 4B or 8B } // ── 5. WRAP burst requires power-of-2 beat count ──────────── constraint c_wrap { (burst_type == 2'b10) -> burst_len inside {8'd1, 8'd3, 8'd7, 8'd15}; } // ── 6. Lock: only INCR single beat allowed ────────────────── constraint c_lock { lock -> (burst_type == 2'b01 && burst_len == 8'd0); lock dist { 1'b1 := 5, 1'b0 := 95 }; // rare but present } // ── 7. ROM window is read-only ─────────────────────────────── constraint c_rom_readonly { (addr inside {[32'h0000_0000 : 32'h0FFF_FFFF]}) -> !write; } // ── 8. Solve write before burst_len for correct probability ── constraint c_order { solve write before burst_len; solve burst_type before burst_len; } // ── 9. Soft defaults (overridable in tests) ────────────────── constraint c_defaults { soft prot == 4'b0010; // non-secure, non-privileged data } // ── Helper: print summary ──────────────────────────────────── function void print(); $display("[AXI4] %s addr=%0h len=%0d size=%0dB type=%0d id=%0d lock=%0d", write ? "WR" : "RD", addr, burst_len+1, (1 << burst_size), burst_type, id, lock); endfunction endclass
randomize() returns 0 (logic false) when the solver cannot find any solution. This is called an over-constrained condition. It does NOT throw a fatal error by default — which means silent failures if you do not check the return value.
txn.randomize() and ignore the result. Use if (!txn.randomize()) `uvm_fatal(...) or assert(txn.randomize()) at minimum.`uvm_fatal or $fatal on failure so it is immediately visible.txn.print_constraints() (VCS/Xcelium extension) to list all active constraint expressions and their current bindings.b -> a instead of a -> b, which constrains the wrong variable.dist block references values that can no longer be reached, the solver fails.AXI4Transaction txn = new(); // WRONG — silent failure if over-constrained txn.randomize(); // CORRECT — fatal on failure if (!txn.randomize()) `uvm_fatal("RAND_FAIL", "randomize() returned 0 — check constraints") // Debug: disable blocks one by one txn.c_wrap.constraint_mode(0); if (txn.randomize()) begin $display("c_wrap was the problem — check WRAP burst length rules"); end txn.c_wrap.constraint_mode(1); // Debug: pin a variable to check one slice if (!txn.randomize() with { burst_type == 2'b10; burst_len == 8'd5; }) $display("WRAP with len=5 is over-constrained (must be 1,3,7,15)");
| Root cause | Example | Fix |
|---|---|---|
| Contradictory ranges | a > 100; a < 50 | Reconcile range bounds |
| dist references unreachable values | x dist {255 := 10}; x < 10 | Align dist set with range |
| Implication chain deadlocks | a -> b; b -> !a | Break circular dependency |
| randc exhausted domain | All values of randc variable filtered out | Widen constraints on randc variable |
| solve-before creates empty domain | Leading variable has only one value that makes second infeasible | Broaden leading variable domain |
:= gives each item a weight; :/ divides the weight across the range — use it to model realistic traffic profiles.! to exclude values.a -> b means "if a then b" — when a is false, b is unconstrained.randomize() with{} overrides apply for one call only — ideal for scenario-specific constraints in test sequences.randomize() return value; use constraint_mode(0) to bisect over-constrained failures.rand variables are randomized independently each time randomize() is called, with the solver free to repeat any value. randc (random cyclic) guarantees that every legal value is generated exactly once before any value repeats — similar to shuffling a deck of cards. randc is useful for ensuring complete coverage without explicit bins, but it only applies to integer variables and the cycle resets whenever constraints change.
A return value of 0 means the solver could not find a valid solution — the constraints are over-constrained. Common causes include: contradictory range constraints (e.g., a > 100 and a < 50 together), an implication constraint that creates an unsatisfiable dependency, a dist block whose weights sum to zero after other constraints filter the domain, or a solve-before ordering that makes the leading variable's range empty. Use constraint_mode(0) to disable constraints one by one until randomize() succeeds, then re-enable them to isolate the conflict.
Use soft constraints when you want a preferred default value but need sub-classes or inline with{} blocks to be able to override it without causing a conflict. Hard constraints always participate in the AND of all active constraints and cannot be overridden — they simply make the problem infeasible if contradicted. Soft constraints are discarded silently when they conflict with a hard constraint or a higher-priority soft constraint. A typical pattern is to put soft constraints in a base transaction class as sensible defaults and override them in specific test scenarios using inline constraints.
solve a before b tells the constraint solver to pick a value for variable a first, then solve for b given that value. Without solve-before, the solver is free to optimise the joint distribution, which often collapses biased distributions. The classic example: if mode is 1-bit (50% chance of 0 or 1) and data is constrained to {0} when mode==0 and {0..255} when mode==1, you want a true 50/50 split on mode. Without solve-before, the solver might pick values that statistically favour mode==1 because there are 256 solutions in that branch vs 1 in the mode==0 branch. Adding solve mode before data forces the 50/50 split before the solver enumerates data's range.