Randomization modes and frozen state

Freeze a field with rand_mode or switch a block off with constraint_mode, predict what the next call does, and give the object back the way you found it.

Lesson
5.5
Time
20 min
Runs on
Verilator
Check
5 questions

A directed test pins one burst to the last word of the page with t.addr = 8'hFC; t.addr.rand_mode(0); and never turns randomization back on. The next 5,000 bursts all start at 0xFC, and every one is a single beat, because a longer burst would leave the page. Address coverage collapses to one bin, and nothing fails, because every one of those bursts is legal.

A frozen address outlives the directed test
0123456addr3c80fcbeats421addr.rand_mode()rand_mode(0), never restored

One column per burst. From the directed burst on, only beats is random, and only 1 fits.

Freezing one field with rand_mode

t.addr.rand_mode(0) turns addr into an ordinary field for every later call: randomize() leaves its value alone. The constraints that mention addr stay active and read its current value as a fixed number.

If that value breaks a hard rule, the call fails. The solver never changes a frozen field to make the rules fit, so a frozen 8'h41 with an alignment rule makes every call return 0.

t.addr.rand_mode() with no argument returns the current mode, 1 or 0. t.rand_mode(0) freezes every random field of the object at once, and t.rand_mode(1) makes them all random again.

A frozen field can still appear in a randomize() with block. The inline constraint then tests the frozen value, so with { addr == 8'h40; } on an object frozen at 0xFC fails.

Setting a frozen field by hand also skips the solver for that field until the next call. Nothing checks the value you wrote, so check it against the rules yourself, or ask for it with an inline constraint, which the solver does check.

Freezing is handy when the rest of the transaction should vary around a fixed value. A test that sweeps every aligned address can freeze addr, set it in a loop, and let beats stay random for each address.

Freeze and forget

t.addr = 8'hFC;
t.addr.rand_mode(0);
if (!t.randomize() with { beats == 1; }) ...
send(t);
// later traffic: addr stays FC

Every later burst starts at 0xFC, long after the directed test has finished.

Ask for the value in one call

if (!t.randomize() with { addr == 8'hFC; beats == 1; })
  $fatal(1, "page-end burst rejected");
send(t);
// later traffic: addr random again

The inline constraint ends with the call, so normal traffic resumes at once.

Switching a block off with constraint_mode

t.legal.constraint_mode(0) disables the whole block named legal for that object, until constraint_mode(1) turns it back on. Called with no argument, it returns the block's current state.

That is how a negative test produces an illegal burst on purpose, such as a misaligned address the DUT must reject. Use a separate object for it, so normal traffic in the same test keeps the rules.

Switching a block off removes every rule in it, not just the one you meant. If legal holds both alignment and the beat range, switching it off to get a misaligned address also allows 0-beat and 200-beat bursts. Split the rules into small blocks, such as align and length, so a negative test can switch off exactly one.

ControlApplies toLasts until
t.randomize() with { ... }one callthe call returns
t.addr.rand_mode(0)one field of one objectt.addr.rand_mode(1)
t.rand_mode(0)every random field of one objectt.rand_mode(1)
t.legal.constraint_mode(0)one block of one objectt.legal.constraint_mode(1)

Four ways to steer a call, and how long each one lasts.

t.constraint_mode(0), called on the object itself, switches off every block at once. That is rarely what a test wants, because it removes the protocol rules along with the defaults.

Modes belong to an object. A new object starts with every field random and every block on, and changing the modes of one object never affects another object of the same class.

Prefer the control that ends by itself

Most directed values need to hold for one call, and randomize() with is made for that. It cannot leak into later traffic, because it ends when the call returns.

Use rand_mode(0) when a field must stay fixed across many calls, such as a channel number for a whole test. Restore it in the same block of code that froze it, so a reader can see both lines together.

When a sequence hands a shared object back to other code, check its state first. A line such as if (!t.addr.rand_mode()) $error("addr left frozen"); catches the leak in the opening story in its first transaction.

A good habit is to make each directed sequence leave the object exactly as it found it, with the same mode for every field and block. A short check at the end of the sequence makes that habit cheap to keep.

The same care applies to constraint modes. A block switched off for one negative burst and never switched back on lets illegal bursts into every later sequence that shares the object. Those bursts then look like DUT failures.

Try it

After the directed burst, addr stays at fc in every later call, and rand_mode() reports 0. Freezing the illegal value 41 makes the call fail and leaves addr at 41.

With legal switched off, a misaligned 12-beat burst is generated on purpose; with it back on, the same request fails. Add t.addr.rand_mode(1); straight after the directed burst and predict the three next lines.

RUNVerilator · simulator service
Edit the code, then run it
Expected: directed: ok=1 addr=fc next: ok=1 addr=fc (addr random? 0) next: ok=1 addr=fc (addr random? 0) next: ok=1 addr=fc (addr random? 0) %Warning-UNSATCONSTR: main.sv:4: Unsatisfied constraint: 'constraint legal { addr[1:0] == 0; beats inside {[1:8]}; }' frozen 41: ok=0 addr=41 rules off: ok=1 addr=41 beats=12 %Warning-UNSATCONSTR: main.sv:4: Unsatisfied constraint: 'constraint legal { addr[1:0] == 0; beats inside {[1:8]}; }' %Warning-UNSATCONSTR: main.sv:30: Unsatisfied constraint: 'ok = t.randomize() with { addr == 8'h41; };' rules on: ok=0

Check yourself

Predict the outputPredict the exact output.
class burst_txn;
  rand bit [7:0]    addr;
  rand int unsigned beats;
  constraint legal { addr[1:0] == 0; beats inside {[1:8]}; }
endclass

module tb;
  burst_txn t = new;
  initial begin
    t.addr = 8'h80;
    t.addr.rand_mode(0);
    repeat (3)
      if (!t.randomize()) $fatal(1, "randomize failed");
    $display("%h %0d %0d", t.addr, t.addr.rand_mode(), t.beats.rand_mode());
  end
endmodule
Spot the bugEach option sends one directed burst at address 0xFC through first, and then the test randomizes t 20 more times. Those 20 bursts must use more than one address. Which option leaves the address stuck?
ConceptA test runs t.addr = 8'h41; t.addr.rand_mode(0); and then t.randomize(), with the class rule addr[1:0] == 0 active. What happens?
ConceptWhat is the difference between t.addr.rand_mode(0) and t.legal.constraint_mode(0)?
ConceptObjects a and b belong to the same burst class. A test runs a.addr.rand_mode(0);. What happens to b.addr in the next b.randomize()?

Remember

  • x.rand_mode(0) freezes a field: later calls keep its value, and the constraints still apply to it.
  • A frozen value that breaks a hard rule makes every call fail.
  • blk.constraint_mode(0) switches a named block off for that object until you switch it back on.
  • Modes persist and belong to one object; inline with constraints end with the call.
  • Restore a mode in the same place you changed it, and check it before handing the object on.

Practice