fix(examples): the common-emitter amplifier never left cutoff
analogRead printed a flat 1021-1023 on every loop — the collector pinned at Vcc. Measured in the running app: base 0.481 V emitter 0.001 V collector 4.994 V 0.481 V is below the ~0.65 V a 2N2222 needs, so the transistor never conducted and the "amplifier" was an open switch. The bias divider was fine on paper (47k/10k -> 0.88 V). What broke it was the 10k "coupling resistor" from the signal generator, whose own comment claimed it let the generator "wiggle the biased base without DC-shifting it". It does the opposite: a generator with 0 V offset is ground at DC, so that resistor sits in parallel with Rb2 — 10k || 10k = 5k -> Vb = 5 * 5/(47+5) = 0.481 V which is exactly the measured value. Replace it with a 1 uF coupling capacitor (159 ohm at the generator's 1 kHz: passes the signal, blocks the DC that was wrecking the bias), and take Rb2 to 15k so the collector sits near mid-rail — the operating point the sketch's own comment promises and the only one with room to swing both ways. The netlist snapshot could never have caught this: the text was exactly what the author intended. So the new test asserts the PHYSICS — bias in the active region, collector off both rails, and a collector that actually swings — resolving node names through the builder's pin map rather than hardcoding `n2`, and adding the 5 V rail the board supplies in the app.
This commit is contained in:
parent
5545089389
commit
08c9459c71
|
|
@ -1494,12 +1494,12 @@ R_autopull_n8 n8 0 100Meg
|
|||
exports[`netlist snapshot — circuits (41 examples) > bjt-common-emitter 1`] = `
|
||||
"* Velxio circuit
|
||||
R_rb1 n0 n2 47000
|
||||
R_rb2 n2 0 10000
|
||||
R_rb2 n2 0 15000
|
||||
R_rc n0 n3 4700
|
||||
R_re n1 0 1000
|
||||
Q_q1 n3 n2 n1 Q2N2222
|
||||
V_sg1 n4 0 SIN(0 0.05 1000)
|
||||
R_rin n4 n2 10000
|
||||
C_cin n4 n2 0.000001 IC=0
|
||||
.model Q2N2222 NPN(IS=1E-14 VAF=100 BF=200 IKF=0.3 XTB=1.5 BR=3 CJC=8E-12 CJE=25E-12 TR=100E-9 TF=400E-12 ITF=1 VTF=2 XTF=3 RB=10 RC=.3 RE=.2)
|
||||
.tran 5.000e-5 5.000e-3
|
||||
.end"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* The common-emitter example must actually AMPLIFY.
|
||||
*
|
||||
* The netlist snapshot froze this circuit's text for a long time while the
|
||||
* circuit did nothing: `analogRead(A0)` printed a flat 1021-1023 on every loop,
|
||||
* which is the collector pinned at Vcc. Measured in the running app, the bias
|
||||
* point was base 0.481 V, emitter 0.001 V, collector 4.994 V — the transistor
|
||||
* sat in cutoff, because the 10k "coupling resistor" from the signal generator
|
||||
* is in parallel with Rb2 at DC (a generator with 0 V offset is ground for DC):
|
||||
*
|
||||
* 10k || 10k = 5k -> Vb = 5 * 5/(47+5) = 0.481 V (needs ~0.65 V)
|
||||
*
|
||||
* A snapshot cannot catch that: the text was exactly what the author intended.
|
||||
* So assert the physics instead — bias in the active region, and an output that
|
||||
* swings with gain.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildNetlist } from '../simulation/spice/NetlistBuilder';
|
||||
import { exampleToBuildNetlistInput } from '../utils/exampleToBuildNetlistInput';
|
||||
import { exampleProjects } from '../data/examples';
|
||||
import { runNetlist } from './helpers/testSolver';
|
||||
|
||||
const EXAMPLE_ID = 'bjt-common-emitter';
|
||||
|
||||
/** The example's real netlist plus the pin->net map, board and all — the same
|
||||
* path the app takes. Dropping the board would drop the 5 V rail with it and
|
||||
* every node would read 0 V: a dead amplifier, but really a dead test.
|
||||
*
|
||||
* Node NAMES are resolved through the map rather than hardcoded: the builder
|
||||
* numbers nets in traversal order, so `n2` is not stably "the base" and a
|
||||
* hardcoded guess silently measures the wrong wire. */
|
||||
function rigFor(id: string) {
|
||||
const ex = exampleProjects.find((e) => e.id === id);
|
||||
if (!ex) throw new Error(`example ${id} not found`);
|
||||
const built = buildNetlist(exampleToBuildNetlistInput(ex));
|
||||
const netOf = (pin: string): string => {
|
||||
const n = built.pinNetMap.get(pin);
|
||||
if (!n) throw new Error(`no net for ${pin} — wiring changed?`);
|
||||
return n;
|
||||
};
|
||||
// In the app the board supplies its own rails (collectBoardPinStates marks 5V
|
||||
// as an output, so the builder emits a source for it). Building from the
|
||||
// example alone leaves that node unpowered, and an unpowered amplifier reads
|
||||
// 0 V everywhere — which would make this test pass or fail for the wrong
|
||||
// reason. Add the rail the board would provide.
|
||||
const vccNet = netOf('arduino-uno:5V');
|
||||
const netlist = built.netlist.replace(
|
||||
/^\.tran/m,
|
||||
`V_testrail ${vccNet} 0 DC 5\n.tran`,
|
||||
);
|
||||
return {
|
||||
netlist,
|
||||
base: `v(${netOf('q1:B')})`,
|
||||
emitter: `v(${netOf('q1:E')})`,
|
||||
collector: `v(${netOf('q1:C')})`,
|
||||
};
|
||||
}
|
||||
|
||||
/** Numeric magnitude of a possibly-complex vector sample. */
|
||||
const mag = (v: number | { real: number; imag: number }): number =>
|
||||
typeof v === 'number' ? v : Math.hypot(v.real, v.imag);
|
||||
|
||||
describe('bjt-common-emitter behaviour', () => {
|
||||
it('biases the transistor into the active region', async () => {
|
||||
const rig = rigFor(EXAMPLE_ID);
|
||||
const result = await runNetlist(rig.netlist);
|
||||
const base = mag(result.vAtLast(rig.base));
|
||||
const emitter = mag(result.vAtLast(rig.emitter));
|
||||
|
||||
expect(base).toBeGreaterThan(0.65); // above Vbe(on) — otherwise: cutoff
|
||||
expect(emitter).toBeGreaterThan(0.1); // current is actually flowing
|
||||
});
|
||||
|
||||
it('does not pin the collector to either rail', async () => {
|
||||
const rig = rigFor(EXAMPLE_ID);
|
||||
const result = await runNetlist(rig.netlist);
|
||||
const collector = mag(result.vAtLast(rig.collector));
|
||||
// The sketch's own comment promises a mid-rail bias. Anything within a few
|
||||
// hundred mV of a rail has no room to swing and reads as a flat ADC line.
|
||||
expect(collector).toBeGreaterThan(1.0);
|
||||
expect(collector).toBeLessThan(4.5);
|
||||
});
|
||||
|
||||
it('swings the collector — the output is an amplified signal, not a flat line', async () => {
|
||||
const rig = rigFor(EXAMPLE_ID);
|
||||
const result = await runNetlist(rig.netlist);
|
||||
const samples = result.vec(rig.collector).map(mag).filter(Number.isFinite);
|
||||
expect(samples.length).toBeGreaterThan(10);
|
||||
const swing = Math.max(...samples) - Math.min(...samples);
|
||||
// Input is 0.05 V peak (0.1 Vpp) and the stage's gain is -Rc/Re = -4.7, so
|
||||
// a working amplifier moves the collector by a good fraction of a volt.
|
||||
// A flat line — the bug — measures ~0.
|
||||
expect(swing).toBeGreaterThan(0.1);
|
||||
});
|
||||
});
|
||||
|
|
@ -553,7 +553,10 @@ void loop() {
|
|||
components: [
|
||||
UNO,
|
||||
{ type: 'wokwi-resistor', id: 'rb1', x: 350, y: 60, properties: { value: '47000' } },
|
||||
{ type: 'wokwi-resistor', id: 'rb2', x: 350, y: 180, properties: { value: '10000' } },
|
||||
// 47k/15k puts the base at 1.2 V -> Ve 0.51 V -> Ic 0.51 mA -> collector at
|
||||
// ~2.6 V, i.e. the mid-rail bias the sketch comment promises, so the
|
||||
// output can swing both ways instead of clipping against a rail.
|
||||
{ type: 'wokwi-resistor', id: 'rb2', x: 350, y: 180, properties: { value: '15000' } },
|
||||
{ type: 'wokwi-resistor', id: 'rc', x: 450, y: 60, properties: { value: '4700' } },
|
||||
{ type: 'wokwi-resistor', id: 're', x: 450, y: 280, properties: { value: '1000' } },
|
||||
{ type: 'wokwi-bjt-2n2222', id: 'q1', x: 450, y: 170, properties: {} },
|
||||
|
|
@ -564,9 +567,15 @@ void loop() {
|
|||
y: 140,
|
||||
properties: { waveform: 'sine', frequency: 1000, amplitude: 0.05, offset: 0 },
|
||||
},
|
||||
// Small "coupling" resistor from SG to the base node so SG can wiggle
|
||||
// the biased base without DC-shifting it.
|
||||
{ type: 'wokwi-resistor', id: 'rin', x: 300, y: 140, properties: { value: '10000' } },
|
||||
// Coupling CAPACITOR from the generator to the base. It has to be a cap:
|
||||
// a resistor here sits in parallel with Rb2 for DC (the generator is 0 V
|
||||
// at DC), which is what the 10k "coupling resistor" used to do —
|
||||
// 10k || 10k = 5k dropped the base from 0.88 V to 0.48 V, below the
|
||||
// ~0.65 V a 2N2222 needs, so the transistor sat in cutoff, the collector
|
||||
// stayed pinned at Vcc and analogRead printed a flat 1021-1023 forever.
|
||||
// 1 uF is 159 ohm at the generator's 1 kHz: it passes the signal and
|
||||
// blocks the DC that was wrecking the bias.
|
||||
{ type: 'wokwi-capacitor', id: 'cin', x: 300, y: 140, properties: { value: '1u' } },
|
||||
],
|
||||
wires: [
|
||||
// Bias divider: 5V → Rb1 → (base) → Rb2 → GND
|
||||
|
|
@ -581,8 +590,8 @@ void loop() {
|
|||
w('w7', ['q1', 'E'], ['re', '1']),
|
||||
w('w8', ['re', '2'], ['arduino-uno', 'GND'], '#000000'),
|
||||
// AC input into base through coupling R
|
||||
w('w9', ['sg1', 'SIG'], ['rin', '1']),
|
||||
w('w10', ['rin', '2'], ['q1', 'B']),
|
||||
w('w9', ['sg1', 'SIG'], ['cin', '1']),
|
||||
w('w10', ['cin', '2'], ['q1', 'B']),
|
||||
w('w11', ['sg1', 'GND'], ['arduino-uno', 'GND'], '#000000'),
|
||||
// A0 probes the collector
|
||||
w('w12', ['q1', 'C'], ['arduino-uno', 'A0'], '#ffaa00'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue