SourceForge.net Logo

Download

Example

Example 2

Documentation

Contact Me


    

ANVIL – ANother Verilog Interaction Layer

ANVIL is a mechanism to faciliate some of the Specification-Driven Development (SDD) ideas so they can be applied towards the design and verification of Verilog RTL.

Specification-Driven Development (SDD), which combines the best features of the agile Test-Driven Development (TDD) methodology with the best features of the plan-driven approach of quality-first Design-by-Contract (DbC).”


Verilog HDL is well-suited for describing bit-level and cycle accurate behavior of a particular implementation - synthesizable designs. However, Verilog lacks significant semantic (and syntax) capability to express beyond bits. There is no notion of objects, interfaces, complex data structures, operator overloading, etc.; such capabilities being the foundation of object-oriented languages such as C++ and Java.

SystemVerilog does fold many of these notions into a superset (of Verilog) language. It is not clear that this solution: one size fits all --- verification + hardware language really works in practice. There never seems to be the one best language for all needs, so perhaps it is best to just have many languages which satisfy many specific needs.

In order to efficiently utilize SDD ideas, we need much higher level levels of abstraction than bits. In fact, the SDD paper touts the power of Eiffel, since it has DbC built-in; but, since C++ seems more familiar to hardware-types, we'll stick with that one, first. (Note: There is a Ruby-VPI package available which is a Ruby interface to VPI.)

Thus, ANVIL was created to move the verification effort to a pure C++ environment with minimal perturbation to existing Verilog RTL. In fact, using ANVIL, one simply instantiates a top-level RTL module (the device under test: dut), adds a rudimentary collar of regs and wires and a simple set of ANVIL task and function calls to essentially connect the dut, running in a Verilog (VPI-compliant) simulator, with a C++ testbench.

The ANVIL architecture is shown on the right. The current implementation uses shared memory between server (the testbench: tb.cxx) and client (the dut: verilog simulator running rtl.v) processes running on a single host.

The icon is a semaphore to control exclusive access (of shared memory) to either server or client.





Example

The Verilog RTL for a n-bit, synchronous reset, loadable up-counter is given: 


module cntrn
    #(parameter N = 8)
    (input clk, input rst, input load,
     input [N-1:0] d, output reg [N-1:0] q, output isZero);

    assign isZero = !|q;

    always @(posedge clk)
    begin
        if (rst)
            q <= 0;
        else if (load)
            q <= d;
        else
            q <= q + 1;
    end
endmodule

Next, a module wrapper (module test1) which instantiates (dut) the counter and connects its inputs to regs and outputs to wires of corresponding names is shown below in green:


module test1
    #(parameter N = 8, TH = 5)();
    parameter time T = 2*TH;
    reg clk;
    reg rst, load;
    reg  [N-1:0] d;
    wire         isZero;
    wire [N-1:0] q;

    cntrn #(N) dut(clk,rst,load,d,q,isZero);

    initial
        clk = #0 0; //need #0 to get onto queue
    always @(clk)
        clk <= #TH ~clk;

    // Used to synchronize back/forth <-> testbench.
    event ev1, ev2;

    initial
    begin: bInitialize
        $anvilYieldSetup(`PORT, "test1", "clk");
        #0 ->ev1; //need #0 to get event onto queue
    end

    always @(ev1)
    begin: bToTestbench
        $anvilYield;
        ->ev2;
    end

    always @(ev2)
    begin: bRunDut
        time t;
        t = $anvilGetNextTime;
        if (0 == t)
            $finish;
        #t ->ev1;
    end
endmodule


The following describe the named blocks in the above (test1) module:

bInitialize

Upon (simulator) initialization, invoke the task anvilYieldSetup with (at least) 2 arguments:

  1. port id to share with testbench process. In this example it would be passed in as a (simulator) command line +define+PORT=... argument. (more on this port stuff later...)

  2. module name which instantiates device-under-test (dut).

  3. Optional list of reg names which should not be driven by external (C++) testbench. Since the system clock is modeled here, the external testbench should not model it.

After the anvilYieldSetup task completes, the simulator passes control to the named block bToTestbench.

bToTestbench

When this block is activated, the anvilYield task is called which:

  1. copies current values of the wires (connected to dut) into a shared memory area (shared between the Verilog process and the external (C++) testbench process).

  2. copies the current simulation time into the shared memory area.

  3. Releases semaphore to allow external (C++) testbench process to assume control.

    External (C++) testbench:

  • reads current wire values (from shared memory)

  • checks against expected values, etc.

  • schedules next (new) values onto regs (driving inputs of dut)

  • specifies how long to run next time step (on Verilog side)

  • releases semaphore so Verilog-side assumes control.

  1. Upon return from testbench, the next (new) values are scheduled and anvilYield returns which then passes control to the bRunDut block.

bRunDut

When this block is activated, the anvilGetNextTime function returns the next time step duration (scheduled from testbench-side). The dut (Verilog side) then advances time by this amount. (By convention, a 0 advance is used to cease simulation.)


The following is an example of the testbench (C++) side which is ultimately running the simulation:


#include <iostream>
#include <cstdlib>
#include "xyzzy/socket.hxx"
#include "anvil/tbshm.hxx"

using namespace std;
using namespace xyzzy;
using namespace anvil;
using namespace anvil::tb;

int
main(int argc, char *argv[])
{
    static const unsigned T = 10;  //same period as dut/Verilog

    const int cMaxCount = 1 << 8;  //N==8 bit counter
    int port = 3000;               //default port id
    if (1 < argc)
        port = atoi(argv[1]);

    // The mgr is the manager; an interface which provides 
    // methods to interact with shared memory and semaphores.
    TRcIManager mgr;
    try  //use try/catch since socket/network can fail
    {
        mgr = new TShmManager(port);
    }
    catch (const TException &ex)
    {
        ex.print();
        return(EXIT_FAILURE);
    }
    // Until the dut has valid outputs, we will ignore
    // X (unknown) values returned from dut.
    mgr->setExceptionOnX(false);

    cout << "Info: Connected to client on port: " << port << endl;

    // Define a macro (DCL) to instance connections to dut by name.
    // Then, connect to corresponding IOs of the dut.
    // A declaration of the form:
    //    TInput foo(“foo”);
    // creates a local (mutable) variable which can be assigned
    // and read.  The current value is assigned to the dut
    // when the assignAndRunDut method is called, subsequently.
    // A declaration of the form:
    //    TOutput bar(“bar”);
    // creates a local (immutable) variable which can be read.
    // The current value is the dut (output) value at the
    // simulation time when control last transferred from dut
    // to this testbench; i.e., after waitOnDut method returns.
#define DCL(_x) _x(#_x)
    TInput  DCL(rst);
    TInput  DCL(load);
    TInput  DCL(d);
    TOutput DCL(isZero);
    TOutput DCL(q);
#undef DCL

    // Done with (tb) initialization; so assign values
    // immediately.  The second argument 0 is required,
    // but ignored.
    mgr->assignAndRunDut(0, 0); 

    mgr->waitOnDut(); // wait for dut to hit $anvilYield

    // Assign some values to inputs
    rst = true;
    load = false;
    d = 8;
    // Schedule input values at #0 then return to dut/verilog and
    // advance by #T+1.
    mgr->assignAndRunDut(0, T+1);

    mgr->waitOnDut();  // wait for dut to transfer control back
    rst = false;       // de-assert reset value
    mgr->assignAndRunDut(0, T); 
    mgr->setExceptionOnX(true);  // notify on dut X output values

    int iter = 1000;    if (2 < argc)
        iter = atoi(argv[2]);

    bool synced = false;
    TInt32 expect = 0;
    unsigned checkCnt = 0;
    // Loop for iter clock cycles; and start checking outputs
    // after 0 is detected; i.e., a known sync point.
    for (int i = 0; i < iter; i++)
    {
        mgr->waitOnDut();
        if (!synced && 0 == q)
            synced = true;
        cout << "i=" << i
             << ", q=" << q
             << ", isZero=" << isZero << endl;
        if (synced)
        {
            ASSERT_TRUE(expect == q);
            expect = ++expect % cMaxCount;
            checkCnt++;
        }
        mgr->runDut(T);
    }

    mgr->waitOnDut();
    mgr->runDut(0);  // signal dut we're all done!

    cout << "Info: check count=" << checkCnt << endl;
    return(EXIT_SUCCESS);
}


The example above is contained in the download and can be run as:

  1. Download and install cver – a GPL'd Verilog simulator.

  2. Download and untar the ANVIL package.

  3. cd to the directory where ANVIL (top directory anvil) is located and change the variable PLI_INCL in the anvil.mk file to point to the directory where cver pli_incs directory is located:

    > cd /your_install_root/anvil
    > your_editor anvil.mk

    # Change appropriately for your cver install
    PLI_INCL := /.../gplcver-2.11a.src/pli_incs

    Next, need to find certain g++-related files, and then update/revise common/common.mk.

    > find /usr/lib/gcc -name crtbeginS.o

    On my 64-bit system this returns:

    /usr/lib/gcc/x86_64-redhat-linux/4.1.1/crtbeginS.o
    /usr/lib/gcc/x86_64-redhat-linux/4.1.1/32/crtbeginS.o


    On a 32-bit system the location may be different.

    Edit common.mk to update the value of LIB_CRT with the correct value(s). If you have a 32-bit system just update both values (in the ifeq/else) with the same (or remove the ifeq clause altogether:

    > cd /your_install_root/common
    > your_editor common.mk

    ifeq (${shell uname -p},x86_64)
    LIB_CRT := /usr/lib/gcc/x86_64-redhat-linux/4.1.1/32/crtbeginS.o
    else
    LIB_CRT := /usr/lib/gcc/i386-redhat-linux/4.1.1/crtbeginS.o
    endif

    All this revision/setup (above) is a one-time deal (unless, of course, you change OSes); and it will be re-used by the examples and subsequent efforts.

    Now, on to the examples...

  4. cd to the test1 directory under the anvil release and make a 32-bit version:

    > cd tests/test1
    > make ndebug32

    you should see 2 primary files created:

    > ls -1 */32/ndebug/*exe ../../*/32/ndebug/*so

    ../../linux_86_64/32/ndebug/anvil.so
    linux_86_64/32/ndebug/test.exe

    NOTE: on my system:

    > uname -si

    Linux x86_64


    so, the subdirectory name on your system may be different than linux_86_64

  5. The anvil.so will be linked into the (cver) simulator during runtime; and, the test.exe is the standalone testbench executable.

    Next, we'll start the simulation. First, the testbench starts and opens a server socket listening on (default) port 3000. Next, the client (Verilog, cver) starts and initializes the shared memory area, then connects to the server (through the same port) and passes information about the shared memory id. The server gathers information from the shared memory area, then closes the socket, passes control back to the dut/client process and then the semaphore and VPI mechanism handles the testbench to dut back and forth behavior.

    For this example:

    > pwd
    /.../tests/test1

    > (./linux_x86_64/32/ndebug/test.exe 3001 | & tee server.log & ) ;\
       sleep 5 ; make -f Makefile.ver ndebug PORT=3001 > & client.log


    The above command starts the server, listening on port 3001; waits 5 sec; then starts the client, connecting on the same port 3001; and, runs the simulation.

    You should see:


Info: Listening on port: 3001
... sleep here for 5 seconds until client connects ... 
Info: Connected to client on port: 3001 
i=0, q=1, isZero=0
i=1, q=2, isZero=0
i=2, q=3, isZero=0
...
i=998, q=231, isZero=0
i=999, q=232, isZero=0
Info: check count=745
>


    The client.log will show:

    

cver +loadvpi=../../linux_x86_64/32/ndebug/anvil.so:vpi_compat_bootstrap \
    +verbose /home/karl/projects/local/anvil/tests/test1/test.v +define+PORT=3001
GPLCVER_2.11a of 07/05/05 (Linux-elf).
Copyright (c) 1991-2005 Pragmatic C Software Corp.
  All Rights reserved.  Licensed under the GNU General Public License (GPL).
  See the 'COPYING' file for details.  NO WARRANTY provided.
Today is Thu Mar 15 12:11:23 2007.
  Verbose mode is on. 
  Invoked by: "cver".
  +loadvpi= dynamic library ../../linux_x86_64/32/ndebug/anvil.so 
  loaded with bootstrap routine(s) :vpi_compat_bootstrap
  `define of PORT value 3001 added from command option.
  P1364 2001 config map library not specified - using -y/-v libraries.
  Begin Translation:
Compiling source file "/home/karl/projects/local/anvil/tests/test1/test.v"
  Begin pass 2:
  Approximately 394973 bytes storage allocated (excluding udps).
Highest level modules:
test1
  Verbose mode statistics:
  71 source lines read (includes -v/-y library files).
  Design contains 2 module types.

  Variable storage in bytes: 23 for scalars, 48 for non scalars.
  Begin load/optimize:
  Approximately 383232 bytes storage allocated (excluding udps).
  Begin simulation:
  Approximately 460384 bytes storage allocated (excluding udps).


Warning: $anvilYieldSetup: "clk": skipping!
Info: Connect to port 3001 to send "/vuVpiKey16678,392"
Info: Wait for tb to release semaphore ...  Got it! 
Halted at location **/home/karl/projects/local/anvil/tests/test1/test.v(60) 
     time 10021 from call to $finish.
3011 simulation events and 3035 declarative immediate assigns processed.
18055 behavioral statements executed (6023 procedural suspends).
  Times (in sec.):  Translate 0.0, load/optimize 0.1, simulation 2.0.


  1. From the same test1 directory, make the debug anvil.so file and re-run to see more details about the dut interaction with the testbench.

    > make debug32
    > (./linux_x86_64/32/ndebug/test.exe 3001 | & tee server.log & ) ;\
       sleep 5 ; make -f Makefile.ver debug PORT=3001 > & client.log


    The client.log contains debug (DBG) messages. These emanate from the ../../src/anvil/anvil.cxx file. This file is the source of the anvil.so (the VPI routines) which the Verilog simulator loads/uses on the client side.

    You should view the anvil.cxx file and search for the DBG string for more details.


Example 2

This example tests a floating point unit from opencores.org.

  1. cd to tests/test2 and look at tfpu.v and test.cxx which are the dut and testbench, respectively. The RTL design files for the dut (instance of fpu) are under test2/fpu/verilog.

    Note that the tfpu.v file is simply an instance of fpu, a collar of regs and wires and the same code above, in the previous example, to coordinate to/from the testbench.

  2. Next, build the testbench (and the same anvil.so, if out-of-date):

    > cd tests/test2
    > make ndebug32

  3. you should see 2 primary files created:

    > ls -1 */32/ndebug/*exe ../../*/32/ndebug/*so

    ../../linux_86_64/32/ndebug/anvil.so
    linux_86_64/32/ndebug/test.exe

    NOTE: on my system:

    > uname -si


    Linux x86_64

    so, the subdirectory name on your system may be different than linux_86_64

  4. As in the previous example, invoke the server (testbench) then the client (dut running in Verilog simulator, cver). Here, we do not specify a port,so it defaults to 3000:

    > (./linux_x86_64/32/ndebug/test.exe | & tee server.log & ) ;\
       sleep 5 ; make -f Makefile.ver ndebug > & client.log

  5. Here, the output is a little more interesting; it shows the progress of 256 ^ 2 iterations of successive floating point adds. Again, the stimulus and response (functional intent) is expressed in the C++ testbench test.cxx.

  6. Invoking test.exe -help shows that random operations and data can be applied for some specfied number of iterations:

    > ./linux_x86_64/32/ndebug/test.exe -help

Usage: ./linux_x86_64/32/ndebug/test.exe fpuOp? iter? port?

fpuOp: 0,1,2,3 for add, sub, mul or div, respectively.
       Or, -1 for random selection of add, sub, mul or div.

iter: 0 for all 2^32 permutations of BOTH a and b operands.
      n>0 for n permutations of BOTH a and b operands.

port: listen to port for dut/verilog client connection.
  1. Thus, we can re-run 256 ^ 2 iterations of random; and we will get more details about the actual Verilog operands by enabling a monitor (see tfpu.v for details on how the MONITOR is used).

    > (./linux_x86_64/32/ndebug/test.exe -1 | & tee server.log & ) ;\
       sleep 5 ; make -f Makefile.ver ndebug MONITOR=1 > & client.log

  2. See the client.log for details.

  3. To see that the division by zero behavior of fpu out (below in red) does not quite jive the x86 processor (below in green) (at least for my AMD Turion-64; though perhaps due to the rounding mode?):

    > (./linux_x86_64/32/ndebug/test.exe 3 10 | & tee server.log & ) ;\
       sleep 5 ; make -f Makefile.ver ndebug MONITOR=1 >& client.log


    Info: Wait for connection on port: 3000
    Info: Connected to client on port: 3000
    FAIL: 0/0 = nan(-4194304): ia=0, ib=0, out=-4194303(nan), [inf,snan,qnan,ine,ovr,uvr,zero,divbz]=00100000
    1% 2% 3% 4% 5% 6% 7% 8% 9% 10% ...