{"id":14,"date":"2017-06-16T00:05:53","date_gmt":"2017-06-16T00:05:53","guid":{"rendered":"http:\/\/www.tazabekov.com\/blog\/?p=14"},"modified":"2017-07-12T05:04:35","modified_gmt":"2017-07-12T05:04:35","slug":"digital-to-analog-converter-modelling-with-pyspice","status":"publish","type":"post","link":"http:\/\/www.tazabekov.com\/blog\/2017\/06\/digital-to-analog-converter-modelling-with-pyspice\/","title":{"rendered":"Digital to Analog Converter Modelling with PySpice"},"content":{"rendered":"<p><em><strong>Author:<\/strong> Olzhas S. Tazabekov (<a href=\"http:\/\/www.linkedin.com\/in\/olzhastazabekov\">LinkedIn<\/a><\/em><em style=\"font-size: 1rem;\">)<\/em><\/p>\n<p><em><strong>Prerequisites:<\/strong> basic programming skills with Python and SPICE<\/em><\/p>\n<h1>Intro: Why is All the Buzz<\/h1>\n<p style=\"text-align: left;\">Throughout our University degree programs, we all learned how to analyze, design and verify electrical circuits. Unfortunately or fortunately, most of us were trained using industry level graded software (e.g. Cadence, Synopsys, etc.). However, in real life (outside of school) that software does come with a pretty heavy price associated with the cost of licenses and IT infrastructure. So, unless you are heavy weight semiconductor giant with millions in corporate accounts it is a quite a challenge to afford. Where does this leave all those half starving tech startups or ordinary people? Open source software has been one of the answers so far (another one is to sell a kidney of one of the board members and purchase an IC design tool license \ud83d\ude09 ).<!--more--><\/p>\n<h1>PySpice<\/h1>\n<p>A while ago I stumbled across PySpice, a library for python 3, that interplays with a SPICE simulator (ngspice to be precise). Even though it lacks a graphical user interface and schematic capture editor, hence, one can\u2019t simply go drag-n-dropping elements or click-n-plotting waveforms the old school way, it does come with a support of powerful high-level object-oriented Python 3 which is perfectly suited for working with circuits and scientific framework like Numpy and Matplotlib which is great for data processing, analysis and visualization.<\/p>\n<p>All the documentation and installation guidelines can be found here:<\/p>\n<p><a href=\"https:\/\/pypi.python.org\/pypi\/PySpice\/0.3.3\">https:\/\/pypi.python.org\/pypi\/PySpice\/0.3.3<\/a><\/p>\n<p>For those who are new with SPICE or Python, there is a pretty well written set of examples explaining how to get started with simple circuits and basic analysis and data processing.<\/p>\n<p><a href=\"https:\/\/pyspice.fabrice-salvaire.fr\/examples\/index.html\">https:\/\/pyspice.fabrice-salvaire.fr\/examples\/index.html<\/a><\/p>\n<h1>Ideal DAC<\/h1>\n<p>This section explains how to develop a model for ideal Digital-to-Analog Converter which could be later plugged into a more complex mixed signal model. \u00a0Our goal is to make use of convenience of python object oriented programming and power of data processing framework.<\/p>\n<p>There are an infinite number of ways to implement a model of an ideal DAC. \u00a0Before we move on to the code let\u2019s review some DAC basics.<\/p>\n<p>Along with input code, supply\/ground and output pins the most simple DAC block usually have two reference voltages, Vref+ and Vref- assuming Vref+&gt;Vref-. When a digital input is at the smallest possible number, the DAC output voltage becomes Vref-. When the input code number is increased by one, the output of the DAC (an analog voltage defined at discrete amplitude levels) increases by least significant bit (LSB). Therefore, we have 1LSB = (Vref+ &#8211; Vref-)\/2^N, where N is a number of bits for the input code. We will be assuming that the DAC output ranges from Vref- up to\u00a0 Vref+ &#8211; 1LSB. We could just as easily have assumed the output ranging from Vref- + 1LSB up to Vref+. The important thing is to notice that the DAC output range is 1LSB smaller than Vref+ &#8211; Vref-. If one needs more resolution the one can simply increase the number of bits, N, used and hence decrease the value of the DAC\u2019s LSB.<\/p>\n<p>One can write the DAC output in terms of reference voltages and digital codes bn (i.e. logic 0 and 1). Assuming, bn = 0 for all the codes Vout is equal to Vref-. Then,<\/p>\n<p class=\"ql-center-displayed-equation\" style=\"line-height: 22px;\"><span class=\"ql-right-eqno\"> &nbsp; <\/span><span class=\"ql-left-eqno\"> &nbsp; <\/span><img loading=\"lazy\" src=\"http:\/\/www.tazabekov.com\/blog\/wp-content\/ql-cache\/quicklatex.com-3a7175ba9dee43ff2d888c0ac742d3c2_l3.png\" height=\"22\" width=\"551\" class=\"ql-img-displayed-equation quicklatex-auto-format\" alt=\"&#92;&#091; &#86;&#95;&#123;&#111;&#117;&#116;&#125;&#32;&#61;&#49;&#47;&#50;&#94;&#78;&#32;&#40;&#86;&#95;&#123;&#114;&#101;&#102;&#43;&#125;&#45;&#86;&#95;&#123;&#114;&#101;&#102;&#45;&#125;&#41;&#40;&#98;&#95;&#123;&#78;&#45;&#49;&#125;&#50;&#94;&#123;&#78;&#45;&#49;&#125;&#43;&#98;&#95;&#123;&#78;&#45;&#50;&#125;&#50;&#94;&#123;&#78;&#45;&#50;&#125;&#43;&#46;&#46;&#46;&#43;&#98;&#95;&#48;&#41;&#43;&#86;&#95;&#123;&#114;&#101;&#102;&#45;&#125;&#41; &#92;&#093;\" title=\"Rendered by QuickLaTeX.com\"\/><\/p>\n<p>This can be implemented using nonlinear dependent source.<\/p>\n<pre class=\"theme:github wrap:true lang:python decode:true\">self.voltage_expression = ''.join(['(v(refp)-v(refn))\/'+str(2**self.nbits)+'*(']+[('v(b'+str(i)+'l)*'+str(2**i)+'+') for i in range(self.nbits)]+['0)+v(refn)'])\r\nself.BehavioralSource('out', 'out', 'vss', voltage_expression=self.voltage_excolpression)\r\n<\/pre>\n<p>voltage_expression is a string, which defines the output voltage value depending on the number of bits and reference voltages. Then this string gets passed as a parameter to a BehavioralSource.<\/p>\n<h1>PySpice Modelling Approach<\/h1>\n<p>As was mentioned earlier, Python is a powerful high level language that gives you a great level of freedom and flexibility implementing things in object oriented way. SPICE most commonly deals with circuits, subcircuits and elements.<\/p>\n<p>Previous section described what is needed to implement an ideal DAC (e.g. trip voltage generating resistive divider, Bit logic elements and behavioral source). In the following implementation the bit logic elements with the resistive divider will be a separate subcircuit inside a DAC subcircuit. The top level circuit is usually a test bench used for characterization.<\/p>\n<p>Below is a diagram of how to structure your test benches. Now, most IC design engineers are familiar with SPICE and concept of netlist, circuits, subcircuits, etc. However, things can get confusing when one tries to transition from SPICE paradigm into a Object-Oriented Programming paradigm.<\/p>\n<p><a href=\"https:\/\/flic.kr\/p\/UAryTZ\"><img loading=\"lazy\" src=\"https:\/\/farm5.staticflickr.com\/4264\/34520301263_62ec03d969.jpg\" alt=\"spice_to_python\" width=\"500\" height=\"217\" \/><\/a><\/p>\n<p><em>Fig. 1. Object-Oriented Mapping from SPICE circuits (Left) to Python classes (Right)<\/em><\/p>\n<p>PySpice has \u00a0Spice.Netlist namespace with Circuit and SubCircuitFactory generic classes, which can be used to create classes for your own circuits and subcircuits. Let&#8217;s move on to an example. The following code creates a class of BitLogic element, which generates a solid logic level, i.e. an input logic code would assumed to be a valid logic &#8220;1&#8221; if its amplitude is greater than VDD\/2 and a logic &#8220;0&#8221; if its amplitude is less than VDD\/2.<\/p>\n<pre class=\"wrap:true lang:python decode:true\">'''\r\nBitLogic Subcircuit Schematic\r\n\r\n    vdd\r\n     |\r\nbx--\\----trip\r\n     |\r\n     |---bxl\r\n     |\r\nbx----\/--trip\r\n     |\r\n    vss\r\n\r\nTrip V generation Schematic\r\n\r\n    vdd\r\n     |\r\n   |res|\r\n     |---trip\r\n   |res|\r\n     |\r\n    vss\r\n\r\n'''\r\nclass BitLogic(SubCircuitFactory):\r\n    __name__='BITLOGIC' # subcircuit name\r\n    __nodes__=('bx', 'bxl', 'vdd', 'vss') # subcircuit nodes set \r\n    def __init__(self):\r\n        super().__init__() # initialize a parent class - SubCircuitFactory\r\n        # add model parameters for the ideal switches to the BitLogic Subcircuit\r\n        self.swmod_params = {'ron':0.1, 'roff':1E6}\r\n        self.model('swmod','sw', **self.swmod_params)\r\n        # add a resistive voltage divider to generate the threshold voltage value for switching\r\n        self.R('top', 'vdd', 'trip', 100E6)\r\n        self.R('bot', 'trip', 'vss', 100E6)\r\n        # add swicthes and specify their model parameters\r\n        self.VCS('top', 'bx', 'trip', 'vdd', 'bxl', model='swmod')\r\n        self.VCS('bot', 'trip', 'bx', 'bxl', 'vss', model='swmod')\r\n<\/pre>\n<p>The netlist will be generated by PySpice when one makes an instance of this Class. The cool thing about this approach is that one can configure the objects (hence, subcircuit netlists) by any means available through the Class methods. On top of that, complex systems can be implemented via incorporating basic \u00a0subcircuit Class instances into other bigger instances. A DAC subsircuit Class would be a good example of that.<\/p>\n<pre class=\"wrap:true lang:python decode:true \">'''\r\nIdeal DAC SubCircuit Shematic\r\n     refp vdd\r\n      |    |\r\n     --------------------------------------\r\nb0--| |bitlogic0|--bxl0--|              |  |\r\nb1--| |bitlogic1|--bxl1--|  behavioral  |--|--out\r\n... | ...                |voltage source|  |\r\nb5--| |bitlogic5|--bxl5--|              |  |\r\n     --------------------------------------\r\n      |    |\r\n     refn vss\r\n'''\r\nclass IdealDac(SubCircuitFactory):\r\n    __name__ = 'IDEALDAC'\r\n    def __init__(self, nbits, **kwargs):\r\n        # number of bits passed as a parameter\r\n        self.nbits = nbits\r\n        # nodes definition based on nbits parameter\r\n        self.__nodes__ = ('refp', 'refn', 'vdd', 'vss') + tuple([('b'+str(i)) for i in range(self.nbits)]) + ('out',)\r\n        super().__init__()\r\n        # add an nbit number of BitLogic subcircuits \r\n        for i in range(0, self.nbits):\r\n            bitstr = str(i)\r\n            self.X('BL'+bitstr, 'BITLOGIC', 'b'+bitstr, 'b'+bitstr+'l', 'vdd', 'vss')\r\n        # make an output voltage expression based on nbits parameter and pass this string into added BehavioralSource \r\n        self.voltage_expression = ''.join(['(v(refp)-v(refn))\/'+str(2**self.nbits)+'*(']+[('v(b'+str(i)+'l)*'+str(2**i)+'+') for i in range(self.nbits)]+['0)+v(refn)'])\r\n        self.BehavioralSource('out', 'out', 'vss', voltage_expression=self.voltage_expression)\r\n<\/pre>\n<p>Now, all that&#8217;s left is to make the Test Bench instance \u00a0of class Circuit, connect instances of the subcircuit classes, specify analysis one needs to run, extract and post process the data.<\/p>\n<p>The following example runs an operating point analysis for 4 bit DAC and prints voltages for all the nodes in the testbench.<\/p>\n<pre class=\"wrap:true lang:python decode:true \">if __name__ == \"__main__\":\r\n    dac_nbits=4 # specify a number of bits for the DAC\r\n    # operating point analysis\r\n    circuit_op = Circuit('DAC_TestBench') # add an instance of class Circuit to be your testbench\r\n    circuit_op.subcircuit(BitLogic()) # add an instance of BitLogic Subcircuit\r\n    circuit_op.subcircuit(IdealDac(nbits=dac_nbits)) # add an instance of DAC Subcircuit\r\n    circuit_op.X('DAC1', 'IDEALDAC', 'vrefp', 'vrefn', 'vdd', 'vss', ','.join([('b'+str(i)) for i in range(dac_nbits)]), 'out') # connect the DAC subcircuit into the testbench\r\n    circuit_op.V('vdd', 'vdd', circuit_op.gnd, 1) # connect VDD voltage source to vdd node of the DAC subcircuit\r\n    circuit_op.V('vss', 'vss', circuit_op.gnd, 0) # connect VSS voltage source to the ground node of the DAC subcircuit\r\n    circuit_op.V('refp', 'vrefp', circuit_op.gnd, 1) # connect positive reference voltage source to the vrefp node of the DAC subcircuit\r\n    circuit_op.V('refn', 'vrefn', circuit_op.gnd, 0) # connect negative reference voltage source to the vrefn node of the DAC subcircuit\r\n    for i in range(dac_nbits):\r\n        istr=str(i)\r\n        circuit_op.V('b'+istr, 'b'+istr, circuit_op.gnd, 1) # connect nbit number of voltage sources to provide the input code (all '1's in this case)\r\n    simulator_op = circuit_op.simulator(temperature=25, nominal_temperature=25) # add simulator conditions (e.g. temperature)\r\n    analysis_op = simulator_op.operating_point() # specify the analysis to be an operating point analysis\r\n    for node in analysis_op.nodes.values():\r\n        print('Node {}:{:5.2f} V'.format(str(node),float(node))) # print voltage values of all the nodes of the tesbench\r\n<\/pre>\n<p>The results confirm that an input code of &#8216;1111&#8217; gives an output voltage of 0.94V. One may configure an input code differently and get the desired output voltage.<\/p>\n<pre class=\"wrap:true lang:default decode:true\">Node vdd [voltage]: 1.00 V\r\nNode xdac1.xbl0.trip [voltage]: 0.50 V\r\nNode vss [voltage]: 0.00 V\r\nNode xdac1.b0l [voltage]: 1.00 V\r\nNode b0 [voltage]: 1.00 V\r\nNode xdac1.xbl1.trip [voltage]: 0.50 V\r\nNode xdac1.b1l [voltage]: 1.00 V\r\nNode b1 [voltage]: 1.00 V\r\nNode xdac1.xbl2.trip [voltage]: 0.50 V\r\nNode xdac1.b2l [voltage]: 1.00 V\r\nNode b2 [voltage]: 1.00 V\r\nNode xdac1.xbl3.trip [voltage]: 0.50 V\r\nNode xdac1.b3l [voltage]: 1.00 V\r\nNode b3 [voltage]: 1.00 V\r\nNode out [voltage]: 0.94 V\r\nNode vrefp [voltage]: 1.00 V\r\nNode vrefn [voltage]: 0.00 V\r\n<\/pre>\n<p>However, one might as well also make use of flexibility provided by Python and print output voltages for all the corresponding input codes. On top of that, there are numerous tools available in Python for data post processing and graphic visualization.<\/p>\n<p>So, the next example shows how to run a series of operating point analysis to calculate the DAC output voltage for each input code and plot its transfer function.<\/p>\n<pre class=\"wrap:true lang:python decode:true \">    # DAC transfer function - outut voltage vs input code test bench based on operating point analysis\r\n    input_codes = np.arange(2**dac_nbits)\r\n    bin_input_codes = np.zeros(2**dac_nbits)\r\n    outputs = np.zeros(2**dac_nbits)\r\n    for number in input_codes:\r\n        circuit_tf = Circuit('DAC Transfer Function')\r\n        circuit_tf.subcircuit(BitLogic())\r\n        circuit_tf.subcircuit(IdealDac(nbits=dac_nbits))\r\n        circuit_tf.X('DAC1', 'IDEALDAC', 'vrefp', 'vrefn', 'vdd', 'vss', ', '.join([('b'+str(i)) for i in range(dac_nbits)]), 'out')\r\n        print(', '.join([('b'+str(i)) for i in range(dac_nbits)]))\r\n        circuit_tf.V('vdd', 'vdd', circuit_tf.gnd, 1)\r\n        circuit_tf.V('vss', 'vss', circuit_tf.gnd, 0)\r\n        circuit_tf.V('refp', 'vrefp', circuit_tf.gnd, 1)\r\n        circuit_tf.V('refn', 'vrefn', circuit_tf.gnd, 0)\r\n        num_bin_array = [int(num) for num in list(format(number, '0'+str(dac_nbits)+'b'))]\r\n        rev_num_bin_array = list(reversed(num_bin_array))\r\n        for i in range(dac_nbits):\r\n            istr=str(i)\r\n            circuit_tf.V('b'+istr, 'b'+istr, circuit_tf.gnd, 1*rev_num_bin_array[i])\r\n        simulator_tf = circuit_tf.simulator(temperature=25, nominal_temperature=25)\r\n        analysis_tf = simulator_tf.operating_point()\r\n        outputs[number] = float(analysis_tf.nodes['out'])\r\n        print('number: {} - output: {:5.3f}'.format(str(num_bin_array), outputs[number]))\r\n    plt.title(circuit_tf.title)\r\n    plt.xlabel('Input Code')\r\n    plt.ylabel('DAC Output Voltage [V]')\r\n    bin_input_codes = [format(code, '0'+str(dac_nbits)+'b') for code in input_codes]\r\n    plt.xticks(input_codes, bin_input_codes, rotation='vertical')\r\n    plt.plot(input_codes, outputs, 'r--')\r\n    plt.plot(input_codes, outputs, 'ko')\r\n    plt.grid()\r\n    plt.show()\r\n<\/pre>\n<p>The &#8220;input code &#8211; output voltage&#8221; table and the transfer function plot will look as follows<\/p>\n<pre class=\"wrap:true lang:default decode:true\">number: [0, 0, 0] - output: 0.000\r\nb0, b1, b2\r\nnumber: [0, 0, 1] - output: 0.125\r\nb0, b1, b2\r\nnumber: [0, 1, 0] - output: 0.250\r\nb0, b1, b2\r\nnumber: [0, 1, 1] - output: 0.375\r\nb0, b1, b2\r\nnumber: [1, 0, 0] - output: 0.500\r\nb0, b1, b2\r\nnumber: [1, 0, 1] - output: 0.625\r\nb0, b1, b2\r\nnumber: [1, 1, 0] - output: 0.750\r\nb0, b1, b2\r\nnumber: [1, 1, 1] - output: 0.875\r\n<\/pre>\n<p><a href=\"https:\/\/flic.kr\/p\/VCGCvB\"><img loading=\"lazy\" src=\"https:\/\/farm5.staticflickr.com\/4237\/35202229861_8d4d94fec6.jpg\" alt=\"3bitdac_transferfunction\" width=\"500\" height=\"375\" \/><\/a><\/p>\n<p><em>Fig. 2. \u00a0Transfer Function of 3 bits DAC<\/em><\/p>\n<p>Even though &#8220;scripting&#8221; the circuits might look like a bit of taking the scenic route to the results while compared to drag-n-dropping the elements with mouse. The approach has its own advantages. Once properly written, verified and documented, one may re-use them and pass it to other people with no complications making the approach ideal for team development and regression testing.<\/p>\n<p>For example, the code above can be easily modified to change the number of bits or reference voltages.<\/p>\n<p><a href=\"https:\/\/flic.kr\/p\/VQchkX\"><img loading=\"lazy\" src=\"https:\/\/farm5.staticflickr.com\/4208\/35332301725_4cbd4187cf.jpg\" alt=\"5bitdac_transferfunction\" width=\"500\" height=\"407\" \/><\/a><\/p>\n<p><em>Fig. 3.\u00a0Transfer Function of 5 bits DAC with Positive Reference of 1 V<\/em><\/p>\n<p><a href=\"https:\/\/flic.kr\/p\/UxEus7\"><img loading=\"lazy\" src=\"https:\/\/farm5.staticflickr.com\/4229\/34488873274_3167a8ebbc.jpg\" alt=\"4bitdac_transferfunction\" width=\"500\" height=\"375\" \/><\/a><\/p>\n<p><em>Fig. 4.\u00a0Transfer Function of 4 bits DAC with Positive Reference of 5 V<\/em><\/p>\n<h1>Conclusions<\/h1>\n<p>This tutorial explains how to model a digital to analog converter with PySpice. It\u00a0is definitely a cool tool for people interested in programming and circuits. Of course, industry graded spice simulators are probably a way more advanced compared to ngspice. On top of that, PySpice doesn&#8217;t offer a graphic user interface and drag-n-drop approach. However, it does come with a support of powerful object-oriented Python3 and scientific frameworks, which are great for data post-processing, analysis and visualization.<\/p>\n<h1>General References<\/h1>\n<ol>\n<li>GitHub Repository with the complete code for this tutorial:\u00a0<a href=\"https:\/\/github.com\/OlzhasT\/PySpiceCircuits-\/blob\/master\/dac_ideal_subcircuit_class_test.py\">https:\/\/github.com\/OlzhasT\/PySpiceCircuits-\/blob\/master\/dac_ideal_subcircuit_class_test.py<\/a>,<\/li>\n<li>All the PySpice documentation and installation guidelines:\u00a0<a href=\"https:\/\/pypi.python.org\/pypi\/PySpice\/0.3.3\">https:\/\/pypi.python.org\/pypi\/PySpice\/0.3.3<\/a>,<\/li>\n<li>PySpice \u00a0examples explaining how to get started with simple circuits and basic analysis and data processing:\u00a0<a href=\"https:\/\/pyspice.fabrice-salvaire.fr\/examples\/index.html\">https:\/\/pyspice.fabrice-salvaire.fr\/examples\/index.html<\/a>;<\/li>\n<\/ol>\n<p>I&#8217;d like to thank Matthew James (<a href=\"https:\/\/www.linkedin.com\/in\/matthew-james-7a12794\/\">LinkedIn<\/a>) for his contribution to this article.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Author: Olzhas S. Tazabekov (LinkedIn) Prerequisites: basic programming skills with Python and SPICE Intro: Why is All the Buzz Throughout our University degree programs, we all learned how to analyze, design and verify electrical circuits. Unfortunately or fortunately, most of us were trained using industry level graded software (e.g. Cadence, Synopsys, etc.). However, in real [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[5,4,3,6,9,7,8],"tags":[],"_links":{"self":[{"href":"http:\/\/www.tazabekov.com\/blog\/wp-json\/wp\/v2\/posts\/14"}],"collection":[{"href":"http:\/\/www.tazabekov.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.tazabekov.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.tazabekov.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.tazabekov.com\/blog\/wp-json\/wp\/v2\/comments?post=14"}],"version-history":[{"count":11,"href":"http:\/\/www.tazabekov.com\/blog\/wp-json\/wp\/v2\/posts\/14\/revisions"}],"predecessor-version":[{"id":20,"href":"http:\/\/www.tazabekov.com\/blog\/wp-json\/wp\/v2\/posts\/14\/revisions\/20"}],"wp:attachment":[{"href":"http:\/\/www.tazabekov.com\/blog\/wp-json\/wp\/v2\/media?parent=14"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.tazabekov.com\/blog\/wp-json\/wp\/v2\/categories?post=14"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.tazabekov.com\/blog\/wp-json\/wp\/v2\/tags?post=14"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}