Debugging Tip: Always Remember The Cause
It is a well known fact that debugging, not development, takes most of the time when verifying an DUT (Device Under Test). This is true for all software development branches, not just in hardware verification.
Beside the simulators, IDEs, machine clusters and any other thing that a company can invest in I believe that the biggest help in debugging must come from the guy developing the verification environment – and this one is free – debatable statement 🙂
In my case, one thing that I am doing over and over again with a failing test is to double check that my verification environment signaled the DUT error correctly. In order to do this I am spending some (o lot of) time understanding:
- what is the function for computing the expected output value – f(x, y)
- what are the input parameters x and y
So in order to save some time I started coding my environment in such a way that I carry information like what is f(x, y), values of x and y, from conception all the way to the DUT output checker.
Let’s see this idea in a relevant example.
Let’s assume that we need to verify a DUT which does a simple job:
On a serial interfaces it gets bits of data. Once it gets a byte (MSB first) it pushes it into a FIFO which can be read from a parallel interface.
A typical waveform for such a DUT looks like the snapshot below.
Notice that the last outputted byte is wrong! Let’s see two ways of implementing the scoreboard and how small changes can impact the debugging time in a significant way.
The Bad Way
From the serial interface monitor we get the collected item modeled like this:
1 2 3 4 5 6 7 8 | class cfs_ser_item_mon extends uvm_sequence_item; //time when data was sampled time sample_time; //sampled value of the serial data line bit data; ... endclass |
From the parallel interface monitor we get the collected item modeled like this:
1 2 3 4 5 | class cfs_des_item_mon extends uvm_sequence_item; //parallel data byte data; ... endclass |
A typical scoreboard implementation might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class cfs_scoreboard extends uvm_component; ... //buffer getting bits of information from serial interface bit serial_bits[$]; //model of the internal fifo byte fifo[$]; ... //analysis port implementation for getting data from the serial monitor virtual function void write_ser(cfs_ser_item_mon serial_info); serial_bits.push_back(serial_info.data); if(serial_bits.size() == 8) begin fifo.push_back(byte'({<<{serial_bits}})); serial_bits = {}; end endfunction //analysis port implementation for getting data from the parallel monitor virtual function void write_des(cfs_des_item_mon parallel_info); byte expected_data = fifo.pop_front(); if(expected_data != parallel_info.data) begin `uvm_fatal("DUT_ERROR", $sformatf("Data mismatch detected - expected: %0x, received: %0x", expected_data, parallel_info.data)) end endfunction endclass |
For the waveforms above we detect that the byte from time 495ns is wrong and we get the following error message:
1 | Data mismatch detected - expected: 54, received: 7c |
Now we must check the waveforms to see if we really sent on the serial interface byte 0x54 – good luck finding that one on the waveforms 🙂
The Good Way
The big problem with the first method is that we stripped down a lot of useful information when we pushed the byte value in our FIFO model. A better alternative is to model our FIFO in such a way that we remember the cause of the expected output byte:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class cfs_fifo_item extends uvm_object; //original information received from the serial monitor cfs_ser_item_mon serial_infos[$]; virtual function byte get_data(); bit serial_bits[$]; for(int i = 0; i < 8; i++) begin serial_bits.push_back(serial_infos[i].data); end return byte'({<<{serial_bits}}); endfunction endclass |
Our scoreboard can now look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | class cfs_scoreboard extends uvm_component; ... //buffer getting information from serial interface cfs_ser_item_mon serial_infos[$]; //model of the internal fifo cfs_fifo_item fifo[$]; ... //analysis port implementation for getting data from the serial monitor virtual function void write_ser(cfs_ser_item_mon serial_info); serial_infos.push_back(serial_info); if(serial_infos.size() == 8) begin cfs_fifo_item fifo_item = cfs_fifo_item::type_id::create("fifo_item"); fifo_item.serial_infos = serial_infos; fifo.push_back(fifo_item); serial_infos = {}; end endfunction //analysis port implementation for getting data from the parallel monitor virtual function void write_des(cfs_des_item_mon parallel_info); cfs_fifo_item fifo_item = fifo.pop_front(); byte expected_data = fifo_item.get_data(); if(expected_data != parallel_info.data) begin //build the mismatch information on bit level string mismatch_info = ""; for(int i = 0; i < 8; i++) begin if(expected_data[i] != parallel_info.data[i]) begin mismatch_info = $sformatf("%s\t\tbit %0d with value %0b sampled at %0dns\n", mismatch_info, i, expected_data[i], fifo_item[7 - i].sample_time); end end `uvm_fatal("DUT_ERROR", $sformatf("Data mismatch detected - expected: %0x, received: %0x\n\t- byte present on serial interface in interval [%0dns..%0dns]\n\t- bit level information: %s", expected_data, parallel_info.data, fifo_item[0].sample_time, fifo_item[7].sample_time, mismatch_info)) end endfunction endclass |
For the same waveforms we now get the following DUT error message:
1 2 3 4 5 | Data mismatch detected - expected: 54, received: 7c - byte present on serial interface in interval [325ns..465ns] - bit level information: bit 3 with value 0 sampled at 405ns bit 5 with value 0 sampled at 365ns |
With an error message like this one checking the correct behavior of the environment is much easier and the debugging time decreases quite a lot.
On the other side, this approach does imply using more memory during simulations and one must argue that this will make the simulation slower. However I’m willing to bet that the extra time for simulating is much smaller than the time gain in debugging 🙂