Every testbench style covered so far has one thing in common: all the input values were decided in advance, either hard-coded in an initial block, stored in a file, or encoded into an FSM. That means the verification engineer's mental model of the design determined which inputs got tested. Any bug hiding in a combination nobody thought to write stays hidden.

The linear random testbench breaks this dependency. Instead of writing down specific values, the testbench calls $random or $urandom to generate inputs automatically, running hundreds or thousands of test cases per simulation run. Different seeds produce different sequences — so each run explores a different part of the input space, and bugs that would never appear in a hand-written test list start to surface.

10 min read | Intermediate Level

What You'll Learn

  • How to replace hand-crafted stimulus with $random and $urandom calls
  • How seeds control randomisation and why they are essential for reproducibility
  • How to keep random values within a legal range using the modulo operator
  • Why random testing finds bugs that directed tests miss — and where it still falls short

The Same DUT: 4-bit Adder

We continue with the 4-bit adder from the previous articles in this chapter. Two 4-bit operands a and b, a carry-in cin, producing sum and cout. The linear testbench covered five hand-picked cases. The linear random testbench will cover a thousand — automatically.


// DUT: 4-bit adder (same as previous articles)
module adder (
    input  [3:0] a,
    input  [3:0] b,
    input        cin,
    output [3:0] sum,
    output       cout
);
    assign {cout, sum} = a + b + cin;
endmodule

$random vs $urandom

Verilog provides two system functions for generating random values:

$random returns a signed 32-bit integer. The sign bit means the upper bits can be 1 even for a small target width, which causes unexpected behaviour when assigning to narrower signals. It has been part of Verilog since the original standard.

$urandom returns an unsigned 32-bit integer. It is safer for generating values that will be assigned to reg signals and is the preferred choice in modern testbenches. It also supports an optional seed argument for explicit seed control.

Prefer $urandom over $random for new testbenches. The signed return value of $random produces surprising results when truncated to narrow widths — for example, a negative 32-bit value assigned to a 4-bit reg will still work due to truncation, but the distribution across the 4-bit space is uneven because the sign bit biases the upper half of the range.

A Basic Linear Random Testbench

The structure is identical to the linear testbench: an initial block, a clock, DUT instantiation, and a stimulus loop. The only change is replacing the hard-coded values with $urandom calls, and running the loop many times instead of five.


module tb_adder_random;

    // ?? DUT signals ????????????????????????????????????????????????
    reg  [3:0] a;
    reg  [3:0] b;
    reg        cin;
    wire [3:0] sum;
    wire       cout;

    // ?? DUT instantiation ??????????????????????????????????????????
    adder dut (
        .a   (a),
        .b   (b),
        .cin (cin),
        .sum (sum),
        .cout(cout)
    );

    // ?? Clock ??????????????????????????????????????????????????????
    reg clk = 0;
    initial clk = 0;
    always #(5ns) clk = ~clk;

    // ?? Loop counter and expected result ???????????????????????????
    integer i;
    reg [4:0] expected;   // 5 bits: bit[4]=carry, bits[3:0]=sum

    initial begin
        // Initialise inputs to known state
        a   = 0;
        b   = 0;
        cin = 0;

        // Wait two clock cycles before applying stimulus
        @(posedge clk);
        @(posedge clk);

        // ?? Run 1000 random test cases ?????????????????????????????
        for (i = 0; i < 1000; i = i + 1) begin

            // Generate random values for all three inputs
            // Truncation to [3:0] and [0:0] happens automatically
            // on assignment — only the lower bits are kept
            a   = $urandom;   // random 4-bit operand A
            b   = $urandom;   // random 4-bit operand B
            cin = $urandom;   // random carry-in (0 or 1)

            // Wait one clock edge for the combinational output to settle
            @(posedge clk);

            // Compute the expected result independently
            expected = a + b + cin;

            // Compare DUT output against expected value
            if ({cout, sum} !== expected) begin
                $error("Test %0d FAIL: a=%0d b=%0d cin=%0d | expected=%0d got=%0d",
                        i, a, b, cin, expected, {cout, sum});
            end
        end

        $display("Random test complete: 1000 cases run");
        $finish;
    end

endmodule

One thousand random cases run in the same simulation time that five directed cases used to take. If the adder has a bug — say, it mis-handles the carry-out at the boundary of the 4-bit range — a random test will hit that boundary far more often than a five-case directed test would.

Quick Check

Q: The loop runs 1000 iterations. The 4-bit adder has 512 possible input combinations (16 × 16 × 2). Why does running 1000 random cases not guarantee that all 512 combinations were exercised?

Show Answer

Because each call to $urandom picks a value independently, with equal probability for every outcome. Some combinations will be picked multiple times while others may never be picked — this is the "coupon collector" problem. To guarantee all 512 combinations are hit, you would need either a directed test (enumerate all 512 explicitly) or constrained random with coverage tracking to detect and fill gaps. The linear random testbench has no coverage measurement, so there is no way to know which combinations were exercised in a given run.

Seeds and Reproducibility

By default, $urandom uses a seed that is either fixed at zero or chosen by the simulator, depending on the tool. Two runs with the same seed produce exactly the same sequence of random values — which is the property that makes random testing useful in practice, not just in theory.

When a random test finds a bug, the engineer needs to be able to reproduce the exact failing sequence to debug it. Without seed control, re-running the same testbench may produce a completely different sequence that does not trigger the bug, making debug impossible. With seed control, saving the seed that caused the failure and re-running with that seed reproduces the failure deterministically.

Controlling the Seed

The seed is passed to the simulator as a command-line argument. The exact flag varies by simulator but follows this pattern:


// Typical simulator invocation — passing seed on the command line:
//   vcs  tb_adder_random.v +ntb_random_seed=42
//   xrun tb_adder_random.v -seed 42

// Inside the testbench, you can read the active seed back with:
//   $urandom(seed_value)  — seeds the generator and returns first value

// To print the seed at the start of every run (good practice):
initial begin
    integer seed;
    seed = $get_initial_random_seed();  // read the seed the simulator used
    $display("Running with seed = %0d", seed);
    // ... rest of testbench
end

Always print the seed at the start of every simulation run. When a failure occurs, the log file contains the seed that caused it. Without this, a failing run produces a bug report with no way to reproduce the failure — the seed is gone the moment the simulator exits.

Running Multiple Seeds

A single seed explores one random path through the input space. The real power of random testing comes from running the same testbench with many different seeds — typically as part of a nightly regression. Each seed explores a different region, and the union of many seeds covers far more of the input space than any single run.


// Shell script: run the testbench with 20 different seeds
// Each run produces a separate log file named by seed number
//
//   for seed in $(seq 1 20); do
//       vcs tb_adder_random.v +ntb_random_seed=$seed \
//           > logs/run_seed_${seed}.log 2>&1
//   done
//
// A failing run is identifiable by seed number.
// Re-run with that seed alone to reproduce the failure for debug.

Generating Random Values Within a Legal Range

Not all inputs accept the full range of random values. A DUT might only accept addresses in the range 0–15, opcodes in the range 0–7, or burst lengths of exactly 1, 2, 4, or 8. Passing a fully random 32-bit value to such an input produces illegal stimulus — the DUT receives something it was never designed to handle, and any resulting failure is a testbench bug, not a DUT bug.

The simplest way to constrain a random value to a range is the modulo operator %. Assigning to a narrower reg also truncates automatically:


    // ?? Range-limiting random values ???????????????????????????????

    reg [7:0]  addr;
    reg [2:0]  opcode;
    reg [31:0] data;
    reg        burst_len;

    // Address must be 0..15 — use modulo to constrain
    addr = $urandom % 16;         // result is 0..15

    // Opcode must be 0..7 — modulo 8
    opcode = $urandom % 8;        // result is 0..7

    // Full 8-bit data — assign directly, truncation handles it
    data = $urandom;              // lower 8 bits used on assignment to [7:0]

    // Burst length from a fixed set {1, 2, 4, 8}
    // Pick a random index 0..3 and look up the table
    begin : burst_sel
        reg [1:0] idx;
        reg [3:0] burst_table [0:3];
        burst_table[0] = 4'd1;
        burst_table[1] = 4'd2;
        burst_table[2] = 4'd4;
        burst_table[3] = 4'd8;
        idx       = $urandom % 4;
        burst_len = burst_table[idx];
    end

The modulo technique introduces a subtle bias: if the random range is not a power of two, lower values are slightly more likely than higher ones. For example, $urandom % 10 picks 0–5 slightly more often than 6–9 because 2³² is not evenly divisible by 10. For most verification purposes this bias is negligible. If truly uniform distribution is critical, use SystemVerilog's $urandom_range(max, min) which handles this correctly.

Quick Check

Q: A testbench generates a random 8-bit address with addr = $urandom % 256 for a DUT that only responds to addresses 0x00–0x0F (the upper 4 bits must be zero). What proportion of generated addresses are legal, and what is the practical effect on the test?

Show Answer

Only 16 out of 256 possible values (6.25%) are legal. The remaining 93.75% of generated addresses are illegal. In a 1000-iteration loop, roughly 937 transactions carry invalid addresses that the DUT may ignore, reject with an error, or handle unpredictably. This wastes most of the simulation time on stimulus the DUT was never designed for. The fix is addr = $urandom % 16 to restrict to the legal range — or in a more advanced testbench, using SystemVerilog constraints.

Why Random Testing Finds Bugs Directed Tests Miss

Directed testing finds the bugs the engineer anticipated. Random testing finds the bugs nobody anticipated. These are different populations, and both matter.

Consider a 4-bit adder with a subtle RTL bug: the carry-out is computed incorrectly when both a and b are exactly 4'b1000 (decimal 8) and cin is 0 — a very specific combination that produces sum=0 and cout=1 correctly, but the internal carry chain has a glitch that would cause a timing violation in a larger design. No engineer writing five directed test cases would choose a=8, b=8, cin=0 unless they already suspected that specific combination. A random test running 1000 cases will hit it roughly twice on average.

More broadly, directed tests reflect the engineer's mental model of how the design works. Bugs that exist outside that mental model — wrong behaviour in edge cases the engineer did not think about — are invisible to directed testing by definition. Randomisation has no mental model: it samples the input space without preconceptions, and that is precisely what gives it the ability to find unexpected bugs.

Limitations

No constraints. $urandom generates values with no knowledge of what the DUT considers legal. Without explicit range-limiting code in the testbench, illegal stimulus is generated freely. Unlike SystemVerilog's rand/constraint mechanism (introduced in constrained random verification), there is no formal way to express complex relationships between signals — such as "address must be aligned to a 4-byte boundary" or "burst length must not exceed the remaining space in the buffer."

No coverage measurement. The testbench has no way to know which parts of the input space were actually hit. It ran 1000 cases — but were the boundary values (0, 15) covered? Were all carry-out conditions exercised? Without a coverage model, these questions cannot be answered. The only signal of progress is the iteration count, which is a very weak proxy for actual coverage.

Output checking is still manual in its basic form. The example above added a comparison against an independently computed expected value — that is the seed of a scoreboard. But in the absence of a reference model for more complex DUTs, the engineer still needs to define what "correct" means for every output, which is not trivial.

These limitations motivate the next two evolutions: the self-checking testbench (which formalises the reference model and scoring) and eventually constrained random verification (which adds a formal constraint language so illegal stimulus can be ruled out structurally).

Common Mistakes

Mistake #1: Not printing or saving the seed, then failing to reproduce a bug found during random testing.

Wrong: Running the testbench with the simulator's default seed, finding a failure, then being unable to reproduce it on the next run because the seed was never recorded.
Right: Adding $display("Seed = %0d", $get_initial_random_seed()) as the very first line of the initial block, so every log file contains the seed that produced that run's sequence.

Why? A random bug that cannot be reproduced cannot be debugged. The seed is the only information that links a failing log to a specific input sequence. Without it, the only option is to run again and hope the same failure recurs — which may take dozens of runs to happen by chance.

Mistake #2: Using $random instead of $urandom and assigning to a narrow signal without understanding the truncation behaviour.

Wrong: Writing a = $random for a 4-bit signal and assuming the result is uniformly distributed across 0–15, when in fact the signed 32-bit return value biases the distribution because negative values become large unsigned numbers after truncation.
Right: Using $urandom for unsigned random generation, or using $urandom % 16 to explicitly constrain to the target range.

Why? $random returns a signed value. Assigning a negative 32-bit signed integer to a 4-bit reg keeps only the lower 4 bits, but the distribution of those lower 4 bits is skewed by the sign bit — values 8–15 (which correspond to a leading 1 bit) appear more often than expected for a truly random 4-bit value.

Mistake #3: Running only a single seed and concluding that random testing has been completed.

Wrong: Running the 1000-iteration loop once with the default seed, seeing all cases pass, and marking random testing as done.
Right: Running the same testbench with at least 10–20 different seeds as part of the regression, treating each seed as an independent test run that explores a different region of the input space.

Why? A single seed produces a single deterministic sequence — it just happens to be a sequence the engineer did not write by hand. The coverage benefit of random testing comes from multiple independent sequences, each probing different input combinations. One seed is barely better than one directed test of the same length.

Practice Exercise

Challenge: Extend to a Memory Interface

Take the state machine testbench from the previous article — the 8×8 memory with write/read cycles — and modify it to use random addresses and data instead of the fixed values addr=4, wr_data=0xA5.

The testbench should run 20 random write-then-read cycles. Each cycle picks a random 3-bit address (0–7) and a random 8-bit data value, writes them to the memory, reads them back from the same address, and verifies the read-back matches. Use $urandom to generate both values and print the seed at the start of the run.

Hint

The state machine structure stays exactly the same — IDLE, WRITE_REQ, WRITE_WAIT, READ_REQ, DONE. The only change is in WRITE_REQ: replace addr = 3'h4; wr_data = 8'hA5; with addr = $urandom % 8; wr_data = $urandom;. Add a loop counter and return to WRITE_REQ from DONE instead of calling $finish, until 20 cycles are complete. Save the expected write data in a register before writing so you can compare it in DONE.

Sample Solution — key changes only

    // Add at top of testbench:
    integer  cycle;          // counts completed write-read pairs
    reg [7:0] written_data;  // saves the value written so DONE can check it

    // In the initial block, before the FSM:
    $display("Seed = %0d", $get_initial_random_seed());
    cycle = 0;

    // WRITE_REQ state — generate random inputs:
    WRITE_REQ: begin
        wr_en        = 1;
        addr         = $urandom % 8;    // random address 0..7
        wr_data      = $urandom;        // random 8-bit data
        written_data = wr_data;         // save for later comparison
        $display("[%0t] Cycle %0d: WRITE addr=0x%0h data=0x%0h",
                  $time, cycle, addr, wr_data);
        state        = WRITE_WAIT;
    end

    // READ_REQ state — read from the same address written above:
    // addr is still set from WRITE_REQ — no change needed

    // DONE state — check and loop:
    DONE: begin
        if (ack) begin
            rd_en = 0;
            if (rd_data !== written_data)
                $error("Cycle %0d MISMATCH: wrote 0x%0h, read 0x%0h",
                        cycle, written_data, rd_data);
            else
                $display("[%0t] Cycle %0d PASS", $time, cycle);

            cycle = cycle + 1;
            if (cycle < 20)
                state = WRITE_REQ;   // run another cycle
            else begin
                $display("All 20 cycles complete");
                $finish;
            end
        end
    end

Summary

  • $urandom generates unsigned random 32-bit values; prefer it over $random which returns a signed value with uneven truncation behaviour for narrow signals
  • Seeds control the random sequence — the same seed always produces the same sequence, making failures reproducible; always print the seed at the start of every run
  • Constrain random values to legal ranges using the modulo operator (% N) or by assigning to a narrower reg — passing fully random values to a DUT with restricted input ranges generates illegal stimulus
  • Random testing finds bugs directed tests miss because it samples the input space without the engineer's preconceptions about which values to test
  • Run the same testbench with many different seeds in regression — a single seed is barely better than a single directed test of the same length
  • Core limitations: no formal constraints on illegal combinations, no coverage measurement to confirm what was actually exercised, and output checking still requires an independently computed expected value
Did you find what you were looking for?

Quiz

No quiz questions available for this article.

Cron Job Starts