Debug case: the X that became zero

Trace a false pass back to the first place a four-state value became two-state, and fix the monitor so the same bug fails loudly.

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

A register read test passes, but the waveform shows rdata as x on the first read after reset. The scoreboard log for that same read says actual=00 expected=00 PASS. Somewhere between the DUT pin and the scoreboard, an unknown value turned into a clean zero, and this case walks through finding that place and closing it.

First read returns x; the transaction says 00
01234567rst_nrewervalidrdatax5atxn.data (bit [7:0])005ascoreboard: actual=00 PASS

The status register has no reset value. Its first read returns x; the read after the write in cycle 5 returns 5a.

Two records that disagree

Start with the disagreement itself. The waveform viewer reads the DUT pin directly and shows x, while the scoreboard reads a transaction built by the monitor and shows 00.

Both tools are reporting what they saw, so the value must have changed between them, inside testbench code. The DUT has a bug too, a register without a reset, but the testbench bug is worse because it hid the DUT bug.

You can search from either end, walking forward from the pins or back from the scoreboard. What matters is writing down the type and the value at every stage instead of guessing.

Follow the value stage by stage

List every variable that the read data passes through, together with its declared type. Then look for the first one that cannot hold x.

StageVariableTypeValue at the first read
DUT outputrdatalogic [7:0]xxxxxxxx
Monitor samplesamplelogic [7:0]xxxxxxxx
Transaction fieldtxn.databit [7:0]00000000
Scoreboard comparetxn.data == exp1-bit result1, so PASS

The first two-state stage is where the unknown value disappears.

The third row is the culprit, because assigning a four-state value to a bit field converts every x to 0 without a warning. Every stage after it worked correctly, but on wrong data.

Notice that the monitor's own sample variable was already logic. Fixing one declaration is not enough if the value passes through a two-state field later on.

Why === is too late

A common first attempt is to change == to === in the scoreboard, but that cannot help. By the time the scoreboard runs, txn.data holds a real 0, and 8'h00 === 8'h00 is 1.

No operator can recover information that an earlier assignment destroyed, so the repair has to happen at the conversion or before it.

Two repairs

The first repair is to keep the field four-state. Declare txn.data as logic [7:0], and the scoreboard sees the x, rejects it with $isunknown, and can show the exact unknown bits in its message.

The second repair is to flag the unknown before converting. If the transaction must stay two-state, add a field has_x, set it from $isunknown(sample) in the monitor before the copy, and make every consumer check it.

Prefer the first repair when you can, because it is simpler and keeps the unknown bit positions for the error message. The second suits a transaction type that many other components already use, where changing a field type would ripple through the whole testbench.

In both cases, check only when the data is meant to be valid. On this bus rdata matters only in the cycle where rvalid is 1. An idle bus may carry x quite legally, so a check in every cycle would raise false errors.

Two-state transaction field

bit [7:0] data;            // in the transaction
...
txn.data = sample;         // x becomes 0
if (txn.data == exp)       // 0 == 0
  pass_count++;

A read of a register that was never reset still passes.

Four-state field, checked when valid

logic [7:0] data;          // in the transaction
...
txn.data = sample;
if (rvalid && $isunknown(txn.data))
  $error("read data has x/z: %b", txn.data);
else if (txn.data !== exp)
  $error("expected %h got %h", exp, txn.data);

Reports read data has x/z: xxxxxxxx on the first read.

Why the DUT drove x

The status register in this design has no reset branch, and nothing writes it before the first read. Its value is therefore x until the write in cycle 5.

That is a real RTL bug, and the four-state monitor reports it on the very first read. Once the designer adds the reset, keep the repaired monitor, so that the next register without a reset is caught the same way.

A regression that separates broken from fixed

A checker has two jobs, failing on bad data and passing on good data, so test both. Run one read before any write, which must fail, and one read after writing 8'h00, which must pass.

If both reads pass, the checker cannot see unknown values. If both fail, it rejects good data, perhaps because it checks in the wrong cycle, and only one failure plus one pass shows that the monitor works.

Keep both reads in the regression after the fix. If someone later turns the field back into bit, the first read passes again and the regression fails at once, which is exactly the warning you want.

Try it

The two-state monitor passes both reads, so it cannot tell a missing reset from a correct one. The four-state monitor fails the first read and passes the second: one failure and one pass, as a working checker should.

Now add a reset to the DUT, so status starts at 0, and run it again. Both monitors should pass both reads.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: read before any write: pin=xxxxxxxx two-state monitor=PASS four-state monitor=FAIL (unknown) read after writing 00: pin=00000000 two-state monitor=PASS four-state monitor=PASS

Check yourself

Predict the outputPredict the exact output.
module tb;
  logic [7:0] pin;
  bit   [7:0] field;
  initial begin
    pin   = 8'bxxxx_1010;
    field = pin;
    $display("%0d %0d", field, field === 8'h0a);
  end
endmodule
ConceptThe transaction field is bit [7:0] data, and the scoreboard compares it with ==. Which change stops the false pass?
ConceptWhen should the repaired monitor check rdata for unknown bits?
Read the waveformThe repaired monitor checks rdata with $isunknown whenever rvalid is 1. In which cycle does it first report an error?
Two reads of the status register
012345rervalidrdatax00

Read data is valid in the cycle after re.

Spot the bugEach option is the body of function automatic bit sample_bad(logic rvalid, logic [7:0] rdata). It must return 1 when rvalid is 1 and rdata has any x or z bit, and 0 otherwise. Which option misses the bad read?

Remember

  • When the waveform and the log disagree, the value changed inside testbench code between them.
  • List each stage the value passes through with its type, and find the first two-state one.
  • No operator can recover an x after a conversion; fix the type or flag the unknown before converting.
  • Check for unknowns only when the protocol says the data is valid.
  • Prove a checker with one read that must fail and one that must pass.

Practice