Monday 10 June 2013

Test Bench Control Part Four

Importing the Verilog VPI into Python

This has been done before. And at least several times. And in Ruby too.
So why have I not reused one of these? Well I wanted a self contained example and I wanted to demonstrate the use of SWIG too.
This blog post is also advocating a different usage model. I don't think it's a good idea to use Python code as a test bench, i.e. generating stimulus and checking the functionality of a DUV. Python is just too slow compared to say C++ or even plain Verilog (when compiled). Frequently executing script code will seriously degrade your effective simulation clock frequency in all but the most trivial of examples.
Instead the embedded Python interpreter can be used to pull in script code that would normally execute before and after a simulation. When used in this mode the script now has access to all the Verilog structures and can manipulate them directly instead of creating hex dumps and arguments to pass to the simulator. Likewise at the end of the simulation state can be examined directly instead of dumped and checked by a script post mortem. So this method should simplify these scripts and make things more robust.

How it's done


The SWIG config file is here. It's fairly minimal, just a wrapper around the callback to enable a Python function to be called and some weak references for simulators that do not implement all of the VPI functions.
To generate the Python library we invoke SWIG with the python/vpi.i file to produce a C wrapper and a Python library that imports the wrapper. In python/Makefile
  vpi_wrap.c : vpi.i
         swig -python -I$(VERILATOR_INC) $<
The same makefile compiles the C into a shared object suitable for Python. The VPI is now available to Python embedded within a simulator by importing a library called vpi (you'll need to adjust LD_LIBRARY_PATH and PYTHONPATH - see the example).
The Python embedding code is here, and is imported as a DPI function into the Verilog code thus
  import "DPI-C" context task `EXM_PYTHON (input string filename);
  /*
   * invoke python shell
   */
  initial if ($test$plusargs("python") > 0) 
    begin : sim_ctrl_python_l
      reg [`std_char_sz_c*128-1:0] sim_ctrl_python_filename_r;
      if ($value$plusargs("python+%s", sim_ctrl_python_filename_r) == 0)
        begin
          sim_ctrl_python_filename_r = "stdin";
        end
      `EXM_INFORMATION("python input set to %s", sim_ctrl_python_filename_r);
      `EXM_PYTHON(sim_ctrl_python_filename_r);
    end : sim_ctrl_python_l
We can now pass a script to the python interpreter with a +python+script argument, to access signals within the test bench we can use the python verilog library which is the wrapper around the SWIG wrapper.
  import verilog
  # open scope
  simctrl = verilog.scope('example.simctrl_0_u')
  # set timeout
  simctrl.direct.sim_ctrl_timeout_i = verilog.vpiInt(1000)
So here we have the richness of Python to help configure the test bench, as opposed to just $value$plusargs and loadmemh. Also unlike the DPI we don't have to explicitly pass signal names through functions, we can just reference a scope and have access to all signals that are available through the VPI.
Of course you have to make these signals visible which might require passing options to your simulator to reduce the level of optimization in the containing scope. Partitioning your test bench and reducing the number of scopes to keep open will minimize any performance degradation this may cause.

Versus the DPI


Some test benches may already have similar functionality written in C/C++ and set values through DPI calls. Disadvantages here are that specific signals must be explicitly passed to the DPI call and any delta will require recompilation of the Verilog and the C/C++. Any delta in the script does not require any recompilation - just rerun the simulation.
It is also possible to enter the script interactively (+python) in this example, which allows interactive development and debug. pdb can be used too, use test_pdb.py as an example.
  [...]
  (        NOTE 12:38:00) entering pdb command line
  > /enalvo/work/rporter/20121220-example/verilog_integration/python/test.py(53)pdb()
  -> pass
  (Pdb) import verilog
  (Pdb) scope = verilog.scope('example.simctrl_0_u')
  (     WARNING 12:38:19) vpi_iterate: Unsupported type vpiNet, nothing will be returned
  (     WARNING 12:38:19) vpi_iterate: Unsupported type vpiIntegerVar, nothing will be returned
  (Pdb) scope
  <verilog.scope object at 0x135b750>
  (Pdb) scope._signals
  {'sim_ctrl_cycles_freq_i': <signal sim_ctrl_cycles_freq_i>, 'sim_ctrl_timeout_i': <signal sim_ctrl_timeout_i>, 'sim_ctrl_rst_op': <signal sim_ctrl_rst_op>, 'sim_ctrl_.directclk_op': 
  <signal sim_ctrl_clk_op>, 'sim_ctrl_cycles_i': <signal sim_ctrl_cycles_i>, 'sim_ctrl_rst_i': <signal sim_ctrl_rst_i>, 'sim_ctrl_finish_r': <signal sim_ctrl_finish_r>}
  (Pdb) scope._signals.items()
  [('sim_ctrl_cycles_freq_i', <signal sim_ctrl_cycles_freq_i>), ('sim_ctrl_timeout_i', <signal sim_ctrl_timeout_i>), ('sim_ctrl_rst_op', <signal sim_ctrl_rst_op>), 
  ('sim_ctrl_clk_op', <signal sim_ctrl_clk_op>), ('sim_ctrl_cycles_i', <signal sim_ctrl_cycles_i>), ('sim_ctrl_rst_i', <signal sim_ctrl_rst_i>), ('sim_ctrl_finish_r', <signal   
  sim_ctrl_finish_r>)]
  (Pdb) scope._signals.keys()
  ['sim_ctrl_cycles_freq_i', 'sim_ctrl_timeout_i', 'sim_ctrl_rst_op', 'sim_ctrl_clk_op', 'sim_ctrl_cycles_i', 'sim_ctrl_rst_i', 'sim_ctrl_finish_r']
  (Pdb) scope.direct.sim_ctrl_cycles_i = verilog.vpiInt(9999)
  (     WARNING 12:39:11) Ignoring vpi_put_value to signal marked read-only, use public_flat_rw instead: 
  (Pdb) scope.direct.sim_ctrl_clk_op = verilog.vpiInt(9999)
  (     WARNING 12:39:32) Ignoring vpi_put_value to signal marked read-only, use public_flat_rw instead: 
  (Pdb) scope.direct.sim_ctrl_finish_r = verilog.vpiInt(9999)
  (Pdb) c
  (        NOTE 12:39:49) leaving pdb command line
  [...]
So we can poke about and see what signals are available and what happens when they are manipulated interactively.

Memories

Note:
  1. verilator 3.850 and before has issues with vpi memory word iterators.
  2. verilator and the example verilog library have no support for vpi arrays (multidimensional memories).
The verilog library has an abstraction for memories that allows them to be accessed as a list of signals. For example :
  mem[0] = 99 # assign to memory location
  for idx, i in enumerate(mem) :
    i = idx # assign address index to each location in memory
This allows for programmatic assignment to any memory instead of relying on a stepped process of script to hex dump to loadmemh.

Callbacks


Also of note is the ability to register VPI callbacks. The raw interface is available in the wrapped vpi library, but the verilog library wrapper gives it a more Pythonic idiom (I hope).
For example to execute some Python when the reset signal changes value see test_reset.py. This example first creates a new callback class inheriting from one in the Verilog library
  # build reset callback on top of verilog abstraction
  class rstCallback(verilog.callback) :
    def __init__(self, obj) :
      verilog.callback.__init__(self, name='reset callback', obj=obj,
        reason=verilog.callback.cbValueChange, func=self.execute)
    def execute(self) :
      message.note('Reset == %(rst)d', rst=self.obj)
And then instantiates it onto the reset signal
  class thistest(test.test) :
    name='test reset callback'
    def prologue(self) :
      # register simulation controller scope
      self.simctrl = verilog.scope('example.simctrl_0_u')
      # register reset callback to reset signal in simulation controller scope
      self.rstCallback = rstCallback(self.simctrl.sim_ctrl_rst_op)
The verilog callback class also provides a filter function that allows e.g. simulation state to be examined and conditionally execute the callback. However don't rely too heavily on this sort of function though, as running such a callback too frequently (e.g. every clock cycle) can slow the simulation excessively because of the repeated Python execution. The best solution is to create an expression in Verilog in the test bench that does some prefiltering to reduce the number of callbacks executed. However they can be used on infrequently changing signals, e.g. to catch error conditions.

  class errorCallback(verilog.callback) :
    def __init__(self, obj, scope) :
      verilog.callback.__init__(self, name='error callback', obj=obj,
         scope=scope, reason=verilog.callback.cbValueChange,
         func=self.execute)
    def cb_filter(self) :
      'filter the callback so only executed on error'
      return str(self.scope.what_is_it) != 'error'
    def execute(self) :
      message.error("that's an error")
 
  cb = errorCallback(scope.error, scope)
The callback code body then has the DUV in scope to do any peeking about to help root cause a failure. It is even possible to fall back to a prompt as detailed above.

Functional Coverage


I did attempt to prototype some functional coverage code using this method, but ended disappointed as it's performance was very poor (for the same reason using Python as a test bench returns poor performance). It did make a very expressive coding environment for coverage points, though. It may be that another C++ library could be created to perform the most frequently executed work in a more efficient manner so that the construction and association of coverage points could still be done in script (along with dumping to e.g. a database). To repeat myself again I feel that this is a great architecture for this type of work, a fast compiled library imported into and configured by script.

Next


We will look at importing a common messaging library into Python so that it can be used across Python, Verilog and any C/C++ test bench code.

No comments:

Post a Comment