Skip navigation
All Places > Keysight Blogs > EEsof EDA > Blog > 2018 > December
2018

In Part 1 we only translated a simple resistor. Let's look at a more complicated model, a diode with a junction capacitance that varies with voltage. Here is the model in VHDL-A, keywords in bold:

library IEEE, Disciplines;
use Disciplines.electrical_system.all;
use IEEE.math_real.all;
entity diode_cap is
   generic (

      i0: REAL := 0.0; -- amps

      tau: REAL := 0.0; -- seconds

      c0: REAL := 0.0; -- farads

      vj: REAL := 0.0); -- volts
   port (terminal a, k: electrical);
end entity diode_cap;
architecture simple of diode_cap is
   quantity vdiode across idiode, icap through a to k;
   quantity qcap: charge;

      constant vt: REAL := 0.0258; -- thermal voltage at Tj = 300K in volts
begin
   idiode == i0 * (exp(vdiode / vt) - 1.0);
   qcap == tau * idiode - 2.0 * c0 * sqrt(vj**2 - vj * vdiode);
   icap == qcap’dot;
end architecture simple;

 

It is similar in structure to the resistor example in part 1, but there are three new ideas added:

 

First, the implicit parallel connection of the two current branches idiode and icap that you can see in the line:

   quantity vdiode across idiode, icap through a to k;

Second, the usage of the "tick dot" notation to apply the time derivative method onto qcap in the line:

   icap == qcap’dot;

Third, charge is a data type in VHDL-A's electrical nature.

 

My translation to Verilog-A is:

`include "disciplines.vams"

module diode_cap(a, k);
   parameter real i0=0.0; // amps
   parameter real tau=0.0; // seconds
   parameter real c0=0.0; // farads
   parameter real vj=0.0; // volts
   real qcap;
   inout a, k;
   electrical a, k;
   branch (a, k) diode, cap;
   analog begin
      I(diode) <+ i0 * (limexp(V(a,k) / $vt(300)) - 1.0);
      qcap = tau * I(diode) - 2.0 * c0 * sqrt(vj**2 - vj * V(a,k));
      I(cap) <+ ddt(qcap);
   end
endmodule

Notable differences compared to part 1 and to the VHDL version are:

 

First, that branch is an explicit keyword in Verilog-A. Here we declare two named branches diode and cap both of which are between nets a and k, which renders them in a parallel (aka shunt) configuration. You can apply the access functions V() and I() to named branches so V(a,k) is the same as V(cap) and V(diode) in our case. (By the way, to connect branches in series instead of parallel, you would declare an internal node, say electrical i; , and write something like branch (a, i) rs, (i, k) diode; )

 

Second, that the time derivative is a function ddt() not a method. If the capacitance had been a constant c, you could have used I(cap) <+ c * ddt(V(cap));

 

Third, I used a real to represent charge. There is a Charge nature in the standard include file disciplines.vams Verilog-A but it's not useful in this context. The variable qcap is internal to the model, so we omit the parameter keyword, because we don't want to expose it in the model's parameter list. Because qcap is an ordinary variable, we cannot use the <+ contribution operator: we must use the assignment operator = .  For this reason, the order of the statements is important. The qcap assignment must be placed before the contribution statement that uses the result. This is a subtle but important difference between Verilog and VHDL and it is worth your time to ponder it. For a discussion, see page 58 of "The Designer's Guide to Verilog-AMS" by Kundert and Zinke.

 

Note that I chose to highlight one of the built-in system functions $vt() for the thermal voltage rather than defining a constant.  You can use an explicit temperature like $vt(300) or the simulator temperature by writing $vt($temperature). If you omit the parentheses and argument, simply $vt, it is equivalent to $vt($temperature). By the way, the temperature is in kelvin, so be sure not to accidentally set it to zero! Zero kelvin is non-physical obviously, and besides that it can trigger horrible math library exceptions like divide by zero or zero to the power zero.

 

Note also that I chose to use limexp() instead of exp(). limexp is short for exponential with limiting. It limits how fast the return value can change from call to call. It often improves simulator convergence versus its cousin, the more usual exp().

 

In Part 5, I turn to a slightly different topic, namely verification.

IC-CAP comes with a powerful library of transforms and examples to help with model parameter extraction but when implementing custom analysis routines, it is sometimes necessary to use Python with external Python libraries like Numpy or SciPy for manipulating your measured data. Numpy, in particular, has a rich set of numerical processing features.  Why not leverage these functions and libraries?

Sounds good, but you should not blindly use 3rd party libraries without understanding the performance or accuracy of the functions and whether or not they meet your requirements. An example is computing the first order derivative of an I-V curve, a common task in device modeling. There are multiple methods for computing the derivative using finite differences, which can produce different results due to round-off errors or other numerical artifacts. I'm going to demonstrate several methods for computing a derivative and compare the results to show you how to evaluate and implement the best numerical method for your application.

What is a Derivative?


Definition 1:
The mathematical definition of the derivative of a function f(x) at point x is to take a limit as "h" goes to zero of the following expression:

df/dx ~ ( f(x+h) - f(x) ) / h

Approximations of this form are called "finite differences", of which there are many variations.

Definition 2:
A better way of numerically computing the derivative for the same value of "h" is by taking a symmetrical interval around x as follows:

df/dx ~ ( f(x + h/2) - f(x - h/2) ) / h

This particular difference formula is often called a "second-order centered difference" method, while Definition 1 is known as a "first-order forward difference" method. Since these numerical methods are really just approximations, the "second-order" and "first-order" refer to the accuracy.  Second-order means that the error in the approximation decreases with decreasing h proportional to h*h, while first-order decreases only proportional to h.
If we think about "h" as a Dx,  we can see from the figure above that the symmetrical central difference method provides a better estimate of the derivative at point x.
Finite difference method for computing derivatives
(Image courtesy of Wikimedia.  By Kakitc - Own work, CC BY-SA 4.0)

 

 

The python_derivative_v1_1.mdl IC-CAP model file attached at the bottom of this post includes several transforms to compute the derivative:

 

  • Using Python Numpy functions - gradient() and diff()

  • Using the PEL (Programming Extraction Language) derivative function explored in a transform named Test_PEL

 

 

I will illustrate the use of IC-CAP's built-in derivative functions in PEL as follows.


########################################################################################
#  Test_PEL 
#  Computes the derivative of I-V data using PEL derivative2 function
########################################################################################
complex x[34] x[0]=0 x[1]=0.05 x[2]=0.1 x[3]=0.15 x[4]=0.2...x[33]=1.65
complex y[34] y[0]=8.33E-12 y[1]=3.607E-11 y[2]=1.577E-10 y[3]=6.9396E-10 y[4]=3.0463E-9...y[33]=1.7874E-5

der = derivative2(x,y,1,34)

print der

complex gm.M.11[size(der)]

i=0
while i < 34
  gm.M.11[i] = der[i]
  i = i + 1
end while
return gm

 

I wanted to implement the same derivative calculation using the IC-CAP Python environment. I've developed a new library for implementing some numerical methods including two different derivative functions that can be called from our transforms. This ic_math.py file is included as an attachment to this article. Download the file and copy it to your ICCAP_USER_PYTHON_PATH (typically "C:\Users\<username>\iccap\python").

 

In my first attempt, I used the diff function from the Numpy library as shown in the function icm_derivative_npdiff code listing from ic_math.py:

 

import numpy as np

def icm_derivative_npdiff(x,y):
   # create numpy array and initialize
   dydx = np.zeros([len(x),])
   # create numpy arrays
   dx = np.zeros([len(x),1])
   dy = np.zeros([len(y),1])

   # flatten and copy arrays
   dx.flat[:] = x
   dy.flat[:] = y
   # compute diff function for x and y values
   # uses first order forward differencing method
   ddx = np.diff(x)
   ddy = np.diff(y)
   # compute the derivative
   dydx = ddy/ddx

   return dydx

 

Now that we have the function defined in our library we can import it from our Test_npdiff transform and call the function to compute the derivative of our sample I-V data set.

 

 

#############################################################################################

#  Test_npdiff.py 
#  Computes the derivative of I-V data using Numpy Diff function
#############################################################################################
from iccap import set_return_array
import numpy as np
import ic_math as m

# Using I-V data Id vs. Vg
x = np.array([0, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.275, 0.3, 0.325, 0.35, 0.375, 0.4, 0.425, 0.45, 0.475, 0.5])

 

y = np.array([1E-14, 5.1E-14, 1.23E-13, 2.52E-13, 4.58E-13, 8.73E-13, 1.612E-12, 2.932E-12, 5.418E-12, 1.0036E-11, 1.8614E-11, 3.4948E-11, 6.5647E-11, 1.23711E-10, 2.33938E-10, 4.42538E-10, 8.39282E-10, 1.59355E-9, 3.04727E-9, 5.797E-9])

 

# forward difference approximation

dydx = m.icm_derivative_npdiff(x,y)

if debug: print dydx

 

gm = [ dydx[i] for i in range(0,len(x)-1) ]

if debug: print "initial gm = {}".format(gm)

 

# insert dydx starting element to gm at index = 0

# dydx array has 1 less value due to diff function return

gm.insert(0,1E-10)

if debug: print "gm = {}".format(gm)

 

set_return_array("M", gm)

 

In my experience, this is usually adequate and the error is negligible when performing extractions of threshold voltage Vth from the Id vs. Vg characteristics of a MOSFET transistor.  During this calculation, we compute the derivative of Id with respect to Vg and then find the maximum of the derivative (i.e. Gm_max).  At that interpolated Vg, we define a linear function whose slope is equal to Gm_max to superimpose on the IdVg curves. This is illustrated in Ma Long's blog post entitled "Device Modeling 101 - How to Extract Threshold Voltage of MOSFETs." The error in performing these steps is usually greater than any round-off error that may have occurred in computing the derivative. In other words, the simple forward difference approximation for the derivative is usually close enough for this application. 

 

After running the Test_npdiff transform and comparing the results to the built-in IC-CAP PEL function, I noticed that there was a discrepancy in the computed derivatives, especially in the first points of the I-V curve. The numpy.diff() function was slightly under-estimating the derivative even-though the overall slope of the line was pretty close. The maximum error was at the lower boundary of the curve. Furthermore, the vector returned was one point less in length.  When plotting the derivative result against the original I-V data, I had to pad the first point to realign the data points.

 

iccap_derivative_comparison_plot

 

Was there an error in my math?  Looking at the PEL implementation of the derivative, I noticed that it was using a central difference approximation method which is often more accurate. The simple algorithm shown above using numpy.diff() uses the forward difference approximation method, which can be subject to round-off errors when the step size is small. 

 

After some more investigation into the Numpy library functions, I discovered a function that will produce the same result of the Test_PEL transform - the numpy.gradient() function. This function uses the second order accurate central difference method for most of the vector. Furthermore, the vector length is preserved, by gracefully handling the lower and upper boundaries. To evaluate this method, I created an additional Python function in the ic_math library called icm_derivative_npgrad to implement the gradient method as shown below in the code listing:

 

def icm_derivative_npgrad( x, y, e1, e2 ):
   # create numpy array and initialize
   dydx = np.zeros([len(x),])
   # create numpy arrays
   dx = np.zeros([len(x),1])
   dy = np.zeros([len(y),1])

   # flatten and copy arrays
   dx.flat[:] = x
   dy.flat[:] = y

   # compute gradient function for x and y values
   # uses second order central differencing method
   ddx = np.gradient(x, edge_order=e1)
   ddy = np.gradient(y, edge_order=e2)

   # compute the derivative
   dydx = ddy/ddx
   return dydx

 

 

Now we can call the new icm_derivative_npgrad function from our transform named Test_npgrad which will uses the gradient function.

 

#############################################################################################
#  Test_npgrad.py 
#  Computes the derivative of I-V data using Numpy Gradient function
#############################################################################################
from iccap import set_return_array
import numpy as np
import ic_math as m

 

# using I-V data Id vs. Vg
x = np.array([0, 0.025, 0.05, 0.075, 0.1, 0.125, 0.15, 0.175, 0.2, 0.225, 0.25, 0.275, 0.3, 0.325, 0.35, 0.375, 0.4, 0.425, 0.45, 0.475, 0.5])

 

y = np.array([1E-14, 5.1E-14, 1.23E-13, 2.52E-13, 4.58E-13, 8.73E-13, 1.612E-12, 2.932E-12, 5.418E-12, 1.0036E-11, 1.8614E-11, 3.4948E-11, 6.5647E-11, 1.23711E-10, 2.33938E-10, 4.42538E-10, 8.39282E-10, 1.59355E-9, 3.04727E-9, 5.797E-9])

 

# forward difference approximation

dydx = m.icm_derivative_npgrad(x,y,1,2)

if debug: print dydx

 

gm = [ dydx[i] for i in range(0,len(x)-1) ]

if debug: print "initial gm = {}".format(gm)

 

set_return_array("M", gm)

 

You may notice that I've used a new function set_return_array() added to the IC-CAP 2018 Python API which returns a list from a temporary variable to the Transform dataset. This allows this data to be accessible for plotting and is analogous to the RETURN statement in PEL. 

 

As you can see from the table below, the icm_derivative_npgrad function yields exactly the same results as the PEL derivative2 function.  Meanwhile, the numpy.diff function underestimates the derivative.  I padded the numpy.diff() generated gm data so the first point is non-zero and of the same magnitude of the other data to allow easily plotting the results above.  See that the blue and green traces in the graph above are right on top of each other.

 

 

 

iccap derivative comparison table

 

Conclusion

There have been some requests on the SciPy forums to improve the derivative function in the SciPy math libraries to include more accurate methods for calculating the derivative.  But it is up to the open-source community to prioritize the development and release this feature. For now you can still use functions like numpy.gradient() to perform the same derivative computation as the built-in libraries in IC-CAP.  This kind of trade-off between accuracy is quite common with numerical methods for computing derivatives. There are some other 3rd Party libraries that support other methods for numerical differentiation but often require more compute time.  

In my previous post, I showed you the "all-in-one" method for adding your Verilog-A models to ADS. In this post, I'll show you another method called "shared library." It's a bit more work to set up, but it saves time in the long run because it avoids duplication of effort in each new project. If you give a project workspace to a colleague you have to remember to give the referenced library also. In fact, this shared library method is just good not only for Verilog-A components but also for component models of other types (e.g. netlist-based models/model cards). It's a good method anytime you have component models that are common to several projects. Like last time, there are quite a few steps, but each one is really easy.

Shared Library

  1. Follow steps 1 through 20 of Part 2, except this time, in step 2, name the workspace something like shared_wrk. Alternatively, make a copy of all_in1_wrk, rename it shared_wrk, and then delete the testbench cell.
  2. In the Main window, select the Library View tab. When you created the workspace, ADS automatically created a default library under it. The default name is created by replacing the _wrk suffix with a _lib suffix. So in our case it is called shared_lib. Right click on the library and select Configure Library... from the popup context menu.
  3. In the Library Configuration Editor, Simulation tab, click on the Browse... button to the right of the Verilog-A directory edit box. Browse up one level and select the veriloga folder. Alternatively, you can just type ..\veriloga into the edit box. The relative path-name is preferred over an absolute path because if you give the library to a colleague, they might install it with different root name. Dismiss the Library Configuration Editor by clicking on the OK button. It reminds you that changes will take effect next time you open it. Click OK.
  4. From the main window menu bar, select File-->Close Workspace. We are done with the shared library for now. Let's imagine we are starting the first of several projects that will refer to the shared library. 
  5. From the ADS Main Window menu bar, select File-->New-->Workspace...
  6. From the New Workspace dialog box, give it a name like project1_wrk and then click the Create Workspace button.
  7. From the Main window menu bar, select File-->Manage Libraries...
  8. From the Manage Libraries dialog box, click on the "Add Library Definition File..." button.
  9. From the Select Library Definition File browser, navigate to the previous workspace (i.e. your shared_wrk folder) and locate and select a file called lib.defs then click the Open button.
  10. Dismiss the Manage Libraries dialog box by clicking on the Close button.
  11. In the Main window Folder View tab, click on the little arrow by the resistor cell folder. You should see the symbol view of the cell.
  12. From the Main window menu bar, select File-->New-->Schematic... and name its cell something like testbench.
  13. Create Arrange the Main window and the new testbench schematic window side-by-side.
  14. Click and drag the resistor symbol from the Main window to the testbench schematic window. Instantiate your resistor by clicking anywhere on the schematic canvas. Set it to 2 ohms.
  15. Create a simple testbench like this:
    testbench
  16. Click on the Simulate icon or select Simulate-->Simulate or just hit the F7 shortcut key. The first time you simulate, there is a short pause while ADS compiles the Verilog code. It saves the compiled model, so if you don't touch the code, there is no need to re-compile the next time you run it.
  17. From the Data Display window, insert a plot, and select the Vout and branch current (_ub_p_n_i) traces:
    voltage and current through 2 ohm resistor
    I added some markers and played around with the axis and trace settings.

Success! Now you create project_2, project_3 and so on. Repeat steps 5-17 for each new project to refer to your shared library thus reusing it.

 

In the next posting, we'll return to the main topic: how to translate the analog parts of VHDL-AMS to Verilog-A. Part 1 was a resistor. Part 4 is a comparison of how the two languages handle time derivatives and internal branches.

In my previous post, showed you how to create the Verilog-A code for a component model. In this post I'll show you how to import the code into ADS. If you don't have access to ADS or you're not familiar with it, I suggest you read our Quick Start Guide first.

 

There are two strategies for adding a model to an ADS workspace, let's call them "all-in-one" and "shared library" methods. "All-in-one" is the simplest and as the same suggests it is self-contained. "Shared library" is a bit more work to set up, but it saves time in the long run because you can reference the same library from each new project, and avoid having to recreate it every time.

I cover the all-in-one method in this post and the shared library in the next.

(Side note: There is also a further Verilog-A flow in the W2319 ADS RFIC Interoperability Element, but I won't be covering it in this series. If you are interested, the doc page is here.) 

All-in-one method

  1. Launch ADS. From the ADS Main Window menu bar, select File-->New-->Workspace
  2. In the "New Workspace" dialog box, give your new workspace a name such as all_in1_wrk 
  3. Click on the "Create Workspace" button.
  4. In the Main window "Folder view" tab, you will see a workspace folder whose name ends in ...\all_in1_wrk. Right click on it and select the last option in the pop up context menu, namely "Explore In File System".
  5. Assuming Windows OS, File Explorer opens in the workspace's folder on your file system. Right click on a blank space in the right panel and select New-->Folder. It is best to name the folder veriloga because then it will be on the default search path that the Verilog-A compiler looks in.
  6. Open your new folder and, using copy-paste and your favorite plain text editor, create the file containing the Verilog-A source code from Part 1. Name the file resistor.va. (The first part can be anything convenient, but the file extension must be either *.va or *.vams for the compiler to recognize it.) You can close your text editor and Windows Explorer if you like.  
  7. Go back to the ADS Main window and from its menu bar, select File-->New-->Symbol view...
  8. In the "New Symbol" dialog box, overwrite the default cell name (cell_1) with  resistor. Note that you are creating not only a view but also a cell. A cell is a container that holds one or more views of a component or a circuit. You never view or edit the cell directly but instead you view and edit it via one of its views. It is important that the cell name, resistor, matches the module name, resistor, in the Verilog code.
  9. In this "New Symbol" dialog box, click the "Create Symbol" button. Two new windows open: The Symbol Generator dialog box and the Symbol canvas itself.
  10. In the "Symbol Generator" dialog box, make sure you are on the Copy/Modify tab, then select "Lumped-Components" from the "Symbol category" drop down list. (Side note: if you want to use the Auto-Generate tab instead, you'll have to add an extra step to specify the number of pins. See Note 1 at the bottom of the this post.) Click on the resistor icon. The "Symbol name" edit box will populate as ads_rflib:R . Click OK to dismiss the "Symbol Generator" dialog box. By the way, you can draw your own symbol if there is no suitable one already: Search for the "Draw a Custom Symbol" topic in the doc.
  11. In the Symbol drawing canvas you can see the symbol as been automatically created, with pins 1 and 2 with default names P1 and P2. Double click on each pin in turn and change the name to match those listed in the Verilog code module resistor(p, n); i.e. set the name of pin 1 and 2, to p and n, respectively.
  12. From the Symbol window menu bar select File-->Design Parameters.
  13. In the "Design Parameters" dialog box's "General Cell Definition" tab, set the "Component Instance Name" to whatever prefix you want instances to have, R for example. Instances will be given default names R1, R2, R3, etc.
  14. Important! Uncheck the "Subcircuit" check box. (If you are curious, the difference between a subcircuit and a leaf node is that you can push into the hierarchy of an instance of a subcircuit, but not into a leaf node. Try it! Right click on any instance of a component in a schematic and select "Push Into Hierarchy" from its pop up context menu. If the instance is a subcircuit, the subcircuit will open. If not, you get a "Cannot push into this instance" error. This check box tells the netlister which type (subcircuit or leaf node) it is dealing with, so it can traverse the hierarchy correctly.)
  15. In the Simulation frame, select "Subnetwork" from the Model drop down list. (Wait a minute Colin, I thought you told me in the last step that what we are building is not a subcircuit? Well, it is isn't. It isn't a subcircuit, but it is a subnetwork. Yikes! A subnetwork is in contrast to a built-in component. The third option "Model" is for non-simulatable items like text annotations.) 
  16. Select the "Cell Parameters" tab of the "Design Parameters" dialog box. Remember the line parameter real r=0.0; in the Verilog code? Here is where you connect to it. In the Edit Parameter: Parameter Name edit box type r, leave the Value Type as "Real", set Parameter Type/Unit to Resistance, and type something helpful like "Resistance in ohms" for the parameter description. It is best to leave the Default Value blank. You can put a value in there, but it will override the default value in the Verilog-A file, which might be confusing if the two values get out of sync at some point. 
  17. Look to the left side of the same tab, and click on the "Add" button. The r parameter will be added to the parameter list box.
  18. Click OK to dismiss the "Design Parameters" dialog box.
  19. Go back to the "Symbol" window menu bar, and select File-->Save. We are done with this window, so you can close it if you like.
  20. Go back to the ADS Main Window, Folder View tab. Click on the little arrow to open your resistor cell folder. You can see that your cell now has one view: symbol.
  21. The next few steps prepares a testbench to instantiate it on. From the ADS Main Window menu bar, create a schematic view of a new cell by selecting "File-->New--Schematic...". This will be our top-level cell. Call this new cell "testbench."
  22. Arrange the Main window and the new testbench schematic window side-by-side.
  23. Click, hold, and drag your resistor symbol from the Main window's Folder view tab across your screen to the testbench schematic window. Instantiate your resistor by clicking anywhere on the testbench schematic canvas. Hit the ESC key to leave instantiation mode. Set the resistance to 2.0 ohms.
  24. Create a simple testbench like this:
    testbench
  25. Click on the Simulate icon or select Simulate-->Simulate or just hit the F7 shortcut key. The first time you simulate, there is a short pause while ADS compiles the Verilog code. It saves the compiled model, so if you don't touch the code, there is no need to re-compile the next time you run it.
  26. From the Data Display window, insert a plot, and select the Vout and branch current (_ub_p_n_i) traces:
    voltage and current through 2 ohm resistor

Success!

The next posting, shows you the shared library method. A lot of the steps are the same, except we create separate workspaces for our component library and our test bench, and link the two.

 

Note 1: In Step 10 above, if you want to use the Auto-Generate tab instead of the Copy/Modify tab, you'll have to add an extra step to specify the number of pins. Create a schematic view in the cell you are working on, then place one pin for every port in the Verilog-A function argument list e.g. 2 pins for module resistor(p, n); . It looks odd to have a schematic with nothin but unconnected pins, but trust me it works! Then save the schematic and go back the symbol view Auto-Generate tab.

Verilog-AMS and VHDL-AMS are hardware description languages, capable of describing mixed-signal (i.e. analog and digital) hardware. In this series of postings, we’ll be talking about Verilog-A (i.e. the officially defined subset of Verilog-AMS that supports analog) and “VHDL-A” (not officially defined, but defined here as "the parts of VHDL-AMS that supports analog").

 

ADS support Verilog-A but not VHDL-A so this series of posting will explain how to create a Verilog-A model if all you have to start from is a VHDL-A model. It isn’t a comprehensive Verilog or VHDL reference manual: you can find plenty of those by searching on the Internet. The idea here is to get you started. You’ll have to refer to those other resources for the in-depth information. Another good resource is Best Practices for Compact Modeling in Verilog-A, J. Electron Devices Soc. (Aug. 2015) by Colin C. McAndrew et al. It's not about how to code models in Verilog-A, it's about how to code them well. 

 

Let jump right in and compare how the two languages express the simplest possible analog component, the resistor:

 

Code fragment 1a: Resistor in VHDL-A

use electrical_system.all;
entity resistor is
   generic (r: real := 0.0);
   port(terminal p, n: electrical);
end entity resistor;
architecture signal_flow of resistor is
   quantity vr across ir through p to n;
begin
   vr == ir * r;
end architecture signal_flow;

 

Code fragment 1b: Resistor in Verilog-A

`include "disciplines.vams"

module resistor(p, n);
     parameter real r=0.0;
     inout p, n;
     electrical p, n;
     analog begin
           V(p,n) <+ I(p,n)  * r;
     end
endmodule

Keywords are in bold. The other identifiers are defined in the code itself, or in the included libraries. For example in both langauges, the identifier electrical is not a keyword: it is defined in a standard library. VHDL-A has:

use electrical_system.all;

…and Verilog-A has:

`include "disciplines.vams"

We won’t go into the contents of these yet. Suffice it to say that they serve a similar purpose, namely to save you from writing the basic “plumbing” required to set up your model.

 

The two languages diverge in the next lines.

 

VHDL-A divides the code up into entity and architecture sections. The entity keyword is used to introduce the description of the interface of the component. Here we define ports p and n and a parameter, r. Then, the architecture keyword is used to introduce the description of its internal details. An entity can have more than one architecture ("polymorphism"), so we have to give each one a name, signal_flow in this case, so we can specify which morphology we want to use later on. To implement an architecture with ohmic behavior, we first state explicitly that the voltage V and current I are the across and through variables, respectively, of our ports with:

quantity vr across ir through p to n

 

(Side note: The terminology “through and across variables” comes from the mathematics of the class of problem that all SPICE-like solvers solve, namely differential algebraic equations or DAEs. In Verilog-A, through and across variables like voltage and currents are called "natures" and a pair of related through and across variables is called a "discipline." Voltage and current natures, plus some other information like tolerances, form the electrical discipline. Confusingly, VHDL-A uses the term "nature" for the pair of natures, whereas Verilog-A uses "discipline.")

 

Then we specify Ohm’s law, V=IR, for the branch constitutive equation:

 

 vr == ir * r

The "double equal signs" operator implies the simulator should force the equation to be true for every time step in the simulation: it isn’t a one-time assignment like the a = b in C and Java nor the a := b in Pascal and ADA. (And it isn't at all like the equality test A == B in C and Java.)

 

In contrast, Verilog-A combines the entity/architecture ideas. The module keyword is used to introduce both interface (the parameter, inout, and electrical lines) and the internals (the block that is bounded by analog begin end ).

The line:

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

...is called a contribution statement. It is similar to the one in VHDL with double equals == except that the operator in Verilog-A is <+ . Why <+ ? It's because Verilog-A allows multiple contributions to add to the voltage V(p,n). For example,

V(p,n) <+ V(p2,n2);

V(p,n) <+ V(p3,n3);

…is equivalent to:

V(p,n) <+ V(p2,n2)+ V(p3,n3);

 

Similar to == in VHDL, <+ in Verilog-A implies the simulator should do whatever it takes (iteration!) to force the equation to be true for every time step in the simulation.

 

In Verilog-A, there is no need for a line like quantity vr across ir through p to n that we had in VHDL because the so-called access functions, V() and I(), are defined in the `included disciplines.vams header file and thereby associated with any instances of the electrical discipline.

 

So, that's a component. Part 2 shows you how include the Verilog-A version into ADS create a testbench that instantiates the component, adds some stimulus, and shows you the response.