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.
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.
| Stage | Variable | Type | Value at the first read |
|---|---|---|---|
| DUT output | rdata | logic [7:0] | xxxxxxxx |
| Monitor sample | sample | logic [7:0] | xxxxxxxx |
| Transaction field | txn.data | bit [7:0] | 00000000 |
| Scoreboard compare | txn.data == exp | 1-bit result | 1, 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.
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=PASSCheck yourself
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
xafter 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
- Repair the transaction field Fix · Easy
- Catch the bad read paths Verify · Medium
- Header monitor Implement · Hard