SystemVerilog · Module 10
randcase Statement
Procedural weighted choice without classes or constraints; config-driven weights as an architectural power move; the safety-branch pattern that prevents all-zero-weight undefined behaviour.
Module 10 · Page 10.13
What randcase Does
randcase is a procedural statement — it runs inside tasks, functions, and initial/always blocks. It has no connection to the constraint solver or randomize(). On each execution, it picks exactly one branch based on relative weights and executes that branch's body.
Think of it as a case statement where the selector is chosen by a weighted coin flip, not by matching a value.
initial begin
repeat (10) begin
randcase
6 : send_read(); // 6/10 = 60% of the time
3 : send_write(); // 3/10 = 30% of the time
1 : send_idle(); // 1/10 = 10% of the time
endcase
end
end
// Each iteration picks exactly one branch.
// Over many iterations the ratio converges to 6:3:1 = 60/30/10%.Full Syntax
Each branch is a weight expression followed by a colon and one or more statements. Multiple statements in a branch are wrapped in begin ... end. There is no default — one branch is always chosen.
randcase
// weight : single_statement;
5 : task_a();
// weight : begin ... end (multiple statements)
3 : begin
task_b();
count_b++;
end
// weight can be any integral constant expression
(max_weight / 2) : task_c();
// weight 0 means this branch is never chosen
0 : task_d(); // dead branch — excluded
endcaseExpression Weights
Weights do not have to be literal integers. Any integral constant expression or a variable readable at that point can be used. This lets you change the distribution at runtime without rewriting the randcase.
int read_weight = 6;
int write_weight = 4;
// Phase 1: 60% reads, 40% writes
randcase
read_weight : do_read();
write_weight : do_write();
endcase
// Flip the ratio for Phase 2 — no code change needed in the randcase
read_weight = 2;
write_weight = 8;
// Phase 2: 20% reads, 80% writes
randcase
read_weight : do_read();
write_weight : do_write();
endcaserandcase vs case vs dist
| Feature | case | randcase | dist (in constraint) |
|---|---|---|---|
| How branch selected | Matches a selector value | Weighted random pick | Weighted random value assignment |
| Where it lives | Procedural (task/function/initial) | Procedural (task/function/initial) | Constraint block or with clause |
| Requires a class? | No | No | Yes (or std::randomize) |
| Executes code? | Yes — any statements | Yes — any statements | No — assigns values only |
| Use for | Deterministic branching on known values | Proportional random operation sequences | Shaping rand field distributions |
Nested randcase
randcase branches can contain other randcase statements. Each inner randcase runs its own independent weighted selection when its branch is reached. This is useful for hierarchical operation selection.
randcase
7 : begin // 70% — normal operations
randcase
6 : do_read(); // within normal: 60% reads
4 : do_write(); // within normal: 40% writes
endcase
end
2 : begin // 20% — burst operations
randcase
5 : do_burst_read();
5 : do_burst_write();
endcase
end
1 : do_idle(); // 10% — idle
endcase
// Net probabilities:
// do_read: 70% × 60% = 42%
// do_write: 70% × 40% = 28%
// do_burst_read: 20% × 50% = 10%
// do_burst_write: 20% × 50% = 10%
// do_idle: 10%Practical Patterns
Pattern 1 — Proportional bus transaction generator
task automatic run_traffic(input int num_txn);
BusTxn t = new();
repeat (num_txn) begin
randcase
50 : begin // 50% — single read
assert(t.randomize() with { write == 0; burst == 0; });
drive(t);
end
30 : begin // 30% — single write
assert(t.randomize() with { write == 1; burst == 0; });
drive(t);
end
15 : begin // 15% — burst read
assert(t.randomize() with { write == 0; burst > 0; });
drive_burst(t);
end
5 : do_idle_cycle(); // 5% — idle
endcase
end
endtaskPattern 2 — Phase-adaptive traffic
typedef enum { IDLE, NORMAL, STRESS } test_phase_e;
task automatic adaptive_traffic(test_phase_e phase, int n);
int w_read, w_write, w_burst, w_idle;
case (phase)
IDLE: begin w_read=0; w_write=0; w_burst=0; w_idle=1; end
NORMAL: begin w_read=5; w_write=3; w_burst=1; w_idle=1; end
STRESS: begin w_read=3; w_write=3; w_burst=4; w_idle=0; end
endcase
repeat (n) begin
randcase
w_read : do_read();
w_write : do_write();
w_burst : do_burst();
w_idle : do_idle();
endcase
end
endtask
// Weight 0 branches are never chosen — cleanly excluded per phaseCommon Pitfalls
All weights zero — fatal error
If every branch has weight 0, there is no valid branch to choose. The simulator either reports a fatal error or exhibits undefined behaviour. Always ensure at least one branch has a non-zero weight.
Expecting equal probability without thinking about totals
1 : a(); 1 : b(); 1 : c(); gives 33%/33%/33%. Adding 2 : d(); changes all probabilities to 20%/20%/20%/40%. Recheck all branch probabilities when you add or change a weight.
Using randcase where dist is the right tool
If you only need to assign a weighted value to a variable with no other side effects, use dist in a constraint instead. randcase is for when the branches have different code to run, not just different values.
Confusing randcase with a constraint
randcase has no interaction with the constraint solver. It does not affect rand variables or randomize(). It is purely a procedural control-flow statement.
Quick Reference
// ── Basic form ────────────────────────────────────────────────
randcase
w1 : stmt1;
w2 : stmt2;
w3 : begin stmt3a; stmt3b; end
endcase
// ── Probability formula ───────────────────────────────────────
// P(branch i) = wi / (w1 + w2 + ... + wN)
// ── Weight 0 = excluded branch ────────────────────────────────
randcase
enable_err ? 1 : 0 : inject_error(); // conditional weight
9 : normal_op();
endcase
// ── Variable weights ──────────────────────────────────────────
int wa = 7, wb = 3;
randcase
wa : op_a();
wb : op_b();
endcase
// ── Key facts ─────────────────────────────────────────────────
// ✓ Procedural — works in tasks, functions, initial, always
// ✓ No class or constraint solver needed
// ✓ Exactly one branch per execution
// ✓ No default keyword — one branch is always chosen
// ✓ No fall-through between branches
// ✗ All weights 0 → fatal/undefinedVerification Usage — Where randcase Earns Its Keep in Real Testbenches
randcase sits in a specific niche: weighted procedural choice without the ceremony of a class-and-constraint setup. Three patterns recur across production environments.
Sequence-level scenario picker
Inside a sequence's body task, pick a sub-scenario based on weighted probabilities — drive write, drive read, drive atomic, idle. The selected branch calls into a specific sub-sequence.
<code>task my_seq::body();
repeat (num_iterations) begin
randcase
70 : drive_write_scenario();
25 : drive_read_scenario();
4 : drive_atomic_scenario();
1 : drive_idle_cycle();
endcase
end
endtask</code>Inter-transaction idle gap distribution
A driver inserts a randomised idle delay between transactions. randcase lets you bias toward back-to-back, short-gap, or long-idle patterns without building a constraint class for what amounts to one scalar choice.
<code>task apb_driver::drive_idle_gap();
int gap_cycles;
randcase
40 : gap_cycles = 0; // back-to-back
30 : gap_cycles = $urandom_range(1, 5);
25 : gap_cycles = $urandom_range(6, 100);
5 : gap_cycles = $urandom_range(101, 1000);
endcase
repeat (gap_cycles) @(posedge clk);
endtask</code>Test-layer weight overrides via configuration
The most powerful use: weights are expressions driven by config-DB values, so different test scenarios apply different distributions to the same randcase without modifying the sequence body.
<code>task scenario_seq::body();
randcase
cfg.weight_write : drive_write();
cfg.weight_read : drive_read();
cfg.weight_atomic : drive_atomic();
cfg.weight_idle : drive_idle();
1 : drive_default(); // safety net
endcase
endtask
// In test:
cfg.weight_write = 80; cfg.weight_read = 15;
cfg.weight_atomic = 5; cfg.weight_idle = 0;
// Default branch fires the 1/101 (~1%) safety net</code>Simulation Behavior — How the Simulator Executes randcase
Sequential weight evaluation
The simulator visits each branch in declaration order, evaluates its weight expression, and accumulates a running total. The weights can be literals, constants, variables, or any integral expression — each is evaluated once per randcase execution.
Weighted random selection
After summing all weights, the simulator draws a single uniform random integer from [0, total_weight). It walks the cumulative weights to find which branch's range contains the drawn number; that branch is the winner. Cost: O(N) for N branches — typically negligible against the executed branch body's cost.
Zero-weight branches are excluded
A branch with weight 0 cannot be selected — the cumulative-walk algorithm skips over it. This is what enables the config-driven pattern: setting a weight to 0 effectively disables that branch for the current test without modifying the randcase code.
All-zero weights are undefined behaviour
If every branch evaluates to 0, total weight is 0, and there is no valid range for the random draw. The LRM marks this as a fatal/undefined case. Defensive practice: include a literal-weight safety branch.
No solver, no constraints, no rand fields
randcase is pure procedural code. It has no relationship to the constraint solver. It can be called from any procedural context — initial blocks, always blocks, tasks, functions, sequences — without any class context required.
<code>randcase
cfg.w_write : drive_write();
cfg.w_read : drive_read();
cfg.w_idle : drive_idle();
1 : drive_default(); // safety
endcase
// Sum always ≥ 1; safe behaviour guaranteed</code><code>randcase
cfg.w_write : drive_write();
cfg.w_read : drive_read();
cfg.w_idle : drive_idle();
endcase
// If test forgot to set all weights:
// total=0 → fatal / undefined / silent skip
// depending on simulator</code>Waveform Analysis — Tracing Which Branch Fired
randcase produces no waveform signals on its own — it's procedural control flow. The way to make branch selection visible is to log the winning branch from inside the branch body.
The instrumentation pattern
<code>task my_seq::body();
repeat (10) begin
randcase
70 : begin `uvm_info("BR", "drive_write", UVM_HIGH) drive_write(); end
25 : begin `uvm_info("BR", "drive_read", UVM_HIGH) drive_read(); end
4 : begin `uvm_info("BR", "drive_atomic", UVM_HIGH) drive_atomic(); end
1 : begin `uvm_info("BR", "drive_idle", UVM_HIGH) drive_idle(); end
endcase
end
endtask</code>ASCII view — branch selection across iterations
<code> 100ns BR drive_write
200ns BR drive_write
350ns BR drive_read
500ns BR drive_write
650ns BR drive_write
750ns BR drive_atomic ← rare branch fired (4% probability)
800ns BR drive_write
...
After 1000 iterations:
drive_write: 712 (expected 700) ✓
drive_read: 247 (expected 250) ✓
drive_atomic: 35 (expected 40) ✓
drive_idle: 6 (expected 10) ✓</code>Verifying weight distribution after long runs
For long-running tests, accumulate branch-selection counts and compare against expected ratios. The actual frequencies should match expected within ±10% after 1000+ iterations; outside that range suggests either a wrong weight, a seed quirk, or a constraint elsewhere that's biasing the runs.
Industry Insights — Hard-Won Lessons About randcase
Debugging Academy — Five Real Bugs From randcase Misuse
"randcase hangs simulator with 'no matching branch'"
DEBUGA regression intermittently dies with the simulator reporting "all randcase weights are zero" or similar. Other runs are fine.
<code>task body();
randcase
cfg.weight_write : drive_write();
cfg.weight_read : drive_read();
endcase // BUG: if both config fields are 0, total weight is 0
endtask
// In some tests, cfg is left at defaults (0, 0)
// → randcase hits undefined behaviour</code>Config-driven weights can legitimately be 0; if every weight is 0, total is 0 and randcase has no valid range for selection. LRM marks this fatal/undefined.
Add a literal-weight safety branch: randcase cfg.weight_write : ...; cfg.weight_read : ...; 1 : drive_default(); endcase. The safety branch fires only when others are all 0; otherwise it's a 1/N+1 contribution to the distribution. Make this a mandatory pattern for any randcase with non-literal weights.
"Branch fires far less often than weight suggests"
DEBUGA randcase declares { 50 : write; 50 : read } but coverage shows reads happening 90% of the time.
<code>task body();
randcase
weight_read : drive_read(); // expects 50/50
weight_write : drive_write();
endcase
endtask
// Configuration code somewhere:
// weight_write = $urandom_range(0, 10); // small
// weight_read = $urandom_range(50, 100); // big
// → distribution biased toward read</code>Weights are expressions evaluated at runtime. The author thought "weight_read = 50" but somewhere upstream the field was randomised to a much larger value. The actual values at randcase time determine the distribution; the assumed values are irrelevant.
Either fix the upstream code so weights match the intended distribution, or use literal weights in the randcase if the distribution should be fixed at code-write time. Log the evaluated weights at runtime to verify they match your mental model: ``uvm_info("RC", $sformatf("w_read=%0d w_write=%0d", weight_read, weight_write), UVM_HIGH)before therandcase`.
"Branch body lacks braces, only first statement is conditional"
DEBUGA randcase branch with multiple statements appears to only execute one of them; subsequent statements seem to run unconditionally.
<code>randcase
50 : drive_write();
start_scoreboard_check(); // BUG: outside the branch
50 : drive_read();
endcase
// Compiler treats: branch 1 body = drive_write() only;
// start_scoreboard_check() is a top-level statement
// between branches → syntax error in some simulators,
// or attached to next branch in others
// Behaviour is implementation-defined</code>A branch's body is a single statement by default. Multiple statements need a begin/end block. Without it, only the first statement is the branch body; subsequent statements have ambiguous attachment.
Always wrap multi-statement branches in begin/end: randcase 50 : begin drive_write(); start_scoreboard_check(); end 50 : drive_read(); endcase. Make this a style rule even for single-statement branches — the brace clarifies scope and prevents the bug class when someone later adds a second statement.
"randcase picks the same branch every run"
DEBUGA "random" test always executes the same randcase branch on every run; the test becomes effectively directed.
<code>module tb;
initial begin
// No seed plusarg passed to simulator
repeat (100) begin
randcase
50 : do_a();
50 : do_b();
endcase
end
end
endmodule
// Every run prints exactly the same sequence:
// do_a, do_b, do_a, do_a, do_b, ...
// Because PRNG seed is fixed across runs</code>randcase uses the same PRNG as the rest of the simulator. Without a seed plusarg, the simulator starts from a deterministic default — same seed, same random sequence, same randcase selections. This is correct behaviour (and desirable for reproducibility), but it surprises developers expecting per-run variation.
Pass a different seed per run from the regression dispatcher: simv +UVM_TESTNAME=my_test +ntb_random_seed=$RANDOM. Log the seed in the test's header. The next run with a different seed produces a different randcase sequence; the run with the same seed reproduces the original sequence bit-for-bit.
"Weights are correct but distribution looks skewed for small sample sizes"
DEBUGA randcase with weights 50/50 runs 20 times in a test and shows 14 "writes" and 6 "reads" — the developer thinks the weights are wrong.
<code>repeat (20) begin
randcase
50 : do_write(); // expected: ~10
50 : do_read(); // expected: ~10
endcase
end
// Actual: 14 writes, 6 reads
// Is this a bug?</code>20 samples is too few to converge to the expected distribution. For a 50/50 distribution, the standard deviation is sqrt(0.25 × 20) ≈ 2.2 — so seeing 14/6 (deviation of 4) is well within normal random fluctuation. The weights are correct; the sample size is just small.
Run more iterations before judging. At 1000 iterations, the expected deviation is ~16, so 540/460 is normal but 700/300 is suspicious. Statistical convergence requires hundreds-to-thousands of samples; 20 is too few for any meaningful frequency analysis. Tools like $display("%0d/%0d", count_a, count_a + count_b) let you sanity-check distributions at the end of long runs.
Interview Q&A — Twelve Questions You Will Be Asked
Executes one of several branches based on weighted random choice. Each branch has a weight expression and a statement (or begin/end block). The simulator picks one branch with probability proportional to its weight, then executes that branch's body. randcase is procedural code — no constraint solver, no rand fields, no class context required.
Best Practices — Ten Rules for randcase Discipline
- Always include a literal-weight safety branch when using expression weights. Prevents the all-zero-weights fatal/undefined case.
- Use weights that sum to 100 for readability. Each weight reads directly as a percentage; no mental ratio computation required.
- Wrap multi-statement branch bodies in
begin/end. Single-statement default has ambiguous attachment for subsequent statements; always use braces. - Use
randcasefor procedural control-flow choice,distfor stimulus generation. Different tools for different jobs. - Drive weights from config-DB whenever possible. One sequence body serves many test scenarios; refactor hard-coded weights to configurable ones at code-review time.
- Log branch selection during bring-up. Per-branch counters at
report_phaseverify the distribution matches expectations. - Pass distinct seeds per regression run. Without seed variation,
randcasepicks the same branches every run — a CRV test degenerates into directed. - Don't judge distribution correctness from small samples. Random distributions need hundreds-to-thousands of iterations to converge statistically.
- Document the meaning of each branch with a comment. A branch's purpose isn't always obvious from its statement; "
// drive a normal write" pays back in code review. - Never use
randcasein synthesisable code. Strictly simulation-only construct; keep in verification directories or behind ``ifdef` guards.