Getting started#

Note

This tutorial assumes sby and boolector installation as per the Installation guide. For this tutorial, it is also recommended to install GTKWave, an open source VCD viewer. Source files used in this tutorial can be found on the sby git, under docs/examples/fifo.

First In, First Out (FIFO) buffer#

From Wikipedia, a FIFO is

a method for organizing the manipulation of a data structure (often, specifically a data buffer) where the oldest (first) entry, or “head” of the queue, is processed first.

Such processing is analogous to servicing people in a queue area on a first-come, first-served (FCFS) basis, i.e. in the same sequence in which they arrive at the queue’s tail.

In hardware we can create such a construct by providing two addresses into a register file. This tutorial will use an example implementation provided in fifo.sv.

First, the address generator module:

// address generator/counter
module addr_gen 
#(  parameter MAX_DATA=16
) ( input en, clk, rst,
    output reg [3:0] addr
);
    initial addr <= 0;

    // async reset
    // increment address when enabled
    always @(posedge clk or posedge rst)
        if (rst)
            addr <= 0;
        else if (en) begin
            if (addr == MAX_DATA-1)
                addr <= 0;
            else
                addr <= addr + 1;
        end
endmodule

This module is instantiated twice; once for the write address and once for the read address. In both cases, the address will start at and reset to 0, and will increment by 1 when an enable signal is received. When the address pointers increment from the maximum storage value they reset back to 0, providing a circular queue.

Next, the register file:

// fifo storage
// async read, sync write
wire [3:0] waddr, raddr;
reg [7:0] data [MAX_DATA-1:0];
always @(posedge clk)
    if (wen) 
        data[waddr] <= wdata;
assign rdata = data[raddr];

Notice that this register design includes a synchronous write and asynchronous read. Each word is 8 bits, and up to 16 words can be stored in the buffer.

Verification properties#

In order to verify our design we must first define properties that it must satisfy. For example, there must never be more than there is memory available. By assigning a signal to count the number of values in the buffer, we can make the following assertion in the code:

a_oflow:  assert (count <= MAX_DATA);

It is also possible to use the prior value of a signal for comparison. This can be used, for example, to ensure that the count is only able to increase or decrease by 1. A case must be added to handle resetting the count directly to 0, as well as if the count does not change. This can be seen in the following code; at least one of these conditions must be true at all times if our design is to be correct.

a_counts: assert (count == 0
               || count == $past(count)
               || count == $past(count) + 1
               || count == $past(count) - 1);

As our count signal is used independently of the read and write pointers, we must verify that the count is always correct. While the write pointer will always be at the same point or after the read pointer, the circular buffer means that the write address could wrap around and appear less than the read address. So we must first perform some simple arithmetic to find the absolute difference in addresses, and then compare with the count signal.

assign addr_diff = waddr >= raddr 
                 ? waddr - raddr 
                 : waddr + MAX_DATA - raddr;
a_count_diff: assert (count == addr_diff 
                   || count == MAX_DATA && addr_diff == 0);

SymbiYosys#

SymbiYosys (sby) uses a .sby file to define a set of tasks used for verification.

basic

Bounded model check of design.

nofullskip

Demonstration of failing model using an unbounded model check.

cover

Cover mode (testing cover statements).

noverific

Test fallback to default Verilog frontend.

The use of the :default tag indicates that by default, basic and cover should be run if no tasks are specified, such as when running the command below.

sby fifo.sby

Note

The default set of tests should all pass. If this is not the case there may be a problem with the installation of sby or one of its solvers.

To see what happens when a test fails, the below command can be used. Note the use of the -f flag to automatically overwrite existing task output. While this may not be necessary on the first run, it is quite useful when making adjustments to code and rerunning tests to validate.

sby -f fifo.sby nofullskip

The nofullskip task disables the code shown below. Because the count signal has been written such that it cannot exceed MAX_DATA, removing this code will lead to the a_count_diff assertion failing. Without this assertion, there is no guarantee that data will be read in the same order it was written should an overflow occur and the oldest data be written.

`ifndef NO_FULL_SKIP
    // write while full => overwrite oldest data, move read pointer
    assign rskip = wen && !ren && data_count >= MAX_DATA;
    // read while empty => read invalid data, keep write pointer in sync
    assign wskip = ren && !wen && data_count == 0;
`endif // NO_FULL_SKIP

The last few lines of output for the nofullskip task should be similar to the following:

SBY [fifo_nofullskip] engine_0.basecase: ##  Assert failed in fifo: a_count_diff
SBY [fifo_nofullskip] engine_0.basecase: ##  Writing trace to VCD file: engine_0/trace.vcd
SBY [fifo_nofullskip] engine_0.basecase: ##  Writing trace to Verilog testbench: engine_0/trace_tb.v
SBY [fifo_nofullskip] engine_0.basecase: ##  Writing trace to constraints file: engine_0/trace.smtc
SBY [fifo_nofullskip] engine_0.basecase: ##  Status: failed
SBY [fifo_nofullskip] engine_0.basecase: finished (returncode=1)
SBY [fifo_nofullskip] engine_0: Status returned by engine for basecase: FAIL
SBY [fifo_nofullskip] engine_0.induction: terminating process
SBY [fifo_nofullskip] summary: Elapsed clock time [H:MM:SS (secs)]: 0:00:02 (2)
SBY [fifo_nofullskip] summary: Elapsed process time unvailable on Windows
SBY [fifo_nofullskip] summary: engine_0 (smtbmc boolector) returned FAIL for basecase
SBY [fifo_nofullskip] summary: counterexample trace: fifo_nofullskip/engine_0/trace.vcd
SBY [fifo_nofullskip] DONE (FAIL, rc=2)
SBY The following tasks failed: ['nofullskip']

Using the noskip.gtkw file provided, use the below command to examine the error trace.

gtkwave fifo_nofullskip/engine_0/trace.vcd noskip.gtkw

This should result in something similar to the below image. We can immediately see that data_count and addr_diff are different. Looking a bit deeper we can see that in order to reach this state the read enable signal was high in the first clock cycle while write enable is low. This leads to an underfill where a value is read while the buffer is empty and the read address increments to a higher value than the write address.

_images/gtkwave_noskip.png

During correct operation, the w_underfill statement will cover the underflow case. Examining fifo_cover/logfile.txt will reveal which trace file includes the cover statment we are looking for. If this file doesn’t exist, run the code below.

sby fifo.sby cover

Searching the file for w_underfill will reveal the below.

$ grep "w_underfill" fifo_cover/logfile.txt -A 1
SBY [fifo_cover] engine_0: ##  Reached cover statement at w_underfill in step 2.
SBY [fifo_cover] engine_0: ##  Writing trace to VCD file: engine_0/trace4.vcd

We can then run gtkwave with the trace file indicated to see the correct operation as in the image below. When the buffer is empty, a read with no write will result in the wksip signal going high, incrementing both read and write addresses and avoiding underflow.

gtkwave fifo_cover/engine_0/trace4.vcd noskip.gtkw

_images/gtkwave_coverskip.png

Note

Implementation of the w_underfill cover statement depends on whether Verific is used or not. See the Concurrent assertions section for more detail.

Exercise#

Adjust the [script] section of fifo.sby so that it looks like the below.

[script]
nofullskip: read -define NO_FULL_SKIP=1
noverific: read -noverific
read -formal fifo.sv
hierarchy -check -top fifo -chparam MAX_DATA 17
prep -top fifo

The hierarchy command we added changes the MAX_DATA parameter of the top module to be 17. Now run the basic task and see what happens. It should fail and give an error like Assert failed in fifo: a_count_diff. Can you modify the verilog code so that it works with larger values of MAX_DATA while still passing all of the tests?

Note

If you need a hint, try increasing the width of the address wires. 4 bits supports up to 24=16 addresses. Are there other signals that need to be wider? Can you make the width parameterisable to support arbitrarily large buffers?

Once the tests are passing with MAX_DATA=17, try something bigger, like 64, or 100. Does the basic task still pass? What about cover? By default, bmc & cover modes will run to a depth of 20 cycles. If a maximum of one value can be loaded in each cycle, how many cycles will it take to load 100 values? Using the .sby reference page, try to increase the cover mode depth to be at least a few cycles larger than the MAX_DATA.

Note

Reference files are provided in the fifo/golden directory, showing how the verilog could have been modified and how a bigtest task could be added.

Concurrent assertions#

Until this point, all of the properties described have been immediate assertions. As the name suggests, immediate assertions are evaluated immediately whereas concurrent assertions allow for the capture of sequences of events which occur across time. The use of concurrent assertions requires a more advanced series of checks.

Compare the difference in implementation of w_underfill depending on the presence of Verific. w_underfill looks for a sequence of events where the write enable is low but the write address changes in the following cycle. This is the expected behaviour for reading while empty and implies that the w_skip signal went high. Verific enables elaboration of SystemVerilog Assertions (SVA) properties. Here we use such a property, write_skip.

property write_skip;
    @(posedge clk) disable iff (rst)
    !wen |=> $changed(waddr);
endproperty
w_underfill: cover property (write_skip);

This property describes a sequence of events which occurs on the clk signal and are disabled/restarted when the rst signal is high. The property first waits for a low wen signal, and then a change in waddr in the following cycle. w_underfill is then a cover of this property to verify that it is possible. Now look at the implementation without Verific.

reg past_nwen;
initial past_nwen <= 0;
always @(posedge clk) begin
    if (rst) past_nwen <= 0;
    if (!rst) begin
        w_underfill: cover (past_nwen && $changed(waddr));
        past_nwen <= !wen;
    end
end

In this case we do not have access to SVA properties and are more limited in the tools available to us. Ideally we would use $past to read the value of wen in the previous cycle and then check for a change in waddr. However, in the first cycle of simulation, reading $past will return a value of X. This results in false triggers of the property so we instead implement the past_nwen register which we can initialise to 0 and ensure it does not trigger in the first cycle.

As verification properties become more complex and check longer sequences, the additional effort of hand-coding without SVA properties becomes much more difficult. Using a parser such as Verific supports these checks without having to write out potentially complicated state machines. Verific is included for use in the Tabby CAD Suite.

Further information#

For more information on the uses of assertions and the difference between immediate and concurrent assertions, refer to appnote 109: Property Checking with SystemVerilog Assertions.