- SVG waveform plots (svg_plot.py): pure-SVG timeseries, Bode, spectrum
generation with plot_waveform MCP tool — no matplotlib dependency
- Circuit tuning tool (tune_circuit): single-shot simulate → measure →
compare targets → suggest adjustments workflow for iterative design
- 5 new circuit templates: Sallen-Key lowpass, boost converter,
instrumentation amplifier, current mirror, transimpedance amplifier
(both netlist and .asc schematic generators, 15 total templates)
- Fix all 6 prompts to return list[Message] per FastMCP 2.x spec
- Add ltspice://templates and ltspice://template/{name} resources
- Add troubleshoot_simulation prompt
- Integration tests for RC lowpass and non-inverting amp (2/4 pass;
CE amp and Colpitts oscillator have pre-existing schematic bugs)
- 360 unit tests passing, ruff clean
1927 lines
72 KiB
Python
1927 lines
72 KiB
Python
"""Programmatic LTspice .asc schematic file generation.
|
|
|
|
Generates graphical schematics (the .asc format LTspice uses for its GUI),
|
|
not just text netlists. Components are placed using absolute pin positions
|
|
derived from the .asy symbol files and rotated with the standard CCW
|
|
transform: R90 applies (px, py) → (-py, px) relative to the component origin.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
# LTspice grid spacing -- all coordinates should be multiples of this
|
|
GRID = 80
|
|
|
|
_SPICE_SUFFIXES = {
|
|
"T": 1e12, "G": 1e9, "MEG": 1e6, "K": 1e3,
|
|
"M": 1e-3, "U": 1e-6, "N": 1e-9, "P": 1e-12, "F": 1e-15,
|
|
}
|
|
|
|
|
|
def _parse_spice_value(value: str) -> float:
|
|
"""Convert a SPICE-style value string to a float (e.g., '100k' → 100000.0)."""
|
|
value = value.strip()
|
|
try:
|
|
return float(value)
|
|
except ValueError:
|
|
pass
|
|
upper = value.upper()
|
|
for suffix, mult in sorted(_SPICE_SUFFIXES.items(), key=lambda x: -len(x[0])):
|
|
if upper.endswith(suffix):
|
|
try:
|
|
return float(value[: len(value) - len(suffix)]) * mult
|
|
except ValueError:
|
|
continue
|
|
raise ValueError(f"Cannot parse SPICE value: {value!r}")
|
|
|
|
# Pin positions (relative to component origin) from LTspice .asy files.
|
|
# R0 orientation. Key = symbol name, value = list of (px, py) per pin.
|
|
_PIN_OFFSETS: dict[str, list[tuple[int, int]]] = {
|
|
# 2-pin passives & sources
|
|
"voltage": [(0, 16), (0, 96)], # pin+ , pin-
|
|
"res": [(16, 16), (16, 96)], # pinA , pinB
|
|
"cap": [(16, 0), (16, 64)], # pinA , pinB
|
|
"ind": [(16, 16), (16, 96)], # pinA , pinB (same body as res)
|
|
"diode": [(16, 0), (16, 64)], # anode(+) , cathode(-)
|
|
# 3-pin semiconductors
|
|
"npn": [(64, 0), (0, 48), (64, 96)], # C , B , E
|
|
"pnp": [(64, 0), (0, 48), (64, 96)], # C , B , E (same geometry)
|
|
"nmos": [(48, 0), (0, 80), (48, 96)], # D , G , S
|
|
"pmos": [(48, 0), (0, 80), (48, 96)], # D , G , S
|
|
# 4-pin MOSFETs (with body)
|
|
"nmos4": [(48, 0), (0, 80), (48, 96), (48, 48)], # D , G , S , B
|
|
"pmos4": [(48, 0), (0, 80), (48, 96), (48, 48)], # D , G , S , B
|
|
# 5-pin op-amp (note: negative offsets = pins extend left/above origin)
|
|
"OpAmps/UniversalOpamp2": [
|
|
(-32, 16), # In+ (non-inverting)
|
|
(-32, -16), # In- (inverting)
|
|
(0, -32), # V+ (positive supply)
|
|
(0, 32), # V- (negative supply)
|
|
(32, 0), # OUT
|
|
],
|
|
}
|
|
|
|
|
|
def _rotate(px: int, py: int, rotation: int) -> tuple[int, int]:
|
|
"""Apply LTspice rotation to a relative pin offset.
|
|
|
|
LTspice rotations are counterclockwise: (px, py) → (-py, px) for R90.
|
|
"""
|
|
if rotation == 0:
|
|
return px, py
|
|
if rotation == 90:
|
|
return -py, px
|
|
if rotation == 180:
|
|
return -px, -py
|
|
if rotation == 270:
|
|
return py, -px
|
|
return px, py
|
|
|
|
|
|
def pin_position(
|
|
symbol: str, pin_index: int, cx: int, cy: int, rotation: int = 0
|
|
) -> tuple[int, int]:
|
|
"""Compute absolute pin position for a component.
|
|
|
|
Args:
|
|
symbol: Base symbol name (``"res"``, ``"cap"``, ``"voltage"``, ...)
|
|
pin_index: 0 for first pin, 1 for second pin, etc.
|
|
cx: Component origin X
|
|
cy: Component origin Y
|
|
rotation: 0, 90, 180, or 270
|
|
|
|
Returns:
|
|
Absolute (x, y) of the pin.
|
|
"""
|
|
offsets = _PIN_OFFSETS.get(symbol, [(0, 0), (0, 80)])
|
|
px, py = offsets[pin_index % len(offsets)]
|
|
rx, ry = _rotate(px, py, rotation)
|
|
return cx + rx, cy + ry
|
|
|
|
|
|
@dataclass
|
|
class _WireEntry:
|
|
x1: int
|
|
y1: int
|
|
x2: int
|
|
y2: int
|
|
|
|
|
|
@dataclass
|
|
class _SymbolEntry:
|
|
symbol: str
|
|
name: str
|
|
value: str
|
|
x: int
|
|
y: int
|
|
rotation: int # 0, 90, 180, 270
|
|
windows: list[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class _FlagEntry:
|
|
x: int
|
|
y: int
|
|
name: str
|
|
|
|
|
|
@dataclass
|
|
class _TextEntry:
|
|
x: int
|
|
y: int
|
|
content: str
|
|
|
|
|
|
class AscSchematic:
|
|
"""Builder for LTspice .asc schematic files.
|
|
|
|
All ``add_*`` methods return ``self`` for chaining::
|
|
|
|
sch = (AscSchematic()
|
|
.add_component("res", "R1", "1k", 160, 176, rotation=90)
|
|
.add_wire(80, 176, 160, 176)
|
|
.add_ground(80, 256))
|
|
"""
|
|
|
|
def __init__(self, sheet_w: int = 880, sheet_h: int = 680) -> None:
|
|
self._sheet_w = sheet_w
|
|
self._sheet_h = sheet_h
|
|
self._wires: list[_WireEntry] = []
|
|
self._symbols: list[_SymbolEntry] = []
|
|
self._flags: list[_FlagEntry] = []
|
|
self._texts: list[_TextEntry] = []
|
|
|
|
# -- builder methods -----------------------------------------------------
|
|
|
|
def add_component(
|
|
self,
|
|
symbol: str,
|
|
name: str,
|
|
value: str,
|
|
x: int,
|
|
y: int,
|
|
rotation: int = 0,
|
|
) -> AscSchematic:
|
|
"""Place a component symbol.
|
|
|
|
Args:
|
|
symbol: LTspice symbol name (``res``, ``cap``, ``voltage``, etc.)
|
|
name: Instance name (``R1``, ``C1``, ``V1``, ...)
|
|
value: Component value (``1k``, ``100n``, ``AC 1``, ...)
|
|
x: X coordinate (should be on 80-pixel grid)
|
|
y: Y coordinate
|
|
rotation: 0, 90, 180, or 270 degrees
|
|
"""
|
|
windows: list[str] = []
|
|
# For resistors and inductors placed at R90, shift the WINDOW lines
|
|
# so labels sit neatly above/below the body
|
|
if rotation == 90 and symbol in ("res", "ind", "ind2"):
|
|
windows = [
|
|
"WINDOW 0 0 56 VBottom 2",
|
|
"WINDOW 3 32 56 VTop 2",
|
|
]
|
|
self._symbols.append(_SymbolEntry(symbol, name, value, x, y, rotation, windows))
|
|
return self
|
|
|
|
def add_wire(self, x1: int, y1: int, x2: int, y2: int) -> AscSchematic:
|
|
"""Add a wire segment between two points."""
|
|
self._wires.append(_WireEntry(x1, y1, x2, y2))
|
|
return self
|
|
|
|
def add_ground(self, x: int, y: int) -> AscSchematic:
|
|
"""Place a ground flag (net name ``0``)."""
|
|
self._flags.append(_FlagEntry(x, y, "0"))
|
|
return self
|
|
|
|
def add_net_label(self, name: str, x: int, y: int) -> AscSchematic:
|
|
"""Place a named net label (e.g., ``out``, ``vdd``)."""
|
|
self._flags.append(_FlagEntry(x, y, name))
|
|
return self
|
|
|
|
def add_directive(self, text: str, x: int, y: int) -> AscSchematic:
|
|
"""Add a SPICE directive (rendered with ``!`` prefix)."""
|
|
self._texts.append(_TextEntry(x, y, text))
|
|
return self
|
|
|
|
# -- output --------------------------------------------------------------
|
|
|
|
def render(self) -> str:
|
|
"""Render the schematic to an ``.asc`` format string."""
|
|
lines: list[str] = []
|
|
lines.append("Version 4")
|
|
lines.append(f"SHEET 1 {self._sheet_w} {self._sheet_h}")
|
|
|
|
for w in self._wires:
|
|
lines.append(f"WIRE {w.x1} {w.y1} {w.x2} {w.y2}")
|
|
|
|
for f in self._flags:
|
|
lines.append(f"FLAG {f.x} {f.y} {f.name}")
|
|
|
|
for s in self._symbols:
|
|
lines.append(f"SYMBOL {s.symbol} {s.x} {s.y} R{s.rotation}")
|
|
for win in s.windows:
|
|
lines.append(win)
|
|
lines.append(f"SYMATTR InstName {s.name}")
|
|
if s.value:
|
|
lines.append(f"SYMATTR Value {s.value}")
|
|
|
|
for t in self._texts:
|
|
lines.append(f"TEXT {t.x} {t.y} Left 2 !{t.content}")
|
|
|
|
return "\n".join(lines) + "\n"
|
|
|
|
def save(self, path: Path | str) -> Path:
|
|
"""Write the schematic to an ``.asc`` file on disk."""
|
|
path = Path(path)
|
|
path.write_text(self.render(), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
# ============================================================================
|
|
# Layout helper functions -- auto-placed, ready-to-simulate schematics
|
|
# ============================================================================
|
|
|
|
|
|
def generate_rc_lowpass(r: str = "1k", c: str = "100n") -> AscSchematic:
|
|
"""Generate an RC lowpass filter schematic with AC analysis.
|
|
|
|
Signal flow (left to right)::
|
|
|
|
V1 --[R1]-- out --+
|
|
|
|
|
[C1]
|
|
|
|
|
GND
|
|
|
|
Args:
|
|
r: Resistor value (e.g., ``"1k"``, ``"4.7k"``)
|
|
c: Capacitor value (e.g., ``"100n"``, ``"10p"``)
|
|
|
|
Returns:
|
|
An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
|
|
"""
|
|
sch = AscSchematic()
|
|
|
|
# Component origins (all on 16-pixel sub-grid for pin alignment)
|
|
# V1 at (80, 80) R0: pin+ = (80, 96), pin- = (80, 176)
|
|
# R1 at (160, 80) R0: pinA = (176, 96), pinB = (176, 176)
|
|
# C1 at (256, 176) R0: pinA = (272, 176), pinB = (272, 240)
|
|
|
|
v1p = pin_position("voltage", 0, 80, 80) # (80, 96)
|
|
v1n = pin_position("voltage", 1, 80, 80) # (80, 176)
|
|
r1a = pin_position("res", 0, 160, 80) # (176, 96)
|
|
r1b = pin_position("res", 1, 160, 80) # (176, 176)
|
|
c1a = pin_position("cap", 0, 256, 176) # (272, 176)
|
|
c1b = pin_position("cap", 1, 256, 176) # (272, 240)
|
|
|
|
# Wires
|
|
sch.add_wire(*v1p, *r1a) # V1+ to R1 top
|
|
sch.add_wire(*r1b, *c1a) # R1 bottom to C1 top (junction = "out")
|
|
|
|
# Components
|
|
sch.add_component("voltage", "V1", "AC 1", 80, 80)
|
|
sch.add_component("res", "R1", r, 160, 80)
|
|
sch.add_component("cap", "C1", c, 256, 176)
|
|
|
|
# Ground flags at V1- and C1 bottom
|
|
sch.add_ground(*v1n)
|
|
sch.add_ground(*c1b)
|
|
|
|
# Output net label at the R1-C1 junction
|
|
sch.add_net_label("out", *r1b)
|
|
|
|
# Simulation directive
|
|
sch.add_directive(".ac dec 100 1 1meg", 80, 296)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_voltage_divider(
|
|
r1: str = "10k",
|
|
r2: str = "10k",
|
|
vin: str = "5",
|
|
) -> AscSchematic:
|
|
"""Generate a voltage divider schematic with operating-point analysis.
|
|
|
|
Topology (vertical)::
|
|
|
|
V1+ --[R1]-- out --[R2]-- GND
|
|
V1- = GND
|
|
|
|
Args:
|
|
r1: Top resistor value
|
|
r2: Bottom resistor value
|
|
vin: DC input voltage
|
|
|
|
Returns:
|
|
An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
|
|
"""
|
|
sch = AscSchematic()
|
|
|
|
# Vertical layout: V1 on left, R1 and R2 stacked vertically on right
|
|
# V1 at (80, 80) R0: pin+ = (80, 96), pin- = (80, 176)
|
|
# R1 at (176, 0) R0: pinA = (192, 16), pinB = (192, 96)
|
|
# R2 at (176, 96) R0: pinA = (192, 112), pinB = (192, 192)
|
|
|
|
v1p = pin_position("voltage", 0, 80, 80) # (80, 96)
|
|
v1n = pin_position("voltage", 1, 80, 80) # (80, 176)
|
|
r1a = pin_position("res", 0, 176, 0) # (192, 16)
|
|
r1b = pin_position("res", 1, 176, 0) # (192, 96)
|
|
r2a = pin_position("res", 0, 176, 96) # (192, 112)
|
|
r2b = pin_position("res", 1, 176, 96) # (192, 192)
|
|
|
|
# Wires: V1+ to R1 top, R1 bottom to R2 top (junction = "out")
|
|
sch.add_wire(*v1p, *r1a) # need to connect (80,96) to (192,16)
|
|
# Route: V1+ up then right then down to R1 top
|
|
sch.add_wire(80, 96, 80, 16)
|
|
sch.add_wire(80, 16, 192, 16)
|
|
# R1 bottom to R2 top
|
|
sch.add_wire(*r1b, *r2a)
|
|
|
|
# Components
|
|
sch.add_component("voltage", "V1", vin, 80, 80)
|
|
sch.add_component("res", "R1", r1, 176, 0)
|
|
sch.add_component("res", "R2", r2, 176, 96)
|
|
|
|
# Ground
|
|
sch.add_ground(*v1n)
|
|
sch.add_ground(*r2b)
|
|
|
|
# Net label at R1-R2 junction
|
|
sch.add_net_label("out", *r1b)
|
|
|
|
# Directive
|
|
sch.add_directive(".op", 80, 240)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_inverting_amp(
|
|
rin: str = "10k",
|
|
rf: str = "100k",
|
|
) -> AscSchematic:
|
|
"""Generate an inverting op-amp amplifier schematic.
|
|
|
|
Topology::
|
|
|
|
V1 --[Rin]--> In-(inv) ---[Rf]--- out
|
|
In+(non-inv) --> GND
|
|
Supply: Vpos=+15V, Vneg=-15V
|
|
|
|
The gain is ``-Rf/Rin``.
|
|
|
|
Args:
|
|
rin: Input resistor value
|
|
rf: Feedback resistor value
|
|
|
|
Returns:
|
|
An ``AscSchematic`` ready to ``.save()`` or ``.render()``.
|
|
"""
|
|
sch = AscSchematic(sheet_w=1200, sheet_h=880)
|
|
oa_sym = "OpAmps/UniversalOpamp2"
|
|
|
|
# Op-amp U1 at (512, 336) — same position as non-inverting amp
|
|
inp = pin_position(oa_sym, 0, 512, 336) # In+ = (480, 352)
|
|
inn = pin_position(oa_sym, 1, 512, 336) # In- = (480, 320)
|
|
vp = pin_position(oa_sym, 2, 512, 336) # V+ = (512, 304)
|
|
vn = pin_position(oa_sym, 3, 512, 336) # V- = (512, 368)
|
|
out = pin_position(oa_sym, 4, 512, 336) # OUT = (544, 336)
|
|
|
|
# Signal source V1 at (80, 256)
|
|
v1p = pin_position("voltage", 0, 80, 256) # (80, 272)
|
|
v1n = pin_position("voltage", 1, 80, 256) # (80, 352)
|
|
|
|
# Rin: horizontal (R90) between V1 and In-
|
|
# Origin (400, 304): pinA=(384,320), pinB=(304,320)
|
|
rin_a = pin_position("res", 0, 400, 304, 90) # (384, 320)
|
|
rin_b = pin_position("res", 1, 400, 304, 90) # (304, 320)
|
|
|
|
# Rf: horizontal (R90) above the op-amp for feedback
|
|
# Origin (560, 208): pinA=(544,224), pinB=(464,224)
|
|
rf_a = pin_position("res", 0, 560, 208, 90) # (544, 224)
|
|
rf_b = pin_position("res", 1, 560, 208, 90) # (464, 224)
|
|
|
|
# Supply: Vpos at (688, 176), Vneg at (688, 416)
|
|
vpos_p = pin_position("voltage", 0, 688, 176) # (688, 192)
|
|
vpos_n = pin_position("voltage", 1, 688, 176) # (688, 272)
|
|
vneg_p = pin_position("voltage", 0, 688, 416) # (688, 432)
|
|
vneg_n = pin_position("voltage", 1, 688, 416) # (688, 512)
|
|
|
|
# === WIRING ===
|
|
# V1+ to Rin pinB: route RIGHT first then down (avoids crossing V1-)
|
|
sch.add_wire(v1p[0], v1p[1], rin_b[0], v1p[1]) # (80,272) → (304,272)
|
|
sch.add_wire(rin_b[0], v1p[1], *rin_b) # (304,272) → (304,320)
|
|
|
|
# Rin pinA to In-
|
|
sch.add_wire(*rin_a, inn[0], inn[1]) # (384,320) → (480,320)
|
|
|
|
# Rf feedback: OUT up to Rf pinA, Rf pinB left and down to inv junction
|
|
sch.add_wire(out[0], out[1], *rf_a) # (544,336) → (544,224)
|
|
sch.add_wire(*rf_b, rin_a[0], rf_b[1]) # (464,224) → (384,224)
|
|
sch.add_wire(rin_a[0], rf_b[1], *rin_a) # (384,224) → (384,320)
|
|
|
|
# Supply wiring
|
|
sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1]) # V+ up to (512,192)
|
|
sch.add_wire(vp[0], vpos_p[1], *vpos_p) # right to Vpos+
|
|
sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1]) # V- down to (512,512)
|
|
sch.add_wire(vn[0], vneg_n[1], *vneg_n) # right to Vneg-
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("voltage", "V1", "AC 1", 80, 256)
|
|
sch.add_component(oa_sym, "U1", "", 512, 336)
|
|
sch.add_component("res", "Rin", rin, 400, 304, rotation=90)
|
|
sch.add_component("res", "Rf", rf, 560, 208, rotation=90)
|
|
sch.add_component("voltage", "Vpos", "15", 688, 176)
|
|
sch.add_component("voltage", "Vneg", "15", 688, 416)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*v1n)
|
|
sch.add_ground(*inp) # In+ to GND (inverting configuration)
|
|
sch.add_ground(*vpos_n)
|
|
sch.add_ground(*vneg_p)
|
|
sch.add_net_label("out", *out)
|
|
|
|
sch.add_directive(".ac dec 100 1 1meg", 80, 560)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_non_inverting_amp(
|
|
rin: str = "10k",
|
|
rf: str = "100k",
|
|
) -> AscSchematic:
|
|
"""Generate a non-inverting op-amp amplifier schematic.
|
|
|
|
Topology::
|
|
|
|
V1 --> In+
|
|
U1 --> out
|
|
GND --> Rin --> In-
|
|
^--- Rf --- out
|
|
|
|
Gain = 1 + Rf/Rin. Supply: +/-15V
|
|
|
|
Args:
|
|
rin: Input resistor (In- to GND)
|
|
rf: Feedback resistor (In- to out)
|
|
"""
|
|
sch = AscSchematic(sheet_w=1200, sheet_h=880)
|
|
oa_sym = "OpAmps/UniversalOpamp2"
|
|
|
|
# Op-amp U1 at (512, 336)
|
|
inp = pin_position(oa_sym, 0, 512, 336) # In+ = (480, 352)
|
|
inn = pin_position(oa_sym, 1, 512, 336) # In- = (480, 320)
|
|
vp = pin_position(oa_sym, 2, 512, 336) # V+ = (512, 304)
|
|
vn = pin_position(oa_sym, 3, 512, 336) # V- = (512, 368)
|
|
out = pin_position(oa_sym, 4, 512, 336) # OUT = (544, 336)
|
|
|
|
# Signal source V1 at (80, 256)
|
|
v1p = pin_position("voltage", 0, 80, 256) # (80, 272)
|
|
v1n = pin_position("voltage", 1, 80, 256) # (80, 352)
|
|
|
|
# Rin: In- node down to GND. Want pinA at In- junction (352, 320).
|
|
# res origin so that pinA = (352, 320): origin = (336, 304)
|
|
rin_a = pin_position("res", 0, 336, 304) # (352, 320)
|
|
rin_b = pin_position("res", 1, 336, 304) # (352, 400)
|
|
|
|
# Rf: horizontal (R90) above the op-amp for feedback path
|
|
# R90 res: pinA offset=(-16,16), pinB offset=(-96,16)
|
|
# Place at origin (560, 208) so: pinA=(544, 224), pinB=(464, 224)
|
|
rf_a = pin_position("res", 0, 560, 208, 90) # (544, 224)
|
|
rf_b = pin_position("res", 1, 560, 208, 90) # (464, 224)
|
|
|
|
# Supply: Vpos at (688, 176), Vneg at (688, 416)
|
|
vpos_p = pin_position("voltage", 0, 688, 176) # (688, 192) = vdd
|
|
vpos_n = pin_position("voltage", 1, 688, 176) # (688, 272)
|
|
vneg_p = pin_position("voltage", 0, 688, 416) # (688, 432)
|
|
vneg_n = pin_position("voltage", 1, 688, 416) # (688, 512) = vss
|
|
|
|
# === WIRING ===
|
|
# V1+ to In+: go RIGHT then DOWN then RIGHT to avoid crossing both
|
|
# V1- at (80,352) and In- at (480,320).
|
|
# Route: (80,272) → (448,272) → (448,352) → (480,352)
|
|
sch.add_wire(v1p[0], v1p[1], 448, v1p[1]) # (80,272) → (448,272) horiz
|
|
sch.add_wire(448, v1p[1], 448, inp[1]) # (448,272) → (448,352) vert
|
|
sch.add_wire(448, inp[1], inp[0], inp[1]) # (448,352) → (480,352) horiz
|
|
|
|
# In- to Rin junction
|
|
sch.add_wire(inn[0], inn[1], rin_a[0], rin_a[1]) # (480,320) → (352,320)
|
|
|
|
# Rf feedback: OUT up to Rf pinA, Rf pinB left and down to junction
|
|
sch.add_wire(out[0], out[1], rf_a[0], rf_a[1]) # (544,336) → (544,224) vertical
|
|
sch.add_wire(rf_b[0], rf_b[1], rin_a[0], rf_b[1]) # (464,224) → (352,224) horizontal
|
|
sch.add_wire(rin_a[0], rf_b[1], rin_a[0], rin_a[1]) # (352,224) → (352,320) vertical
|
|
|
|
# Supply wiring
|
|
sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1]) # V+ up to (512,192)
|
|
sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1]) # right to Vpos+
|
|
sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1]) # V- down to (512,512)
|
|
sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1]) # right to Vneg-
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("voltage", "V1", "AC 1", 80, 256)
|
|
sch.add_component(oa_sym, "U1", "", 512, 336)
|
|
sch.add_component("res", "Rin", rin, 336, 304)
|
|
sch.add_component("res", "Rf", rf, 560, 208, rotation=90)
|
|
sch.add_component("voltage", "Vpos", "15", 688, 176)
|
|
sch.add_component("voltage", "Vneg", "15", 688, 416)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*v1n)
|
|
sch.add_ground(*rin_b)
|
|
sch.add_ground(*vpos_n)
|
|
sch.add_ground(*vneg_p)
|
|
sch.add_net_label("out", *out)
|
|
|
|
sch.add_directive(".ac dec 100 1 1meg", 80, 560)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_common_emitter_amp(
|
|
rc: str = "2.2k",
|
|
rb1: str = "56k",
|
|
rb2: str = "12k",
|
|
re: str = "1k",
|
|
cc_in: str = "10u",
|
|
cc_out: str = "10u",
|
|
ce: str = "47u",
|
|
vcc: str = "12",
|
|
bjt_model: str = "2N2222",
|
|
) -> AscSchematic:
|
|
"""Generate a common-emitter amplifier schematic.
|
|
|
|
Topology::
|
|
|
|
Vcc --[RC]--> collector --[CC_out]--> out
|
|
Vcc --[RB1]--> base
|
|
in --[CC_in]--> base
|
|
base --[RB2]--> GND
|
|
emitter --[RE]--> GND
|
|
emitter --[CE]--> GND (bypass)
|
|
|
|
Args:
|
|
rc: Collector resistor
|
|
rb1: Base bias resistor (to Vcc)
|
|
rb2: Base bias resistor (to GND)
|
|
re: Emitter resistor
|
|
cc_in: Input coupling capacitor
|
|
cc_out: Output coupling capacitor
|
|
ce: Emitter bypass capacitor
|
|
vcc: Supply voltage
|
|
bjt_model: BJT model name
|
|
"""
|
|
sch = AscSchematic(sheet_w=1200, sheet_h=880)
|
|
|
|
# Q1 (NPN) at (400, 288): C=(464, 288), B=(400, 336), E=(464, 384)
|
|
qc = pin_position("npn", 0, 400, 288) # collector
|
|
qb = pin_position("npn", 1, 400, 288) # base
|
|
qe = pin_position("npn", 2, 400, 288) # emitter
|
|
|
|
# RC from Vcc rail to collector.
|
|
# Want pinB at collector (464, 288). res at (448, 192): pinB=(464, 288). Good.
|
|
rc_a = pin_position("res", 0, 448, 192) # (464, 208) = vcc rail
|
|
|
|
# RE from emitter to GND.
|
|
# Want pinA at emitter (464, 384). res at (448, 368): pinA=(464, 384).
|
|
re_b = pin_position("res", 1, 448, 368) # (464, 464) = GND
|
|
|
|
# RB1 from Vcc rail to base. Vertical, placed to the left of Q1.
|
|
# Want pinB at base Y (336). res at (240, 240): pinA=(256, 256), pinB=(256, 336)
|
|
rb1_a = pin_position("res", 0, 240, 240) # (256, 256)
|
|
rb1_b = pin_position("res", 1, 240, 240) # (256, 336)
|
|
|
|
# RB2 from base to GND.
|
|
# Want pinA at base Y (336). res at (240, 320): pinA=(256, 336), pinB=(256, 416)
|
|
rb2_b = pin_position("res", 1, 240, 320) # (256, 416)
|
|
|
|
# CC_in (input coupling cap) horizontal, connecting input to base.
|
|
# cap R90: pinA=origin+(-16,0)→(origin_x, origin_y+16)... wait let me recalculate.
|
|
# cap R90: pinA offset (16,0) → rotate90 → (0, 16), pinB offset (16,64) → (-64, 16)
|
|
# At origin (368, 320): pinA = (368, 336), pinB = (304, 336)
|
|
# pinA at (368, 336) close to base (400, 336). pinB at (304, 336).
|
|
# Wire from pinA (368, 336) to base (400, 336).
|
|
ccin_a = pin_position("cap", 0, 368, 320, 90) # (368, 336)
|
|
ccin_b = pin_position("cap", 1, 368, 320, 90) # (304, 336)
|
|
|
|
# CC_out (output coupling cap) to the right of collector.
|
|
# cap R90 at origin (560, 272): pinA = (560, 288), pinB = (496, 288)
|
|
# pinB near collector (464, 288), pinA extends right to output
|
|
# Actually pinB = (560-64, 288) = (496, 288). Wire from collector (464,288) to (496,288).
|
|
ccout_a = pin_position("cap", 0, 560, 272, 90) # (560, 288)
|
|
ccout_b = pin_position("cap", 1, 560, 272, 90) # (496, 288)
|
|
|
|
# CE (emitter bypass cap) parallel to RE.
|
|
# Place to the right of RE. cap at (528, 384): pinA=(544, 384), pinB=(544, 448)
|
|
ce_a = pin_position("cap", 0, 528, 384) # (544, 384)
|
|
ce_b = pin_position("cap", 1, 528, 384) # (544, 448)
|
|
|
|
# Vcc source at (80, 96): pin+=(80, 112), pin-=(80, 192)
|
|
vcc_p = pin_position("voltage", 0, 80, 96) # (80, 112)
|
|
vcc_n = pin_position("voltage", 1, 80, 96) # (80, 192)
|
|
|
|
# Vin source at (80, 288): pin+=(80, 304), pin-=(80, 384)
|
|
vin_p = pin_position("voltage", 0, 80, 288) # (80, 304)
|
|
vin_n = pin_position("voltage", 1, 80, 288) # (80, 384)
|
|
|
|
# === WIRING ===
|
|
# Vcc rail at y=208 — route RIGHT from Vcc+ first (avoid crossing Vcc- at 80,192)
|
|
vcc_y = rc_a[1] # 208
|
|
sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1]) # (80,112) → (160,112)
|
|
sch.add_wire(160, vcc_p[1], 160, vcc_y) # (160,112) → (160,208)
|
|
sch.add_wire(160, vcc_y, rc_a[0], vcc_y) # (160,208) → (464,208) = RC top
|
|
sch.add_wire(rb1_a[0], rb1_a[1], rb1_a[0], vcc_y) # RB1 top up to rail
|
|
sch.add_wire(rb1_a[0], vcc_y, rc_a[0], vcc_y) # connect to rail
|
|
|
|
# RB1 bottom to base, RB2 top at same junction
|
|
sch.add_wire(rb1_b[0], rb1_b[1], qb[0], qb[1]) # RB1 bottom → base
|
|
|
|
# CC_in to base
|
|
sch.add_wire(ccin_a[0], ccin_a[1], qb[0], qb[1]) # CC_in right → base
|
|
|
|
# Input: Vin+ to CC_in left
|
|
sch.add_wire(vin_p[0], vin_p[1], ccin_b[0], ccin_b[1]) # Vin+ → CC_in left
|
|
|
|
# Collector to CC_out
|
|
sch.add_wire(qc[0], qc[1], ccout_b[0], ccout_b[1]) # collector → CC_out left
|
|
|
|
# Emitter to CE (bypass cap in parallel with RE)
|
|
sch.add_wire(qe[0], qe[1], ce_a[0], ce_a[1]) # emitter → CE top
|
|
# CE bottom to RE bottom (shared GND rail)
|
|
sch.add_wire(ce_b[0], ce_b[1], re_b[0], re_b[1]) # CE bot → RE bot
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("npn", "Q1", bjt_model, 400, 288)
|
|
sch.add_component("res", "RC", rc, 448, 192)
|
|
sch.add_component("res", "RE", re, 448, 368)
|
|
sch.add_component("res", "RB1", rb1, 240, 240)
|
|
sch.add_component("res", "RB2", rb2, 240, 320)
|
|
sch.add_component("cap", "CC1", cc_in, 368, 320, rotation=90)
|
|
sch.add_component("cap", "CC2", cc_out, 560, 272, rotation=90)
|
|
sch.add_component("cap", "CE", ce, 528, 384)
|
|
sch.add_component("voltage", "Vcc", vcc, 80, 96)
|
|
sch.add_component("voltage", "Vin", "SINE(0 10m 1k)", 80, 288)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*vcc_n)
|
|
sch.add_ground(*vin_n)
|
|
sch.add_ground(*rb2_b)
|
|
sch.add_ground(*re_b)
|
|
sch.add_net_label("out", *ccout_a)
|
|
|
|
sch.add_directive(".tran 5m", 80, 528)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_colpitts_oscillator(
|
|
ind: str = "1u",
|
|
c1: str = "100p",
|
|
c2: str = "100p",
|
|
rb: str = "47k",
|
|
rc: str = "1k",
|
|
re: str = "470",
|
|
vcc: str = "12",
|
|
bjt_model: str = "2N2222",
|
|
) -> AscSchematic:
|
|
"""Generate a Colpitts oscillator schematic.
|
|
|
|
Topology::
|
|
|
|
Vcc --[RC]--> collector --[L1]--> tank
|
|
Vcc --[RB]--> base
|
|
tank --[C1]--> base
|
|
tank --[C2]--> GND
|
|
emitter --[RE]--> GND
|
|
f ~ 1/(2*pi*sqrt(L*C1*C2/(C1+C2)))
|
|
"""
|
|
sch = AscSchematic(sheet_w=1200, sheet_h=880)
|
|
|
|
# Q1 NPN at (400, 288): C=(464,288), B=(400,336), E=(464,384)
|
|
qc = pin_position("npn", 0, 400, 288)
|
|
qb = pin_position("npn", 1, 400, 288)
|
|
|
|
# RC: Vcc to collector. res at (448, 192): pinA=(464,208), pinB=(464,288)
|
|
rc_a = pin_position("res", 0, 448, 192)
|
|
|
|
# RE: emitter to GND. res at (448, 368): pinA=(464,384), pinB=(464,464)
|
|
re_b = pin_position("res", 1, 448, 368)
|
|
|
|
# RB: Vcc to base. res at (240, 240): pinA=(256,256), pinB=(256,336)
|
|
rb_a = pin_position("res", 0, 240, 240)
|
|
rb_b = pin_position("res", 1, 240, 240)
|
|
|
|
# Vcc source at (80, 96): pin+=(80,112), pin-=(80,192)
|
|
vcc_p = pin_position("voltage", 0, 80, 96)
|
|
vcc_n = pin_position("voltage", 1, 80, 96)
|
|
|
|
# L1 horizontal (R90) from collector to tank node.
|
|
# ind R90 at origin (576, 272):
|
|
# pinA = origin+(-16,16) = (560, 288) — right/tank side
|
|
# pinB = origin+(-96,16) = (480, 288) — left/collector side
|
|
l1_a = pin_position("ind", 0, 576, 272, 90) # (560, 288) = tank
|
|
l1_b = pin_position("ind", 1, 576, 272, 90) # (480, 288) = near collector
|
|
|
|
# C1 from tank down to base (feedback).
|
|
# cap at (544, 288): pinA=(560, 288)=tank (same as l1_a), pinB=(560, 352)
|
|
c1_b = pin_position("cap", 1, 544, 288) # (560, 352)
|
|
|
|
# C2 from tank to GND.
|
|
# cap at (640, 288): pinA=(656, 288), pinB=(656, 352)
|
|
c2_a = pin_position("cap", 0, 640, 288) # (656, 288)
|
|
c2_b = pin_position("cap", 1, 640, 288) # (656, 352)
|
|
|
|
# === WIRING ===
|
|
vcc_y = rc_a[1] # 208
|
|
|
|
# Vcc rail: route RIGHT from Vcc+ first (avoid crossing Vcc- at 80,192)
|
|
sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1]) # (80,112) → (160,112)
|
|
sch.add_wire(160, vcc_p[1], 160, vcc_y) # (160,112) → (160,208)
|
|
sch.add_wire(160, vcc_y, rc_a[0], vcc_y) # (160,208) → RC top
|
|
sch.add_wire(rb_a[0], rb_a[1], rb_a[0], vcc_y)
|
|
sch.add_wire(rb_a[0], vcc_y, rc_a[0], vcc_y)
|
|
|
|
# RB bottom to base
|
|
sch.add_wire(rb_b[0], rb_b[1], qb[0], qb[1])
|
|
|
|
# Collector to L1 left pin
|
|
sch.add_wire(qc[0], qc[1], l1_b[0], l1_b[1])
|
|
|
|
# Tank node: L1 right pin to C1 top (same point at 560,288)
|
|
# Wire from tank to C2 top
|
|
sch.add_wire(l1_a[0], l1_a[1], c2_a[0], c2_a[1])
|
|
|
|
# C1 bottom to base: L-route down then left
|
|
sch.add_wire(c1_b[0], c1_b[1], qb[0], c1_b[1]) # horizontal
|
|
sch.add_wire(qb[0], c1_b[1], qb[0], qb[1]) # vertical to base
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("npn", "Q1", bjt_model, 400, 288)
|
|
sch.add_component("res", "RC", rc, 448, 192)
|
|
sch.add_component("res", "RE", re, 448, 368)
|
|
sch.add_component("res", "RB", rb, 240, 240)
|
|
sch.add_component("ind", "L1", ind, 576, 272, rotation=90)
|
|
sch.add_component("cap", "C1", c1, 544, 288)
|
|
sch.add_component("cap", "C2", c2, 640, 288)
|
|
sch.add_component("voltage", "Vcc", vcc, 80, 96)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*vcc_n)
|
|
sch.add_ground(*re_b)
|
|
sch.add_ground(*c2_b)
|
|
sch.add_net_label("out", *qc)
|
|
|
|
sch.add_directive(".tran 100u", 80, 528)
|
|
sch.add_directive(".ic V(out)=6", 80, 560)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_differential_amp(
|
|
r1: str = "10k",
|
|
r2: str = "10k",
|
|
r3: str = "10k",
|
|
r4: str = "10k",
|
|
) -> AscSchematic:
|
|
"""Generate a differential amplifier schematic.
|
|
|
|
Topology::
|
|
|
|
V1 --[R1]--> inv(-) --[R2]--> out
|
|
V2 --[R3]--> non-inv(+)
|
|
non-inv(+) --[R4]--> GND
|
|
Supply: +/-15V
|
|
Vout = (R2/R1) * (V2 - V1) when R2/R1 = R4/R3
|
|
|
|
Args:
|
|
r1: Input resistor to inverting node
|
|
r2: Feedback resistor
|
|
r3: Input resistor to non-inverting node
|
|
r4: Non-inverting node to GND
|
|
"""
|
|
sch = AscSchematic(sheet_w=1200, sheet_h=880)
|
|
oa_sym = "OpAmps/UniversalOpamp2"
|
|
|
|
# U1 at (512, 336): In+=(480,352), In-=(480,320), V+=(512,304), V-=(512,368), OUT=(544,336)
|
|
inp = pin_position(oa_sym, 0, 512, 336) # (480, 352)
|
|
inn = pin_position(oa_sym, 1, 512, 336) # (480, 320)
|
|
vp = pin_position(oa_sym, 2, 512, 336) # (512, 304)
|
|
vn = pin_position(oa_sym, 3, 512, 336) # (512, 368)
|
|
out = pin_position(oa_sym, 4, 512, 336) # (544, 336)
|
|
|
|
# R1 horizontal (R90) from V1 to inv junction.
|
|
# R90 at (448, 304): pinA=(432,320), pinB=(352,320)
|
|
r1_a = pin_position("res", 0, 448, 304, 90) # (432, 320) near In-
|
|
r1_b = pin_position("res", 1, 448, 304, 90) # (352, 320) toward V1
|
|
|
|
# R2 horizontal (R90) above op-amp for feedback.
|
|
# R90 at (560, 208): pinA=(544,224), pinB=(464,224)
|
|
r2_a = pin_position("res", 0, 560, 208, 90) # (544, 224) near OUT
|
|
r2_b = pin_position("res", 1, 560, 208, 90) # (464, 224)
|
|
|
|
# R3 horizontal (R90) from V2 to noninv junction.
|
|
# R90 at (448, 336): pinA=(432,352), pinB=(352,352)
|
|
r3_a = pin_position("res", 0, 448, 336, 90) # (432, 352) near In+
|
|
r3_b = pin_position("res", 1, 448, 336, 90) # (352, 352) toward V2
|
|
|
|
# R4 vertical from noninv junction to GND.
|
|
# res at (336, 352): pinA=(352,368), pinB=(352,448)
|
|
r4_a = pin_position("res", 0, 336, 352) # (352, 368)
|
|
r4_b = pin_position("res", 1, 336, 352) # (352, 448)
|
|
|
|
# V1 at (80, 224): pin+=(80,240), pin-=(80,320)
|
|
v1p = pin_position("voltage", 0, 80, 224)
|
|
v1n = pin_position("voltage", 1, 80, 224)
|
|
|
|
# V2 at (80, 384): pin+=(80,400), pin-=(80,480)
|
|
v2p = pin_position("voltage", 0, 80, 384)
|
|
v2n = pin_position("voltage", 1, 80, 384)
|
|
|
|
# Supply: Vpos at (688, 176), Vneg at (688, 416)
|
|
vpos_p = pin_position("voltage", 0, 688, 176)
|
|
vpos_n = pin_position("voltage", 1, 688, 176)
|
|
vneg_p = pin_position("voltage", 0, 688, 416)
|
|
vneg_n = pin_position("voltage", 1, 688, 416)
|
|
|
|
# === WIRING ===
|
|
# Inv path: R1 pinA to In-
|
|
sch.add_wire(r1_a[0], r1_a[1], inn[0], inn[1])
|
|
|
|
# V1 to R1 pinB: route RIGHT first (avoid crossing V1- at 80,320)
|
|
sch.add_wire(v1p[0], v1p[1], r1_b[0], v1p[1]) # (80,240) → (352,240)
|
|
sch.add_wire(r1_b[0], v1p[1], r1_b[0], r1_b[1]) # (352,240) → (352,320)
|
|
|
|
# R2 feedback: OUT up to R2 pinA, R2 pinB left and down to inv junction
|
|
sch.add_wire(out[0], out[1], r2_a[0], r2_a[1]) # (544,336)→(544,224)
|
|
sch.add_wire(r2_b[0], r2_b[1], r1_a[0], r2_b[1]) # (464,224)→(432,224)
|
|
sch.add_wire(r1_a[0], r2_b[1], r1_a[0], r1_a[1]) # (432,224)→(432,320)
|
|
|
|
# Noninv path: R3 pinA to In+
|
|
sch.add_wire(r3_a[0], r3_a[1], inp[0], inp[1])
|
|
|
|
# V2 to R3 pinB: route (80,400) → (80,352) → (352,352)
|
|
sch.add_wire(v2p[0], v2p[1], v2p[0], r3_b[1])
|
|
sch.add_wire(v2p[0], r3_b[1], r3_b[0], r3_b[1])
|
|
|
|
# R4 from noninv junction to GND
|
|
sch.add_wire(r3_b[0], r3_b[1], r4_a[0], r4_a[1]) # (352,352)→(352,368)
|
|
|
|
# Supply wiring
|
|
sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1])
|
|
sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1])
|
|
sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1])
|
|
sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1])
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("voltage", "V1", "AC 1", 80, 224)
|
|
sch.add_component("voltage", "V2", "AC 1", 80, 384)
|
|
sch.add_component(oa_sym, "U1", "", 512, 336)
|
|
sch.add_component("res", "R1", r1, 448, 304, rotation=90)
|
|
sch.add_component("res", "R2", r2, 560, 208, rotation=90)
|
|
sch.add_component("res", "R3", r3, 448, 336, rotation=90)
|
|
sch.add_component("res", "R4", r4, 336, 352)
|
|
sch.add_component("voltage", "Vpos", "15", 688, 176)
|
|
sch.add_component("voltage", "Vneg", "15", 688, 416)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*v1n)
|
|
sch.add_ground(*v2n)
|
|
sch.add_ground(*r4_b)
|
|
sch.add_ground(*vpos_n)
|
|
sch.add_ground(*vneg_p)
|
|
sch.add_net_label("out", *out)
|
|
|
|
sch.add_directive(".ac dec 100 1 1meg", 80, 560)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_buck_converter(
|
|
ind: str = "10u",
|
|
c_out: str = "100u",
|
|
r_load: str = "10",
|
|
v_in: str = "12",
|
|
duty_cycle: float = 0.5,
|
|
freq: str = "100k",
|
|
mosfet_model: str = "IRF540N",
|
|
diode_model: str = "1N5819",
|
|
) -> AscSchematic:
|
|
"""Generate a buck (step-down) converter schematic.
|
|
|
|
Topology::
|
|
|
|
Vin+ ─── drain
|
|
M1 (NMOS switch)
|
|
source ── sw ──[L1]── out ──+
|
|
| |
|
|
[D1] [Cout] [Rload]
|
|
| | |
|
|
GND GND GND
|
|
|
|
Gate driven by PULSE source at specified frequency and duty cycle.
|
|
|
|
Args:
|
|
ind: Inductor value
|
|
c_out: Output capacitor
|
|
r_load: Load resistor
|
|
v_in: Input voltage
|
|
duty_cycle: Switching duty cycle (0.0-1.0)
|
|
freq: Switching frequency
|
|
mosfet_model: NMOS model name
|
|
diode_model: Freewheeling diode model
|
|
"""
|
|
sch = AscSchematic(sheet_w=1040, sheet_h=680)
|
|
|
|
# Compute PULSE timing
|
|
freq_hz = _parse_spice_value(freq)
|
|
period = 1.0 / freq_hz
|
|
t_on = period * duty_cycle
|
|
t_rise = period * 0.01
|
|
t_fall = t_rise
|
|
pulse_val = (
|
|
f"PULSE(0 {v_in} 0 {t_rise:.4g} {t_fall:.4g} {t_on:.4g} {period:.4g})"
|
|
)
|
|
|
|
# Vin at (80, 48): pin+=(80,64), pin-=(80,144)
|
|
vin_p = pin_position("voltage", 0, 80, 48)
|
|
vin_n = pin_position("voltage", 1, 80, 48)
|
|
|
|
# Vgate at (80, 256): pin+=(80,272), pin-=(80,352)
|
|
vg_p = pin_position("voltage", 0, 80, 256)
|
|
vg_n = pin_position("voltage", 1, 80, 256)
|
|
|
|
# NMOS at (256, 64): D=(304,64), G=(256,144), S=(304,160)
|
|
md = pin_position("nmos", 0, 256, 64) # drain
|
|
mg = pin_position("nmos", 1, 256, 64) # gate
|
|
ms = pin_position("nmos", 2, 256, 64) # source
|
|
|
|
# Diode R180 at (320, 224): cathode at sw (auto-connects), anode at GND
|
|
d_anode = pin_position("diode", 0, 320, 224, 180) # (304, 224)
|
|
|
|
# L1 R270 at (288, 176): pinA=(304,160)=sw (auto-connects), pinB=(384,160)=output
|
|
l1_b = pin_position("ind", 1, 288, 176, 270) # (384, 160)
|
|
|
|
# Cout at (368, 160): pinA=(384,160)=output, pinB=(384,224)
|
|
cout_b = pin_position("cap", 1, 368, 160) # (384, 224)
|
|
|
|
# Rload at (448, 144): pinA=(464,160), pinB=(464,240)
|
|
rl_a = pin_position("res", 0, 448, 144) # (464, 160)
|
|
rl_b = pin_position("res", 1, 448, 144) # (464, 240)
|
|
|
|
# === WIRING ===
|
|
sch.add_wire(vin_p[0], vin_p[1], md[0], md[1]) # Vin+ to drain
|
|
|
|
# Gate drive: Vgate+ → route to gate
|
|
sch.add_wire(vg_p[0], vg_p[1], 160, vg_p[1])
|
|
sch.add_wire(160, vg_p[1], 160, mg[1])
|
|
sch.add_wire(160, mg[1], mg[0], mg[1])
|
|
|
|
# Output to Rload
|
|
sch.add_wire(l1_b[0], l1_b[1], rl_a[0], rl_a[1])
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("voltage", "Vin", v_in, 80, 48)
|
|
sch.add_component("voltage", "Vgate", pulse_val, 80, 256)
|
|
sch.add_component("nmos", "M1", mosfet_model, 256, 64)
|
|
sch.add_component("diode", "D1", diode_model, 320, 224, rotation=180)
|
|
sch.add_component("ind", "L1", ind, 288, 176, rotation=270)
|
|
sch.add_component("cap", "Cout", c_out, 368, 160)
|
|
sch.add_component("res", "Rload", r_load, 448, 144)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*vin_n)
|
|
sch.add_ground(*vg_n)
|
|
sch.add_ground(*d_anode)
|
|
sch.add_ground(*cout_b)
|
|
sch.add_ground(*rl_b)
|
|
sch.add_net_label("sw", *ms)
|
|
sch.add_net_label("out", *l1_b)
|
|
|
|
sch.add_directive(f".tran {period * 200:.4g}", 80, 420)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_ldo_regulator(
|
|
r1: str = "10k",
|
|
r2: str = "10k",
|
|
pass_transistor: str = "IRF9540N",
|
|
v_in: str = "8",
|
|
v_ref: str = "2.5",
|
|
) -> AscSchematic:
|
|
"""Generate a simple LDO voltage regulator schematic.
|
|
|
|
Topology::
|
|
|
|
Vin ──[PMOS source]──[PMOS drain]── out ──+
|
|
gate |
|
|
| [R1] [Cout] [Rload]
|
|
[Op-amp OUT] | | |
|
|
+ = Vref fb GND GND
|
|
- = fb |
|
|
[R2]
|
|
|
|
|
GND
|
|
|
|
Vout = Vref * (1 + R1/R2)
|
|
|
|
Args:
|
|
r1: Top feedback resistor (out to fb)
|
|
r2: Bottom feedback resistor (fb to GND)
|
|
pass_transistor: PMOS model name
|
|
v_in: Input voltage
|
|
v_ref: Reference voltage
|
|
"""
|
|
sch = AscSchematic(sheet_w=1200, sheet_h=880)
|
|
oa_sym = "OpAmps/UniversalOpamp2"
|
|
|
|
# PMOS M1 at (400, 48): D=(448,48)=out, G=(400,128)=gate, S=(448,144)=vin
|
|
m_d = pin_position("pmos", 0, 400, 48) # drain (448, 48) = output
|
|
m_g = pin_position("pmos", 1, 400, 48) # gate (400, 128)
|
|
m_s = pin_position("pmos", 2, 400, 48) # source (448, 144) = vin
|
|
|
|
# Vin at (80, 48): pin+=(80,64), pin-=(80,144)
|
|
vin_p = pin_position("voltage", 0, 80, 48)
|
|
vin_n = pin_position("voltage", 1, 80, 48)
|
|
|
|
# Vref at (80, 336): pin+=(80,352), pin-=(80,432)
|
|
vref_p = pin_position("voltage", 0, 80, 336)
|
|
vref_n = pin_position("voltage", 1, 80, 336)
|
|
|
|
# Op-amp U1 at (304, 304): In+=(272,320), In-=(272,288), V+=(304,272),
|
|
# V-=(304,336), OUT=(336,304)
|
|
oa_inp = pin_position(oa_sym, 0, 304, 304) # (272, 320) = vref
|
|
oa_inn = pin_position(oa_sym, 1, 304, 304) # (272, 288) = fb
|
|
oa_vp = pin_position(oa_sym, 2, 304, 304) # (304, 272) = V+
|
|
oa_vn = pin_position(oa_sym, 3, 304, 304) # (304, 336) = V-
|
|
oa_out = pin_position(oa_sym, 4, 304, 304) # (336, 304) = gate drive
|
|
|
|
# R1 from output down to fb node. res at (560, 160): pinA=(576,176), pinB=(576,256)
|
|
r1_a = pin_position("res", 0, 560, 160) # (576, 176)
|
|
r1_b = pin_position("res", 1, 560, 160) # (576, 256) = fb
|
|
|
|
# R2 from fb to GND. res at (560, 256): pinA=(576,272), pinB=(576,352)
|
|
r2_a = pin_position("res", 0, 560, 256) # (576, 272) = fb
|
|
r2_b = pin_position("res", 1, 560, 256) # (576, 352) = GND
|
|
|
|
# Cout at (640, 48): pinA=(656,48)=out, pinB=(656,112)=GND
|
|
cout_a = pin_position("cap", 0, 640, 48) # (656, 48)
|
|
cout_b = pin_position("cap", 1, 640, 48) # (656, 112)
|
|
|
|
# Rload at (720, 48): pinA=(736,64), pinB=(736,144)
|
|
rl_a = pin_position("res", 0, 720, 48) # (736, 64)
|
|
rl_b = pin_position("res", 1, 720, 48) # (736, 144)
|
|
|
|
# === WIRING ===
|
|
# Vin to PMOS source: (80,64) right to (448,64), down to source (448,144)
|
|
sch.add_wire(vin_p[0], vin_p[1], m_s[0], vin_p[1]) # horizontal
|
|
sch.add_wire(m_s[0], vin_p[1], m_s[0], m_s[1]) # vertical to source
|
|
|
|
# PMOS drain (output) right to R1, Cout, Rload
|
|
# Drain at (448, 48). Wire right to Cout pinA (656, 48)
|
|
sch.add_wire(m_d[0], m_d[1], cout_a[0], cout_a[1])
|
|
# Continue to Rload: (656,48) → (736,48), down to pinA (736,64)
|
|
sch.add_wire(cout_a[0], cout_a[1], rl_a[0], cout_a[1])
|
|
sch.add_wire(rl_a[0], cout_a[1], rl_a[0], rl_a[1])
|
|
# R1 top: down from output rail. (576,48) → (576,176)
|
|
sch.add_wire(m_d[0], m_d[1], r1_a[0], m_d[1]) # (448,48)→(576,48)
|
|
sch.add_wire(r1_a[0], m_d[1], r1_a[0], r1_a[1]) # (576,48)→(576,176)
|
|
|
|
# Op-amp output to PMOS gate
|
|
sch.add_wire(oa_out[0], oa_out[1], m_g[0], oa_out[1]) # horizontal
|
|
sch.add_wire(m_g[0], oa_out[1], m_g[0], m_g[1]) # vertical to gate
|
|
|
|
# Vref to op-amp In+
|
|
sch.add_wire(vref_p[0], vref_p[1], vref_p[0], oa_inp[1])
|
|
sch.add_wire(vref_p[0], oa_inp[1], oa_inp[0], oa_inp[1])
|
|
|
|
# Feedback (fb) to op-amp In-: R1 bottom/R2 top junction at (576,256/272)
|
|
# Wire from fb junction to In- (272, 288)
|
|
sch.add_wire(r2_a[0], r2_a[1], r2_a[0], oa_inn[1]) # (576,272)→(576,288)
|
|
sch.add_wire(r2_a[0], oa_inn[1], oa_inn[0], oa_inn[1]) # →(272,288)
|
|
|
|
# Op-amp supply: V+ to Vin rail, V- to GND
|
|
sch.add_wire(oa_vp[0], oa_vp[1], oa_vp[0], vin_p[1]) # V+ up to y=64
|
|
sch.add_wire(oa_vp[0], vin_p[1], vin_p[0], vin_p[1]) # left to Vin+
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("pmos", "M1", pass_transistor, 400, 48)
|
|
sch.add_component(oa_sym, "U1", "", 304, 304)
|
|
sch.add_component("voltage", "Vin", v_in, 80, 48)
|
|
sch.add_component("voltage", "Vref", v_ref, 80, 336)
|
|
sch.add_component("res", "R1", r1, 560, 160)
|
|
sch.add_component("res", "R2", r2, 560, 256)
|
|
sch.add_component("cap", "Cout", "10u", 640, 48)
|
|
sch.add_component("res", "Rload", "100", 720, 48)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*vin_n)
|
|
sch.add_ground(*vref_n)
|
|
sch.add_ground(*r2_b)
|
|
sch.add_ground(*cout_b)
|
|
sch.add_ground(*rl_b)
|
|
sch.add_ground(oa_vn[0], oa_vn[1])
|
|
sch.add_net_label("out", *m_d)
|
|
sch.add_net_label("fb", *r1_b)
|
|
|
|
sch.add_directive(".tran 10m", 80, 500)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_h_bridge(
|
|
v_supply: str = "12",
|
|
r_load: str = "10",
|
|
mosfet_model: str = "IRF540N",
|
|
) -> AscSchematic:
|
|
"""Generate an H-bridge motor driver schematic.
|
|
|
|
Topology (two half-bridges with load between them)::
|
|
|
|
Vcc ──┬── M1_D M2_D ──┬── Vcc
|
|
M1(fwd) M2(rev)
|
|
outA ─┤── M1_S M2_S ──├─ outB
|
|
│ │
|
|
├──── [Rload] ──────┤
|
|
│ │
|
|
outA ─┤── M3_D M4_D ──├─ outB
|
|
M3(rev) M4(fwd)
|
|
GND ──┴── M3_S M4_S ──┴── GND
|
|
|
|
M1+M4 driven together (forward), M2+M3 driven together (reverse).
|
|
Complementary PULSE gate drives with dead time.
|
|
|
|
Args:
|
|
v_supply: Supply voltage
|
|
r_load: Load resistance
|
|
mosfet_model: NMOS model name (used for all 4 switches)
|
|
"""
|
|
sch = AscSchematic(sheet_w=1200, sheet_h=880)
|
|
|
|
# PULSE gate drive parameters
|
|
period = "1m"
|
|
t_on = "450u"
|
|
t_dead = "25u"
|
|
fwd_pulse = f"PULSE(0 {v_supply} {t_dead} 10n 10n {t_on} {period})"
|
|
rev_pulse = f"PULSE(0 {v_supply} 525u 10n 10n {t_on} {period})"
|
|
|
|
# --- Left column (A side) ---
|
|
# M1 (high-A) at (320, 48): D=(368,48), G=(320,128), S=(368,144)
|
|
m1_d = pin_position("nmos", 0, 320, 48) # (368, 48) = vcc
|
|
m1_g = pin_position("nmos", 1, 320, 48) # (320, 128) = gate_fwd
|
|
m1_s = pin_position("nmos", 2, 320, 48) # (368, 144) = outA
|
|
|
|
# M3 (low-A) at (320, 240): D=(368,240), G=(320,320), S=(368,336)
|
|
m3_d = pin_position("nmos", 0, 320, 240) # (368, 240) = outA
|
|
m3_g = pin_position("nmos", 1, 320, 240) # (320, 320) = gate_rev
|
|
m3_s = pin_position("nmos", 2, 320, 240) # (368, 336) = GND
|
|
|
|
# --- Right column (B side) ---
|
|
# M2 (high-B) at (560, 48): D=(608,48), G=(560,128), S=(608,144)
|
|
m2_d = pin_position("nmos", 0, 560, 48) # (608, 48) = vcc
|
|
m2_g = pin_position("nmos", 1, 560, 48) # (560, 128) = gate_rev
|
|
m2_s = pin_position("nmos", 2, 560, 48) # (608, 144) = outB
|
|
|
|
# M4 (low-B) at (560, 240): D=(608,240), G=(560,320), S=(608,336)
|
|
m4_d = pin_position("nmos", 0, 560, 240) # (608, 240) = outB
|
|
m4_g = pin_position("nmos", 1, 560, 240) # (560, 320) = gate_fwd
|
|
m4_s = pin_position("nmos", 2, 560, 240) # (608, 336) = GND
|
|
|
|
# Rload R90 between outA and outB at y=192
|
|
# R90 at (544, 176): pinA=(528,192), pinB=(448,192)
|
|
rl_a = pin_position("res", 0, 544, 176, 90) # (528, 192)
|
|
rl_b = pin_position("res", 1, 544, 176, 90) # (448, 192)
|
|
|
|
# Sources
|
|
# Vsupply at (80, 48): pin+=(80,64), pin-=(80,144)
|
|
vs_p = pin_position("voltage", 0, 80, 48)
|
|
vs_n = pin_position("voltage", 1, 80, 48)
|
|
|
|
# Vg_fwd at (80, 272): pin+=(80,288), pin-=(80,368)
|
|
vgf_p = pin_position("voltage", 0, 80, 272)
|
|
vgf_n = pin_position("voltage", 1, 80, 272)
|
|
|
|
# Vg_rev at (80, 432): pin+=(80,448), pin-=(80,528)
|
|
vgr_p = pin_position("voltage", 0, 80, 432)
|
|
vgr_n = pin_position("voltage", 1, 80, 432)
|
|
|
|
# === WIRING ===
|
|
# Vcc rail: Vsupply+ to M1_D to M2_D
|
|
sch.add_wire(vs_p[0], vs_p[1], vs_p[0], m1_d[1]) # (80,64)→(80,48)
|
|
sch.add_wire(vs_p[0], m1_d[1], m1_d[0], m1_d[1]) # (80,48)→(368,48)
|
|
sch.add_wire(m1_d[0], m1_d[1], m2_d[0], m2_d[1]) # (368,48)→(608,48)
|
|
|
|
# Left column vertical: M1_S → outA junction → M3_D
|
|
sch.add_wire(m1_s[0], m1_s[1], m1_s[0], 192) # (368,144)→(368,192)
|
|
sch.add_wire(m1_s[0], 192, m3_d[0], m3_d[1]) # (368,192)→(368,240)
|
|
|
|
# Right column vertical: M2_S → outB junction → M4_D
|
|
sch.add_wire(m2_s[0], m2_s[1], m2_s[0], 192) # (608,144)→(608,192)
|
|
sch.add_wire(m2_s[0], 192, m4_d[0], m4_d[1]) # (608,192)→(608,240)
|
|
|
|
# Rload connections
|
|
sch.add_wire(m1_s[0], 192, rl_b[0], rl_b[1]) # outA→Rload pinB
|
|
sch.add_wire(rl_a[0], rl_a[1], m2_s[0], 192) # Rload pinA→outB
|
|
|
|
# Gate connections via net labels (cleaner than long wires)
|
|
# gate_fwd: M1_G, M4_G, Vg_fwd+
|
|
# gate_rev: M2_G, M3_G, Vg_rev+
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("voltage", "Vsupply", v_supply, 80, 48)
|
|
sch.add_component("voltage", "Vg_fwd", fwd_pulse, 80, 272)
|
|
sch.add_component("voltage", "Vg_rev", rev_pulse, 80, 432)
|
|
sch.add_component("nmos", "M1", mosfet_model, 320, 48)
|
|
sch.add_component("nmos", "M2", mosfet_model, 560, 48)
|
|
sch.add_component("nmos", "M3", mosfet_model, 320, 240)
|
|
sch.add_component("nmos", "M4", mosfet_model, 560, 240)
|
|
sch.add_component("res", "Rload", r_load, 544, 176, rotation=90)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*vs_n)
|
|
sch.add_ground(*vgf_n)
|
|
sch.add_ground(*vgr_n)
|
|
sch.add_ground(*m3_s)
|
|
sch.add_ground(*m4_s)
|
|
sch.add_net_label("gate_fwd", *m1_g)
|
|
sch.add_net_label("gate_fwd", *m4_g)
|
|
sch.add_net_label("gate_fwd", *vgf_p)
|
|
sch.add_net_label("gate_rev", *m2_g)
|
|
sch.add_net_label("gate_rev", *m3_g)
|
|
sch.add_net_label("gate_rev", *vgr_p)
|
|
sch.add_net_label("outA", *m1_s)
|
|
sch.add_net_label("outB", *m2_s)
|
|
|
|
sch.add_directive(".tran 5m", 80, 592)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_sallen_key_lowpass(
|
|
r1: str = "10k",
|
|
r2: str = "10k",
|
|
c1: str = "10n",
|
|
c2: str = "10n",
|
|
) -> AscSchematic:
|
|
"""Generate a Sallen-Key lowpass filter schematic (unity gain).
|
|
|
|
Topology::
|
|
|
|
V1 --[R1]-- n1 --[R2]-- n2 (In+)
|
|
| --> U1 --> out
|
|
[C1]--> out In- --> out (unity gain)
|
|
|
|
|
[C2]
|
|
|
|
|
GND
|
|
|
|
The C1 connects n1 to out (feedback). C2 connects n2 to GND.
|
|
f_c = 1 / (2*pi*sqrt(R1*R2*C1*C2)). Supply: +/-15V.
|
|
|
|
Args:
|
|
r1: First series resistor
|
|
r2: Second series resistor
|
|
c1: Feedback capacitor (n1 to out)
|
|
c2: Shunt capacitor (n2 to GND)
|
|
"""
|
|
sch = AscSchematic(sheet_w=1200, sheet_h=880)
|
|
oa_sym = "OpAmps/UniversalOpamp2"
|
|
|
|
# Op-amp U1 at (576, 336)
|
|
inp = pin_position(oa_sym, 0, 576, 336) # In+ = (544, 352)
|
|
inn = pin_position(oa_sym, 1, 576, 336) # In- = (544, 320)
|
|
vp = pin_position(oa_sym, 2, 576, 336) # V+ = (576, 304)
|
|
vn = pin_position(oa_sym, 3, 576, 336) # V- = (576, 368)
|
|
out = pin_position(oa_sym, 4, 576, 336) # OUT = (608, 336)
|
|
|
|
# V1 at (80, 256): pin+=(80,272), pin-=(80,352)
|
|
v1p = pin_position("voltage", 0, 80, 256)
|
|
v1n = pin_position("voltage", 1, 80, 256)
|
|
|
|
# R1 horizontal (R90) at origin (256, 256): pinA=(240,272), pinB=(160,272)
|
|
r1_a = pin_position("res", 0, 256, 256, 90) # (240, 272) = n1 side
|
|
r1_b = pin_position("res", 1, 256, 256, 90) # (160, 272) = input side
|
|
|
|
# R2 horizontal (R90) at origin (416, 256): pinA=(400,272), pinB=(320,272)
|
|
r2_a = pin_position("res", 0, 416, 256, 90) # (400, 272) = n2 side
|
|
r2_b = pin_position("res", 1, 416, 256, 90) # (320, 272) = n1 side
|
|
|
|
# C1 vertical: connects n1 (junction R1_a/R2_b at x=272-ish) to out
|
|
# We route n1 down to C1, then C1 bottom goes right to out.
|
|
# C1 at (304, 336): pinA=(320, 336), pinB=(320, 400)
|
|
# Actually let's place C1 horizontal (R90) above the feedback path.
|
|
# Better: C1 as a horizontal cap (R90) from n1 to out.
|
|
# cap R90: pinA offset (16,0)-> (0,16), pinB offset (16,64)->(-64,16)
|
|
# At origin (560, 192): pinA=(560,208), pinB=(496,208)
|
|
c1_a = pin_position("cap", 0, 560, 192, 90) # (560, 208) = out side
|
|
c1_b = pin_position("cap", 1, 560, 192, 90) # (496, 208) = n1 side
|
|
|
|
# C2 vertical at (480, 352): pinA=(496,352), pinB=(496,416)
|
|
c2_a = pin_position("cap", 0, 480, 352) # (496, 352)
|
|
c2_b = pin_position("cap", 1, 480, 352) # (496, 416)
|
|
|
|
# Supply: Vpos at (752, 176), Vneg at (752, 416)
|
|
vpos_p = pin_position("voltage", 0, 752, 176) # (752, 192)
|
|
vpos_n = pin_position("voltage", 1, 752, 176) # (752, 272)
|
|
vneg_p = pin_position("voltage", 0, 752, 416) # (752, 432)
|
|
vneg_n = pin_position("voltage", 1, 752, 416) # (752, 512)
|
|
|
|
# === WIRING ===
|
|
# V1+ to R1 pinB
|
|
sch.add_wire(*v1p, *r1_b)
|
|
|
|
# R1 pinA to R2 pinB (n1 junction at roughly x=272..320)
|
|
# n1 junction: R1_a at (240,272), R2_b at (320,272) -- wire between
|
|
sch.add_wire(*r1_a, *r2_b)
|
|
|
|
# R2 pinA to In+ : (400,272) down to (400,352) then right to (544,352)
|
|
sch.add_wire(r2_a[0], r2_a[1], r2_a[0], inp[1])
|
|
sch.add_wire(r2_a[0], inp[1], inp[0], inp[1])
|
|
|
|
# n2 junction to C2: from R2_a/In+ junction at (400,352) left to C2 pinA (496,352)
|
|
# Actually, n2 is at the In+ side. C2 from n2 to GND.
|
|
# n2 is where R2 meets In+. Let's tap off at (496,352) for C2.
|
|
sch.add_wire(r2_a[0], inp[1], c2_a[0], c2_a[1])
|
|
|
|
# C1 feedback: n1 to out via C1
|
|
# n1 is at the R1_a / R2_b junction. Wire from n1 up to C1 pinB.
|
|
# n1 at y=272, C1 pinB at (496, 208)
|
|
# Route from (240,272) up to (240,208) then right to (496,208)
|
|
sch.add_wire(r1_a[0], r1_a[1], r1_a[0], c1_b[1])
|
|
sch.add_wire(r1_a[0], c1_b[1], c1_b[0], c1_b[1])
|
|
|
|
# C1 pinA to out: (560,208) down to out (608,336)
|
|
sch.add_wire(c1_a[0], c1_a[1], out[0], c1_a[1])
|
|
sch.add_wire(out[0], c1_a[1], out[0], out[1])
|
|
|
|
# Unity gain feedback: out to In-
|
|
# (608,336) up to (608,320) ... In- is at (544,320)
|
|
# Route: out (608,336) left to (544,336) up... no.
|
|
# Actually wire from In- (544,320) right and down to out (608,336).
|
|
# Route: (544,320) right to (620,320), down to (620,336), left to (608,336)
|
|
# Simpler: go below. (608,336) to (608,400), left to (544,400), up to (544,320)
|
|
sch.add_wire(out[0], out[1], out[0], 400)
|
|
sch.add_wire(out[0], 400, inn[0], 400)
|
|
sch.add_wire(inn[0], 400, inn[0], inn[1])
|
|
|
|
# Supply wiring
|
|
sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1])
|
|
sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1])
|
|
sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1])
|
|
sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1])
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("voltage", "V1", "AC 1", 80, 256)
|
|
sch.add_component(oa_sym, "U1", "", 576, 336)
|
|
sch.add_component("res", "R1", r1, 256, 256, rotation=90)
|
|
sch.add_component("res", "R2", r2, 416, 256, rotation=90)
|
|
sch.add_component("cap", "C1", c1, 560, 192, rotation=90)
|
|
sch.add_component("cap", "C2", c2, 480, 352)
|
|
sch.add_component("voltage", "Vpos", "15", 752, 176)
|
|
sch.add_component("voltage", "Vneg", "15", 752, 416)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*v1n)
|
|
sch.add_ground(*c2_b)
|
|
sch.add_ground(*vpos_n)
|
|
sch.add_ground(*vneg_p)
|
|
sch.add_net_label("out", *out)
|
|
sch.add_net_label("n1", r1_a[0], r1_a[1])
|
|
sch.add_net_label("n2", r2_a[0], r2_a[1])
|
|
|
|
sch.add_directive(".ac dec 100 1 1meg", 80, 560)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_boost_converter(
|
|
ind: str = "10u",
|
|
c_out: str = "100u",
|
|
r_load: str = "50",
|
|
v_in: str = "5",
|
|
duty_cycle: float = 0.5,
|
|
freq: str = "100k",
|
|
) -> AscSchematic:
|
|
"""Generate a boost (step-up) converter schematic.
|
|
|
|
Topology::
|
|
|
|
Vin --[L1]--> sw_node --[D1]--> out
|
|
| |
|
|
[MOSFET] [Cout] [Rload]
|
|
D=sw | |
|
|
G=gate GND GND
|
|
S=GND
|
|
|
|
Gate driven by PULSE at switching frequency and duty cycle.
|
|
Vout_ideal = Vin / (1 - duty_cycle).
|
|
|
|
Args:
|
|
ind: Inductor value
|
|
c_out: Output capacitor
|
|
r_load: Load resistor
|
|
v_in: Input voltage
|
|
duty_cycle: Switching duty cycle (0.0-1.0)
|
|
freq: Switching frequency
|
|
"""
|
|
sch = AscSchematic(sheet_w=1040, sheet_h=680)
|
|
|
|
# Compute PULSE timing
|
|
freq_hz = _parse_spice_value(freq)
|
|
period = 1.0 / freq_hz
|
|
t_on = period * duty_cycle
|
|
t_rise = period * 0.01
|
|
t_fall = t_rise
|
|
pulse_val = (
|
|
f"PULSE(0 {v_in} 0 {t_rise:.4g} {t_fall:.4g} {t_on:.4g} {period:.4g})"
|
|
)
|
|
|
|
mosfet_model = "IRF540N"
|
|
diode_model = "1N5819"
|
|
|
|
# Vin at (80, 48): pin+=(80,64), pin-=(80,144)
|
|
vin_p = pin_position("voltage", 0, 80, 48)
|
|
vin_n = pin_position("voltage", 1, 80, 48)
|
|
|
|
# Vgate at (80, 256): pin+=(80,272), pin-=(80,352)
|
|
vg_p = pin_position("voltage", 0, 80, 256)
|
|
vg_n = pin_position("voltage", 1, 80, 256)
|
|
|
|
# L1 horizontal (R90) at (192, 48):
|
|
# ind R90: pinA offset (16,16)->(-16,16), pinB offset (16,96)->(-96,16)
|
|
# At (192, 48): pinA = (176, 64), pinB = (96, 64)
|
|
# Actually re-derive: R90 transform: (px,py) -> (-py, px)
|
|
# pinA raw = (16,16) -> R90 -> (-16, 16). origin(192,48) + (-16,16) = (176, 64)
|
|
# pinB raw = (16,96) -> R90 -> (-96, 16). origin(192,48) + (-96,16) = (96, 64)
|
|
# So pinB is on the Vin side, pinA on the sw side. Good.
|
|
l1_a = pin_position("ind", 0, 192, 48, 90) # (176, 64) = sw side
|
|
l1_b = pin_position("ind", 1, 192, 48, 90) # (96, 64) = vin side
|
|
|
|
# NMOS at (224, 112): D=(272,112)=sw, G=(224,192), S=(272,208)=GND
|
|
# Actually let's position so drain lines up with L1 pinA.
|
|
# L1_a = (176, 64). NMOS drain should be at (176, something).
|
|
# nmos D offset = (48, 0). So origin = (176-48, y) = (128, y).
|
|
# Want drain at y ~ 112 range. Put origin at (128, 112): D=(176,112).
|
|
# Then wire from L1_a (176,64) down to D (176,112).
|
|
md = pin_position("nmos", 0, 128, 112) # drain (176, 112)
|
|
mg = pin_position("nmos", 1, 128, 112) # gate (128, 192)
|
|
ms = pin_position("nmos", 2, 128, 112) # source (176, 208)
|
|
|
|
# Diode from sw_node to output.
|
|
# sw_node is at L1_a (176,64). Diode horizontal.
|
|
# diode R90: anode offset (16,0)->(0,16), cathode offset (16,64)->(-64,16)
|
|
# At origin (320, 48): anode=(320,64), cathode=(256,64)
|
|
# We want anode at sw side, cathode at out side. But R90 has cathode to left.
|
|
# Use R270 instead: anode (16,0)->R270->(0,-16), cathode (16,64)->R270->(64,-16)
|
|
# At origin (240, 80): anode=(240,64), cathode=(304,64)
|
|
# Wire from sw (176,64) right to anode (240,64). Cathode (304,64) = output side.
|
|
d_anode = pin_position("diode", 0, 240, 80, 270) # (240, 64)
|
|
d_cathode = pin_position("diode", 1, 240, 80, 270) # (304, 64)
|
|
|
|
# Wire from L1_a (176,64) to sw junction, then to diode anode (240,64)
|
|
# And from diode cathode (304,64) to out
|
|
|
|
# Cout at (384, 64): pinA=(400,64), pinB=(400,128)
|
|
cout_a = pin_position("cap", 0, 384, 64) # (400, 64)
|
|
cout_b = pin_position("cap", 1, 384, 64) # (400, 128)
|
|
|
|
# Rload at (464, 48): pinA=(480,64), pinB=(480,144)
|
|
rl_a = pin_position("res", 0, 464, 48) # (480, 64)
|
|
rl_b = pin_position("res", 1, 464, 48) # (480, 144)
|
|
|
|
# === WIRING ===
|
|
# Vin+ to L1 pinB
|
|
sch.add_wire(*vin_p, *l1_b)
|
|
|
|
# L1 pinA (sw) to MOSFET drain
|
|
sch.add_wire(l1_a[0], l1_a[1], md[0], md[1])
|
|
|
|
# L1 pinA (sw) to diode anode
|
|
sch.add_wire(l1_a[0], l1_a[1], d_anode[0], d_anode[1])
|
|
|
|
# Diode cathode to Cout and Rload (output rail)
|
|
sch.add_wire(d_cathode[0], d_cathode[1], cout_a[0], cout_a[1])
|
|
sch.add_wire(cout_a[0], cout_a[1], rl_a[0], rl_a[1])
|
|
|
|
# Gate drive: Vgate+ to MOSFET gate
|
|
sch.add_wire(vg_p[0], vg_p[1], mg[0], vg_p[1])
|
|
sch.add_wire(mg[0], vg_p[1], mg[0], mg[1])
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("voltage", "Vin", v_in, 80, 48)
|
|
sch.add_component("voltage", "Vgate", pulse_val, 80, 256)
|
|
sch.add_component("ind", "L1", ind, 192, 48, rotation=90)
|
|
sch.add_component("nmos", "M1", mosfet_model, 128, 112)
|
|
sch.add_component("diode", "D1", diode_model, 240, 80, rotation=270)
|
|
sch.add_component("cap", "Cout", c_out, 384, 64)
|
|
sch.add_component("res", "Rload", r_load, 464, 48)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*vin_n)
|
|
sch.add_ground(*vg_n)
|
|
sch.add_ground(*ms)
|
|
sch.add_ground(*cout_b)
|
|
sch.add_ground(*rl_b)
|
|
sch.add_net_label("sw", l1_a[0], l1_a[1])
|
|
sch.add_net_label("out", d_cathode[0], d_cathode[1])
|
|
|
|
sch.add_directive(f".tran {period * 200:.4g}", 80, 420)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_instrumentation_amp(
|
|
r1: str = "10k",
|
|
r2: str = "10k",
|
|
r3: str = "10k",
|
|
r_gain: str = "10k",
|
|
) -> AscSchematic:
|
|
"""Generate a 3-opamp instrumentation amplifier schematic.
|
|
|
|
Stage 1 (input buffers with gain)::
|
|
|
|
Vin+ --> In+(X1) --> out1
|
|
Vin- --> In+(X2) --> out2
|
|
R1 from out1 to In-(X1), R1b from out2 to In-(X2)
|
|
Rgain between In-(X1) and In-(X2)
|
|
|
|
Stage 2 (difference amp)::
|
|
|
|
out1 --[R2]--> In-(X3) --[R3]--> Vout
|
|
out2 --[R2b]--> In+(X3)
|
|
In+(X3) --[R3b]--> GND
|
|
|
|
Gain = (1 + 2*R1/Rgain) * (R3/R2). Supply: +/-15V.
|
|
|
|
Args:
|
|
r1: Stage 1 feedback resistor
|
|
r2: Stage 2 input resistor
|
|
r3: Stage 2 feedback resistor
|
|
r_gain: Gain-setting resistor between X1 and X2 inverting inputs
|
|
"""
|
|
sch = AscSchematic(sheet_w=1600, sheet_h=1040)
|
|
oa_sym = "OpAmps/UniversalOpamp2"
|
|
|
|
# === STAGE 1 ===
|
|
# X1 at (480, 208)
|
|
x1_inp = pin_position(oa_sym, 0, 480, 208) # In+ = (448, 224)
|
|
x1_inn = pin_position(oa_sym, 1, 480, 208) # In- = (448, 192)
|
|
x1_vp = pin_position(oa_sym, 2, 480, 208) # V+ = (480, 176)
|
|
x1_vn = pin_position(oa_sym, 3, 480, 208) # V- = (480, 240)
|
|
x1_out = pin_position(oa_sym, 4, 480, 208) # OUT = (512, 208)
|
|
|
|
# X2 at (480, 480)
|
|
x2_inp = pin_position(oa_sym, 0, 480, 480) # In+ = (448, 496)
|
|
x2_inn = pin_position(oa_sym, 1, 480, 480) # In- = (448, 464)
|
|
x2_vp = pin_position(oa_sym, 2, 480, 480) # V+ = (480, 448)
|
|
x2_vn = pin_position(oa_sym, 3, 480, 480) # V- = (480, 512)
|
|
x2_out = pin_position(oa_sym, 4, 480, 480) # OUT = (512, 480)
|
|
|
|
# R1: feedback from X1 out to X1 In- (horizontal R90)
|
|
# Place at (576, 96): pinA=(560,112), pinB=(480,112)
|
|
# We want to connect out1 (512,208) up to (512,112) left to (560,112)=R1_a
|
|
# and R1_b (480,112) left to (448,112) down to In- (448,192)
|
|
r1_a = pin_position("res", 0, 576, 96, 90) # (560, 112)
|
|
r1_b = pin_position("res", 1, 576, 96, 90) # (480, 112)
|
|
|
|
# R1b: feedback from X2 out to X2 In- (horizontal R90)
|
|
# Place at (576, 544): pinA=(560,560), pinB=(480,560)
|
|
r1b_a = pin_position("res", 0, 576, 544, 90) # (560, 560)
|
|
r1b_b = pin_position("res", 1, 576, 544, 90) # (480, 560)
|
|
|
|
# Rgain: vertical between X1 In- junction and X2 In- junction
|
|
# X1 In- at y=192 area, X2 In- at y=464 area
|
|
# We'll connect to the R1/R1b feedback junctions.
|
|
# Place Rgain at (400, 208): pinA=(416,224), pinB=(416,304)
|
|
# Actually we need it to span from node_a (X1 In- junction) to node_b (X2 In- junction).
|
|
# The junctions are at the R1_b / R1b_b x positions.
|
|
# Let's use net labels for cleanliness: "node_a" at R1_b junction, "node_b" at R1b_b junction.
|
|
# Rgain vertical res at (336, 256): pinA=(352, 272), pinB=(352, 352)
|
|
rg_a = pin_position("res", 0, 336, 256) # (352, 272) = node_a side
|
|
rg_b = pin_position("res", 1, 336, 256) # (352, 352) = node_b side
|
|
|
|
# === STAGE 2 ===
|
|
# X3 at (880, 352)
|
|
x3_inp = pin_position(oa_sym, 0, 880, 352) # In+ = (848, 368)
|
|
x3_inn = pin_position(oa_sym, 1, 880, 352) # In- = (848, 336)
|
|
x3_vp = pin_position(oa_sym, 2, 880, 352) # V+ = (880, 320)
|
|
x3_vn = pin_position(oa_sym, 3, 880, 352) # V- = (880, 384)
|
|
x3_out = pin_position(oa_sym, 4, 880, 352) # OUT = (912, 352)
|
|
|
|
# R2: out1 to X3 In- (horizontal R90)
|
|
# Place at (768, 320): pinA=(752,336), pinB=(672,336)
|
|
r2_a = pin_position("res", 0, 768, 320, 90) # (752, 336) near X3 In-
|
|
r2_b = pin_position("res", 1, 768, 320, 90) # (672, 336) from out1
|
|
|
|
# R3: X3 In- to Vout (horizontal R90, above X3 for feedback)
|
|
# Place at (960, 240): pinA=(944, 256), pinB=(864, 256)
|
|
r3_a = pin_position("res", 0, 960, 240, 90) # (944, 256) near Vout
|
|
r3_b = pin_position("res", 1, 960, 240, 90) # (864, 256) near In-
|
|
|
|
# R2b: out2 to X3 In+ (horizontal R90)
|
|
# Place at (768, 352): pinA=(752,368), pinB=(672,368)
|
|
r2b_a = pin_position("res", 0, 768, 352, 90) # (752, 368) near X3 In+
|
|
r2b_b = pin_position("res", 1, 768, 352, 90) # (672, 368) from out2
|
|
|
|
# R3b: X3 In+ to GND (vertical)
|
|
# Place at (832, 416): pinA=(848,432), pinB=(848,512)
|
|
r3b_a = pin_position("res", 0, 832, 416) # (848, 432)
|
|
r3b_b = pin_position("res", 1, 832, 416) # (848, 512)
|
|
|
|
# Sources
|
|
# V1 at (80, 160): pin+=(80,176), pin-=(80,256)
|
|
v1p = pin_position("voltage", 0, 80, 160)
|
|
v1n = pin_position("voltage", 1, 80, 160)
|
|
|
|
# V2 at (80, 432): pin+=(80,448), pin-=(80,528)
|
|
v2p = pin_position("voltage", 0, 80, 432)
|
|
v2n = pin_position("voltage", 1, 80, 432)
|
|
|
|
# Supply: Vpos at (1056, 176), Vneg at (1056, 480)
|
|
vpos_p = pin_position("voltage", 0, 1056, 176)
|
|
vpos_n = pin_position("voltage", 1, 1056, 176)
|
|
vneg_p = pin_position("voltage", 0, 1056, 480)
|
|
vneg_n = pin_position("voltage", 1, 1056, 480)
|
|
|
|
# === WIRING ===
|
|
# V1+ to X1 In+
|
|
sch.add_wire(v1p[0], v1p[1], v1p[0], x1_inp[1])
|
|
sch.add_wire(v1p[0], x1_inp[1], x1_inp[0], x1_inp[1])
|
|
|
|
# V2+ to X2 In+
|
|
sch.add_wire(v2p[0], v2p[1], v2p[0], x2_inp[1])
|
|
sch.add_wire(v2p[0], x2_inp[1], x2_inp[0], x2_inp[1])
|
|
|
|
# R1 feedback: X1 out (512,208) up to (512,112), right to R1_a (560,112)
|
|
sch.add_wire(x1_out[0], x1_out[1], x1_out[0], r1_a[1])
|
|
sch.add_wire(x1_out[0], r1_a[1], r1_a[0], r1_a[1])
|
|
# R1_b (480,112) down to X1 In- (448,192): (480,112) left to (448,112), down to (448,192)
|
|
sch.add_wire(r1_b[0], r1_b[1], x1_inn[0], r1_b[1])
|
|
sch.add_wire(x1_inn[0], r1_b[1], x1_inn[0], x1_inn[1])
|
|
|
|
# R1b feedback: X2 out (512,480) down to (512,560), right to R1b_a (560,560)
|
|
sch.add_wire(x2_out[0], x2_out[1], x2_out[0], r1b_a[1])
|
|
sch.add_wire(x2_out[0], r1b_a[1], r1b_a[0], r1b_a[1])
|
|
# R1b_b (480,560) down to X2 In- (448,464): (480,560) left to (448,560), up to (448,464)
|
|
sch.add_wire(r1b_b[0], r1b_b[1], x2_inn[0], r1b_b[1])
|
|
sch.add_wire(x2_inn[0], r1b_b[1], x2_inn[0], x2_inn[1])
|
|
|
|
# Rgain: node_a (junction at X1 In-) to Rgain pinA, node_b (X2 In-) to Rgain pinB
|
|
# Route from X1 In- junction (448,192) left to (352,192), down to Rg_a (352,272)
|
|
sch.add_wire(x1_inn[0], x1_inn[1], rg_a[0], x1_inn[1])
|
|
sch.add_wire(rg_a[0], x1_inn[1], rg_a[0], rg_a[1])
|
|
# Route from X2 In- junction (448,464) left to (352,464), up to Rg_b (352,352)
|
|
sch.add_wire(x2_inn[0], x2_inn[1], rg_b[0], x2_inn[1])
|
|
sch.add_wire(rg_b[0], x2_inn[1], rg_b[0], rg_b[1])
|
|
|
|
# Stage 2 wiring
|
|
# out1 (512,208) to R2_b (672,336): route (512,208) right to (672,208), down to (672,336)
|
|
sch.add_wire(x1_out[0], x1_out[1], r2_b[0], x1_out[1])
|
|
sch.add_wire(r2_b[0], x1_out[1], r2_b[0], r2_b[1])
|
|
# R2_a (752,336) to X3 In- (848,336)
|
|
sch.add_wire(r2_a[0], r2_a[1], x3_inn[0], x3_inn[1])
|
|
|
|
# out2 (512,480) to R2b_b (672,368): route (512,480) right to (672,480), up to (672,368)
|
|
sch.add_wire(x2_out[0], x2_out[1], r2b_b[0], x2_out[1])
|
|
sch.add_wire(r2b_b[0], x2_out[1], r2b_b[0], r2b_b[1])
|
|
# R2b_a (752,368) to X3 In+ (848,368)
|
|
sch.add_wire(r2b_a[0], r2b_a[1], x3_inp[0], x3_inp[1])
|
|
|
|
# R3 feedback: X3 out (912,352) up to (912,256), left to R3_a (944,256)
|
|
sch.add_wire(x3_out[0], x3_out[1], x3_out[0], r3_a[1])
|
|
sch.add_wire(x3_out[0], r3_a[1], r3_a[0], r3_a[1])
|
|
# R3_b (864,256) down to X3 In- junction: (864,256) left to (848,256), down to (848,336)
|
|
sch.add_wire(r3_b[0], r3_b[1], x3_inn[0], r3_b[1])
|
|
sch.add_wire(x3_inn[0], r3_b[1], x3_inn[0], x3_inn[1])
|
|
|
|
# R3b: X3 In+ to GND via R3b
|
|
sch.add_wire(x3_inp[0], x3_inp[1], r3b_a[0], r3b_a[1])
|
|
|
|
# Supply wiring (use net labels for cleanliness with 3 opamps)
|
|
# Just wire X1, X2, X3 supply pins to net labels
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("voltage", "V1", "AC 1", 80, 160)
|
|
sch.add_component("voltage", "V2", "0", 80, 432)
|
|
sch.add_component(oa_sym, "X1", "", 480, 208)
|
|
sch.add_component(oa_sym, "X2", "", 480, 480)
|
|
sch.add_component(oa_sym, "X3", "", 880, 352)
|
|
sch.add_component("res", "R1", r1, 576, 96, rotation=90)
|
|
sch.add_component("res", "R1b", r1, 576, 544, rotation=90)
|
|
sch.add_component("res", "Rgain", r_gain, 336, 256)
|
|
sch.add_component("res", "R2", r2, 768, 320, rotation=90)
|
|
sch.add_component("res", "R3", r3, 960, 240, rotation=90)
|
|
sch.add_component("res", "R2b", r2, 768, 352, rotation=90)
|
|
sch.add_component("res", "R3b", r3, 832, 416)
|
|
sch.add_component("voltage", "Vpos", "15", 1056, 176)
|
|
sch.add_component("voltage", "Vneg", "15", 1056, 480)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*v1n)
|
|
sch.add_ground(*v2n)
|
|
sch.add_ground(*r3b_b)
|
|
sch.add_ground(*vpos_n)
|
|
sch.add_ground(*vneg_p)
|
|
# Supply net labels for all three opamps
|
|
sch.add_net_label("vdd", *x1_vp)
|
|
sch.add_net_label("vdd", *x2_vp)
|
|
sch.add_net_label("vdd", *x3_vp)
|
|
sch.add_net_label("vdd", *vpos_p)
|
|
sch.add_net_label("vss", *x1_vn)
|
|
sch.add_net_label("vss", *x2_vn)
|
|
sch.add_net_label("vss", *x3_vn)
|
|
sch.add_net_label("vss", *vneg_n)
|
|
sch.add_net_label("vout", *x3_out)
|
|
|
|
sch.add_directive(".ac dec 100 1 1meg", 80, 640)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_current_mirror(
|
|
r_ref: str = "10k",
|
|
r_load: str = "1k",
|
|
vcc: str = "12",
|
|
) -> AscSchematic:
|
|
"""Generate a BJT current mirror schematic.
|
|
|
|
Topology::
|
|
|
|
Vcc --[Rref]--> collector_Q1 = base_Q1 = base_Q2
|
|
emitter_Q1 = GND
|
|
Vcc --[Rload]--> collector_Q2
|
|
emitter_Q2 = GND
|
|
|
|
Q1 is diode-connected (collector tied to base).
|
|
I_ref ~ (Vcc - Vbe) / Rref, I_load ~ I_ref.
|
|
|
|
Args:
|
|
r_ref: Reference resistor
|
|
r_load: Load resistor
|
|
vcc: Supply voltage
|
|
"""
|
|
sch = AscSchematic(sheet_w=880, sheet_h=680)
|
|
bjt_model = "2N2222"
|
|
|
|
# Q1 (NPN, diode-connected) at (256, 288): C=(320,288), B=(256,336), E=(320,384)
|
|
q1c = pin_position("npn", 0, 256, 288) # collector (320, 288)
|
|
q1b = pin_position("npn", 1, 256, 288) # base (256, 336)
|
|
q1e = pin_position("npn", 2, 256, 288) # emitter (320, 384)
|
|
|
|
# Q2 (NPN) at (448, 288): C=(512,288), B=(448,336), E=(512,384)
|
|
q2c = pin_position("npn", 0, 448, 288) # collector (512, 288)
|
|
q2b = pin_position("npn", 1, 448, 288) # base (448, 336)
|
|
q2e = pin_position("npn", 2, 448, 288) # emitter (512, 384)
|
|
|
|
# Rref from Vcc to Q1 collector. res at (304, 192): pinA=(320,208)=vcc, pinB=(320,288)=Q1c
|
|
rref_a = pin_position("res", 0, 304, 192) # (320, 208)
|
|
|
|
# Rload from Vcc to Q2 collector. res at (496, 192): pinA=(512,208)=vcc, pinB=(512,288)=Q2c
|
|
rload_a = pin_position("res", 0, 496, 192) # (512, 208)
|
|
|
|
# Vcc source at (80, 96): pin+=(80,112), pin-=(80,192)
|
|
vcc_p = pin_position("voltage", 0, 80, 96)
|
|
vcc_n = pin_position("voltage", 1, 80, 96)
|
|
|
|
# Vcc rail at y=208
|
|
vcc_y = rref_a[1] # 208
|
|
|
|
# === WIRING ===
|
|
# Vcc rail
|
|
sch.add_wire(vcc_p[0], vcc_p[1], 160, vcc_p[1]) # (80,112)->(160,112)
|
|
sch.add_wire(160, vcc_p[1], 160, vcc_y) # (160,112)->(160,208)
|
|
sch.add_wire(160, vcc_y, rload_a[0], vcc_y) # (160,208)->(512,208) spans both
|
|
|
|
# Diode connection: Q1 collector to Q1 base
|
|
# Q1c = (320,288), Q1b = (256,336)
|
|
# Route: (320,288) left to (256,288) down to (256,336)
|
|
sch.add_wire(q1c[0], q1c[1], q1b[0], q1c[1])
|
|
sch.add_wire(q1b[0], q1c[1], q1b[0], q1b[1])
|
|
|
|
# Base connection: Q1 base to Q2 base
|
|
sch.add_wire(q1b[0], q1b[1], q2b[0], q2b[1])
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("npn", "Q1", bjt_model, 256, 288)
|
|
sch.add_component("npn", "Q2", bjt_model, 448, 288)
|
|
sch.add_component("res", "Rref", r_ref, 304, 192)
|
|
sch.add_component("res", "Rload", r_load, 496, 192)
|
|
sch.add_component("voltage", "Vcc", vcc, 80, 96)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*vcc_n)
|
|
sch.add_ground(*q1e)
|
|
sch.add_ground(*q2e)
|
|
sch.add_net_label("mirror", *q1b)
|
|
sch.add_net_label("out", *q2c)
|
|
|
|
sch.add_directive(".op", 80, 448)
|
|
sch.add_directive(".tran 1m", 80, 480)
|
|
|
|
return sch
|
|
|
|
|
|
def generate_transimpedance_amp(
|
|
rf: str = "100k",
|
|
cf: str = "1p",
|
|
i_source: str = "1u",
|
|
) -> AscSchematic:
|
|
"""Generate a transimpedance amplifier (TIA) schematic.
|
|
|
|
Topology::
|
|
|
|
I1 (current src, AC) --> In- (inv)
|
|
--> U1 --> out
|
|
GND --> In+ (noninv)
|
|
Rf from In- to out (feedback)
|
|
Cf from In- to out (parallel with Rf, for stability)
|
|
|
|
Vout = -I_in * Rf (at low frequencies).
|
|
Bandwidth limited by Cf. Supply: +/-15V.
|
|
|
|
Args:
|
|
rf: Feedback resistor
|
|
cf: Feedback capacitor (for stability)
|
|
i_source: AC current source magnitude
|
|
"""
|
|
sch = AscSchematic(sheet_w=1200, sheet_h=880)
|
|
oa_sym = "OpAmps/UniversalOpamp2"
|
|
|
|
# Op-amp U1 at (512, 336)
|
|
inp = pin_position(oa_sym, 0, 512, 336) # In+ = (480, 352)
|
|
inn = pin_position(oa_sym, 1, 512, 336) # In- = (480, 320)
|
|
vp = pin_position(oa_sym, 2, 512, 336) # V+ = (512, 304)
|
|
vn = pin_position(oa_sym, 3, 512, 336) # V- = (512, 368)
|
|
out = pin_position(oa_sym, 4, 512, 336) # OUT = (544, 336)
|
|
|
|
# Current source I1 at (320, 224) (vertical, like voltage source)
|
|
# Using "voltage" pin positions since current source shares the same symbol geometry
|
|
# I1 at origin (320, 224): pin+ = (320, 240), pin- = (320, 320)
|
|
# Current flows from pin+ to pin- inside the source (conventional).
|
|
# We want current into In- node: connect pin+ to In- node, pin- to GND.
|
|
i1p = pin_position("voltage", 0, 320, 224) # (320, 240)
|
|
i1n = pin_position("voltage", 1, 320, 224) # (320, 320)
|
|
|
|
# Rf horizontal (R90) above op-amp for feedback
|
|
# At (560, 208): pinA=(544,224), pinB=(464,224)
|
|
rf_a = pin_position("res", 0, 560, 208, 90) # (544, 224) near out
|
|
rf_b = pin_position("res", 1, 560, 208, 90) # (464, 224) near In-
|
|
|
|
# Cf horizontal (R90) above Rf for parallel feedback
|
|
# At (560, 160): pinA=(544,176), pinB=(464,176)
|
|
# Actually cap R90: pinA offset (16,0)->(0,16), pinB offset (16,64)->(-64,16)
|
|
# At (560, 160): pinA=(560,176), pinB=(496,176)
|
|
cf_a = pin_position("cap", 0, 560, 160, 90) # (560, 176) near out
|
|
cf_b = pin_position("cap", 1, 560, 160, 90) # (496, 176) near In-
|
|
|
|
# Supply: Vpos at (688, 176), Vneg at (688, 416)
|
|
vpos_p = pin_position("voltage", 0, 688, 176)
|
|
vpos_n = pin_position("voltage", 1, 688, 176)
|
|
vneg_p = pin_position("voltage", 0, 688, 416)
|
|
vneg_n = pin_position("voltage", 1, 688, 416)
|
|
|
|
# === WIRING ===
|
|
# I1 pin+ (320,240) to In- node (480,320)
|
|
# Route: (320,240) right to (480,240), down to (480,320)
|
|
sch.add_wire(i1p[0], i1p[1], inn[0], i1p[1])
|
|
sch.add_wire(inn[0], i1p[1], inn[0], inn[1])
|
|
|
|
# Rf feedback: out up to Rf pinA, Rf pinB left and down to In- junction
|
|
sch.add_wire(out[0], out[1], rf_a[0], rf_a[1]) # (544,336)->(544,224)
|
|
sch.add_wire(rf_b[0], rf_b[1], rf_b[0], inn[1]) # (464,224)->(464,320)
|
|
sch.add_wire(rf_b[0], inn[1], inn[0], inn[1]) # (464,320)->(480,320)
|
|
|
|
# Cf feedback: parallel path
|
|
# Connect Cf pinA to Rf pinA column, Cf pinB to Rf pinB column
|
|
sch.add_wire(cf_a[0], cf_a[1], rf_a[0], cf_a[1]) # (560,176)->(544,176)
|
|
sch.add_wire(rf_a[0], cf_a[1], rf_a[0], rf_a[1]) # (544,176)->(544,224) vertical
|
|
sch.add_wire(cf_b[0], cf_b[1], rf_b[0], cf_b[1]) # (496,176)->(464,176)
|
|
sch.add_wire(rf_b[0], cf_b[1], rf_b[0], rf_b[1]) # (464,176)->(464,224) vertical
|
|
|
|
# Supply wiring
|
|
sch.add_wire(vp[0], vp[1], vp[0], vpos_p[1])
|
|
sch.add_wire(vp[0], vpos_p[1], vpos_p[0], vpos_p[1])
|
|
sch.add_wire(vn[0], vn[1], vn[0], vneg_n[1])
|
|
sch.add_wire(vn[0], vneg_n[1], vneg_n[0], vneg_n[1])
|
|
|
|
# === COMPONENTS ===
|
|
sch.add_component("voltage", "I1", f"AC {i_source}", 320, 224)
|
|
sch.add_component(oa_sym, "U1", "", 512, 336)
|
|
sch.add_component("res", "Rf", rf, 560, 208, rotation=90)
|
|
sch.add_component("cap", "Cf", cf, 560, 160, rotation=90)
|
|
sch.add_component("voltage", "Vpos", "15", 688, 176)
|
|
sch.add_component("voltage", "Vneg", "15", 688, 416)
|
|
|
|
# === FLAGS ===
|
|
sch.add_ground(*i1n)
|
|
sch.add_ground(*inp) # In+ to GND
|
|
sch.add_ground(*vpos_n)
|
|
sch.add_ground(*vneg_p)
|
|
sch.add_net_label("out", *out)
|
|
|
|
sch.add_directive(".ac dec 100 1 1meg", 80, 560)
|
|
|
|
return sch
|