feat(sim): Phase 2.1 — migrate MOSFETs to VDMOS macro-models

Switches 3 of the 4 simulated MOSFETs from Level=1 Shichman-Hodges
to LTSpice VDMOS macro-models. VDMOS captures real-device behaviour
(Ron, gate charge Qg, gate-drain Miller capacitance Cgdmax/Cgdmin,
body diode) that Level=1 fundamentally can't model.

Instance line changes from
  M_id D G S S MODEL L=2u W=200u            (4-terminal NMOS + W/L)
to
  M_id D G S MODEL                          (3-terminal VDMOS)

Parts migrated:
  mosfet-2n7000  → 2N7002 VDMOS (Vto=1.6, Ron=2 ohm — matches old Vto)
  mosfet-irf540  → IRF530 VDMOS (Vto=4, Ron=160m — IRF540 missing
                                  from LTSpice library, IRF530 is the
                                  closest same-series part)
  mosfet-irf9540 → IRF9640 VDMOS (pchan, Vto=-3.5 — IRF9540 missing,
                                  IRF9640 is the 200V P-channel sub)

mosfet-fqp27p06 kept on Level=1 (no upstream VDMOS equivalent yet).

spice-mosfet-pwm regression test still passes: Id=8.6 mA at Vgs=5V,
0 at Vgs=0V, monotonic across the ramp. All 155 SPICE + analog
examples + lockdown tests pass.

Phase 2.1 lockdown test added — verifies VDMOS-shape instance line
(5 tokens, no L=/W=) and that the .model card carries `VDMOS(`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
davidmonterocrespo24 2026-05-15 17:03:49 +02:00
parent 7508423c6e
commit d9945d13b8
2 changed files with 60 additions and 10 deletions

View File

@ -71,6 +71,37 @@ describe('Phase 2 — diode reverse-recovery and junction capacitance', () => {
});
});
describe('Phase 2.1 — MOSFETs use VDMOS 3-terminal syntax', () => {
it.each([
['mosfet-2n7000', 'M2N7000'],
['mosfet-irf540', 'MIRF540'],
['mosfet-irf9540', 'MIRF9540'],
])('%s emits a VDMOS .model and a 3-terminal Mxxx card', (id, modelName) => {
const comp: PlacedComponent = { id: 'q1', metadataId: id, properties: {} };
const result = componentToSpice(comp, netLookupStub, { vcc: 5 });
if (!result) throw new Error(`no emission for ${id}`);
const m = Array.from(result.modelsUsed).find((s) => s.includes(modelName));
expect(m, `no model card found for ${modelName}`).toBeDefined();
expect(m).toMatch(/VDMOS\(/);
// Instance card must NOT include the old Level=1 body terminal + W/L.
const instance = result.cards.find((c) => c.startsWith('M_'));
expect(instance).toBeDefined();
expect(instance).not.toMatch(/L=2u/);
expect(instance).not.toMatch(/W=\d/);
// 3-terminal Mxxx: `M_id D G S MODEL` → 5 tokens.
expect((instance ?? '').trim().split(/\s+/)).toHaveLength(5);
});
it('mosfet-fqp27p06 still on Level=1 (no upstream VDMOS model yet)', () => {
const comp: PlacedComponent = { id: 'q1', metadataId: 'mosfet-fqp27p06', properties: {} };
const result = componentToSpice(comp, netLookupStub, { vcc: 5 });
if (!result) throw new Error('no emission for mosfet-fqp27p06');
const m = Array.from(result.modelsUsed).find((s) => s.includes('MFQP27P06'));
expect(m).toMatch(/Level=1/);
});
});
describe('Phase 2 — relay flyback keeps the canonical D1N4148 string', () => {
it('relay emits the same D1N4148 .model string as a standalone diode', () => {
const standaloneModels = emit('diode-1n4148');

View File

@ -281,18 +281,26 @@ const MAPPERS: Record<string, Mapper> = {
};
},
// MOSFETs — NMOS
// Level=1 Shichman-Hodges with moderate W/L avoids ngspice convergence
// issues seen with Level=3 + W=0.1 m (which is literally 100 mm channel
// width — unphysical and causes .op to hang).
// MOSFETs — NMOS. Phase 2.1: VDMOS macro-models from LTSpice-Libraries.
// VDMOS is 3-terminal (D G S) — no body, no L/W — and uses physical
// parameters (Ron, Vto, gate capacitances, Qg) instead of Level=1's
// process-level Kp/W/L. Models reflect manufacturer datasheets so the
// simulator now sees real Ron, switching speed, and gate-charge effects.
//
// Library coverage gaps: IRF540 and IRF9540 are not in LTSpice's
// `standard.mos`. IRF530 (100 V N-MOS, same series) substitutes for IRF540.
// IRF9640 (200 V P-MOS) substitutes for IRF9540 — both close enough that
// a casual circuit drawn around "IRF540" still behaves correctly.
'mosfet-2n7000': (comp, netLookup) => {
const d = netLookup('D');
const g = netLookup('G');
const s = netLookup('S');
if (!d || !g || !s) return null;
return {
cards: [`M_${comp.id} ${d} ${g} ${s} ${s} M2N7000 L=2u W=200u`],
modelsUsed: new Set(['.model M2N7000 NMOS(Level=1 Vto=1.6 Kp=50u Lambda=0.01)']),
cards: [`M_${comp.id} ${d} ${g} ${s} M2N7000`],
modelsUsed: new Set([
'.model M2N7000 VDMOS(Rg=3 Vto=1.6 Rd=0 Rs=.75 Rb=.14 Kp=.17 mtriode=1.25 Cgdmax=80p Cgdmin=12p Cgs=50p Cjo=50p Is=.04p Vds=60 Ron=2 Qg=1.5n)',
]),
};
},
'mosfet-irf540': (comp, netLookup) => {
@ -301,8 +309,12 @@ const MAPPERS: Record<string, Mapper> = {
const s = netLookup('S');
if (!d || !g || !s) return null;
return {
cards: [`M_${comp.id} ${d} ${g} ${s} ${s} MIRF540 L=2u W=2m`],
modelsUsed: new Set(['.model MIRF540 NMOS(Level=1 Vto=3 Kp=20u Lambda=0.01)']),
cards: [`M_${comp.id} ${d} ${g} ${s} MIRF540`],
modelsUsed: new Set([
// Substitute: LTSpice ships IRF530 (same TO-220, 100 V, slightly
// smaller die). Close enough for the canvas.
'.model MIRF540 VDMOS(Rg=3 Vto=4 Rd=50m Rs=12m Rb=60m Kp=5 lambda=.01 Cgdmax=1n Cgdmin=.26n Cgs=.2n Cjo=.4n Is=52p Vds=100 Ron=160m Qg=26n)',
]),
};
},
@ -313,10 +325,17 @@ const MAPPERS: Record<string, Mapper> = {
const s = netLookup('S');
if (!d || !g || !s) return null;
return {
cards: [`M_${comp.id} ${d} ${g} ${s} ${s} MIRF9540 L=2u W=2m`],
modelsUsed: new Set(['.model MIRF9540 PMOS(Level=1 Vto=-3 Kp=20u Lambda=0.01)']),
cards: [`M_${comp.id} ${d} ${g} ${s} MIRF9540`],
modelsUsed: new Set([
// Substitute: LTSpice ships IRF9640. 200 V vs IRF9540's 100 V, but
// identical pin-out and similar Vto. The `pchan` keyword is what
// tells the VDMOS engine to flip polarity.
'.model MIRF9540 VDMOS(pchan Rg=3 Vto=-3.5 Rd=.15 Rs=.15 Rb=.15 Kp=8 lambda=.01 mtriode=.5 Cgdmax=1.5n Cgdmin=.07n Cgs=1n Cjo=1n Is=38p Vds=-200 Ron=.5 Qg=44n)',
]),
};
},
// FQP27P06 has no good LTSpice equivalent — kept on Level=1 PMOS with the
// historical parameter set. Upgrade when a Fairchild VDMOS model is found.
'mosfet-fqp27p06': (comp, netLookup) => {
const d = netLookup('D');
const g = netLookup('G');