Rand fields and checked randomization

Declare the random fields of a transaction and the rules they must obey, and treat every call to randomize() as one that can fail.

Lesson
5.1
Time
20 min
Runs on
Verilator
Check
5 questions

A directed test asks for 16-beat bursts with t.randomize() with { beats == 16; }, sends 200 of them and passes. Coverage later shows that no 16-beat burst ever reached the DUT. The burst class allows only 1 to 8 beats, so every call failed and returned 0. Nobody checked the return value, so the test sent the previous burst 200 times.

Every call fails, and the old burst goes out again
012345randomize() returnedbeats requested316beats driven3first failed call

One column per transaction. The driver sends whatever the object holds.

Random fields and the rules they obey

A class property marked rand is a random variable: each call to randomize() chooses a new value for it. A constraint block lists rules about those values, and every rule must hold at the same time.

The examples in this module use burst_txn, a smaller relative of Module 4's axi_txn. It holds an 8-bit byte address and a beat count, and lesson 5.2 adds a flag that marks writes.

Constraints are relations, not statements that run in order. bytes == 4 * beats does not compute bytes from beats. The solver picks both values so that the equation holds, and it can work from either side.

DeclarationWhat randomize() does with it
rand bit [7:0] addr;chooses a new value that satisfies every constraint
randc bit [1:0] tag;chooses values in a shuffled order that uses every value once before any repeats
bit [7:0] max_addr;leaves it alone; constraints read it as a fixed number

Three kinds of field in a transaction class.

Fields without rand make good settings. A constraint such as addr <= max_addr reads the current max_addr, so a test can narrow the address range without editing the class.

A rand field that no constraint mentions can take any value of its type. For a 32-bit beats, that means lengths in the billions, so give every random field a legal range, even when no rule seems to need one.

What randomize() returns

randomize() is a built-in function of every class. It looks for values of all random fields that satisfy all active constraints. If it finds them, it assigns them and returns 1.

If no combination satisfies every rule, the constraints are unsatisfiable. The call then returns 0 and leaves every random field holding its previous value. Nothing stops the simulation, and an unchecked failure looks exactly like a successful call that happened to repeat the last values.

Return value ignored

void'(t.randomize() with { beats == 16; });
drive(t);   // the previous burst again

Sends the previous burst again whenever the request is impossible.

Return value checked

if (!t.randomize() with { beats == 16; })
  $fatal(1, "burst request cannot be met");
drive(t);

Stops at the first impossible request and says which one it was.

Avoid assert (t.randomize()); as the only call. A test can switch assertions off, and then the call inside the assertion never runs, so no new values are chosen at all. Call randomize() in ordinary code and test what it returns.

The fix costs one line per call. Many teams wrap it in a small task or macro that calls $fatal with the class name and the request, so a failed call can never pass silently.

The same trap hides in a retry loop. A generator that calls randomize() until it succeeds, with no limit, hangs forever on an impossible request. Give retries a limit, and report the request when the limit runs out.

Constraints for one call with randomize() with

t.randomize() with { ... } adds constraints for that call only. They join the class constraints instead of replacing them, so with { beats == 16; } and the class rule beats inside {[1:8]} must both hold, which is impossible.

Inside the braces, a name is looked up in the object first. If the test has its own variable called beats, then with { beats == beats; } compares the object's field with itself and constrains nothing. Give the test's variable another name, or write local::beats to reach it.

Inline constraints are the usual way to write a directed test on top of random stimulus. The test states only what it cares about, such as beats == 8, and the class still guarantees every other rule, such as alignment.

Check the result with separate code

A successful call proves only that the values satisfy the constraints you wrote. If the class forgot the alignment rule, every burst is legal by its own constraints and wrong by the protocol.

So check the stimulus against the protocol with code written apart from the class, such as a function legal_burst(addr, beats) that tests each rule again. The class and the check then have to be wrong in the same way before a bad burst slips through. Lesson 5.7 builds such a check for the burst generator.

Run the check on every transaction, in the driver or the scoreboard, not only in a unit test of the class. It costs a few lines and catches a broken class the first time it produces a bad burst.

Try it

Call 2 asks for 16 beats, which the class forbids, so it returns 0 and the object still holds call 1's burst. Call 3 makes the same request but checks the result and sends nothing.

The warning lines are Verilator's: they name the class constraint and the inline constraint that conflict. Change 16 to 8 and predict all three lines before you run it.

RUNVerilator · simulator service
Edit the code, then run it
Expected: call 1: ok=1 addr=40 beats=4 %Warning-UNSATCONSTR: main.sv:4: Unsatisfied constraint: 'constraint legal { addr[1:0] == 0; beats inside {[1:8]}; }' %Warning-UNSATCONSTR: main.sv:15: Unsatisfied constraint: 'ok = t.randomize() with { beats == 16; };' call 2: ok=0 addr=40 beats=4 %Warning-UNSATCONSTR: main.sv:4: Unsatisfied constraint: 'constraint legal { addr[1:0] == 0; beats inside {[1:8]}; }' %Warning-UNSATCONSTR: main.sv:18: Unsatisfied constraint: 'if (!t.randomize() with { beats == 16; })' call 3: randomize failed; nothing sent

Check yourself

Predict the outputPredict the exact output. Both constraints must hold at once.
class sizes;
  rand int unsigned beats, bytes;
  constraint c { bytes == 4 * beats; bytes + beats == 20; }
endclass

module tb;
  sizes s = new;
  initial begin
    if (!s.randomize()) $fatal(1, "randomize failed");
    $display("beats=%0d bytes=%0d", s.beats, s.bytes);
  end
endmodule
Spot the bugA test asks for bursts of 4, 16 and 2 beats; the class allows 1 to 8. Each option is the loop body that randomizes and sends one burst. The scoreboard must receive exactly the 4-beat and the 2-beat burst. Which option sends a burst it should not?
ConceptA call t.randomize() with { addr == 8'h41; } fails because the class requires addr[1:0] == 0. What does t.addr hold afterwards?
ConceptWhy is assert (t.randomize()); a poor way to randomize?
ConceptA test declares int beats = 2; and calls t.randomize() with { beats == beats; }. The class has rand int unsigned beats with the rule beats inside {[1:8]}. What does t.beats become?

Remember

  • rand fields get new values on every call; fields without rand act as fixed settings in constraints.
  • Constraints are rules that hold together, not assignments that run in order.
  • randomize() returns 0 when the rules cannot all be met, and the fields keep their old values.
  • Check every call in ordinary code; never rely on assert (t.randomize()).
  • Check generated stimulus against the protocol with code written apart from the class.

Practice