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.
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.
| Declaration | What 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.
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 sentCheck yourself
Remember
randfields get new values on every call; fields withoutrandact 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
- Legal packet length Implement · Easy
- The packet that kept its old length Fix · Easy