Add 7 new graphical schematic templates (differential amp, buck converter, LDO regulator, H-bridge, common emitter, Colpitts oscillator) and rewrite inverting amp to actually include an op-amp instead of just passive components. Fix UniversalOpamp2 subcircuit error: the .asy symbol defines SpiceModel as "level2", so SYMATTR Value must be omitted to let the built-in model name resolve. Previously emitting SYMATTR Value UniversalOpamp2 caused LTspice to search for a non-existent subcircuit. Fix wire-through-pin routing bugs: vertical wires crossing intermediate opamp/source pins auto-connect at those pins, creating unintended shorts. Rerouted V1-to-In+ paths to avoid crossing In- pins in non-inverting, common-emitter, Colpitts, and differential amp templates. Refactor generate_schematic tool from hardcoded if/elif to registry dispatch via _ASC_TEMPLATES dict, matching the _TEMPLATES pattern for netlists. All 10 templates verified: simulate with zero errors and zero NC nodes. 255 tests pass, source lint clean.
1260 lines
46 KiB
Python
1260 lines
46 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
|