====== 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. For tech notes on Verilog support in the simulator, see [[verilog|Verilog]]. ==== Acknowledgement ==== This work is carried out with financial support from the NGI0 Entrust Fund, see [[gnucap:projects:nlnet:verilogAMS]]. ==== 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 - check if evaluation is required - read probes - evaluate analog expressions - load (followed by solving the matrix in the simulator) - 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. - open new scope ''{''; - (''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 ''*=''. - 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. ===== compiled paramset ===== TODO: move to tech:modelgen The motivation for compiled paramsets, as opposed to interpreted ones, or legacy spice .model instances is speed. Compiled paramsets can be an order of magnitude lighter due to the obvious constant pruning and structural collapse. According to the LRM, Section 6.4.2, paramset identifiers need not be unique, and offer a basis for overloading. We make use of this mechanism in slight deviation from the standard. In Gnucap, a paramset provides a device prototype, and device prototypes need not be unique. Moreover device prototypes are not distinguishable by their origin. In particular, a paramset name may be identical to the prototype module name. This offers the possibility to bundle multiple optimised paramsets alongside a generic module into a single plugin. When a compilation unit contains such a module and a set of (supposedly optimised, simplified) specialisations of the same one, modelgen-verilog may bundle them into a single plugin. In this situation the plugin installs a set of device prototypes, and the generic one is installed last. Subsequently declared interpreted paramsets refer to this last installed and generic prototype. This way, interpreted paramsets remain flexible, while compiled paramsets may coexist for speed. ===== Partial specialisation ===== TODO: move to tech:modelgen Given a module declaration, the standard does not offer a way to fix a subset of its parameters, while transparently passing through the others. Paramset sort of does it, but not without changing the behaviour of the specialised device relative to the generic one. Typical use cases are devices specialised without noise, without initial conditions, without additional resistors, without temperature, or with their levels fixed. One issue shows up when $param_given is used in a model. Consider a module/paramset combo as follows. module M([...]); parameter real p=1; [..] pg = $param given(p) [..] endparamset paramset M M1 parameter real p=17; .p=p; .something_else=42; // specialise some parameters endparamset In an instance of M1, ''$param given(p)'' will unconditionally evaluate to ''true', which may break the behaviour otherwise inherited from M. The possible solution is to introduce parameters without a default value, as shown in the following example paramset M M2 parameter real p; .p=p; .something_else=42; // specialise some parameters endparamset The intent is that the user takes control of $param_given(p) in instances of M2 as shown below. M2 #() myexample1(..); // $param_given(p) == false M2 #(.p(3.14)) myexample2(..); // $param_given(p) == true With this mechanism, it ought to be possible to wrap M into a paramset that exposes behaviour identical to M. Another issue is the lack of a portlist in a paramset declaration, allowing for re-using a prototype with its ports renamed. A singleton subcircuit (possibly combined with a paramset) may serve as a workaround, and hence this is low priority. ===== transition filter ===== TODO: move to tech:modelgen The LRM description for the transition function is unclear. The situation where a new transition overlaps with another needs clarification. A transition filter holds a waveform, similar to the delay line. It is updated whenever transition is called. In any case, the new waveform must be continuous in all arguments of the new_transition call. Here's how this may be achieved. Suppose, transition is called at time t0, with delay d, new start time ts=t0+d, rise or fall time rf and destination l. If the waveform has a sample at t0, future samples are deleted. Otherwise, the future samples are deleted, excluding the first one after t0, say td1. Now let s0 be the slope at t0. The case s0=0 is simple, otherwise assume s0>0 w.l.o.g. The sample at td1 has a value v1. This gives rise to a slope ps of (ts, v1), (ts+rf, l). Also, let vs be the value at ts. If ps>s0, we drop the sample at td1 and put in (ts,vs). Else if v0