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.
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.
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
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.
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.
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.
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.
{
;a
). refers to a run time variable. Emit ddouble t0(a);
and push 0
on the stack.b
). refers to a run time variable. Emit ddouble t1(b);
and push 1
on the stack.-
). find and pop 1
, find 0
on the top. Emit t0 -= t1;
c
). find t1 unused, put 1
back on the stack and emit t1 = c;
*
). same as 3. but *=
.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…
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.