velxio/frontend/src/__tests__/spice-mosfet-pwm.test.ts

140 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* End-to-end regression test for the `mosfet-pwm-led` example.
*
* Reproduces the exact topology of the gallery example (5V → R220 → LED →
* MOSFET drain, source-to-GND low-side switch, gate driven by an Arduino
* pin with a 100 kΩ pull-down) and verifies that:
*
* 1. The LED's V-sense source is emitted so ngspice exposes the branch
* current under `i(v_led1_sense)` — the key `BasicParts.ts` reads.
* 2. With the gate held LOW, almost no current flows through the LED.
* 3. With the gate driven HIGH (5V), a realistic LED current flows (a
* few mA, bounded by the 220 Ω series resistor and the LED forward
* drop + MOSFET R_DS(on)).
* 4. Intermediate gate voltages produce monotonically increasing current,
* i.e. the analog PWM-dimming behaviour that was previously broken.
*/
import { describe, it, expect } from 'vitest';
import { buildNetlist } from '../simulation/spice/NetlistBuilder';
import { runNetlist } from './helpers/testSolver';
import type { BuildNetlistInput } from '../simulation/spice/types';
function mosfetPwmLedNetlist(gateVolts: number) {
const input: BuildNetlistInput = {
components: [
{ id: 'rl', metadataId: 'resistor', properties: { value: '220' } },
{ id: 'led1', metadataId: 'led', properties: { color: 'white' } },
{ id: 'q1', metadataId: 'mosfet-2n7000', properties: {} },
{ id: 'rg', metadataId: 'resistor', properties: { value: '100000' } },
],
wires: [
// 5V → R → LED anode
{
id: 'w1',
start: { componentId: 'uno', pinName: '5V' },
end: { componentId: 'rl', pinName: '1' },
},
{
id: 'w2',
start: { componentId: 'rl', pinName: '2' },
end: { componentId: 'led1', pinName: 'A' },
},
// LED cathode → MOSFET drain
{
id: 'w3',
start: { componentId: 'led1', pinName: 'C' },
end: { componentId: 'q1', pinName: 'D' },
},
// Source to GND (low-side)
{
id: 'w4',
start: { componentId: 'q1', pinName: 'S' },
end: { componentId: 'uno', pinName: 'GND' },
},
// Gate driven from GPIO 9, plus pull-down to GND
{
id: 'w5',
start: { componentId: 'uno', pinName: '9' },
end: { componentId: 'q1', pinName: 'G' },
},
{
id: 'w6',
start: { componentId: 'q1', pinName: 'G' },
end: { componentId: 'rg', pinName: '1' },
},
{
id: 'w7',
start: { componentId: 'rg', pinName: '2' },
end: { componentId: 'uno', pinName: 'GND' },
},
],
boards: [
{
id: 'uno',
vcc: 5,
pins: {
'5V': { type: 'digital', v: 5 },
GND: { type: 'digital', v: 0 },
'9': { type: 'digital', v: gateVolts },
},
groundPinNames: ['GND'],
vccPinNames: ['5V'],
},
],
analysis: { kind: 'op' },
};
return buildNetlist(input).netlist;
}
describe('MOSFET PWM LED dimmer (mosfet-pwm-led example)', () => {
it('emits V-sense card so ngspice exposes i(v_led1_sense)', { timeout: 30_000 }, async () => {
const netlist = mosfetPwmLedNetlist(5);
expect(netlist).toMatch(/V_led1_sense /);
expect(netlist).toMatch(/D_led1 led1_sense_mid /);
const { variableNames } = await runNetlist(netlist);
const lowered = variableNames.map((n) => n.toLowerCase());
expect(lowered).toContain('i(v_led1_sense)');
});
it('gate LOW → LED current is ~0 (MOSFET off)', { timeout: 30_000 }, async () => {
const netlist = mosfetPwmLedNetlist(0);
const { dcValue } = await runNetlist(netlist);
// Convention inside the builder: V-sense sources are oriented from
// anode → mid-net, so conducting current is *negative* (flows into
// the V+ terminal). Compare magnitudes.
const i = Math.abs(dcValue('i(v_led1_sense)'));
expect(i).toBeLessThan(1e-6); // sub-µA leakage is fine
});
it('gate HIGH → LED conducts a realistic current (220 mA)', { timeout: 30_000 }, async () => {
const netlist = mosfetPwmLedNetlist(5);
const { dcValue } = await runNetlist(netlist);
const i = Math.abs(dcValue('i(v_led1_sense)'));
expect(i).toBeGreaterThan(2e-3);
expect(i).toBeLessThan(20e-3);
});
it(
'LED current increases monotonically as the gate voltage ramps 0 → 5V',
{ timeout: 60_000 },
async () => {
const gatePoints = [0, 1.0, 1.5, 2.0, 2.5, 3.5, 5.0];
const currents: number[] = [];
for (const vg of gatePoints) {
const { dcValue } = await runNetlist(mosfetPwmLedNetlist(vg));
currents.push(Math.abs(dcValue('i(v_led1_sense)')));
}
// 0V and 1V are below the MOSFET Vto (1.6V) — both near-zero.
expect(currents[0]).toBeLessThan(1e-6);
expect(currents[1]).toBeLessThan(1e-5);
// By 5V the MOSFET is fully on.
expect(currents[currents.length - 1]).toBeGreaterThan(1e-3);
// Each step above threshold should be ≥ the previous (within noise).
for (let i = 2; i < currents.length; i++) {
expect(currents[i]).toBeGreaterThanOrEqual(currents[i - 1] - 1e-6);
}
},
);
});