114 lines
3.4 KiB
JavaScript
114 lines
3.4 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { runNetlist } from '../src/spice/SpiceEngine.js';
|
|
|
|
/**
|
|
* Digital logic via ngspice B-sources (behavioral voltage sources).
|
|
* Uses the u() step function: u(x) = 1 if x>0, else 0.
|
|
*
|
|
* AND: 5 * u(V(a)-2.5) * u(V(b)-2.5)
|
|
* OR: 5 * (1 - (1-u(V(a)-2.5)) * (1-u(V(b)-2.5)))
|
|
* NAND: 5 * (1 - u(V(a)-2.5) * u(V(b)-2.5))
|
|
* XOR: 5 * ((u(V(a)-2.5) + u(V(b)-2.5)) - 2*u(V(a)-2.5)*u(V(b)-2.5))
|
|
* NOT: 5 * (1 - u(V(a)-2.5))
|
|
*/
|
|
|
|
describe('ngspice — digital gates via B-sources (u-function)', () => {
|
|
it('AND truth table', { timeout: 60_000 }, async () => {
|
|
const rows = [
|
|
{ a: 0, b: 0, expected: 0 },
|
|
{ a: 0, b: 5, expected: 0 },
|
|
{ a: 5, b: 0, expected: 0 },
|
|
{ a: 5, b: 5, expected: 5 },
|
|
];
|
|
for (const r of rows) {
|
|
const netlist = `AND
|
|
Va a 0 DC ${r.a}
|
|
Vb b 0 DC ${r.b}
|
|
Band y 0 V = 5 * u(V(a)-2.5) * u(V(b)-2.5)
|
|
Rload y 0 1k
|
|
.op
|
|
.end`;
|
|
const { dcValue } = await runNetlist(netlist);
|
|
expect(dcValue('v(y)')).toBeCloseTo(r.expected, 0);
|
|
}
|
|
});
|
|
|
|
it('NAND truth table', { timeout: 60_000 }, async () => {
|
|
const rows = [
|
|
{ a: 0, b: 0, expected: 5 },
|
|
{ a: 0, b: 5, expected: 5 },
|
|
{ a: 5, b: 0, expected: 5 },
|
|
{ a: 5, b: 5, expected: 0 },
|
|
];
|
|
for (const r of rows) {
|
|
const netlist = `NAND
|
|
Va a 0 DC ${r.a}
|
|
Vb b 0 DC ${r.b}
|
|
Bnand y 0 V = 5 * (1 - u(V(a)-2.5) * u(V(b)-2.5))
|
|
Rload y 0 1k
|
|
.op
|
|
.end`;
|
|
const { dcValue } = await runNetlist(netlist);
|
|
expect(dcValue('v(y)')).toBeCloseTo(r.expected, 0);
|
|
}
|
|
});
|
|
|
|
it('XOR truth table', { timeout: 60_000 }, async () => {
|
|
const rows = [
|
|
{ a: 0, b: 0, expected: 0 },
|
|
{ a: 0, b: 5, expected: 5 },
|
|
{ a: 5, b: 0, expected: 5 },
|
|
{ a: 5, b: 5, expected: 0 },
|
|
];
|
|
for (const r of rows) {
|
|
const netlist = `XOR
|
|
Va a 0 DC ${r.a}
|
|
Vb b 0 DC ${r.b}
|
|
Bxor y 0 V = 5 * (u(V(a)-2.5) + u(V(b)-2.5) - 2*u(V(a)-2.5)*u(V(b)-2.5))
|
|
Rload y 0 1k
|
|
.op
|
|
.end`;
|
|
const { dcValue } = await runNetlist(netlist);
|
|
expect(dcValue('v(y)')).toBeCloseTo(r.expected, 0);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('ngspice — logic + analog mixed', () => {
|
|
it('AND gate drives an RC low-pass: filter smooths the square wave', { timeout: 60_000 }, async () => {
|
|
const netlist = `AND -> RC
|
|
Va a 0 PULSE(0 5 0 1n 1n 1m 2m)
|
|
Vb b 0 PULSE(0 5 0 1n 1n 0.75m 1.5m)
|
|
Band y 0 V = 5 * u(V(a)-2.5) * u(V(b)-2.5)
|
|
R1 y filt 10k
|
|
C1 filt 0 1u IC=0
|
|
.tran 10u 20m
|
|
.end`;
|
|
const { vec } = await runNetlist(netlist);
|
|
const t = vec('time');
|
|
const filt = vec('v(filt)');
|
|
const y = vec('v(y)');
|
|
|
|
// After 5τ (50ms wait is overkill — actually 5·10kΩ·1µF = 50ms exactly,
|
|
// but 20ms is enough to see settling happen).
|
|
const last = filt.slice(-50);
|
|
const mean = last.reduce((s, v) => s + v, 0) / last.length;
|
|
|
|
// Filter ripple should be much smaller than 5 V swing (the raw AND gate output)
|
|
const filtMin = Math.min(...last);
|
|
const filtMax = Math.max(...last);
|
|
const ripple = filtMax - filtMin;
|
|
expect(ripple).toBeLessThan(1.5);
|
|
|
|
// Over the whole run, the raw AND output must have toggled between 0 and 5
|
|
const rawMin = Math.min(...y);
|
|
const rawMax = Math.max(...y);
|
|
expect(rawMin).toBeLessThan(0.5);
|
|
expect(rawMax).toBeGreaterThan(4.5);
|
|
|
|
// Mean of the filtered signal is strictly between 0 and 5 V
|
|
expect(mean).toBeGreaterThan(0);
|
|
expect(mean).toBeLessThan(5);
|
|
});
|
|
});
|