Xyce/ADMS Users Guide

The Xyce/ADMS Verilog-A compiler tool

Xyce/ADMS is a set of XML templates that provide a code-generating "back-end" to the open source Verilog-A compiler ADMS. With the Xyce/ADMS templates and the ADMS compiler, the Xyce team has been able to import industry-standard device models written in Verilog-A into Xyce. To date, the capability is still an "alpha-quality" tool that has mostly been used internally by the Xyce team; but it has matured enough in recent releases to attract the interest of device model developers inside and outside of Sandia National Laboratories.

The purpose of this document is to describe the use of the Xyce/ADMS capability, its features, and its limitations.

What is ADMS?

ADMS ("Automatic Device Model Synthesizer") is an open-source Verilog-A translator. It reads a Verilog-A input file and produces a complex internal data structure representing the module. It then processes a set of XML templates written in an XSLT-based templating language called ADMST. These user-provided templates access the internal ADMS data structure, and can be written to emit code in any desired language targeting any desired simulator.

Though it is not completely consistent with the use of the term in computer science, since all code generation is provided through the use of XML templates after all Verilog-A parsing has been completed, we refer to these templates throughout this document as the "back-end" of an ADMS compiler. ADMS itself does not come with any code generation templates—these must all be provided by the developer of a simulator for which the code is targeted.

What is Xyce/ADMS?

The term "Xyce/ADMS" refers only to the set of XML templates provided by the Xyce team for use with ADMS. These templates allow ADMS to emit C++ code for a Xyce device model. In most cases, the code produced by this process is ready to compile into Xyce; in a few cases the C++ code emitted requires some manual editing to add features or Xyce-specific changes that cannot be expressed in Verilog-A.

The Xyce/ADMS templates are all found in the utils/ADMS subdirectory of a Xyce source distribution. The primary template files used in Xyce/ADMS are:

  • adms.implicit.xml — a slightly modified version of the "implicit" templates that are distributed with ADMS itself.  The modifications provide fixes for certain special cases of analog function use that are not covered in the version provided with ADMS, fixes for ddx() handling, and support for $port_connected.
  • xyceVersion_nosac.xml — provides basic simulator variables to ADMS, mostly used to output comments.
  • xyceBasicTemplates_nosac.xml — a set of ADMST templates (subroutine-like code) used to process the ADMS data structure and emit fragments of code
  • xyceAnalogFunction_nosac.xml — ADMST templates for generation of functions that evaluate the derivatives of user-defined analog functions
  • xyceHeaderFile_nosac.xml — a template that generates the C++ ".h" header file defining the device class
  • xyceImplementationFile_nosac.xml — template that generates the C++ ".C" file implementing the device

In addition to templates that generate C++ model code, the Xyce team provides templates that can produce other artifacts of a model:

  • html_params.xml — generates an HTML file describing the device model, its variables, parameters, and other details. This is primarily of interest to Xyce/ADMS developers as a debugging tool.
  • xyceBootstrapFile.xml — generates a small C++ object declaration that can be used in creation of a shared-library plugin for the model
  • xyceMakefile.am.xml — generates a Makefile.am file that can be used to create a shared-library plugin as part of a Xyce build
  • xyceOutVarsDoc.xml — generates a table of the output variables defined by a model in LaTeX form suitable for inclusion in the Xyce Reference Guide.

A directory of all the source code for Verilog-A models that have been incorporated into Xyce (e.g. VBIC, PSP, MEXTRAM, etc.) is provided with the Xyce source code in the utils/ADMS/examples directory.  A "toys" subdirectory under this examples directory has a set of simple Verilog-A models is also included.  The toys directory also contains a DiodeClipper.cir netlist that consists entirely of devices provided as Verilog-A source in that directory.

Table of Contents

Obtaining ADMS

ADMS is no longer maintained as an open source product by its original author, Laurent Lamaitre.  Development of ADMS as open source software has been largely taken over by the Qucs project. 

ADMS source code may be downloaded from the Qucs/ADMS GitHub project.  Before using Xyce/ADMS to import device models into Xyce, you must install ADMS.  Some operating systems have ADMS in their package management systems, but on others you must install it from source according to the directions on the ADMS project site.

Once installed, be sure that the directory into which ADMS was installed is in your PATH variable.  You should be able to run admsXml --version and have ADMS report its version number.  At the time of this writing, the most recent version of ADMS is version 2.3.6.

Systems known to have ADMS in their current package management systems include Ubuntu (beginning with version 20.04), Debian (buster and later), OpenSUSE (15.2 and later), FreeBSD (all versions), and NetBSD.

Running Xyce/ADMS

In the remainder of this documentation, we will refer to several directories using the syntax of shell variables.  The following variables will be used:

  • XYCE_SRCDIR:  the top level of the Xyce source tree
  • XYCE_BUILDDIR:  the top level of the directory in which Xyce has been compiled
  • ${prefix}:  The prefix given to configure as --prefix=.  This is where Xyce and its components will be installed by "make install".
  • XYCE_ADMSDIR:  A subdirectory of XYCE_SRCDIR that contains the Xyce/ADMS XML templates.  This will usually be $XYCE_SRCDIR/utils/ADMS.  When using an installed copy of Xyce, it will be ${prefix}/share/xml.

How it works

Xyce does not at this time have any capability for direct import of Verilog-A models through a netlist.  Before a model written in Verilog-A can be used, it must be converted to C++ using Xyce/ADMS, compiled, and linked into Xyce. 

There are two ways to link a model created in this way into Xyce: direct linking at build time, and building a shared-library plugin.  When the Xyce team imports a model into Xyce we use the former method, and we anticipate that most users of the capability will want to use the latter.

Directly linking a model into Xyce requires full access to the Xyce source code, all libraries that are required for building Xyce, and access to a Xyce build directory that has been used to compile Xyce.  It is done by modifying the Xyce build system to include compilation of the C++ version of the model, linking this into the binary, and registering the device.

The plugin method requires that Xyce be built specially to support shared library loads — our standard binary distributions of Xyce do not support this feature yet.  A script is provided with Xyce that simplifies the generation and installation of such plugins, and a command-line option is available to load the plugin at run time.

Note: Due to limitations of the Windows linker and the Xyce build system, the plugin method does not work on Windows at this time, and can only be used on Unix-like operating systems.  Cygwin, though it provides facilities that emulate Unix under the Windows operating system, is unable to overcome the linker limitations.

Creating C++ code from Verilog-A

A Verilog-A input file, "mymodel.va," can be processed into C++ suitable for linking into Xyce using the following command line:

admsXml -x -e $XYCE_ADMSDIR/adms.implicit.xml \
-e $XYCE_ADMSDIR/xyceVersion_nosac.xml \
-e $XYCE_ADMSDIR/xyceBasicTemplates_nosac.xml \
-e $XYCE_ADMSDIR/xyceAnalogFunction_nosac.xml \
-e $XYCE_ADMSDIR/xyceHeaderFile_nosac.xml \
-e $XYCE_ADMSDIR/xyceImplementationFile_nosac.xml mymodel.va

This will create a .h and .C file with base names "N_DEV_ADMS" followed by the name of the model on the Verilog-A "module" line.  In the command lines above, the shell continuation-line character "\" has been used to show that the last line is a continuation of the line before it.  You may type the entire admsXml invocation on a single line, or use the "\" character with no following spaces to continue typing the command on a new line.  This convention is used throughout this document in order to produce command lines that are neatly formatted for the web.

For most models, this C++ code can be added to Xyce directly.  In some special cases it might need to be modified by hand.

Creating a shared-library plug-in: Quickstart

Once generated, the fastest approach to testing out a new Verilog-A conversion in Xyce is to create a shared-library plugin for it.  In order to do so, Xyce must be built with some non-default options.  Since this feature is still somewhat embryonic, no binary of Xyce provided by the Xyce team has been built with these options, so this means you must build Xyce from source yourself.  

Prerequisites:

You must configure Xyce using the --enable-shared and --enable-xyce-shareable and rebuild it.  The "Quick Start" instructions below are also intended to be used with an installed build of Xyce, so you should set --prefix= to a directory into which you have permission to write.  Follow the instructions in the Building Guide as for any other compilation, adding these three options to your configure invocation.  Once you’ve compiled Xyce and tested it using the test suite, execute "make install" in your build directory to install Xyce, its shared libraries, the Xyce/ADMS templates, and the headers necessary for compiling a plugin into your chosen prefix.  

The Xyce team recommends that you choose a prefix other than the default /usr/local or the standard Linux /usr prefixes, both because you might want both serial and parallel builds of Xyce installed, and because the install process places a copy of the "libtool" script created at configure time into ${prefix}/libexec.

NOTE:  The shared library plugin system works properly in parallel runs only since Xyce Release 6.7.  A shared library built according to the directions below using the "buildxyceplugin" script for a serial build should work in a parallel build as long as the  OpenMPI used for the parallel build uses the same base compiler as the serial build, and that parallel Xyce and Trilinos have both been built with the same compiler options as those for the serial build.

Building a plugin:

Once Xyce is installed, a script called "buildxyceplugin" will be installed in the "bin" directory under the prefix you selected.  This script is created at configure time and captures all of the options that were used to build Xyce.  There will also be a script called "libtool" installed into the "libexec" directory under that prefix.  This "libtool" contains configuration necessary to build shared libraries on your system, and is also created at the time you configure your Xyce build.

"buildxyceplugin" takes the following command-line syntax:

buildxyceplugin [-o <name>] [-d] <verilog-a input file>* <destination directory>

It will perform the following actions:

  • For each Verilog-A source file given, run ADMS with the Xyce/ADMS templates to create C++ code.
  • Compile each C++ file created into an object file.
  • Create a "bootstrap" file used by Xyce to add each of the new modules as a device type accessible from a netlist, and compile it into an object file.
  • Link all of the modules given and the bootstrap file into a shared library file, and install it into the destination directory.  By default, the shared library will be named "Xyce_Plugin_<modulename>.so", with the module name taken from the last Verilog-A file on the command line.  If the optional "-o <name>" argument is given, the plugin will be called "<name>.so".  The plugin will have the ".so" suffix even on Mac OS X, where the normal dynamic library suffix is ".dylib". 

Unless module attributes are added to the Verilog-A input, devices added in this manner will all be "Y" devices, and therefore must be accessed from a netlist using the Y syntax:

   Y<module name> <unique instance name>  <node>* <model name> <instance parameter list>

Module attributes may be added to the Verilog-A input to plug the device into Xyce as a new MOSFET (M), BJT (Q), JFET (J), Resistor (R), Capacitor (C), or Diode (D) model levels instead of as Y devices.

If the model has model parameters, then a model name is required, and the model card will be of the same type as the module name:

   .model <unique model name> <module name>  <model parameter list>

By default, all parameters defined in a Verilog-A module are model parameters, and not instance parameters.  Using parameter attributes, the developer can designate some parameters as instance parameters instead, or as both instance and model.  See the section below on parameter attributes for details.

Using your plugin:

Once the buildxyceplugin script completes without error, the plugin may be accessed by adding the "-plugin" option to Xyce, followed by the complete path to the plugin shared library.  This command line option is printed at the end of the buildxyceplugin run, so you can just cut and paste it into a Xyce command line.  Add this command-line option to the Xyce command line anytime you need to use the devices in your plugin, as in:

Xyce -plugin /my/plugin/directory/myplugin.so  netlist.cir

The "-plugin" command line option must be followed by a comma-separated list of paths to plugin libraries. There may be as many plugins as you need.

Xyce -plugin /my/plugin/directory/myplugin.so,/my/other/plugin2.so,/still/another/plugin3.so netlist.cir

When your device needs special code to work in Xyce

If you are writing a new model in Verilog-A, in an ideal world you would write it in such a manner that all standard-compliant Verilog-A compilers and simulators could run it.  This ideal can be hard to realize, and Xyce/ADMS has some limitations that may make it necessary to insert code or variant lines that are only used when building for Xyce.  As of release 7.3, buildxyceplugin defines a symbol "__XYCE__" that can be used in ifdefs in your Verilog-A source to isolate this sort of code.

For example, you could have code that is structured like this:

`ifdef __XYCE__
   //Insert Xyce-specific code here
`else
  // insert code that cannot run in Xyce here
`endif

If you are simply adding a bit of code unique to Xyce you could leave out the "else" part of this structure.

If you are not using the buildxyceplugin script to build your model and are compiling it into C++ via a custom "admsXml" command line, simply add "-D__XYCE__" to that command line to accomplish the same thing that buildxyceplugin is doing.

Building the "toys" plugin

Now that we have presented the basic "quick start" process, you might want to try Xyce/ADMS plugin creation using the "toys" subdirectory.  Here is a simple step-by-step process (making use of the pseudo-shell variables mentioned earlier, which you should replace with the actual paths to the Xyce source and your installation directory):

 # navigate to the directory containing the Verilog source 
cd ${XYCE_SRCDIR}/utils/ADMS/examples/toys
#Build a plugin
${prefix}/bin/buildxyceplugin -o toys *.va ${prefix}/lib
# Test the plugin
${prefix}/bin/Xyce -plugin ${prefix}/lib/toys.so DiodeClipper.cir

The result should be a file DiodeClipper.cir.prn containing the output signals for a diode clipper circuit as described in the Xyce Users’ Guide.  You may plot these signals using the graphics tool of your choice.  The DiodeClipper.cir netlist exercises most of the models in the directory, including a Verilog-A Vsrc, diode, resistor, and capacitor. 

The toys plugin created here also has two versions of a series RLC device, rlc.va and rlc2.va.  The netlist ${XYCE_SRCDIR}/utils/ADMS/examples/toys/rlc_series.cir can be used to test the second of these:

cd ${XYCE_SRCDIR}/utils/ADMS/examples/toys
${prefix}/bin/Xyce -plugin ${prefix}/lib/toys.so rlc_series.cir

This netlist can be modified to replace "YRLC2" with "YRLC" to test the second variant of the RLC device.

The rlc_parallel.cir netlist exercises the rlc3.va parallel RLC-network model, which is described later in this document.

For more detail…

This document has documented a simple process for creating a shared-library plugin for Xyce using the "buildxyceplugin" script.  More detail, including documentation of how to add a device to Xyce without the shared-library approach, may be found in the "Adding a device to Xyce" tutorial on this site.  Users of the Windows operating system who wish to develop models in Verilog-A must follow the more involved method described there in order to link devices into Xyce statically.

Verilog-A features implemented in Xyce/ADMS

Xyce/ADMS implements only the analog features of Verilog-A.  The list below references the Verilog-A Language Reference manual version 2.3.1.

  • Data Types (LRM chapter 3)
    • Integer, real and string data types
    • parameter declarations
      • defaults may depend on other parameters
      • parameter ranges are supported, including use of "include" and "exclude" values
      • parameter aliases via the aliasparam statement are supported
      • parameter attributes are supported
    • electrical and thermal disciplines are currently supported
    • Scalar branch declarations are supported
    • Output variables are supported
  • Expressions (LRM chapter 4)
    • Arithmetic, relational, logical, bitwise, and conditional operators of LRM table 4-2 are implemented, with the exception of the exponentiation operator "**".
      • only operators that translate directly to valid operators of C++ are currently recognized
      • In versions of Xyce/ADMS prior to Xyce 6.4, the conditional operator would not work if any arguments depend on probes. 
    • Bitwise shift operators are supported.
    • The functions in table 4-14 are supported, but only in the "Traditional Verilog-AMS" style (without the leading $).
    • Functions in table 4-15 are supported if the C++ compiler in use supports them.  The code emitted is exactly the "Equivalent C function" in table 4-15.
    • Signal access functions listed in table 4-16 are supported with the exception of the current into the module (e.g. I(<port>)). IMPORTANT: Do not use the port current (I(<port>)) feature in Xyce/ADMS, as incorrect code will be generated that will likely lead to convergence failure.
    • The ddt function is supported, but only in contribution statements.
    • The ddx function is supported, with minor limitations.
    • The limexp function is supported.
    • The white_noise and flicker_noise functions are supported, but only in contribution statements
    • Analog functions are supported, but with some minor limitations.
  • Analog behavior (LRM chapter 5)
    • Electrical and thermal discipline potential (V and Temp) and flow (I and Pwr) accessors are supported.
    • Flow and potential probes (LRM 5.4.2.1) are both supported, but flow probes are only supported when there is an associated potential source.
    • Both flow and potential sources (LRM 5.4.2.2) are supported, with some limitations.
    • Direct branch contributions are supported with limitations.
    • @(initial_step) is supported, but only without any analysis-specific argument.  See the Limitations section below for more detail.
  • System tasks and functions (LRM chapter 9)
    • Of the functions defined in LRM tables 9-1 through 9-19, only the following functions are supported:
      • $strobe — not fully implemented.  In this partial implementation, the format and all variable arguments are simply printed in order.  The format specifiers are not filled in printf- style
      • $display is a synonym for $strobe.
      • $write works as $strobe, except for emitting a newline at the end of its output.
      • $bound_step(expression) signals to the Xyce time integrator that it should not take a time step larger than the value of the expression.  This can have profound negative effects on performance and should only be used when absolutely necessary (as for time-dependent sources).
      • $finish — causes Xyce to abort execution and exit with the given message.  As such it functions more like $fatal should.
      • $abstime — returns the current simulation time (requires a recent version of ADMS — older versions of ADMS did not recognize $abstime)
      • $realtime — in older versions of ADMS this was the only recognized way to obtain simulation time, and Xyce/ADMS treats it as a synonym for $abstime.  Later versions of the Verilog-A language reference state that this function is not valid in analog context, so it should be considered deprecated.
      • $temperature — returns the current simulation temperature in Kelvin
      • $vt  — the thermal voltage.  If an argument is given, return thermal voltage at given temperature, otherwise return thermal voltage at current simulation temperature
      • $simparam — partially implemented.  Only "gmin" currently recognized.
      • $limit — partially implemented, with limitations and Xyce-specific features.
      • $param_given — returns true if the named parameter has been specified by the user
      • $port_connected — returns 1 if the named port is connected externally on the instance line, 0 if not.  Allows a module to have optional external nodes.  This is supported in a limited sense.

Limitations of the Xyce/ADMS implementation

Even when a Verilog-A feature is supported by Xyce/ADMS, it may be subject to some restrictions.

  • The ddt function may only be used in contribution statements.
  • Static, dynamic, and noise contributions must be separately specified.
    • it is not legal to mix terms with ddt() on the same contribution line as any terms without ddt()
    • Due to the way Xyce implements ddt() contributions, it is best if only one term with ddt() appears in each contribution statement. 
    • It is not legal to mix  noise functions with non-noise terms on a contribution line.
    • The right-hand side of a noise contribution must contain nothing but a single noise function.
  • Only contributions to current branches may contain noise functions.  Noise functions are not valid in contributions into voltage branches.
  • Since noise functions may only appear in contribution statements, correlated noise must be handled by adding additional nodes to the model, and referencing the node voltage as the correlated noise source.  It is not legal to set a variable to a noise function and use the value of the variable.
  • Branch declarations are supported, but branches so defined are treated simply as aliases for the pair of nodes they represent.  Two named branches between the same pair of nodes are treated as identical.  This has consequences when flow accessors are used on the right-hand side of contributions or expressions—it is not possible in Xyce/ADMS to declare multiple branches and then access the currents on those branches as if they were parallel.  This limitation is a consequence of how ADMS constructs its data tree; ADMS will substitute the branch’s nodes into the data tree wherever the accessor is used, and therefore information about the alias is obscured from the code generation back-end ("Xyce/ADMS").  The only workaround for this limitation is to make the parallel branches distinct by other means.  Examples are given in the "Guidance" and "Special Topics" sections below.
  • Analog functions are well supported in Xyce/ADMS as of release 7.1, with few limitations.  One known limitation is that second derivatives of analog functions cannot be evaluated at this time.  This limitation would only be important if one were to use ddx() on an expression that involves analog functions (even through an intermediate variable), and then use the result of that ddx() computation in a contribution—such a usage would require first derivatives for the ddx(), and second derivatives for the jacobian matrix elements impacted by the contribution.  Use of ddx() in source expressions/contributions is advised against in most best-practices articles about compact model design in Verilog-A; if one abides by those best practices, this limitation will not bite.
  • As of release 7.1, Xyce/ADMS supports the ddx() function for symbolic differentiation of expressions.  Its primary limitation for most analyses is the one mentioned earlier regarding analog functions—use of the output of ddx() in computation of variables that will ultimately be used (even indirectly) on the right hand side of contributions will require computation of second derivatives, and second derivatives of analog functions is not implemented. 

    A second limitation of ddx() is that it is unimplemented in the functions that are used for sensitivity analysis in Xyce.  This is inconsequential if ddx() is used only to compute output variables (to which sensitivity analysis is not applied in the current design of Xyce’s sensitivity), but is a real problem if ddx() is ever used to compute variables that are used (even indirectly) to impact the right hand side of any contributions.

    This latter limitation comes back to what was said earlier:  best practice is to use ddx() only for output variables, never to compute quantities that will be used on the right hand side of contributions.

    ddx() may be used in computation of the arguments of noise functions (white_noise, flicker_noise).  Contributions from noise functions are handled differently than other contributions, and at this time Xyce does not do sensitivity analysis on noise—therefore the limitation that ddx() is not implemented in sensitivity has no bearing.
  • While both flow and potential sources are supported, assignment of a constant zero (0 or 0.0) to a potential in a contribution statement is special.  Such a contribution represents a collapse of the branch, and should always be enclosed in a conditional whose condition depends only on model or instance parameters (never on solution variables).  See the "Special Topics" section below, under the "node collapse" subsection.
    • Xyce uses modified nodal analysis to set up the circuit problem differential-algebraic equations.  As a result, non-zero or solution-dependent contributions into a potential results in Xyce/ADMS generating an extra solution variable for the "branch current," and an extra equation (the "branch equation").  This branch current is available to the Verilog-A model as a flow probe.
    • Xyce/ADMS supports both flow and potential contributions (with nonzero RHS) as described in LRM 5.4.2.2.  This support is not strictly compliant with the LRM.  Specifically, Xyce/ADMS does not currently adhere to the value retention rules of LRM section 5.6.1.3.  This applies primarily to the special case where both a flow contribution and potential contribution exist between the same pair of nodes and both are executed.  As currently implemented, contributions into potential sources (e.g. "V(a,b)<+expression;") are actually contributions into a "branch equation", and the current associated with them becomes an extra unknown.  Contributions into flow sources are instead summing current contributions into components of the differential-algebraic equations associated with the endpoint nodes.  Thus, in Xyce/ADMS the following pair of contributions represents an inductor in parallel with a resistor:
      I(a,b) <+ V(a,b)/R;
      V(a,b) <+ ddt(L*I(a,b));
      Strict adherence to the value retention rules of the LRM would make this code snippet implement only the inductor, discarding the flow associated with the resistor.
  • Since the idt function is not supported, capacitors cannot be modeled as suggested in LRM section 5.6.4.  They must be handled by differentiating the charge and contributing into a current source rather than integrating I/C with respect to time and contributing into a voltage source.  The examples rlc.va and rlc2.va in the utils/ADMS/examples directory of the Xyce source tree demonstrate this alternate approach.
    • The example in LRM section 5.6.4 is also invalid in Xyce/ADMS because it combines the static (non-differentiated) contribution of the resistor on the same contribution statement as dynamic (time-differentiated) contributions from the capacitor and inductor.
  • The @(initial_step) analog event is true for all Newton iterations of the DC operating point associated with any analysis.  It is incorrect to use this event to perform one-time initialization of bias-independent quantities, as they will be recomputed for every Newton iteration of the DC OP.  It is also incorrect to use this block to attempt to apply initial conditions or set initial junction voltages. 
    • Bias-independent quantities should be computed in the ADMS-specific pseudo-analog-event blocks @(initial_instance) or @(initial_model)  (see the Xyce/ADMS extensions section below).
  • The $limit function is implemented, but in a very Xyce-specific manner. 
    • ADMS does not properly recognize (unquoted) analog function names as the second argument of a $limit call.  To deal with this, Xyce/ADMS will treat any string in this position that it does not recognize as an analog function name.
    • Xyce/ADMS recognizes only a few pre-defined limiter function names in the second argument.  These are documented separately in the "Special Topics" section of this document. 
    • Given the way Xyce sets up the nonlinear problem, voltage limiting in Xyce is more difficult to do than in SPICE, and the implementation of $limit does more than simply apply a function to its arguments. 
  • While $port_connected is supported, its support is limited and fragile at this time. 
    • When a module tests $port_connected(node) it is implicitly declaring that node as optional on the instance line.
      module foo(a,b,c);
      ...
      if ($port_connected(c))
      begin
      ...
      end
      designates the node "c" as optional.  If given on the instance line, it is an external node connected as specified in the netlist.  If not given, it is an internal node of the device.
    • Xyce can only support optional ports of a device if all optional ports are at the end of the port list of the module, and cannot support having optional nodes in the middle of the list with required nodes following.

      Correct:
      module foo(a,b,c,d);
      ...
      if ($port_connected(c))
      begin
      ...
      if ($port_connected(d))
      begin
      ...
      end
      end

      This designates both ports c and d as connected, and if c is connected checks if d is also connected. 
      Incorrect
      module foo(a,b,c,d);
      ...
      if ($port_connected(c))
      begin
      ...
      end

      In this case, only port c is tested with $port_connected, and therefore this module is attempting to define port d as required but port c as optional.  Xyce cannot handle this case, and Xyce/ADMS is not yet robust enough to detect this error and warn against it.  If any node in the module’s external node list is to be optional and is tested with $port_connected, all subsequent nodes must also be optional and therefore must have corresponding $port_connected tests.
    • Xyce is only able to support optional nodes by counting how many were given on the instance line and comparing them to the minimum number of required (non-optional) nodes of the device—any in excess of the minimum number must be the optional nodes.  Due to the positional nature of nodes on SPICE-like instance lines, it is impossible to distinguish optional nodes by name alone.
      • For example:  a device with two required nodes a and b and three optional nodes c, d, and e must be written in such a way that the device works if c is specified as the third node , if c and d are specified as the third and fourth, and if c, d, and e are all specified as third, fourth, and fifth nodes respectively.  Do not write your device assuming it is possible for the user to specify c and e without specifying d.  Any four-node instance line will be interpreted as having been given a c and d node.
      • This limitation is why Xyce does not support the BSIM-SOI in 5- or 6-node configurations with TNODEOUT=1:  the device uses $port_connected in a way that presumes it is possible to detect the difference between "fifth node is t" and "fifth node is p".
    • Xyce supports node collapse of internal nodes to ground and of internal nodes to external nodes using the "V(a,b)<+0;" construct inside a conditional.  But Xyce is unable to collapse external nodes to ground or to collapse external nodes onto each other.  This vastly complicates handling of node collapse for optional nodes. 
      • If a module attempts to collapse an optional node to ground, Xyce/ADMS will issue a warning at the time of code generation that this use can only work if the optional node is not given and is left internal.
      • If at run time a device attempts to collapse an optional node to ground when that node has been specified on the netlist, the device will not actually do the collapse and will instead simply discard all contributions added to that node—that is, the node will float.  Because this may well lead to convergence problems (if the external node is not actually specified as being the ground node, 0), Xyce will issue a warning at runtime when the device is constructed if the optional node is connected and the device tries to collapse it to ground, but will try to run anyway.
    • Xyce can support collapsing optional nodes onto each other only when they are both not specified (i.e., they are internal).  Compile time warnings will be issued when a module specifies that optional nodes can collapse onto each other, and run-time fatal errors may result if the device is instantiated with those nodes as external and collapse is attempted.
    • The safest and most well tested use case for $port_connected in Xyce is when it is used only to allow an optional final node, which may or may not be external.  Most devices that use this capability use it to allow access to an internal node used to represent the temperature rise from self-heating, to allow connection of a thermal coupling network.
  • While the current implementation of Xyce/ADMS will not emit any error messages if a port current accessor (e.g. I(<B>) for the base port of a BJT) is used, it will generate incorrect code that will lead to a convergence failure. Do not use this feature when targeting a module for Xyce through Xyce/ADMS. If your code uses it for other simulators, wrap it in an ifdef to disable it when processing it for Xyce.

Verilog-A features not implemented in Xyce/ADMS

Xyce/ADMS does not implement any of the features of Verilog-A (or rather Verilog-AMS) intended for digital, discrete-event simulation.  

Xyce/ADMS is unable to optimize initializations of bias-independent quantities, automatically detecting them and computing them only once at the beginning of a run.  If it is desired that a block of computations be carried out only at the beginning of a run, it is necessary to designate these blocks using the "@(initial_instance)" or "@(initial_model)" analog event extensions of ADMS.

Xyce/ADMS does not implement all analog features specified in the Verilog-AMS language reference manual.

  • Data types (LRM chapter 3)
    • Arrays are not implemented
    • genvars are not implemented
    • Only electrical and thermal net disciplines are currently recognized. 
    • Nodeset initializers are not supported.
    • Ground declarations are not supported.
    • implicit nets cannot be used.  All nodes in the model must be declared.
    • Digital nets are not supported, so ports of type "wreal" are not recognized.  Ports must be declared as either electrical or thermal.
    • Only scalar branches are supported.  Vector branches are not implemented.
  • Expressions (LRM chapter 4)
    • The exponentiation operator (**) does  not work.  Attempting to use this operator will result in invalid C++ code.  Use the "pow" function instead.
    • Arithmetic shift operators (<<< and >>>) are not valid in analog blocks, and therefore are not supported by Xyce/ADMS.
    • Concatenation is not supported.
    • The functions in table 4-14 are supported, but the "Verilog" style (with the leading $) is not.
    • Functions in table 4-15 are only supported if the C++ compiler in use supports them.  The code emitted is exactly the "Equivalent C function" in table 4-15.
    • Signal access functions that return the current flow into the module (e.g. I(<port>)) are not supported. IMPORTANT: Do not use the port current (I(<port>)) feature in Xyce/ADMS, as incorrect code will be generated that will likely lead to convergence failure.
    • The ddt function is not supported except in contribution statements.
    • The idt, absdelay, transition, idtmod, slew, and last_crossing functions are not supported.
    • Laplace transform and Z transform filters are not supported
    • Analysis-dependent functions other than white_noise and flicker_noise are not supported.
    • The noise_table function is not supported.
  • Analog behavior (LRM chapter 5)
    • "analog initial" blocks are not supported.
    • Flow and Potential accessors for nodes of disciplines other than electrical and thermal are not fully supported.
    • Flow accessors (e.g. "I(a,b)") are supported, but only if there is an associated potential source (e.g. contribution of form "V(a,b) <+ expression")—these flow accessors represent extra unknowns in the DAE that Xyce solves.  Use of flow accessors to define probe branches in the absence of a potential source is currently unsupported.
    • Flow accessors for port branches (LRM 5.4.3) are not supported.
    • Vector branches are not supported.
    • Access of net or branch attributes (e.g. abstol) is not supported.
    • Switch branches (LRM 5.6.5) are not supported in Xyce/ADMS.  The example in LRM 5.6.5 is also invalid in Xyce/ADMS because a contribution of zero into a voltage source always indicates node collapse, and must be in a conditional that is independent of probes (dependent only on model or instance parameters).
    • Indirect branch contributions are not supported.
    • Analog events other than @(initial_step) are not supported.
    • Analysis-specific initial_step events are not supported by ADMS.
    • The event "OR" operator (LRM section 5.10.1) is not supported.
    • Monitored events (LRM 5.10.3) and named events (LRM 5.10.4) are not supported.
  • Hierarchical structures (LRM chapter 6)
    • Xyce/ADMS does not support hierarchical instantiation of Verilog-A modules
    • Modules to be imported into Xyce must be stand-alone modules, and cannot instantiate other modules from Verilog-A.
    • paramset is not supported
  • Mixed-signal (LRM chapter 7)
    • Xyce/ADMS does not support mixed-signal use of Verilog-AMS.  Only the analog compact modeling features are supported.
  • System tasks and functions (LRM chapter 9)
    • Only the functions listed in the prior section are implemented.  Most are not.

Xyce-specific Verilog-A extensions in Xyce/ADMS

In order to allow more seamless integration of new Verilog-A models into the existing structure of the Xyce device model package, several extensions of basic Verilog-A are supported by Xyce/ADMS.  Most of these are implemented by adding non-standard "attributes" to parameters and modules.  This technique has the advantage that models decorated with these attributes will have those attributes ignored by simulators other than Xyce.  One additional feature is provided by ADMS itself: the "@(initial_instance)" and "@(initial_model)" blocks.

Attributes are a standard part of the Verilog-A language, documented in section 2.9 of the Verilog-A Language Reference Manual.  In the 2.3.1 version of the LRM, only two standard attributes are defined:  "desc" and "units".  Xyce/ADMS supports these two standard attributes, but adds a number of others.

Attributes are specified by enclosing attribute specifications inside the delimiter pair "(*" and "*)".

NOTE:  The Verilog-A standard section 2.9 clearly states that attributes should be placed as a PREFIX to the item being attributed.  Some Verilog-A compilers (including ADMS) accept attributes being placed as suffix to the item and many legacy standard models do just that, but this is a violation of the standard.  Best practice would to place attributes before the item to which they apply when writing new models.

Output variables

The Verilog-A standard (section 3.2.1) states that any variable that is declared at module scope and which has either a "desc" or "units" attribute (or both) should be available for output. Xyce supports this.

Module-scoped variables are those that are defined inside a module/endmodule pair, but outside of the analog block.

For example:

module mymodule(a,b); inout a,b; electrical a,b; ... (* desc="My Output Var", units="V" *) real myOutputVar; analog   begin     ...   end endmodule

This example defines an output variable "myOutputVar" that can be accessed on a ".PRINT" line in a netlist using the "N(<instance>:MYOUTPUTVAR)" syntax as described in the Xyce Reference Guide.

Parameter attributes

Parameter attributes are enclosed in "(*"/"*)" delimiters on parameter declaration lines, as in

  (* desc="Resistance", units="Ohm" *) parameter real  R=1 from (0:inf);

A parameter declared with no attributes will be a model parameter, have unknown units, and have no name assigned in the "Description" column of LaTeX tables that are generated using the "-doc" or "-doc_cat" options of Xyce.

Note: While ADMS itself (and therefore Xyce/ADMS) will recognize parameter attributes separated by only spaces, the Verilog-A standard requires them to be comma separated as in the example above. When writing portable models, make sure to keep them comma separated.

The parameter attributes recognized by Xyce/ADMS are:

  • desc — a description of the parameter.  Note:  The text of this parameter is used directly in LaTeX output, and should not contain the "^" or "_" characters, which are treated specially by LaTeX.
  • info   — a synonym for "desc"
  • units — Units of the parameter.  This is used only in automated generation of tables for the reference manual.  Xyce/ADMS recognizes specific units and if given, will add decorations to the internal parameter definition so that LaTeX tables that include the parameter will have appropriate markup to render the unit well.  Where recognized, the text unit is converted into the appropriate internal unit identifier as if it had been specified with "xyceUnit" (below).  The complete list of recognized text units was chosen by attempting to incorporate all of the text units that have appeared in Verilog-A source code for industry standard models.  Unfortunately, there is no standard for this, and some models use arbitrary combinations that are not supported by Xyce/ADMS.
    • m — meters
    • m^2 — square meters
    • m^-3 — 1/meters^3
    • F  — Farad
    • Ohm — Ohms (rendered as upper case Greek letter Omega)
    • V — volt
    • /V  or V^-1 — 1/Volt
    • V^-0.5 — 1/(sqrt(Volt))
    • A   — Ampere
    • S   — Siemens, rendered as 1/(upper case Omega).
    • K   — Kelvin
    • K^-1 or 1/K — 1/Kelvin
    • degC — degrees Celsius (rendered with degree symbol and upper case C)
    • /C   or 1/C or degC^-1  — 1/degC
    • C      — Coulomb.  Note: this choice of C to mean "Coulomb" is based on the use of that unit string in the VBIC model.  We have seen other models that use "C" for "degrees C". 
    • V/K — Volt/Kelvin
    • m^2/V/s — meters^2/Volt/second
    • m/V — meters/Volt
    • Vm  — meters*Volt
    • Vm^-1 — Volt/meters
    • A/V^3 or AV^-3 — Amperes/Volts^3
    • Am/V^3 or AV^-3m — Amperes*meters/Volts^3
    • Am^-1 — Amperes/meters
    • Am^-2 — Amperes/meters^2
    • Am^-3 — Amperes/meters^3
    • Ohm m^2 — Ohms * meters^2
    • Ohm/sq — Ohms per square
    • Fm^-1  — Farad/meters
    • Fm^-2  — Farad/meters^2
    • any unit not listed above will be considered an "unknown" unit for the purpose of Xyce documentation
  • type  — indicates whether the parameter is an instance or model parameter. 
    • Though some simulators appear to accept "both" as a parameter type, Xyce/ADMS (and ADMS itself) does not.  Only "instance" and "model" are accepted.
    • Though some best-practices articles state that models should never have to specify whether a parameter is model or instance, there is an overhead to allowing a parameter to be both model and instance in Xyce.  Therefore in Xyce/ADMS all parameters are assumed to be model parameters unless designated as instance, and if designated as instance they are not also model parameters unless a second attribute, xyceAlsoModel (below) is given.
  • xyceUnit — Similar to "units" but takes a unit designator as listed in the "ParameterUnit" enum in the "src/DeviceModelPKG/Core/include/N_DEV_Units.h" file.  A far wider range of parameter units may be selected in this manner than by the "units" attribute.  If neither units nor xyceUnit is given, the parameter is listed as having unknown units.
  • xyceCategory — Allows parameters to be grouped in categories.  These categories are used only in generation of LaTeX tables for Xyce documentation with the "-doc_cat" option of Xyce.  Valid values are those listed in the ParameterCategory enum in "src/DeviceModelPKG/Core/include/N_DEV_Units.h.  If not given, the parameter will appear in categorized tables in the "unknown category" section.
  • xyceAlsoModel — Since ADMS cannot recognize "both" as an option for the type attribute, this attribute is provided in order to allow a parameter to be specified as being both an instance and model parameter.  To use it, set "type" to "instance" and set xyceAlsoModel to "yes."  
    • Parameters designated as both instance and model in this manner will be processed as follows:
      • Two internal parameters are created, one belonging to the model to be specified on the .model line, and one belonging to each instance, specified on the device instance line in the netlist.
      • If the instance parameter is specified by the user, the value given is used.
      • if the instance parameter is not specified by the user, the value of the model parameter is copied into the instance parameter.
      • If the model parameter is not given by the user, the default value of the model parameter is used.  If the instance parameter is not given either, the default value of the model parameter is copied into the instance parameter, too.
      • All code in "instance scope" (i.e., the main analog code block and anything inside @(initial_instance)) will access only the instance version of this parameter.  Only code in model scope (that explicitly wrapped in @(initial_model)) and the code that initializes the instance version of the parameter will ever use the model version.
    • WARNING:  Because this feature creates two parameters of different scope and copies the model scope version into the instance if the instance version isn’t given, there are subtle ways in which you can break your device by using this feature if you do not keep in mind how it works.  Notably, if you have any other parameters that have default values that use a parameter that is declared as "xyceAlsoModel", they must also be declared that way.  It is a mistake to declare, say, a parameter "L" as instance/model, and then use L in an expression for the default value of a model-only parameter, say, "foo". 
        (* type="instance", xyceAlsoModel="yes" *) parameter real L=1e-6;
        parameter real foo=5*L;

      The result here would be that "foo" will always take its default value from the MODEL version of parameter "L".  If the user had specified "L" on the instance line, this version of L would have no impact on the parameter "foo".
      If one instead declares "foo" as:
        (* type="instance", xyceAlsoModel="yes" *) parameter real L=1e-6;
        (*type="instance", xyceAlsoModel="yes" *) parameter real foo=5*L;

      then everywhere where "foo" is used in the code will use the version of that parameter appropriate to its scope, and the instance version of "foo" will take its default value from the instance version of "L".

      This is as much a warning about making parameters both instance and model only where strictly necessary as it is about being careful when doing so.
  • dependence — Allows the model developer to designate that the parameter may take a time-dependent expression.  Only two values are permitted:
    • NO_DEP — the default, requires that any expression used to define this parameter’s value in a netlist be independent of time.
    • TIME_DEP — designates that it is legal for the user to use a time-dependent expression for this parameter’s value in a netlist.

Module attributes

Module attributes are specified inside "(*"/"*)" delimiters on a module line, as in

  (* xyceModelGroup="MOSFET", xyceLevelNumber="2468″ *) module mymodel(p,n);

The module attributes are used mainly to allow models to be used with standard SPICE device type specifiers like "M" (MOSFET) and "Q" (BJT) instead of the default "device extension" device "Y".  A module with no attributes will be imported into Xyce in such a manner that it will need to be instantiated in a netlist with "Y<modulename> <uniquename> <nodelist> <model name>", and have an associated model card of ".model <modelname> <module name>".

Note: While ADMS itself (and therefore Xyce/ADMS) will recognize parameter attributes separated by only spaces, the Verilog-A standard requires them to be comma separated as in the example above. When writing portable models, make sure to keep them comma separated.

Recognized attributes for modules are:

  • xyceModelGroup — allows a module to be registered as one of the standard SPICE types.  Accepted values are:
    • MOSFET: registers device as an "M" device, recognizing both NMOS and PMOS model cards
    • JFET:  registers device as a "J" device, recognizing both NJF and PJF model cards.
    • BJT: registers device as a "Q" device, recognizing both NPN and PNP model cards
    • Diode: registers device as a "D" device, recognizing D model cards
    • Resistor:  registers as an "R" device, with optional "R" model cards if the device has model parameters
    • Capacitor: Registers as a "C" device
    • If no xyceModelGroup attribute is given, the device will be a Y device, and will recognize only model cards with the type name equal to the module name.
    • If an unrecognized model group is given, Xyce/ADMS will exit with a fatal error telling you to remove the attribute.
  • xyceLevelNumber — for devices with xyceModelGroup specified, gives the level number that must be specified on the ".model" line to select this model
    • If not given, defaults to 1.  ALWAYS specify xyceLevelNumber if you specify xyceModelGroup.
  • xyceDeviceName — the name given to the device in documentation and "-param" output
    • If not given, defaults to "ADMS <module name>"
  • xyceTypeVariable — for devices with N and P types (MOSFETs, JFETs and BJTs), the variable to set to signify which type is selected by the model card
    • This is only needed if the device has been added as one of the standard SPICE devices, e.g., with xyceModelGroup="MOSFET" or xyceModelGroup="BJT".
    • If given, Xyce/ADMS will generate code to set this variable if the model card is of type PMOS (for MOSFETs), PJF (for JFETs) or PNP (for BJTs).
    • By default, the variable will be set to -1 if PMOS, PJF or PNP model cards are used.
    • If the value the model expects for PMOS/PJF/PNP is not -1, you must specify it with the xycePTypeValue attribute.
    • If no xyceTypeVariable is set, then users must identify P type devices in a non-standard way, such as through the use of a type variable on the model card.
  • xycePTypeValue — specify a value of xyceTypeVariable that signifies P type instead of N type
    • If not given, -1 will be used.
  • xyceSpiceDeviceName — used to specify what SPICE-type instance line should invoke this model
    • This is overridden by any xyceModelGroup attribute.
    • Use this only to wire in the device as something other than a MOSFET, JFET, BJT, Diode, Resistor, or Capacitor
    • If neither xyceModelGroup nor xyceSpiceDeviceName is given, the device will be a Y device with model type defined by the module name.
  • xyceModelCardType — used to specify what .model type is associated with this device
    • This is overridden by any xyceModelGroup attribute
    • Used along with xyceSpiceDeviceName to wire in devices that aren’t MOSFET, JFET, BJT, Diode, Resistor or Capacitor

@(initial_model) and @(initial_instance)

ADMS and the Xyce/ADMS back-end support constructs "@(initial_model)" and "@(initial_instance)" that are used to designate code blocks that should be executed only once, on instantiation of a device or instantiation of an associated model.  These two constructs are not part of the Verilog-A language, but are an extension provided by ADMS.

Code in the @(initial_model) block is inserted into the module’s "Model::processParams" method, which is called once at the time the model card is processed, and each time any .STEP directive causes device model parameters to change.

Code in @(initial_instance) is inserted into the module’s "Instance::processParams" method, which is called once for each instance of the device when the netlist is first processed, and again any time a .STEP directive changes instance or model parameters.

Variables assigned to in the @(initial_instance) and @(initial_model) may be referenced from outside these blocks.  Xyce/ADMS will emit declarations for such variables to assure their availability in all contexts where they are used.

Since these pseudo-analog-event constructs are not recognized as part of the Verilog-A language, they should be used wrapped in an "ifdef":

`ifdef insideADMS
@(initial_model)
`endif
begin
<bias-independent assignments that depend only on model parameters>
end
`ifdef insideADMS
@(initial_instance)
`endif
begin
<bias-independent assignments that depend only on instance and model parameters>
end

It is best practice when writing a Verilog-A model for use in Xyce that these bias-independent calculations be inside such blocks. If this is not done, the bias-independent calculations will be done on every Newton iteration of every step of the simulation.To summarize:

  • Variables that are used only inside an @(initial_instance) or @(initial_model) block will be declared as local variables in the processParams() function into which the code will be generated.
  • Variables assigned to inside @(initlal_instance) and used elsewhere in the analog code block will be declared as member variables of the Instance class.
  • Variables assigned to inside @(initial_model) and used elswhere (either in the main analog code block or in the @(initial_instance) block) will be declared as member variables of the Model class.
  • Assignments in @(initial_instance) and @(initial_model) must have bias-independent right hand sides.
  • Assignments in @(initial_model) may only reference model parameters on the right-hand side.
  • Assignments in @(initial_instance) may reference model or instance parameters on the right hand side, and may also reference variables set in @(initial_model).  A common use is to compute temperature-corrected versions of model parameters in @(initial_model) so they are shared by all instances, and then compute size-dependent quantities (which may differ from instance to instance) from the temperature-corrected quantities in @(initial_instance).
  • Assignments in @(initial_model) are executed once per model card present in the netlist, upon initialization or whenever model parameters or the temperature change (as by a .step directive).  The variables set here are shared by all instances of the device that reference the same model card.
  • Assignments in @(initial_instance) are executed once per instance of the device in the circuit, upon initialization or whenver instance parameters, model parameters, or the temperature changes (as by a .step directive).  Each instance of the device has its own copy of the variable.
  • The processParams functions are also called whenever a model or instance parameter is written in the netlist as an expression dependent on global_param variables, and a .step causes the global_param to change value.

Guidance for writing models for use with Xyce

Xyce/ADMS is still a work in progress, and does not support every Verilog-A construct defined in the Language Reference Manual.  The supported and unsupported features were detailed earlier in this document.  Even where Xyce/ADMS accepts a construct, there may be issues of compatibility with the Xyce simulator that can prevent a Verilog-A model from working properly in Xyce.  In this section, we present a list of DOs and DO NOTs for building a Verilog-A model for Xyce with ADMS and Xyce/ADMS.

DO:

  • Make your model well-behaved in all ranges of possible solution variable
    Xyce uses a Newton-Raphson method to converge to a solution.  In the course of finding a solution, it may explore parts of the solution space that are well outside what you might consider "normal" ranges of values.  Make sure your model behaves well even in these regimes, and you will avoid many of the problems that force other models to use techniques like voltage limiting (à la "pnjlim").  It is not enough to argue that the device should never run in that regime—unconverged solutions might go there.
  • Use @(initial_instance) and @(initial_model) pseudo-analog events for blocks that initialize bias-independent variables that depend only on instance or model parameters, respectively. 
    While some authors have stated that model developers should not be forced to be concerned with such things, it is phenomenally difficult in ADMS to make the necessary determinations at code-generation time that allow a back-end to segregate bias-independent code so that it can be evaluated only at an initialization step.  Using @(initial_instance) and @(initial_model) makes it trivial.

    Since this is an ADMS-specific extension, if you wish your model to be portable between simulators that do not use ADMS for Verilog-A processing, you should use an ifdef.  A simple example would be:
    `ifdef insideADMS
    @(initial_model)
    `endif
    begin : initializeModel
    [...]
    end

    A slightly cleaner look can be obtained by defining macros in an include file:
    `ifdef insideADMS
    `define MODEL @(initial_model)
    `define INSTANCE @(initial_instance)
    `else
    `define MODEL
    `define INSTANCE
    `endif

    And in the main code:
    `MODEL
    begin : initializeModel
    [...]
    end
  • Always declare variables that are used inside @(initial_instance) or @(initial_model) blocks outside the "analog begin" block, at module scope.
    This is a limitation of how ADMS parses the Verilog-A and flags variable scopes to the back-end:  ADMS marks variables used inside one of these special blocks and also used inside the main analog block as having "global_instance" or "global_model" scope.  These variables must be declared in Xyce’s C++ code as member variables in the Instance or Model classes so that they can be accessed by the several functions that are called in the process of running a simulation; variables used only locally in one or the other place can simply be local variables to the function that is generated.  When a variable is declared in the Verilog-A at module scope and used both inside and outside of the @(initial_*) blocks, ADMS flags them in a way that can be used to trigger this decision making.

    Declaring variables inside any begin/end block makes ADMS always scope these variables as block local variables, and they are never flagged as "global_instance" or "global_model" scoped variables.  This makes it impossible for Xyce/ADMS to determine that it must move the declaration of those variables into the Instance or Model classes.

    RIGHT:
    module mymodel(p,n);
    electrical p,n;
    inout p,n;
    real myvar1, myvar2;
    parameter real someparam=1;
    analog begin
    @(initial_model)
    begin : initial_model
    myvar1 = someparam*5.0;
    end
    myvar2 = myvar1*V(p,n)
    [....]
    end
    endmodule

    WRONG:
    module mymodel(p,n);
    electrical p,n;
    inout p,n;
    parameter real someparam=1;
    analog begin
    real myvar1, myvar2;
    @(initial_model)
    begin : initial_model
    myvar1 = someparam*5.0;
    end
    myvar2 = myvar1*V(p,n)
    [....]
    end
    endmodule

    The first of these will generate correct code in Xyce, with myvar1 being a member of the Model class associated with the module.  The second will generate incorrect code, with myvar1 being declared local in both the Model::processParams function (where the initial_model code is placed) and the Instance::updateIntermediateVars function (where the rest of the analog block is placed).

    When myvar1 is used in a node-collapse conditional (see below), Xyce/ADMS will throw a fatal error in the second case, because only variables of global_instance or global_model scope may be used in such conditionals.

    The Xyce team has found that most published Verilog-A models have variables declared at module scope.  We have found only two that put the declarations inside the analog block, and fail to process correctly in Xyce/ADMS (usually with a fatal error as described in the last paragraph).  Moving the declarations outside of the analog block completely fixes the problem.
  • Limit "OP" computations, and consider making them optionally compiled
    As of Xyce Release  7.1, the SPICE-like ".OP" function does not output device-specific operating point variables.  It does, however, support limited access to device-specific internal variables via its "N()" notation on .PRINT lines.

    Many Verilog-A models have large numbers of computations of such variables.  Each such variable increases the size of the object used to store a device instance, so one should avoid having more output variables than necessary.  We have found a number of models that have hundreds of them, some clearly only of value while debugging the model.  We recommend restraint when crafting a new model.

    Output variables are defined in Verilog-A as module-scoped (as opposed to local to a block) variables that have "desc" and/or "units" attributes.  Xyce supports this definition, with one limitation:  variables that are used inside an @(initial_model) block won’t work, and at the time of this writing will lead to a compile-time error.

    You may also consider the value of placing your output variable’s declaration and  computation inside an ifdef so that a user can turn them off when building your model.  Placing all of these operations inside an ifdef allows them to be turned on with a simple "`define" addition to the source code.  This can be advantageous, as supporting output variables does increase the size of the "instance" class for the model, and increases the amount of computation performed.  It is not necessary to do this.
    RIGHT:
    `ifdef __DO_OP_CALC__
      real CGSI (*desc="Gat-Source capacitance", units="F"*);
    `endif
    analog
      begin
      [...]
    `ifdef __DO_OP_CALC__
    CGSI = - ddx(QGI,V(Branch1));
    [...]
    `endif
    end
    When processing this model for Xyce, one can choose not to define the "__DO_OP_CALC__" symbol if one does not need output variables.  Having it undefined saves both storage and computation effort.  If "__DO_OP_CALC__" is defined, Xyce users can print the value of CGSI by adding "N(<deviceinstance>:CGSI)" to their .PRINT line in their netlists.  Remember: at this time Xyce does not add information like this to the output of .OP analysis.
    WRONG:

    real SomeVar (*desc="Variable initialized from model scope", units="Ohm"*);
    analog
      begin
        @(initial_model)
        begin
          SomeVar=SomeModelParameter*$temperature;
        end
        [...]
    SomeVar = SomeLocalVariable*$temperature;
    [...]
    end
    This will result in a C++ compile time error stating that no "SomeVar" variable is a member of the Instance class.  The reason for that is that the use of SomeVar in @(initial_model) has made SomeVar a member of the Model class, inaccessible from the piece of code that stores the variable for later printing.  if it is necessary to have an output variable that reflects the value of the SomeVar model-scoped variable, create another one that is expressly used to hold the value, and is only used at module scope, and copy SomeVar’s value into it.

DO NOT:

  • Do not use @(initial_step) to denote computations to be done only once, at initialization.
    The @(initial_step) analog event is defined in the Verilog-A Language Reference Manual as being true at the operating point step of every analysis.  Xyce uses a Newton-Raphson solver (among others) to obtain the operating point, and this means that any code inside an @(initial_step) block is run at every iteration in the course of solving the operating point. 

    Since other simulators may implement @(initial_step) in a different way, if your model is designed for portability then you should use ifdefs to turn off this usage when using ADMS.  An example:
    `ifdef insideADMS
    @(initial_model)
    `else
    @(initial_step)
    `endif
    begin : initializeModel
    [...]
    end

    In this example, the correct @(initial_model) ADMS extension will be used when compiling with ADMS, and the @(initial_step) analog event used for other simulators you know work with that approach.
  • Do not use analysis-specific @(initial_step("type")) analog events
    These are not recognized by ADMS, and will cause ADMS to exit with an error.  Use an ifdef to prevent these from being seen when compiling with ADMS.
  • Do not attempt to apply initial biases in @(initial_step)
    We have seen models that require initialization of junction voltages (e.g. VBIC) in order to converge well.  The VBIC authors point out the need for such initialization and leave this initialization to simulator implementations, but we have seen other models that attempt to set initial conditions from the Verilog using user-provided "IC"  parameters and the @(initial_step) analog event, e.g.:
    WRONG:
    @(initial_step)
    begin
      CapacitorCharge = C*IC;
    end

    This will not work in Xyce, due to the way the nonlinear problem is solved.  Any attempt by a device to compute bias-dependent quantities using anything other than the biases currently applied from the solution vector will lead to convergence failures. There is currently no way to do junction voltage initialization directly from Verilog-A in Xyce.  Applying initial biases in devices requires manual post-ADMS patching of the device using the "voltage limiting" machinery described below.  The post-code-generation patches that are applied to the VBIC models in the utils/ADMS/examples/vbic_r1.3_prerelease directory of the source tree give examples of how this junction initialization should be performed.
  • Do not use a single temporary variable to store multiple bias-dependent temporary computations
    Due to the way ADMS records variable dependencies on probes, a variable that is used in multiple temporary bias-dependent computations will be marked as depending on all of the probes that appear in all expressions that set that variable.   Any other variable that depends on the first will inherit all of those dependencies.  This can lead to unnecessary Jacobian elements being defined only to be filled with zeros when the actual derivatives of expressions are computed.

    Instead, you should either use unique temporaries for different contexts, or wrap expressions containing temporaries in a begin/end block:
    INEFFICIENT:
    real temp,foo,bar;
    temp=5.0*v(a,b);
    foo=10.0*temp;
    temp=10.0*v(c,d);
    bar=5.0*temp;


    EFFICIENT:
    real foo,bar;
    begin
      real temp;
      temp=5.0*v(a,b);
      foo=10.0*temp;
    end
    begin
      real temp;
      temp=10*v(c,d);
      bar=5.0*temp;
    end


    In the first case, ADMS will mark that "temp" depends on both V(a,b) and V(c,d), even though in context it only depends on one of these at a time.  Therefore both foo and bar will be marked as depending on both probes, and this could lead to extra (numerically zero) Jacobian elements in the linear system that Xyce uses.  In the second case, because each "temp" is block-local, ADMS knows to mark it as dependent only on one of the probes.

    This kind of thing shows up commonly when model developers create macros that reference a single, global temporary variable.  Making the macro create a local block with a local temporary is just as straightforward, and avoids this problem.  Further, using macros that reference a global temporary can lead to scoping problems if the same macro is used both inside and outside of an @(initial_instance) or @(initial_model) block.
  • Do not assume that multiple named branches between the same pair of nodes can be used to access distinct currents
    In Verilog-A, one may declare named branches between nodes, and use the names instead of the node pair when constructing probes and contributions.  Due to a limitation in the way ADMS constructs its data tree, such a declaration defines only an alias for the node pair: the information about the use of these branch names is obscured by the time the code generation templates get access to it, and it is not currently possible for Xyce/ADMS to treat these named branches as anything other than an alias for a node pair.  This means that any access of flow probes using multiple branch names for the same pair of nodes lead to access of single branch current between those nodes.
    module foobar(a,b)
    inout a,b
    electrical a,b;
    branch (a,b) branch1;
    branch (a,b) branch2;
    ...
    V(branch1) <+ ddt(L*I(branch1));
    V(branch2) <+ I(branch2)*R;

    In this example, I(branch1) and I(branch2) are in fact the same current I(a,b), and the two potential contributions are in fact adding their contributions to the potential drop between the two nodes. The effect of the snippet above would be a resistor and inductor in series between a and b, not in parallel (as would be the case if the two differently named branches were in fact distinct). ADMS does not provide any means for the back-end to detect that these two contributions are written to apply to different branch names — by the time the back-end sees it, ADMS has replaced the branch name with the pair of nodes.

Special topics

There are a number of issues that require special attention when writing Verilog-A models for inclusion into Xyce, or for porting existing Verilog-A models into Xyce.

Voltage limiting

Device models that contain terms that depend on exponentials of applied voltages can be especially problematic for the convergence of circuit simulators.  The original SPICE3F5 models for diodes, BJTs, and MOSFETs as a result all make use of a special limiting function "pnjlim" to prevent runaway convergence issues.  It does so by restricting the amount that particular junction voltages can change from one Newton iteration to the next during the search for a solution.  When starting from a reasonable initial junction voltage, devices that have this limiting applied can usually converge quickly, even though without the limiting they might often diverge badly.

The best solution to this problem is to write your model so that it does not compute currents that depend on exponentials of applied voltages.

Some models can avoid these convergence problems simply by using the standard "limexp" function instead of the exponential function.  In Xyce, as in other open-source circuit simulators, limexp is the exponential function up to a certain value of the argument, after which it is a linear function.  This is often sufficient to eliminate some of the worst convergence problems in simple models when importing them into Xyce.  It is not sufficient, however, for many of the published BJT and HBT models.

Most recent models of MOSFETs that have been written in Verilog-A avoid these convergence problems from the start.  Some BJT and HBT models, however, continue to cause convergence problems and require limiting.  In some cases (e.g. the VBIC 1.2 model) the need for this limiting is explictly mentioned in documentation about the model, even though the Verilog-A contains no limiting functions.  In others (e.g. MEXTRAM), one can find implementations in open source codes where limiting has been applied in the C++ or C code for the model, even though such limiting is not present in the Verilog-A source.

Verilog-A provides a standard construct, "$limit", that is intended for use in device models and which could be used by model developers to make explicit the need for this kind of convergence assistance.  To date, the Xyce team has not found any published Verilog-A models that use it.  When the Xyce team has encountered Verilog-A models that require voltage limiting, we have always had to add it ourselves.  Because the code required to do voltage limiting in Xyce is more involved than in SPICE3F5, we have implemented in Xyce/ADMS a way of using $limit to generate the required code, and have inserted use of this function into the Verilog-A source for the model.  The models for which this was required are the VBIC (both 1.2 and 1.3 versions), MEXTRAM, and FBH HBT models.

Our implementation of $limit is somewhat non-standard, because we have found that the Verilog-A standard $limit function is not quite flexible enough to support the needs of those three models, and because ADMS parsing limitations prevent strict implementation of the Verilog-A standard $limit function.

The Verilog-A $limit function according to the LRM:

The Verilog-A Language Reference Manual states that the $limit function has the following syntaxes and meaning:

limited_variable = $limit(access_function_reference, string[, argument list]);
limited_variable = $limit(access_function_reference, analog_function_identifier[, argument list]);

An access function reference is a potential or flow accessor such as "V(a,c)" or "I(a,c)".  The string in the first variant is used to specify a built in limiting function of the simulator,such as "pnjlim", and must be quoted.  In the second variant, the analog_function_identifier is supposed to be specified without quotation marks.  When the first form is used with an unrecognized function, it is up to the simulator to determine which function to apply.

The return value of the $limit function is supposed to be the value of the access function reference when the simulator is converged, but the output of the simulator’s limiting function while unconverged.

The limiter function will be called in the following manner:

limter_function(value_of_access_function, stored_state_access_function [, argument list]);

where value_of_access_function is the value of the access function at the current iteration, and stored_state_access_function is that value from the previous iteration.

The Verilog-A $limit function as implemented by Xyce/ADMS

Because ADMS will not recognize the name of an analog function in a function argument list, only the first form of the $limit function can work in Xyce/ADMS:

limited_variable = $limit(access_function_reference, string, argument list);

The only internally recognized limiter function at this time are "pnjlim," "typedpnjlim", "pnjlim_new", and "typedpnjlim_new".  If any other string is given, it is assumed to be the name of a user-defined analog function, and code will be emitted to call that analog function.  pnjlim is the standard SPICE3F5 function.  pnjlim_new is a newer implementation of pnjlim copied from ngspice, which performs correctly when called with large negative voltages, and identically to pnjlim when the voltage is positive.  pnjlim does not limit negative voltages, only positive voltages.  This variant is necessary when the negative of the voltage may be used in exponentials, as it is in a few places in the VBIC model.

Since the first argument of $limit must be an access function reference, this causes problems when the device model can have both a P-type and N-type variant, such as a BJT.  The standard SPICE3F5 pnjlim function works on the value of the junction voltage after multiplication by the type variable (which is -1 for PNP transistors).  It is not possible to apply pnjlim in this manner given the standard Verilog-A definition of $limit.   As a result, Xyce/ADMS recognizes special strings "typedpnjlim" and "typedpnjlim_new" — when these limiter "functions" are called, the voltage being limited is premultiplied by the last argument in the $limit call, which should be the variable that is +1 for N type and -1 for P type.

limited_variable = $limit(V(b,e),"typedpnjlim",$vt,tVcrit, TYPE);

Here, V(b,e) will be multiplied by TYPE, and the resulting product limited by pnjlim or pnjlim_new with its standard arguments.  The value of the limited product will be stored in limited_variable.

For the case where pnjlim is not sufficient and a user-defined analog function is needed for limiting of a voltage that must be multiplied by a type variable (as in the MEXTRAM), a special form of $limit is recognized:

limited_variable = $limit(access_function,string,"typed",type_variable,argument_list);

In this variant, the literal string "typed" in the third argument indicates that the access function should be multiplied by the fourth argument before being passed to the limiter function.

In all cases, the limiter function is called as:

limiter_function(value, last_value[,argument_list]);

with value being premultiplied by the type variable if necessary.Examples of these limiter functions in use may be found in the utils/ADMS/examples subdirectories, in the VBIC 1.3, MEXTRAM, and FBH_HBT codes.  A simple example of this applied to a diode may be found in the utils/ADMS/examples/toys/diode2.va file.

How is voltage limiting done in Xyce, and why is it different from SPICE?

The answer to this question is found in the Voltage Limiting section of the Xyce Mathematical Formulation document.

Node collapse

When a model has internal nodes connected with resistors that may be zero, this is commonly represented in Verilog-A using a pattern like this:

if ( Resistance != 0 )
I(a,b) <+ V(a,b)/R;
else
V(a,b) <+ 0;

This pattern is interpreted by Xyce/ADMS as a node collapse condition:  if the resistance is zero, the a and b nodes are collapsed together, and one of the nodes is removed from the problem.  If one of the nodes is internal and the other is external (e.g. present on the instance line and connected to other devices), the internal node is collapsed onto the external node.  If both nodes are internal, the negative node is collapsed onto the positive.  It is legal to collapse an internal node onto ground (completely removing the branch from the problem), but it is not legal to collapse an external node onto ground.

The collapse of nodes must be detectable at the time the device is instantiated — and therefore the conditionals must depend only on bias-independent quantities.  It is not possible in Xyce to modify the network topology of a circuit during a run, so parameter sweeps must not be performed that take a device’s parameter through a value that triggers collapse (i.e. you can’t use a .STEP loop to sweep a device’s resistance over a range that includes zero).  Further, due to the complexity of code generation for node collapse, Xyce/ADMS imposes a number of constraints on how this capability may be used:

  • The variables used in the conditionals containing the node collapse contribution ( V(a,b) <+ 0 in the example above) must be dependent only on model or instance parameters.
  • If the conditional involves variables that are neither model nor instance parameters, these variables ADMS must be able to determine that they depend only on model or instance parameters.  That is, you should never use a temporary variable in this context that is also used elsewhere to hold bias-dependent values.  ADMS does not keep track of variable dependence changes from one context to another — if a variable ever holds bias-dependent quantities in a module, it is flagged by ADMS as a bias-dependent variable.
  • If the conditional uses variables that depend on model or instance parameters, these variables MUST be initialized in an @(initial_model) or @(initial_instance) block.
    • This is required because the node collapse will evaluated in a function that is separate from the function that evaluates model equations, and the variables must therefore be "global_instance" or "global_model" scoped for them to be visible to this function.
  • It is best if one uses model or instance parameters directly in these conditionals instead of using variables computed from them.  This is what most published models do anyway, and is the simplest case for Xyce/ADMS to handle.

Because Xyce/ADMS interprets all contributions of the form V(a,b) <+ 0 as indicating node collapse, and given the constraints placed by Xyce/ADMS on the conditionals that may contain such contributions, it is not possible to use this sort of contribution to implement "switch branches."  Switch branches would have this kind of contribution inside a bias-dependent conditional. 

Integration in Xyce of standard device types

By default, if a module is imported into Xyce without adding module attributes to the Verilog-A input, it is accessible from a netlist using the Xyce extension device type, "Y".  The Y device type is unusual in that the real type of the device is determined by the characters immediately following the Y, up to the first space, and the name of the instance is the next word on the line.  For example:

    YRLC rlc1 1 2 MYMODEL R=1k L=1u C=1p

would invoke an "RLC" device named "rlc1", using a model card named MYMODEL, between nodes 1 and 2, using the given R,L, and C parameters.  The model card used here would be of the form:

    .model MYMODEL RLC ([parameters…])

This may be acceptable, but if you are writing a MOSFET or BJT model, it might be desirable to make your new device plug in to Xyce as an M or Q device instead.  Xyce/ADMS provides a capability to do this using module attributes, as documented above.  This is currently supported for MOSFET, BJT, capacitor, resistor, and diode devices.

The advantages of doing this include:

  • Your netlist can easily be tested using several different models of the same device type for debugging or QA, simply by swapping model cards with a different level number
  • BJTs will recognize both NPN and PNP model cards, automatically selecting the correct sign for the internal variable used to modify equations.  Similarly, MOSFETS will recognize NMOS and PMOS model cards.  JFETs will recognize NJF and PJF.
  • Your netlist will use standard SPICE syntax for devices of that type (Q for BJT, M for MOSFET, etc.)

The essential attributes needed to accomplish this kind of usage are:

  • xyceModelGroup — may take the values "MOSFET", "BJT", "JFET", "Diode", "Resistor" or "Capacitor".  If given, the module will be instantiated using the standard SPICE syntax for that type of device.
  • xyceLevelNumber — you should always specify an unused model level for this attribute.  Model levels currently in use may be listed by running Xyce with the "-param" option — a list of all known devices will be emitted, and this can be searched for all the levels (e.g. "Xyce -param | grep -i ‘^q level'" to search for all currently implemented BJT models, or "Xyce -param | grep -i ‘^m level'" for MOSFETS).
  • xyceTypeVariable — set to the name of the internal variable in your module that is used to determine P or N type devices (applicable mainly to MOSFETs and BJTs).  Xyce/ADMS will set this variable to -1 when a PMOS (for MOSFETs), PJF (JFETs), or PNP (BJTs) model card is used.
  • xycePTypeValue — set this if your device uses a value other than "-1" for the P type device.  So far, only the Xyce team has found only one device that requires this, the BSIM-CMG, which uses the value 0 for the DEVTYPE parameter to denote PMOS devices.

In addition, you may wish to set other module attributes such as "xyceDeviceName" — these are generally used only in the automated creation of tables for inclusion in Xyce manuals, and also is emitted by the "-param" informational output function.