New modules: - patterns/ library: decoupling bank, pull resistor, crystal oscillator placement with power symbol attachment and grid math helpers - tools/batch.py: atomic file-based batch operations with dry_run - tools/power_symbols.py: add_power_symbol with auto #PWR refs - tools/schematic_patterns.py: MCP wrappers for pattern library - tools/schematic_edit.py: modify/remove components, title blocks, annotations - resources/schematic.py: schematic data resources 43 new tests (99 total), lint clean.
171 lines
5.3 KiB
Python
171 lines
5.3 KiB
Python
"""
|
|
Crystal oscillator with load capacitors pattern.
|
|
|
|
Places a crystal at a specified position with load capacitors on each
|
|
side, ground symbols on the caps, and optional wires to IC pins.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from mckicad.patterns._geometry import add_power_symbol_to_pin, snap_to_grid
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def place_crystal_with_caps(
|
|
sch: Any,
|
|
xtal_value: str,
|
|
cap_value: str,
|
|
x: float,
|
|
y: float,
|
|
ic_ref: str | None = None,
|
|
ic_xin_pin: str | None = None,
|
|
ic_xout_pin: str | None = None,
|
|
ground_net: str = "GND",
|
|
xtal_lib_id: str = "Device:Crystal",
|
|
cap_lib_id: str = "Device:C",
|
|
) -> dict[str, Any]:
|
|
"""Place a crystal oscillator with load capacitors.
|
|
|
|
Creates the classic crystal + 2x load cap circuit:
|
|
- Crystal at center
|
|
- Load cap on each side (XIN and XOUT)
|
|
- GND on each cap's second pin
|
|
- Optional wires to IC oscillator pins
|
|
|
|
Does NOT call ``sch.save()`` — caller manages persistence.
|
|
|
|
Args:
|
|
sch: A kicad-sch-api SchematicDocument.
|
|
xtal_value: Crystal frequency string (e.g. "16MHz", "32.768kHz").
|
|
cap_value: Load capacitor value (e.g. "22pF", "15pF").
|
|
x: Center X position for the crystal.
|
|
y: Center Y position for the crystal.
|
|
ic_ref: Optional IC reference for wiring (e.g. "U1").
|
|
ic_xin_pin: XIN pin number on the IC.
|
|
ic_xout_pin: XOUT pin number on the IC.
|
|
ground_net: Ground rail name.
|
|
xtal_lib_id: Crystal symbol library ID.
|
|
cap_lib_id: Capacitor symbol library ID.
|
|
|
|
Returns:
|
|
Dictionary with crystal and cap references, wire IDs, and
|
|
power symbol details.
|
|
"""
|
|
cap_offset = 10.16 # Horizontal offset for load caps from crystal
|
|
|
|
# Auto-generate references
|
|
max_y_num = 0
|
|
max_c_num = 0
|
|
for comp in sch.components:
|
|
ref = getattr(comp, "reference", "")
|
|
if ref.startswith("Y") and ref[1:].isdigit():
|
|
max_y_num = max(max_y_num, int(ref[1:]))
|
|
elif ref.startswith("C") and ref[1:].isdigit():
|
|
max_c_num = max(max_c_num, int(ref[1:]))
|
|
|
|
xtal_ref = f"Y{max_y_num + 1}"
|
|
cap_xin_ref = f"C{max_c_num + 1}"
|
|
cap_xout_ref = f"C{max_c_num + 2}"
|
|
|
|
# Place crystal at center
|
|
xtal_x = snap_to_grid(x)
|
|
xtal_y = snap_to_grid(y)
|
|
sch.components.add(
|
|
lib_id=xtal_lib_id,
|
|
reference=xtal_ref,
|
|
value=xtal_value,
|
|
position=(xtal_x, xtal_y),
|
|
)
|
|
|
|
# Place load caps on each side
|
|
cap_xin_x = snap_to_grid(x - cap_offset)
|
|
cap_xout_x = snap_to_grid(x + cap_offset)
|
|
cap_y = snap_to_grid(y + 7.62) # Below the crystal
|
|
|
|
sch.components.add(
|
|
lib_id=cap_lib_id,
|
|
reference=cap_xin_ref,
|
|
value=cap_value,
|
|
position=(cap_xin_x, cap_y),
|
|
)
|
|
sch.components.add(
|
|
lib_id=cap_lib_id,
|
|
reference=cap_xout_ref,
|
|
value=cap_value,
|
|
position=(cap_xout_x, cap_y),
|
|
)
|
|
|
|
# GND on each cap's pin 2
|
|
gnd_symbols: list[dict[str, Any]] = []
|
|
for cap_ref in (cap_xin_ref, cap_xout_ref):
|
|
pin2_pos = sch.get_component_pin_position(cap_ref, "2")
|
|
if pin2_pos:
|
|
gnd_result = add_power_symbol_to_pin(
|
|
sch=sch,
|
|
pin_position=(pin2_pos.x, pin2_pos.y),
|
|
net=ground_net,
|
|
)
|
|
gnd_symbols.append(gnd_result)
|
|
|
|
# Wire crystal pins to cap pins
|
|
internal_wires: list[str] = []
|
|
|
|
# Crystal pin 1 -> XIN cap pin 1
|
|
xtal_pin1 = sch.get_component_pin_position(xtal_ref, "1")
|
|
cap_xin_pin1 = sch.get_component_pin_position(cap_xin_ref, "1")
|
|
if xtal_pin1 and cap_xin_pin1:
|
|
wid = sch.add_wire(
|
|
start=(xtal_pin1.x, xtal_pin1.y),
|
|
end=(cap_xin_pin1.x, cap_xin_pin1.y),
|
|
)
|
|
internal_wires.append(str(wid))
|
|
|
|
# Crystal pin 2 -> XOUT cap pin 1
|
|
xtal_pin2 = sch.get_component_pin_position(xtal_ref, "2")
|
|
cap_xout_pin1 = sch.get_component_pin_position(cap_xout_ref, "1")
|
|
if xtal_pin2 and cap_xout_pin1:
|
|
wid = sch.add_wire(
|
|
start=(xtal_pin2.x, xtal_pin2.y),
|
|
end=(cap_xout_pin1.x, cap_xout_pin1.y),
|
|
)
|
|
internal_wires.append(str(wid))
|
|
|
|
# Optional wires to IC pins
|
|
ic_wires: list[str] = []
|
|
if ic_ref and ic_xin_pin:
|
|
ic_xin_pos = sch.get_component_pin_position(ic_ref, ic_xin_pin)
|
|
if ic_xin_pos and xtal_pin1:
|
|
wid = sch.add_wire(
|
|
start=(xtal_pin1.x, xtal_pin1.y),
|
|
end=(ic_xin_pos.x, ic_xin_pos.y),
|
|
)
|
|
ic_wires.append(str(wid))
|
|
|
|
if ic_ref and ic_xout_pin:
|
|
ic_xout_pos = sch.get_component_pin_position(ic_ref, ic_xout_pin)
|
|
if ic_xout_pos and xtal_pin2:
|
|
wid = sch.add_wire(
|
|
start=(xtal_pin2.x, xtal_pin2.y),
|
|
end=(ic_xout_pos.x, ic_xout_pos.y),
|
|
)
|
|
ic_wires.append(str(wid))
|
|
|
|
logger.info(
|
|
"Placed crystal %s (%s) with caps %s, %s (%s) at (%.1f, %.1f)",
|
|
xtal_ref, xtal_value, cap_xin_ref, cap_xout_ref, cap_value, x, y,
|
|
)
|
|
|
|
return {
|
|
"crystal_ref": xtal_ref,
|
|
"xtal_value": xtal_value,
|
|
"cap_xin_ref": cap_xin_ref,
|
|
"cap_xout_ref": cap_xout_ref,
|
|
"cap_value": cap_value,
|
|
"gnd_symbols": gnd_symbols,
|
|
"internal_wires": internal_wires,
|
|
"ic_wires": ic_wires,
|
|
"ground_net": ground_net,
|
|
}
|