Four-state vs two-state types

Choose between four-state and two-state types, and keep an unknown DUT value visible until your checker has looked at it.

Lesson
1.1
Time
20 min
Runs on
Your browser
Check
5 questions

A reset-value test passes every night, yet the first silicon powers up with a random value in its ctrl register. In simulation the register was never reset, so it held x, the value SystemVerilog uses for an unknown bit. The monitor copied the bus into an int, which cannot hold x, so every unknown bit quietly became 0. The checker expected 0, saw 0 and logged a pass, night after night.

Reset never arrives: the DUT drives x, the monitor stores 0
012345rst_nrd_enctrl (logic [7:0])mon_ctrl (int)checker: 0 == 0, PASS

The testbench never drives rst_n low, so ctrl stays unknown. The monitor's int copy reads 0 in every cycle.

Four values and two

Real wires carry more than 0 and 1: a flip-flop that was never reset holds an unknown value, and a bus that nobody drives floats.

SystemVerilog models this with four logic values: 0, 1, x and z. The value x means the simulator cannot tell whether a bit is 0 or 1, and z means high impedance, with nothing driving the wire.

A four-state type stores all four values, while a two-state type stores only 0 and 1. Two-state types use less memory and simulate faster, which is why the language offers both.

TypeStatesWidthSignedValue before first assignment
logic, reg4you choose (1 by default)nox
integer432yesx
bit2you choose (1 by default)no0
byte28yes0
shortint216yes0
int232yes0
longint264yes0

The integer types you will use most.

The last column matters most in a testbench. A logic that nobody assigns reads as x, so the mistake shows up at once, but an int that nobody assigns reads as 0 and looks exactly like real data.

Where unknown values come from

Inside a DUT, the design under test, an x almost always points at a real problem, and four sources cover most of the cases you will meet.

A register without a reset starts as x and stays that way until the design writes it. An input port that the testbench forgot to connect floats at z, and logic that reads a z produces x, so 1 & z is x.

Two drivers that fight also produce x: a wire driven to 0 by one source and to 1 by another resolves to x. Reading a memory at an address that contains an x bit returns x as well.

Each of these is a bug you want to see, and a four-state testbench shows it to you. A two-state copy turns the same bug into an ordinary-looking 0 and lets it pass.

What a two-state copy does to x and z

Assigning a four-state value to a two-state variable turns each x bit and each z bit into 0. The simulator prints no warning, so the copy simply holds a different number from the original.

For example, copying 4'b10xz into a bit [3:0] gives 4'b1000, and copying 4'bx1x1 into an int gives 5.

Arithmetic can erase even more information. If any operand bit of + is x, every bit of the sum is x, so n = a + 1 stores 0 in an int whenever a has a single unknown bit.

A monitor is testbench code that records what the DUT drives. A checker, such as a scoreboard, compares those records with the expected values. This is why the monitor in the opening story passed: the information was lost at the assignment, before any comparison ran.

How comparisons treat x

The equality operators == and != return 0, 1 or x, and they return x whenever an unknown bit could change the answer.

So 4'b10x0 == 4'b1000 is x, because the values match if the unknown bit is 0 and differ if it is 1. In contrast, 4'b00x0 == 4'b1000 is 0, because bit 3 already differs whatever the unknown bit holds.

An if whose condition is x takes the else branch. A check written as if (got != exp) $error(...) therefore stays silent when got is unknown, and the checker passes without checking anything.

The case equality operators === and !== compare x and z as plain symbols and always return 0 or 1. Since 8'hxx !== 8'h00 is 1, that error does fire. But x === x is also 1, so a reference model that produced x as well would let the check pass.

The reliable test is $isunknown(value), which returns 1 if any bit is x or z. Call it first, and compare the value only once you know that every bit is 0 or 1.

Two-state sample, != check

int sample;
sample = dut_ctrl;              // x bits become 0
if (sample != 0)
  $error("ctrl=%0d", sample);   // never fires

Passes when dut_ctrl is 8'hxx, because the unknown value is gone before the check runs.

Four-state sample, unknown check first

logic [7:0] sample;
sample = dut_ctrl;
if ($isunknown(sample))
  $error("ctrl has x/z: %b", sample);
else if (sample !== 8'h00)
  $error("ctrl=%h", sample);

Reports ctrl has x/z: xxxxxxxx, and still checks the value once every bit is known.

When two-state is the right choice

Two-state types suit values that the testbench creates itself, such as loop counters, transaction counts, random stimulus and scoreboard indexes. None of these ever needs an x.

Watch the sign, though, because byte, shortint, int and longint are all signed. A byte holding 127 becomes -128 when you add 1, so use bit [7:0] when you mean 0 to 255.

Two-state stimulus has a hidden benefit as well. A testbench that drives the DUT from bit variables can never send an x by accident, so an x inside the DUT started in the design or in its connections.

A simple rule covers most cases: anything sampled from the DUT stays four-state until a checker has looked at it, and anything the testbench generates can be two-state.

Try it

The two-state copy turned 10xz into 1000, and that copy compares equal to 4'b1000. The four-state copy kept the unknown bits, so == returns x.

The last line is the trap. Even with a four-state sample, != returns x, the if takes the else branch and no mismatch is reported. Add a $isunknown(seen4) check before the comparison and run it again.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: seen4=10xz seen2=1000 seen4 == 4'b1000 -> x seen2 == 4'b1000 -> 1 unknown bits: seen4=1 seen2=0 checker: no mismatch reported

Check yourself

Predict the outputPredict the exact output. a has one unknown bit.
module tb;
  logic [7:0] a;
  int n;
  initial begin
    a = 8'b0000_00x1;
    n = a + 1;
    $display("n=%0d", n);
  end
endmodule
ConceptA monitor stores read data in bit [7:0] data. The DUT drives 8'bxxxx_0101 because its upper nibble was never reset. What value reaches the scoreboard?
ConceptThe expected value is 8'h00 and the sampled value s is 8'hxx. Which check reports an error?
Read the waveformEvery cycle, a testbench copies the DUT counter q into an int called q_int. In which cycles does q_int show a value the DUT never produced?
Counter sampled into an int
012345rst_nq (logic [7:0])x000102q_int (int)012reset pulse

The copy is taken in the same cycle as q.

Spot the bugEach option is the body of function automatic bit is_bad(logic [7:0] got, logic [7:0] exp). It must return 1 when got has any x or z bit, or differs from exp. Which option lets an unknown sample through?

Remember

  • logic and integer hold 0, 1, x and z; bit, byte, int and longint hold only 0 and 1.
  • Copying into a two-state variable turns every x and z bit into 0, silently.
  • == and != return x when an unknown bit matters, and if (x) takes the else branch.
  • Check DUT samples with $isunknown before comparing them; remember that x === x is true.
  • Use two-state types for values the testbench creates, such as counters, indexes and stimulus.

Practice