Nonlinear Components

5 minute read

Published:

Contents

  1. Intro to SPICE Algorithm
  2. Framework and your first circuit!
  3. More Static Linear Components
  4. Nonlinear and Diode (this article)
  5. MOSFET
  6. Time Variance
  7. Applications

Time for the inner loop of the SPICE algorithm: nonlinear components!

We use the infamous Newton-Raphson method to take successive linear approximations until a solution is reached

This part of the report is pretty well-written, so I’ll just quote it.

Newton’s Method

Diodes has a nonlinear current-voltage relation:

\[i_d = I_s (e^{v_d/V_t} - 1)\]

where \(I_s\), \(V_t\) are constants.

\input{circuits/diode-demo}

At step \(0\), let \(G_{eq}\) be derivative \(\frac{di_d}{dv_d}\), where \(i_d = i_{d0}\), \(v_d = v_{d0}\): \(G_{eq} \equiv \frac{di_d}{dv_d} = \frac{I_s}{V_t}\, e^{v_{d0}/V_t}\)

We apply the Newton–Raphson algorithm to build a linear estimate model around an initial guess.

\[\begin{align*} i_{d} &\approx i_{d0} + G_{eq} (v_d - v_{d0})\\ &= (i_{d0} -G_{eq}v_{d0}) + G_{eq}v_d \end{align*}\]

Fortunately, any linear relation can be modelled by circuit elements. The constant term can be represented by a current source \(I_{eq} = (i_{d0} -G_{eq}v_{d0})\), and the linear term can be represented by a resistor \(G_{eq} = \frac{1}{R_{eq}}\). We add them to the matrix. This process is called stamping.

\[\usepackage[american]{circuitikz} \usepackage{tikz} \begin{circuitikz}[american] % Coordinates for nodes and ground \coordinate (v1) at (0,2); \coordinate (v2) at (5,2); \coordinate (vn1) at (2, 2); \coordinate (vn2) at (2, 0); \coordinate (vn) at (5, 0); \coordinate (gnd) at (0,0); \draw (vn1) to[R, l_=$R$, *-*] (vn2); \draw (vn1) -- (v2); % Resistor R3 between v2 and ground \draw (v2) to[D, l_=$D$, *-*] (vn); % Current source I1 between v1 and ground \draw (gnd) to[I, l_=$I_1$] (v1); \draw (v1) -- (vn1); \draw (vn2) -- (vn); \draw (vn2) -- (gnd); % Label for v1 and v2 nodes \node at (vn1) [above] {$v_1$}; %\node at (v2) [above] {$v_2$}; \node at (vn2) [below] {GND}; \end{circuitikz}\]

We can solve this linear circuit and get the node voltages.

From the node voltages, we update our initial guess.

The above procedure of stamp-solve-update is repeated until the solution converges, which is detected when the deviation of the solution \(\mathbf{v}\) is below a threshold.

Registration

Same format, but the following are new:

  • struct Diode
    • Is, Vt are constants from the diode equation
    • v_prev is the memory element from the last approximation
  • type: NL_T: for CircuitCim to recognize nonlinear components and optimize
  • update(): pointer to the updater function. Will be mentioned below
  • Initial voltage guess: This is tricky
    • You obviously don’t want a value too large (like 5 V). Vt is small number. This will blow up the floating point
    • You also don’t want it to be small (smaller than voltage drop, like 0.5 V). This approximation will have a very shallow slope, leading to a very high V for the next step, and falling back to the previous case
    • A moderate value, like 1 V is good. You will get some large initial currents, but they will converge.
typedef struct { 
    int n1, n2; 
    float Is, Vt;
    float v_prev; 
} Diode;

void add_diode(int n1, int n2, float Is, float Vt) {
    Component *c = &comps[ncomps++];
    c->type      = NL_T;
    c->stamp     = dio_stamp_nl;
    c->update    = dio_update;
    c->u.dio     = (Diode){n1,n2,Is,Vt,1.0f};   // initial guess: 1V
}

Stamping

stamp() uses the initial guess/prev values to make a linear approximation of the diode model: a conductor(resistor) and a current source in parallel.

void dio_stamp_nl(Component *c, float Gm[][MAT_SIZE], float I[]) {
    Diode *d = &c->u.dio;
    // 1. read last stage's voltage
    float Vd = d->v_prev;

    // 2. compute new iteration parameters
    float ExpV = expf(Vd / d->Vt);  
    float Geq = (d->Is / d->Vt) * ExpV;             // Geq = Is/Vt * exp(Vd/Vt)
    float Ieq = d->Is * (ExpV - 1.0f) - Geq * Vd;   // Ieq = Id - Geq*Vd

    // 3. stamp G and I
    int n1 = d->n1, n2 = d->n2;
    if (n1 != -1) Gm[n1][n1] += (Geq);
    if (n2 != -1) Gm[n2][n2] += (Geq);
    if (n1 != -1 && n2 != -1) {
        Gm[n1][n2] -= (Geq);
        Gm[n2][n1] -= (Geq);
    }
    if (n1 != -1) I[n1] -= (Ieq);
    if (n2 != -1) I[n2] += (Ieq);
}

Updating

This is done after solving the matrix

void dio_update(Component *c) {
    Diode *d = &c->u.dio;
    d->v_prev = (d->n1!=-1? v[d->n1]:0) - (d->n2!=-1? v[d->n2]:0);
}

Solving an operating point

Now we will use this to solve an operating point (one point in time)

/* clear everything EXCEPT static components */
void clear_system_sta(void) {
    memcpy(G_lin, G_sta, sizeof G_sta);
    memcpy(G, G_sta, sizeof G_sta);
    memcpy(I_lin, I_sta, sizeof I_sta);
    memcpy(Ivec, I_sta, sizeof I_sta);
}

/* stamp all static components */
void stamp_static(void) {
    for (int i = 0; i < ncomps; i++) {
        if (comps[i].type == STA_T && comps[i].stamp_lin) {
            comps[i].stamp_lin(&comps[i], G_sta, I_sta);
        }
    }
}

void operating_point() {
    (void)t;
    int ret = 0;

    // Newton-Raphson loop for all nonlinear components
    for (int iter = 0; iter < MAX_NR_ITER; iter++) {

        // 1 restore the static linear stamps
        clear_system_sta();

        // 2 stamp all nonlinear components
        for (int i = 0; i < ncomps; i++) {
            if (comps[i].type == NL_T && comps[i].stamp)
                comps[i].stamp_nl(&comps[i], G, Ivec);
        }

        // 3 solve the system
        solve_system(nnodes);

        // 4 compute dv, check for convergence
        float maxdv = 0.0f;
        for (int n = 0; n < nnodes; n++) {
            float dv = fabsf(v[n] - prev_v[n]);
            if (dv > maxdv) maxdv = dv;
        }

        if (maxdv < NR_TOL) {   // converged!
            break;
        }

        // 5 Update every device's internal operating point
        for (int i = 0; i < ncomps; i++) {
            if (comps[i].type == NL_T && comps[i].update) {
                comps[i].update(&comps[i]); // update diode only
            }
        }

        // 6 prepare for next iteration
        memcpy(prev_v, v, sizeof prev_v);
    }
}