Table of Contents

Modelgen-Verilog

The term “modelgen” refers to the device model generator and language that has been part of the Gnucap project from early on. Modelgen reads device descriptions and emits C++ code to be compiled into plugins. Support for Verilog-AMS compact models has been implemented in a modelgen successor, “modelgen-verilog”, following the design patterns and device architecture. Major technical advantages of the latter are automatic differentiation and support for device specific numerical tolerances. Others will follow by adding subsequent Verilog-AMS features.

This work is carried out with financial support from the NGI0 Entrust Fund, see verilogAMS.

TODO: there is some overlap with verilog.

Preprocessing

Verilog-AMS inherits a few “compiler directives” from IEEE Std 1364-2005 Verilog HDL. The important ones are '`define', '`include', '`if(n)def', '`else', '`endif'. These are dealt with in the input stage of the model compiler, where we also strip comments and whitespace.

The semantics are similar to C, relevant differences are - Verilog does not support arithmetic expression in macros, - In Verilog, '`include' only takes a '“quoted”' argument.

Like ordinary C preprocessors, gnucap-verilog accepts macro definitions from the command line using '-D', and include paths with '-I'. We currently process command line options from left to right, and the order of the arguments matters.

The preprocessor functionality is exposed to users through the '–pp' option, it displays the input stream as it will be parsed. The complementary '–dump' option prints the final state of the data base, i.e. after parsing.

Branches and Contributions

In Verilog-AMS, analog behaviour is modelled in terms of controlled sources. Sources of either flow or potential nature are expressed implicitly as contribution statements to branches. A branch basically refers to a pair of nodes, but details matter when it comes to named branches and switch branches. In Gnucap these controlled sources are represented by subdevices derived from ELEMENT.

It is the model compilers responsibility to identify the branches that require sources to be instanciated, select the suitable one. A branch that has a contribution associated with it anywhere in the module (reachable or not), becomes a source branch. Otherwise, if there is a an access statement anywhere in an expression, the branch becomes a probe branch. A flow probe is essentially a potential source, and implemented as such.

We use variants of “d_poly_g”, the transconductance ELEMENT used in (legacy) modelgen, which provides current sources with voltage control. The ELEMENTs used in modelgen-verilog are tailored to the contribution type (flow, potential, switch), and add current control.

In Gnucap, the model evaluation involves 5 phases on the component level. These are

  1. check if evaluation is required
  2. read probes
  3. evaluate analog expressions
  4. load (followed by solving the matrix in the simulator)
  5. check for convergence

The first and last step involve tolerances specified through disciplines. Ultimately, disciplines need to become part of the nodes, currently they are directly attached to the source ELEMENTs.

Named Branches

A named branch is an additional path between two nodes that can be a source/switch or a probe following the rules above. It is implemented as an additional and independent ELEMENT, sharing the output ports.

Probes

A branch that has no contribution statement associated with it, but is used in an access function anywhere in the module becomes a probe branch. According to the LRM, 5.4.2.1, it's not allowed to “use” both the flow and the potential of such a branch. What it means is, a flow probe cannot co-exist with a potential probe (on the same probe branch) within the same module, regardless of the use. There is no ELEMENT instanciated for a potential probe branch. A flow probe branch requires one, as it works similar to a zero-potential source.

Computing Partial Derivatives

In Verilog-A, analog components are essentially modelled as controlled sources. In this section, think of a current source controlled by voltage sources. For example, a linear admittance would boil down to a contribution statement like

I(p, n) <+ V(p, n) * g;

given nodes p, n, and a real parameter g. More generally, a current may depend on multiple node voltages, as in

I(p, n) <+ f(V(c1), V(c2), ... V(cn));

modelled by some real valued multivariate function f.

In a nutshell, to solve the circuit equations, we need to evaluate the partial derivatives of f wrt. to its arguments. Writing v=(v1..vn)=(V(c1) .. V(cn) This amounts to computing \del f(v) / \del v_i for all i.

In practice f is provided as a program involving assignments, loops and conditionals. For simplicity, think of something like

real v0;
real gain;
gain = 10;
v0 = V(c0);
v_in = v0 - V(c1)
I(p, n) <+ v_in * g;

forming an ordinary ccvs. The approach we use is referred to as “forward mode” in Chapter 6 “Implementation and Software” of “Evaluating Derivatives (2nd Ed.)” by Andreas Griewank and Andrea Walther. We implement it as described using operator overloading, with a data type that bundles each value with the derivatives wrt. to the input voltages. We hence reduce the problem to emitting ordinary C++ evaluation code for each rhs of an assignment or contribution statement using a datatype ddouble. ddouble is a struct with a double member variable and additional doubles for each of the 'v_i', arithmetic overloads and some helper functions.

This reduction is particularly handy, because Gnucap parses expressions into reverse polish representation. Remember that the rhs of an assignment like x = (a-b)*c is stored as a token sequence a b - c *. From there, all we need to do is scan the tokens from left to right, emitting code for each operand while keeping track of intermediates on a stack.

Here's how it works with the assignment above. It is transcribed as follows.

  1. open new scope {;
  2. (a). refers to a run time variable. Emit ddouble t0(a); and push 0 on the stack.
  3. (b). refers to a run time variable. Emit ddouble t1(b); and push 1 on the stack.
  4. (-). find and pop 1, find 0 on the top. Emit t0 -= t1;
  5. (c). find t1 unused, put 1 back on the stack and emit t1 = c;
  6. (*). same as 3. but *=.
  7. print x = t0; and close }.

Now, x holds the value of the expression, and partial derivative values.

NB: This is similar in principle to the ADMS approach, but a little less obfuscated. Of course, we also keep track of unused derivatives, but (at the time of writing), pass them to the C++ compiler as literal zeroes. Gcc is pretty good at optimising them out…

Filter operators

Verilog-AMS defines ddt and idt operators as a means to describe dynamic behaviour in terms of symbolic time derivatives and integrals respectively. The model generator turns these into subdevice elements, similar to source elements that represent analog contribution statements. The “ddt” implementation is derived from the traditional “fpoly_cap” storage element that serves a similar purpose. For this to work in the generality required by Verilog-AMS, we use an additional internal node for each filter. This way the expression evaluation and possible operator nesting remains manageable. Corner-case optimisations remain possible, and will be considered later on. The idt operator is a simple adaptation of the ddt operator.

To illustrate the implementation of a ddt filter, consider the contribution statement I(p,n) <+ f2(ddt(f1(V(p,n))). It splits into a voltage probe, a filter and a controlled source as follows

real t0;
t0 = V(p,n);
t0 = f1(t0);
t0 = ddt(t0); // (*)
t0 = f2(t0);
I(p,n) <+ t0;

and happens to model a capacitor, if f1(x)==f2(x)==x. All we need for the general case is ddt(t0). The following subcircuit model implements a capacitor corresponding to the simplified contribution statement.

module cap(a, b)
  parameter c
  tcap #(c) store(i 0 a b);
  resistor #(.r(1)) shunt(0 i);
  vccs #(.gm(1)) branch_i(b a i 0);
endmodule

It contains a trans-capacitance device named “store”. This device outputs a current proportional to the time derivative of the voltage across (a,b). In combination with the shunt resistor and the internal node i it represents a ddt filter as required in (*), where the rhs implicitly acts as a voltage probe V(i).

In terms of implementation, the tcap device is a version of the Modelgen fpoly_cap limited to 4 external nodes and without self-capacitance. The va_ddt filter in Modelgen-Verilog retains the arbitrary number of nodes and adds the shunt resistance.