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:
parent
7508423c6e
commit
d9945d13b8
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue