Skip to main content

dune_core/
mos6.rs

1//! MOSFET level 6 — Sakurai–Newton's n-th power-law model from the
2//! 3f5 (`dev/mos6/mos6load.c`, `mos6temp.c`, `mos6set.c`,
3//! `mos6trunc.c`; author Takayasu Sakurai, 1989). Saturation current
4//! `KC·(vgs−von)^NC` and saturation voltage `KV·(vgs−von)^NV`, with
5//! `LAMBDA0−LAMBDA1·vbs` channel-length modulation and the
6//! `GAMMA1`/`SIGMA` threshold corrections. The scaffolding (bias
7//! ladder, limiting, bulk diodes, depletion and Meyer charges,
8//! companions, stamps, 17 states, 22 slots) is the MOS1's.
9//!
10//! The 3f5 has no AC load for MOS6 (`DEVacLoad` is null), so
11//! [`crate::devices::Device::load_ac`] keeps its no-op default.
12
13use crate::consts::{BOLTZ, CHARGE, KOVERQ, REFTEMP, ROOT2};
14use crate::diode::pnjlim;
15use crate::matrix::{AcCtx, AnKind, InitMode, LoadCtx, MatrixBuilder, Slot, TRASH, Var};
16use crate::mos1::{MosGeom, fetlim, limvds, qmeyer};
17
18/// `MAX_EXP_ARG` (`hw_ieee.h`).
19const MAX_EXP_ARG: f64 = 709.0;
20
21/// `.model … nmos(level=6 …)/pmos(level=6 …)` parameters, 3f5 names
22/// and defaults (`mos6.c` keyword table; defaults from `mos6set.c`).
23#[derive(Clone, Debug)]
24pub struct Mos6Model {
25    /// `NMOS` (+1) or `PMOS` (−1).
26    pub nmos: bool,
27    /// `VTO`/`VT0`, default 0, or computed from `NSUB`/`NSS`/`TPG`.
28    pub vto: Option<f64>,
29    /// `KV`: saturation-voltage factor, default 2.
30    pub kv: f64,
31    /// `NV`: saturation-voltage exponent, default 0.5.
32    pub nv: f64,
33    /// `KC`: saturation-current factor, default 5e-5, or
34    /// `0.5·U0·COX` when `TOX` is given.
35    pub kc: Option<f64>,
36    /// `NC`: saturation-current exponent, default 1.
37    pub nc: f64,
38    /// `NVTH`/`PS`: parsed by the 3f5 but never used by its load.
39    pub nvth: f64,
40    pub ps: f64,
41    /// `GAMMA`, default 0, or computed from `NSUB`.
42    pub gamma: Option<f64>,
43    /// `GAMMA1`: bulk threshold slope, default 0.
44    pub gamma1: f64,
45    /// `SIGMA`: static feedback, default 0 (dead in the 3f5's `von`
46    /// due to a stray semicolon; ngspice fixed it and so do we —
47    /// `corpus/KNOWN_DIFFS.md`).
48    pub sigma: f64,
49    /// `PHI`, default 0.6, or from `NSUB`.
50    pub phi: Option<f64>,
51    /// `LAMBDA`: alias that seeds `LAMBDA0` when that is not given.
52    pub lambda: Option<f64>,
53    /// `LAMBDA0`/`LAMBDA1`, default 0.
54    pub lambda0: Option<f64>,
55    pub lambda1: f64,
56    /// `RD`/`RS`, default 0.
57    pub rd: Option<f64>,
58    pub rs: Option<f64>,
59    /// `CBD`/`CBS`, default 0.
60    pub cbd: Option<f64>,
61    pub cbs: Option<f64>,
62    /// `IS`, default 1e-14.
63    pub is: f64,
64    /// `PB`, default 0.8.
65    pub pb: f64,
66    /// `CGSO`/`CGDO`/`CGBO`, default 0.
67    pub cgso: f64,
68    pub cgdo: f64,
69    pub cgbo: f64,
70    /// `RSH`, default 0.
71    pub rsh: Option<f64>,
72    /// `CJ`, per m², default 0.
73    pub cj: Option<f64>,
74    /// `MJ`, default 0.5.
75    pub mj: f64,
76    /// `CJSW`, per meter, default 0.
77    pub cjsw: Option<f64>,
78    /// `MJSW`, default 0.5.
79    pub mjsw: f64,
80    /// `JS`, per m², default 0.
81    pub js: f64,
82    /// `TOX`, meters.
83    pub tox: Option<f64>,
84    /// `LD`, default 0.
85    pub ld: f64,
86    /// `U0`/`UO`, cm²/V·s, default 600.
87    pub u0: Option<f64>,
88    /// `FC`, default 0.5.
89    pub fc: f64,
90    /// `NSUB`, 1/cm³.
91    pub nsub: Option<f64>,
92    /// `TPG`, default 1 when `NSUB` is given.
93    pub tpg: Option<f64>,
94    /// `NSS`, 1/cm².
95    pub nss: Option<f64>,
96    /// `KF`/`AF` (noise only).
97    pub kf: f64,
98    pub af: f64,
99    /// `TNOM` in kelvin.
100    pub tnom: Option<f64>,
101}
102
103impl Mos6Model {
104    pub fn new(nmos: bool) -> Self {
105        Mos6Model {
106            nmos,
107            vto: None,
108            kv: 2.0,
109            nv: 0.5,
110            kc: None,
111            nc: 1.0,
112            nvth: 0.5,
113            ps: 0.0,
114            gamma: None,
115            gamma1: 0.0,
116            sigma: 0.0,
117            phi: None,
118            lambda: None,
119            lambda0: None,
120            lambda1: 0.0,
121            rd: None,
122            rs: None,
123            cbd: None,
124            cbs: None,
125            is: 1e-14,
126            pb: 0.8,
127            cgso: 0.0,
128            cgdo: 0.0,
129            cgbo: 0.0,
130            rsh: None,
131            cj: None,
132            mj: 0.5,
133            cjsw: None,
134            mjsw: 0.5,
135            js: 0.0,
136            tox: None,
137            ld: 0.0,
138            u0: None,
139            fc: 0.5,
140            nsub: None,
141            tpg: None,
142            nss: None,
143            kf: 0.0,
144            af: 1.0,
145            tnom: None,
146        }
147    }
148
149    /// Same prime-node rule as MOS1 (`mos6set.c`).
150    pub(crate) fn needs_prime(&self, squares: f64) -> bool {
151        self.rd.unwrap_or(0.0) != 0.0 || (self.rsh.unwrap_or(0.0) != 0.0 && squares != 0.0)
152    }
153}
154
155fn egfet(t: f64) -> f64 {
156    1.16 - (7.02e-4 * t * t) / (t + 1108.0)
157}
158
159/// Model- and temperature-derived values (`MOS6temp`).
160#[derive(Debug)]
161pub(crate) struct Mos6Temp {
162    pub type_: f64,
163    pub vt: f64,
164    pub lat_diff: f64,
165    pub cgso: f64,
166    pub cgdo: f64,
167    pub cgbo: f64,
168    pub oxide_cap_factor: f64,
169    pub gamma: f64,
170    pub gamma1: f64,
171    pub sigma: f64,
172    pub kv: f64,
173    pub nv: f64,
174    pub nc: f64,
175    pub lambda0: f64,
176    pub lambda1: f64,
177    pub mj: f64,
178    pub mjsw: f64,
179    /// `MOS6tKc` (`KC / ratio^1.5`).
180    pub t_kc: f64,
181    pub t_phi: f64,
182    pub t_vbi: f64,
183    pub t_vto: f64,
184    pub t_sat_cur: f64,
185    pub t_sat_cur_dens: f64,
186    pub t_bulk_pot: f64,
187    pub t_dep_cap: f64,
188    pub drain_vcrit: f64,
189    pub source_vcrit: f64,
190    pub cbd: f64,
191    pub cbdsw: f64,
192    pub cbs: f64,
193    pub cbssw: f64,
194    pub f2d: f64,
195    pub f3d: f64,
196    pub f4d: f64,
197    pub f2s: f64,
198    pub f3s: f64,
199    pub f4s: f64,
200    pub drain_conductance: f64,
201    pub source_conductance: f64,
202    pub notes: Vec<String>,
203}
204
205impl Mos6Temp {
206    pub fn new(
207        name: &str,
208        m: &Mos6Model,
209        g: &MosGeom,
210        temp: f64,
211        default_tnom: f64,
212    ) -> Result<Self, String> {
213        let mut notes = Vec::new();
214        let typ = if m.nmos { 1.0 } else { -1.0 };
215        let tnom = m.tnom.unwrap_or(default_tnom);
216
217        let fact1 = tnom / REFTEMP;
218        let vtnom = tnom * KOVERQ;
219        let kt1 = BOLTZ * tnom;
220        let egfet1 = egfet(tnom);
221        let arg1 = -egfet1 / (kt1 + kt1) + 1.1150877 / (BOLTZ * (REFTEMP + REFTEMP));
222        let pbfact1 = -2.0 * vtnom * (1.5 * f64::ln(fact1) + CHARGE * arg1);
223
224        // Model preprocessing (`mos6temp.c`): KC from `0.5·U0·COX`,
225        // PHI/GAMMA/VTO from NSUB — same structure as MOS1's.
226        let mut kc = m.kc;
227        let mut phi = m.phi;
228        let mut gamma = m.gamma;
229        let mut vto = m.vto;
230        let oxide_cap_factor = match m.tox {
231            Some(tox) if tox != 0.0 => {
232                let ocf = 3.9 * 8.854214871e-12 / tox;
233                if kc.is_none() {
234                    let u0 = m.u0.unwrap_or(600.0);
235                    kc = Some(0.5 * u0 * ocf * 1e-4 /* m²/cm² */);
236                }
237                if let Some(nsub) = m.nsub {
238                    if nsub * 1e6 /* cm³/m³ */ > 1.45e16 {
239                        if phi.is_none() {
240                            phi = Some(f64::max(0.1, 2.0 * vtnom * f64::ln(nsub * 1e6 / 1.45e16)));
241                        }
242                        let phi_v = phi.unwrap_or(0.6);
243                        let fermis = typ * 0.5 * phi_v;
244                        let tpg = m.tpg.unwrap_or(1.0);
245                        let mut wkfng = 3.2;
246                        if tpg != 0.0 {
247                            let fermig = typ * tpg * 0.5 * egfet1;
248                            wkfng = 3.25 + 0.5 * egfet1 - fermig;
249                        }
250                        let wkfngs = wkfng - (3.25 + 0.5 * egfet1 + fermis);
251                        if gamma.is_none() {
252                            gamma = Some(
253                                f64::sqrt(2.0 * 11.70 * 8.854214871e-12 * CHARGE * nsub * 1e6)
254                                    / ocf,
255                            );
256                        }
257                        if vto.is_none() {
258                            let nss = m.nss.unwrap_or(0.0);
259                            let vfb = wkfngs - nss * 1e4 * CHARGE / ocf;
260                            vto = Some(vfb + typ * (gamma.unwrap_or(0.0) * phi_v.sqrt() + phi_v));
261                        }
262                    } else {
263                        return Err(format!("{name}: Nsub < Ni"));
264                    }
265                }
266                ocf
267            }
268            _ => 0.0,
269        };
270        let kc = kc.unwrap_or(5e-5);
271        let phi = phi.unwrap_or(0.6);
272        let gamma = gamma.unwrap_or(0.0);
273        let vto = vto.unwrap_or(0.0);
274        // `LAMBDA` seeds `LAMBDA0` when the latter is absent.
275        let lambda0 = m.lambda0.or(m.lambda).unwrap_or(0.0);
276
277        // Per-instance temperature block (same formulas as MOS1).
278        let vt = temp * KOVERQ;
279        let ratio = temp / tnom;
280        let fact2 = temp / REFTEMP;
281        let kt = temp * BOLTZ;
282        let egfet_t = egfet(temp);
283        let arg = -egfet_t / (kt + kt) + 1.1150877 / (BOLTZ * (REFTEMP + REFTEMP));
284        let pbfact = -2.0 * vt * (1.5 * f64::ln(fact2) + CHARGE * arg);
285
286        if g.l - 2.0 * m.ld <= 0.0 {
287            notes.push(format!("{name}: effective channel length less than zero"));
288        }
289        let ratio4 = ratio * ratio.sqrt();
290        let t_kc = kc / ratio4;
291        let phio = (phi - pbfact1) / fact1;
292        let t_phi = fact2 * phio + pbfact;
293        let t_vbi =
294            vto - typ * (gamma * phi.sqrt()) + 0.5 * (egfet1 - egfet_t) + typ * 0.5 * (t_phi - phi);
295        let t_vto = t_vbi + typ * gamma * t_phi.sqrt();
296        let t_sat_cur = m.is * f64::exp(-egfet_t / vt + egfet1 / vtnom);
297        let t_sat_cur_dens = m.js * f64::exp(-egfet_t / vt + egfet1 / vtnom);
298
299        let pbo = (m.pb - pbfact1) / fact1;
300        let gmaold = (m.pb - pbo) / pbo;
301        let mut capfact = 1.0 / (1.0 + m.mj * (4e-4 * (tnom - REFTEMP) - gmaold));
302        let mut t_cbd = m.cbd.unwrap_or(0.0) * capfact;
303        let mut t_cbs = m.cbs.unwrap_or(0.0) * capfact;
304        let mut t_cj = m.cj.unwrap_or(0.0) * capfact;
305        capfact = 1.0 / (1.0 + m.mjsw * (4e-4 * (tnom - REFTEMP) - gmaold));
306        let mut t_cjsw = m.cjsw.unwrap_or(0.0) * capfact;
307        let t_bulk_pot = fact2 * pbo + pbfact;
308        let gmanew = (t_bulk_pot - pbo) / pbo;
309        capfact = 1.0 + m.mj * (4e-4 * (temp - REFTEMP) - gmanew);
310        t_cbd *= capfact;
311        t_cbs *= capfact;
312        t_cj *= capfact;
313        capfact = 1.0 + m.mjsw * (4e-4 * (temp - REFTEMP) - gmanew);
314        t_cjsw *= capfact;
315        let t_dep_cap = m.fc * t_bulk_pot;
316
317        let (drain_vcrit, source_vcrit) = if t_sat_cur_dens == 0.0 || g.ad == 0.0 || g.as_ == 0.0 {
318            let v = vt * f64::ln(vt / (ROOT2 * t_sat_cur));
319            (v, v)
320        } else {
321            (
322                vt * f64::ln(vt / (ROOT2 * t_sat_cur_dens * g.ad)),
323                vt * f64::ln(vt / (ROOT2 * t_sat_cur_dens * g.as_)),
324            )
325        };
326
327        let czbd = if m.cbd.is_some() {
328            t_cbd
329        } else if m.cj.is_some() {
330            t_cj * g.ad
331        } else {
332            0.0
333        };
334        let czbdsw = if m.cjsw.is_some() { t_cjsw * g.pd } else { 0.0 };
335        let arg = 1.0 - m.fc;
336        let sarg = f64::exp(-m.mj * f64::ln(arg));
337        let sargsw = f64::exp(-m.mjsw * f64::ln(arg));
338        let f2d = czbd * (1.0 - m.fc * (1.0 + m.mj)) * sarg / arg
339            + czbdsw * (1.0 - m.fc * (1.0 + m.mjsw)) * sargsw / arg;
340        let f3d =
341            czbd * m.mj * sarg / arg / t_bulk_pot + czbdsw * m.mjsw * sargsw / arg / t_bulk_pot;
342        let f4d = czbd * t_bulk_pot * (1.0 - arg * sarg) / (1.0 - m.mj)
343            + czbdsw * t_bulk_pot * (1.0 - arg * sargsw) / (1.0 - m.mjsw)
344            - f3d / 2.0 * (t_dep_cap * t_dep_cap)
345            - t_dep_cap * f2d;
346        let czbs = if m.cbs.is_some() {
347            t_cbs
348        } else if m.cj.is_some() {
349            t_cj * g.as_
350        } else {
351            0.0
352        };
353        let czbssw = if m.cjsw.is_some() { t_cjsw * g.ps } else { 0.0 };
354        let f2s = czbs * (1.0 - m.fc * (1.0 + m.mj)) * sarg / arg
355            + czbssw * (1.0 - m.fc * (1.0 + m.mjsw)) * sargsw / arg;
356        let f3s =
357            czbs * m.mj * sarg / arg / t_bulk_pot + czbssw * m.mjsw * sargsw / arg / t_bulk_pot;
358        let f4s = czbs * t_bulk_pot * (1.0 - arg * sarg) / (1.0 - m.mj)
359            + czbssw * t_bulk_pot * (1.0 - arg * sargsw) / (1.0 - m.mjsw)
360            - f3s / 2.0 * (t_dep_cap * t_dep_cap)
361            - t_dep_cap * f2s;
362
363        let series = |r: Option<f64>, squares: f64| match (r, m.rsh) {
364            (Some(r), _) => {
365                if r != 0.0 {
366                    1.0 / r
367                } else {
368                    0.0
369                }
370            }
371            (None, Some(rsh)) => {
372                if rsh != 0.0 {
373                    1.0 / (rsh * squares)
374                } else {
375                    0.0
376                }
377            }
378            (None, None) => 0.0,
379        };
380
381        Ok(Mos6Temp {
382            type_: typ,
383            vt,
384            lat_diff: m.ld,
385            cgso: m.cgso,
386            cgdo: m.cgdo,
387            cgbo: m.cgbo,
388            oxide_cap_factor,
389            gamma,
390            gamma1: m.gamma1,
391            sigma: m.sigma,
392            kv: m.kv,
393            nv: m.nv,
394            nc: m.nc,
395            lambda0,
396            lambda1: m.lambda1,
397            mj: m.mj,
398            mjsw: m.mjsw,
399            t_kc,
400            t_phi,
401            t_vbi,
402            t_vto,
403            t_sat_cur,
404            t_sat_cur_dens,
405            t_bulk_pot,
406            t_dep_cap,
407            drain_vcrit,
408            source_vcrit,
409            cbd: czbd,
410            cbdsw: czbdsw,
411            cbs: czbs,
412            cbssw: czbssw,
413            f2d,
414            f3d,
415            f4d,
416            f2s,
417            f3s,
418            f4s,
419            drain_conductance: series(m.rd, g.nrd),
420            source_conductance: series(m.rs, g.nrs),
421            notes,
422        })
423    }
424}
425
426// State offsets (`mos6defs.h` — identical to MOS1's).
427const VBD: usize = 0;
428const VBS: usize = 1;
429const VGS: usize = 2;
430const VDS: usize = 3;
431const CAPGS: usize = 4;
432const QGS: usize = 5;
433const CQGS: usize = 6;
434const CAPGD: usize = 7;
435const QGD: usize = 8;
436const CQGD: usize = 9;
437const CAPGB: usize = 10;
438const QGB: usize = 11;
439const CQGB: usize = 12;
440const QBD: usize = 13;
441const CQBD: usize = 14;
442const QBS: usize = 15;
443const CQBS: usize = 16;
444pub(crate) const MOS6_NUM_STATES: usize = 17;
445
446// Slot order (matches `mos6set.c`'s TSTALLOC list — same as MOS1).
447const D_D: usize = 0;
448const G_G: usize = 1;
449const S_S: usize = 2;
450const B_B: usize = 3;
451const DP_DP: usize = 4;
452const SP_SP: usize = 5;
453const D_DP: usize = 6;
454const G_B: usize = 7;
455const G_DP: usize = 8;
456const G_SP: usize = 9;
457const S_SP: usize = 10;
458const B_DP: usize = 11;
459const B_SP: usize = 12;
460const DP_SP: usize = 13;
461const DP_D: usize = 14;
462const B_G: usize = 15;
463const DP_G: usize = 16;
464const SP_G: usize = 17;
465const SP_S: usize = 18;
466const DP_B: usize = 19;
467const SP_B: usize = 20;
468const SP_DP: usize = 21;
469
470pub(crate) struct Mos6I {
471    drain: Var,
472    gate: Var,
473    source: Var,
474    bulk: Var,
475    d_p: Var,
476    s_p: Var,
477    t: Mos6Temp,
478    geom: MosGeom,
479    off: bool,
480    ic_vds: f64,
481    ic_vgs: f64,
482    ic_vbs: f64,
483    ic_given: crate::devices::IcGiven,
484    st: usize,
485    s: [Slot; 22],
486    // Values that persist between iterations (`MOS6instance`).
487    mode: f64,
488    von: f64,
489    vdsat: f64,
490    gm: f64,
491    gds: f64,
492    gmbs: f64,
493    gbd: f64,
494    gbs: f64,
495    capbd: f64,
496    capbs: f64,
497    cd: f64,
498    cbs: f64,
499    cbd: f64,
500}
501
502impl Mos6I {
503    #[allow(clippy::too_many_arguments)]
504    pub fn new(
505        nodes: [Var; 4],  // d g s b
506        primes: [Var; 2], // d' s'
507        t: Mos6Temp,
508        geom: MosGeom,
509        off: bool,
510        ic: (f64, f64, f64), // (vds, vgs, vbs)
511        st: usize,
512    ) -> Self {
513        Mos6I {
514            drain: nodes[0],
515            gate: nodes[1],
516            source: nodes[2],
517            bulk: nodes[3],
518            d_p: primes[0],
519            s_p: primes[1],
520            t,
521            geom,
522            off,
523            ic_vds: ic.0,
524            ic_vgs: ic.1,
525            ic_vbs: ic.2,
526            ic_given: crate::devices::IcGiven::ALL,
527            st,
528            s: [TRASH; 22],
529            mode: 1.0,
530            von: 0.0,
531            vdsat: 0.0,
532            gm: 0.0,
533            gds: 0.0,
534            gmbs: 0.0,
535            gbd: 0.0,
536            gbs: 0.0,
537            capbd: 0.0,
538            capbs: 0.0,
539            cd: 0.0,
540            cbs: 0.0,
541            cbd: 0.0,
542        }
543    }
544}
545
546impl crate::devices::Device for Mos6I {
547    fn par_load(&self) -> bool {
548        true
549    }
550
551    /// `MOS6getic` (`mos6ic.c`): VBS, VDS, VGS not given
552    /// on the card from the external nodes.
553    fn set_ic(&mut self, rhs: &[f64]) {
554        use crate::devices::getic;
555        let g = self.ic_given;
556        getic(g, 2, &mut self.ic_vbs, rhs, self.bulk, self.source);
557        getic(g, 0, &mut self.ic_vds, rhs, self.drain, self.source);
558        getic(g, 1, &mut self.ic_vgs, rhs, self.gate, self.source);
559    }
560
561    fn set_ic_given(&mut self, given: crate::devices::IcGiven) {
562        self.ic_given = given;
563    }
564
565    /// `MOS6convTest` (same shape as MOS1's).
566    fn conv_test(&self, ctx: &crate::matrix::ConvCtx) -> bool {
567        let typ = self.t.type_;
568        let st = self.st;
569        let vbs = typ * (ctx.rhs[self.bulk] - ctx.rhs[self.s_p]);
570        let vgs = typ * (ctx.rhs[self.gate] - ctx.rhs[self.s_p]);
571        let vds = typ * (ctx.rhs[self.d_p] - ctx.rhs[self.s_p]);
572        let vbd = vbs - vds;
573        let vgd = vgs - vds;
574        let vgdo = ctx.states.get(0, st + VGS) - ctx.states.get(0, st + VDS);
575        let delvbs = vbs - ctx.states.get(0, st + VBS);
576        let delvbd = vbd - ctx.states.get(0, st + VBD);
577        let delvgs = vgs - ctx.states.get(0, st + VGS);
578        let delvds = vds - ctx.states.get(0, st + VDS);
579        let delvgd = vgd - vgdo;
580        let cdhat = if self.mode >= 0.0 {
581            self.cd - self.gbd * delvbd + self.gmbs * delvbs + self.gm * delvgs + self.gds * delvds
582        } else {
583            self.cd - (self.gbd - self.gmbs) * delvbd - self.gm * delvgd + self.gds * delvds
584        };
585        let cbhat = self.cbs + self.cbd + self.gbd * delvbd + self.gbs * delvbs;
586        let tol = ctx.reltol * f64::max(cdhat.abs(), self.cd.abs()) + ctx.abstol;
587        if (cdhat - self.cd).abs() >= tol {
588            return false;
589        }
590        let cb = self.cbs + self.cbd;
591        let tol = ctx.reltol * f64::max(cbhat.abs(), cb.abs()) + ctx.abstol;
592        (cbhat - cb).abs() <= tol
593    }
594
595    fn alloc(&mut self, mb: &mut MatrixBuilder) {
596        let (d, g, s, b) = (self.drain, self.gate, self.source, self.bulk);
597        let (dp, sp) = (self.d_p, self.s_p);
598        self.s = [
599            mb.slot(d, d),
600            mb.slot(g, g),
601            mb.slot(s, s),
602            mb.slot(b, b),
603            mb.slot(dp, dp),
604            mb.slot(sp, sp),
605            mb.slot(d, dp),
606            mb.slot(g, b),
607            mb.slot(g, dp),
608            mb.slot(g, sp),
609            mb.slot(s, sp),
610            mb.slot(b, dp),
611            mb.slot(b, sp),
612            mb.slot(dp, sp),
613            mb.slot(dp, d),
614            mb.slot(b, g),
615            mb.slot(dp, g),
616            mb.slot(sp, g),
617            mb.slot(sp, s),
618            mb.slot(dp, b),
619            mb.slot(sp, b),
620            mb.slot(sp, dp),
621        ];
622    }
623
624    #[allow(clippy::too_many_lines)]
625    fn load(&mut self, ctx: &mut LoadCtx) {
626        let t = &self.t;
627        let typ = t.type_;
628        let vt = t.vt;
629        let st = self.st;
630
631        let eff_l = self.geom.l - 2.0 * t.lat_diff;
632        let (drain_sat_cur, source_sat_cur) =
633            if t.t_sat_cur_dens == 0.0 || self.geom.ad == 0.0 || self.geom.as_ == 0.0 {
634                (t.t_sat_cur, t.t_sat_cur)
635            } else {
636                (
637                    t.t_sat_cur_dens * self.geom.ad,
638                    t.t_sat_cur_dens * self.geom.as_,
639                )
640            };
641        let gate_source_overlap_cap = t.cgso * self.geom.w;
642        let gate_drain_overlap_cap = t.cgdo * self.geom.w;
643        let gate_bulk_overlap_cap = t.cgbo * eff_l;
644        let betac = t.t_kc * self.geom.w / eff_l;
645        let oxide_cap = t.oxide_cap_factor * eff_l * self.geom.w;
646
647        // Bias resolution — same ladder as MOS1.
648        let mut check = true;
649        let (mut vbs, mut vgs, mut vds);
650        let predictor = matches!(ctx.mode, InitMode::Pred | InitMode::Tran);
651        let general = matches!(ctx.mode, InitMode::Float | InitMode::Smsig)
652            || (ctx.mode == InitMode::Fix && !self.off);
653        if predictor || general {
654            if predictor {
655                let xfact = ctx.pred_xfact;
656                let mut pred = |o: usize| {
657                    let s1 = ctx.states.get(1, st + o);
658                    ctx.states.set(0, st + o, s1);
659                    (1.0 + xfact) * s1 - xfact * ctx.states.get(2, st + o)
660                };
661                vbs = pred(VBS);
662                vgs = pred(VGS);
663                vds = pred(VDS);
664                let vbd0 = ctx.states.get(0, st + VBS) - ctx.states.get(0, st + VDS);
665                ctx.states.set(0, st + VBD, vbd0);
666            } else {
667                vbs = typ * (ctx.sol[self.bulk] - ctx.sol[self.s_p]);
668                vgs = typ * (ctx.sol[self.gate] - ctx.sol[self.s_p]);
669                vds = typ * (ctx.sol[self.d_p] - ctx.sol[self.s_p]);
670            }
671            let mut vbd = vbs - vds;
672            let mut vgd = vgs - vds;
673            let vgdo = ctx.states.get(0, st + VGS) - ctx.states.get(0, st + VDS);
674
675            let von = typ * self.von;
676            if ctx.states.get(0, st + VDS) >= 0.0 {
677                vgs = fetlim(vgs, ctx.states.get(0, st + VGS), von);
678                vds = vgs - vgd;
679                vds = limvds(vds, ctx.states.get(0, st + VDS));
680            } else {
681                vgd = fetlim(vgd, vgdo, von);
682                vds = vgs - vgd;
683                vds = -limvds(-vds, -ctx.states.get(0, st + VDS));
684                vgs = vgd + vds;
685            }
686            if vds >= 0.0 {
687                vbs = pnjlim(
688                    vbs,
689                    ctx.states.get(0, st + VBS),
690                    vt,
691                    t.source_vcrit,
692                    &mut check,
693                );
694            } else {
695                vbd = pnjlim(
696                    vbd,
697                    ctx.states.get(0, st + VBD),
698                    vt,
699                    t.drain_vcrit,
700                    &mut check,
701                );
702                vbs = vbd + vds;
703            }
704        } else if ctx.mode == InitMode::Jct && !self.off {
705            vds = typ * self.ic_vds;
706            vgs = typ * self.ic_vgs;
707            vbs = typ * self.ic_vbs;
708            if vds == 0.0 && vgs == 0.0 && vbs == 0.0 && !(ctx.kind == AnKind::TranOp && ctx.uic) {
709                vbs = -1.0;
710                vgs = typ * t.t_vto;
711                vds = 0.0;
712            }
713        } else {
714            vbs = 0.0;
715            vgs = 0.0;
716            vds = 0.0;
717        }
718
719        let vbd = vbs - vds;
720        let vgd = vgs - vds;
721        let vgb = vgs - vbs;
722
723        // Bulk diodes, in the ngspice form (`corpus/KNOWN_DIFFS.md`).
724        let (mut gbs, mut cbs);
725        if vbs <= -3.0 * vt {
726            gbs = ctx.gmin;
727            cbs = gbs * vbs - source_sat_cur;
728        } else {
729            let evbs = f64::exp(f64::min(MAX_EXP_ARG, vbs / vt));
730            gbs = source_sat_cur * evbs / vt + ctx.gmin;
731            cbs = source_sat_cur * (evbs - 1.0) + ctx.gmin * vbs;
732        }
733        let (mut gbd, mut cbd);
734        if vbd <= -3.0 * vt {
735            gbd = ctx.gmin;
736            cbd = gbd * vbd - drain_sat_cur;
737        } else {
738            let evbd = f64::exp(f64::min(MAX_EXP_ARG, vbd / vt));
739            gbd = drain_sat_cur * evbd / vt + ctx.gmin;
740            cbd = drain_sat_cur * (evbd - 1.0) + ctx.gmin * vbd;
741        }
742
743        self.mode = if vds >= 0.0 { 1.0 } else { -1.0 };
744        let mode = self.mode;
745
746        // Sakurai-Newton n-th power drain current (`mos6load.c`,
747        // with ngspice's fix of the stray semicolon that killed the
748        // SIGMA term in `von`).
749        let cdrain;
750        let (gm, gds, gmbs);
751        let (von, vdsat);
752        {
753            let vbsvbd = if mode == 1.0 { vbs } else { vbd };
754            let sarg = if vbsvbd <= 0.0 {
755                (t.t_phi - vbsvbd).sqrt()
756            } else {
757                let s = t.t_phi.sqrt();
758                f64::max(0.0, s - vbsvbd / (s + s))
759            };
760            let vdshere = vds * mode;
761            von = t.t_vbi * typ + t.gamma * sarg - t.gamma1 * vbsvbd - t.sigma * vdshere;
762            let vgon = (if mode == 1.0 { vgs } else { vgd }) - von;
763            if vgon <= 0.0 {
764                // Cutoff region.
765                vdsat = 0.0;
766                cdrain = 0.0;
767                gm = 0.0;
768                gds = 0.0;
769                gmbs = 0.0;
770            } else {
771                // The 3f5 leaves `vonbm` uninitialized when
772                // `sarg <= 0`; we use 0.
773                let mut vonbm = 0.0;
774                if sarg > 0.0 {
775                    vonbm = if vbsvbd <= 0.0 {
776                        t.gamma1 + t.gamma / (sarg + sarg)
777                    } else {
778                        t.gamma1 + t.gamma / 2.0 / t.t_phi.sqrt()
779                    };
780                }
781                let lnvgon = vgon.ln();
782                vdsat = t.kv * f64::exp(lnvgon * t.nv);
783                let idsat = betac * f64::exp(lnvgon * t.nc);
784                let lambda = t.lambda0 - t.lambda1 * vbsvbd;
785                // Saturation region first…
786                let mut cdr = idsat * (1.0 + lambda * vdshere);
787                let mut gm_l = cdr * t.nc / vgon;
788                let mut gds_l = gm_l * t.sigma + idsat * lambda;
789                let mut gmbs_l = gm_l * vonbm - idsat * t.lambda1 * vdshere;
790                if vdsat > vdshere {
791                    // …then the linear-region correction.
792                    let vdst = vdshere / vdsat;
793                    let vdst2 = (2.0 - vdst) * vdst;
794                    let vdstg = -vdst * t.nv / vgon;
795                    let ivdst1 = cdr * (2.0 - vdst - vdst);
796                    cdr *= vdst2;
797                    gm_l = gm_l * vdst2 + ivdst1 * vdstg;
798                    gds_l = gds_l * vdst2 + ivdst1 * (1.0 / vdsat + vdstg * t.sigma);
799                    gmbs_l = gmbs_l * vdst2 + ivdst1 * vdstg * vonbm;
800                }
801                cdrain = cdr;
802                gm = gm_l;
803                gds = gds_l;
804                gmbs = gmbs_l;
805            }
806        }
807        self.von = typ * von;
808        self.vdsat = typ * vdsat;
809        self.gm = gm;
810        self.gds = gds;
811        self.gmbs = gmbs;
812
813        let mut cd = mode * cdrain - cbd;
814
815        let smsig = ctx.mode == InitMode::Smsig;
816        if ctx.kind == AnKind::Tran || ctx.kind == AnKind::TranOp || smsig {
817            // Bulk junction depletion charge and capacitance.
818            let dep = |v: f64, cz: f64, czsw: f64, f2: f64, f3: f64, f4: f64| {
819                if cz != 0.0 || czsw != 0.0 {
820                    if v < t.t_dep_cap {
821                        let arg = 1.0 - v / t.t_bulk_pot;
822                        let (sarg, sargsw);
823                        if t.mj == t.mjsw {
824                            if t.mj == 0.5 {
825                                sarg = 1.0 / arg.sqrt();
826                                sargsw = sarg;
827                            } else {
828                                sarg = f64::exp(-t.mj * f64::ln(arg));
829                                sargsw = sarg;
830                            }
831                        } else {
832                            sarg = if t.mj == 0.5 {
833                                1.0 / arg.sqrt()
834                            } else {
835                                f64::exp(-t.mj * f64::ln(arg))
836                            };
837                            sargsw = if t.mjsw == 0.5 {
838                                1.0 / arg.sqrt()
839                            } else {
840                                f64::exp(-t.mjsw * f64::ln(arg))
841                            };
842                        }
843                        let q = t.t_bulk_pot
844                            * (cz * (1.0 - arg * sarg) / (1.0 - t.mj)
845                                + czsw * (1.0 - arg * sargsw) / (1.0 - t.mjsw));
846                        (q, cz * sarg + czsw * sargsw)
847                    } else {
848                        (f4 + v * (f2 + v * f3 / 2.0), f2 + v * f3)
849                    }
850                } else {
851                    (0.0, 0.0)
852                }
853            };
854            let (qbs, capbs) = dep(vbs, t.cbs, t.cbssw, t.f2s, t.f3s, t.f4s);
855            ctx.states.set(0, st + QBS, qbs);
856            self.capbs = capbs;
857            let (qbd, capbd) = dep(vbd, t.cbd, t.cbdsw, t.f2d, t.f3d, t.f4d);
858            ctx.states.set(0, st + QBD, qbd);
859            self.capbd = capbd;
860
861            if ctx.kind == AnKind::Tran {
862                let (geq, _) = ctx.integrate(self.capbd, st + QBD);
863                gbd += geq;
864                cbd += ctx.states.get(0, st + CQBD);
865                cd -= ctx.states.get(0, st + CQBD);
866                let (geq, _) = ctx.integrate(self.capbs, st + QBS);
867                gbs += geq;
868                cbs += ctx.states.get(0, st + CQBS);
869            }
870        }
871        self.gbs = gbs;
872        self.gbd = gbd;
873
874        if (!self.off || !matches!(ctx.mode, InitMode::Fix | InitMode::Smsig)) && check {
875            ctx.noncon = true;
876        }
877
878        ctx.states.set(0, st + VBS, vbs);
879        ctx.states.set(0, st + VBD, vbd);
880        ctx.states.set(0, st + VGS, vgs);
881        ctx.states.set(0, st + VDS, vds);
882
883        // Meyer's capacitor model.
884        let (mut capgs, mut capgd, mut capgb) = (0.0, 0.0, 0.0);
885        if ctx.kind == AnKind::Tran || ctx.kind == AnKind::TranOp || smsig {
886            let (half_gs, half_gd, half_gb) = if mode > 0.0 {
887                qmeyer(vgs, vgd, vgb, von, vdsat, t.t_phi, oxide_cap)
888            } else {
889                let (o1, o2, o3) = qmeyer(vgd, vgs, vgb, von, vdsat, t.t_phi, oxide_cap);
890                (o2, o1, o3)
891            };
892            ctx.states.set(0, st + CAPGS, half_gs);
893            ctx.states.set(0, st + CAPGD, half_gd);
894            ctx.states.set(0, st + CAPGB, half_gb);
895            let vgs1 = ctx.states.get(1, st + VGS);
896            let vgd1 = vgs1 - ctx.states.get(1, st + VDS);
897            let vgb1 = vgs1 - ctx.states.get(1, st + VBS);
898            if ctx.kind == AnKind::TranOp || smsig {
899                capgs = 2.0 * ctx.states.get(0, st + CAPGS) + gate_source_overlap_cap;
900                capgd = 2.0 * ctx.states.get(0, st + CAPGD) + gate_drain_overlap_cap;
901                capgb = 2.0 * ctx.states.get(0, st + CAPGB) + gate_bulk_overlap_cap;
902            } else {
903                capgs = ctx.states.get(0, st + CAPGS)
904                    + ctx.states.get(1, st + CAPGS)
905                    + gate_source_overlap_cap;
906                capgd = ctx.states.get(0, st + CAPGD)
907                    + ctx.states.get(1, st + CAPGD)
908                    + gate_drain_overlap_cap;
909                capgb = ctx.states.get(0, st + CAPGB)
910                    + ctx.states.get(1, st + CAPGB)
911                    + gate_bulk_overlap_cap;
912            }
913
914            if smsig {
915                return;
916            }
917
918            if predictor {
919                let xfact = ctx.pred_xfact;
920                for o in [QGS, QGD, QGB] {
921                    let v = (1.0 + xfact) * ctx.states.get(1, st + o)
922                        - xfact * ctx.states.get(2, st + o);
923                    ctx.states.set(0, st + o, v);
924                }
925            } else if ctx.kind == AnKind::Tran {
926                let q = (vgs - vgs1) * capgs + ctx.states.get(1, st + QGS);
927                ctx.states.set(0, st + QGS, q);
928                let q = (vgd - vgd1) * capgd + ctx.states.get(1, st + QGD);
929                ctx.states.set(0, st + QGD, q);
930                let q = (vgb - vgb1) * capgb + ctx.states.get(1, st + QGB);
931                ctx.states.set(0, st + QGB, q);
932            } else {
933                ctx.states.set(0, st + QGS, capgs * vgs);
934                ctx.states.set(0, st + QGD, capgd * vgd);
935                ctx.states.set(0, st + QGB, capgb * vgb);
936            }
937        }
938
939        // Meyer companion models.
940        let (mut gcgs, mut ceqgs, mut gcgd, mut ceqgd, mut gcgb, mut ceqgb) =
941            (0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
942        if ctx.mode != InitMode::Tran && ctx.kind == AnKind::Tran {
943            if capgs == 0.0 {
944                ctx.states.set(0, st + CQGS, 0.0);
945            }
946            if capgd == 0.0 {
947                ctx.states.set(0, st + CQGD, 0.0);
948            }
949            if capgb == 0.0 {
950                ctx.states.set(0, st + CQGB, 0.0);
951            }
952            let (g, ceq) = ctx.integrate(capgs, st + QGS);
953            gcgs = g;
954            ceqgs = ceq - gcgs * vgs + ctx.ag[0] * ctx.states.get(0, st + QGS);
955            let (g, ceq) = ctx.integrate(capgd, st + QGD);
956            gcgd = g;
957            ceqgd = ceq - gcgd * vgd + ctx.ag[0] * ctx.states.get(0, st + QGD);
958            let (g, ceq) = ctx.integrate(capgb, st + QGB);
959            gcgb = g;
960            ceqgb = ceq - gcgb * vgb + ctx.ag[0] * ctx.states.get(0, st + QGB);
961        }
962
963        // Load the current vector.
964        let ceqbs = typ * (cbs - gbs * vbs);
965        let ceqbd = typ * (cbd - gbd * vbd);
966        let (xnrm, xrev, cdreq);
967        if mode >= 0.0 {
968            xnrm = 1.0;
969            xrev = 0.0;
970            cdreq = typ * (cdrain - gds * vds - gm * vgs - gmbs * vbs);
971        } else {
972            xnrm = 0.0;
973            xrev = 1.0;
974            cdreq = -typ * (cdrain - gds * (-vds) - gm * vgd - gmbs * vbd);
975        }
976        // Saved for the convergence test (`mos6conv.c`).
977        self.cd = cd;
978        self.cbs = cbs;
979        self.cbd = cbd;
980        ctx.add_rhs(self.gate, -(typ * (ceqgs + ceqgb + ceqgd)));
981        ctx.add_rhs(self.bulk, -(ceqbs + ceqbd - typ * ceqgb));
982        ctx.add_rhs(self.d_p, ceqbd - cdreq + typ * ceqgd);
983        ctx.add_rhs(self.s_p, cdreq + ceqbs + typ * ceqgs);
984
985        // Load the Y matrix.
986        let gdpr = t.drain_conductance;
987        let gspr = t.source_conductance;
988        ctx.add(self.s[D_D], gdpr);
989        ctx.add(self.s[G_G], gcgd + gcgs + gcgb);
990        ctx.add(self.s[S_S], gspr);
991        ctx.add(self.s[B_B], gbd + gbs + gcgb);
992        ctx.add(self.s[DP_DP], gdpr + gds + gbd + xrev * (gm + gmbs) + gcgd);
993        ctx.add(self.s[SP_SP], gspr + gds + gbs + xnrm * (gm + gmbs) + gcgs);
994        ctx.add(self.s[D_DP], -gdpr);
995        ctx.add(self.s[G_B], -gcgb);
996        ctx.add(self.s[G_DP], -gcgd);
997        ctx.add(self.s[G_SP], -gcgs);
998        ctx.add(self.s[S_SP], -gspr);
999        ctx.add(self.s[B_G], -gcgb);
1000        ctx.add(self.s[B_DP], -gbd);
1001        ctx.add(self.s[B_SP], -gbs);
1002        ctx.add(self.s[DP_D], -gdpr);
1003        ctx.add(self.s[DP_G], (xnrm - xrev) * gm - gcgd);
1004        ctx.add(self.s[DP_B], -gbd + (xnrm - xrev) * gmbs);
1005        ctx.add(self.s[DP_SP], -gds - xnrm * (gm + gmbs));
1006        ctx.add(self.s[SP_G], -(xnrm - xrev) * gm - gcgs);
1007        ctx.add(self.s[SP_S], -gspr);
1008        ctx.add(self.s[SP_B], -gbs - (xnrm - xrev) * gmbs);
1009        ctx.add(self.s[SP_DP], -gds - xrev * (gm + gmbs));
1010    }
1011
1012    fn trunc(&self, t: &crate::integ::TerrCtx, time_step: &mut f64) {
1013        crate::integ::terr(self.st + QGS, t, time_step);
1014        crate::integ::terr(self.st + QGD, t, time_step);
1015        crate::integ::terr(self.st + QGB, t, time_step);
1016    }
1017
1018    /// The 3f5 has no `mos6acld.c`; a MOS6 contributes nothing to
1019    /// the AC matrix, exactly like the C's null `DEVacLoad`.
1020    fn load_ac(&mut self, _ctx: &mut AcCtx) {}
1021
1022    /// Neither the 3f5 nor ngspice-47 has a `mos6pzld.c`: the C skips
1023    /// the device and the determinant comes out without it. Dune
1024    /// refuses the deck instead (a silently missing device is a wrong
1025    /// answer).
1026    fn load_pz(&mut self, _ctx: &mut crate::matrix::PzCtx) -> Result<(), &'static str> {
1027        Err("the level-6 MOSFET has no pole-zero load (no mos6pzld.c in the 3f5 or ngspice)")
1028    }
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033    use super::*;
1034
1035    /// mos6inv's N10L5 model card, at TNOM.
1036    fn n10l5() -> Mos6Model {
1037        let mut m = Mos6Model::new(true);
1038        m.kc = Some(3.8921e-5);
1039        m.nc = 1.1739;
1040        m.kv = 0.91602;
1041        m.nv = 0.87225;
1042        m.lambda0 = Some(0.013333);
1043        m.lambda1 = 0.0046901;
1044        m.vto = Some(0.69486);
1045        m.gamma = Some(0.60309);
1046        m.phi = Some(1.0);
1047        m.tox = Some(1.98e-8);
1048        m.ld = 0.1e-6;
1049        m.nsub = Some(4.9999999e16);
1050        m.nss = Some(0.0);
1051        m.cj = Some(4.091e-4);
1052        m.mj = 0.307;
1053        m.pb = 1.0;
1054        m.cjsw = Some(3.078e-10);
1055        m.mjsw = 1.0e-2;
1056        m.cgso = 3.93e-10;
1057        m.cgdo = 3.93e-10;
1058        m.tpg = Some(1.0);
1059        m
1060    }
1061
1062    #[test]
1063    fn temp_uses_given_parameters_at_tnom() {
1064        let g = MosGeom {
1065            l: 1e-6,
1066            w: 5e-6,
1067            ad: 0.0,
1068            as_: 0.0,
1069            pd: 0.0,
1070            ps: 0.0,
1071            nrd: 1.0,
1072            nrs: 1.0,
1073        };
1074        let t = Mos6Temp::new("mn11", &n10l5(), &g, REFTEMP, REFTEMP).unwrap();
1075        assert!((t.t_kc - 3.8921e-5).abs() < 1e-12);
1076        assert!((t.t_phi - 1.0).abs() < 1e-12);
1077        assert!((t.t_vto - 0.69486).abs() < 1e-9);
1078        assert!((t.lambda0 - 0.013333).abs() < 1e-12);
1079        assert_eq!(t.drain_conductance, 0.0);
1080    }
1081
1082    #[test]
1083    fn lambda_alias_seeds_lambda0() {
1084        let mut m = Mos6Model::new(true);
1085        m.lambda = Some(0.02);
1086        let g = MosGeom {
1087            l: 1e-6,
1088            w: 1e-6,
1089            ad: 0.0,
1090            as_: 0.0,
1091            pd: 0.0,
1092            ps: 0.0,
1093            nrd: 1.0,
1094            nrs: 1.0,
1095        };
1096        let t = Mos6Temp::new("m1", &m, &g, REFTEMP, REFTEMP).unwrap();
1097        assert!((t.lambda0 - 0.02).abs() < 1e-15);
1098        // An explicit LAMBDA0 wins.
1099        m.lambda0 = Some(0.05);
1100        let t = Mos6Temp::new("m1", &m, &g, REFTEMP, REFTEMP).unwrap();
1101        assert!((t.lambda0 - 0.05).abs() < 1e-15);
1102    }
1103}