Rebrand Pinscope to Periscope across product and codebase.
Rename the core package to periscopex, update UI/docs/Docker/deploy defaults to periscope.michelebigi.it, and keep legacy version/storage key aliases so existing projects keep working. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
"""Crystal load capacitance vs load caps — numbers only when present.
|
||||
|
||||
CL_eff ≈ (C1·C2)/(C1+C2) + Cstray. Cstray used only if specs list it;
|
||||
never invent a stray default. Without CL in specs → skip.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.periscopex.functional_groups import (
|
||||
_cap_farads,
|
||||
_is_ground_net,
|
||||
load_capacitance_farads,
|
||||
)
|
||||
from backend.periscopex.models import (
|
||||
Component,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
Finding,
|
||||
SimpleComponentSpecs,
|
||||
)
|
||||
|
||||
_STRAY_KEYS = ("stray_capacitance_f", "board_stray_f", "cstray_f")
|
||||
|
||||
|
||||
def check_crystal_cl(graph: DesignGraph) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
for ref, comp in sorted(graph.components.items()):
|
||||
if comp.component_type != ComponentType.CRYSTAL:
|
||||
continue
|
||||
cl = load_capacitance_farads(comp)
|
||||
if cl is None:
|
||||
continue
|
||||
load_caps = _load_caps_for_crystal(graph, comp)
|
||||
if len(load_caps) < 2:
|
||||
findings.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="clock",
|
||||
source="crystal_cl_check",
|
||||
status="WARNING",
|
||||
finding=(
|
||||
f"{ref} specifies CL={_fmt_f(cl)} but fewer than two load "
|
||||
f"capacitors were found on its non-ground nets "
|
||||
f"({[c.reference for c in load_caps] or 'none'})."
|
||||
),
|
||||
why="Crystal load capacitance needs a matched C1/C2 pair.",
|
||||
recommendation="Add or value the two load capacitors on XIN/XOUT.",
|
||||
reference="netlist topology",
|
||||
rule_id="PE-XTAL-001",
|
||||
pins=[ref],
|
||||
))
|
||||
continue
|
||||
|
||||
# Use the two caps with known farads closest to equal (typical C1≈C2).
|
||||
valued = [(c, _cap_farads(c)) for c in load_caps]
|
||||
known = [(c, f) for c, f in valued if f is not None]
|
||||
if len(known) < 2:
|
||||
continue
|
||||
known.sort(key=lambda x: x[1])
|
||||
# Prefer a pair with similar values: take the two largest known if many.
|
||||
c1, f1 = known[-2]
|
||||
c2, f2 = known[-1]
|
||||
series = (f1 * f2) / (f1 + f2) if (f1 + f2) > 0 else None
|
||||
if series is None:
|
||||
continue
|
||||
stray = _stray_farads(comp)
|
||||
c_eff = series + (stray or 0.0)
|
||||
|
||||
if stray is None:
|
||||
# Without stray: only flag when series alone already exceeds CL.
|
||||
if series > cl * 1.25:
|
||||
findings.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="clock",
|
||||
source="crystal_cl_check",
|
||||
status="WARNING",
|
||||
finding=(
|
||||
f"{ref} CL={_fmt_f(cl)}; C1={c1.reference} {_fmt_f(f1)} and "
|
||||
f"C2={c2.reference} {_fmt_f(f2)} give series≈{_fmt_f(series)} "
|
||||
f"(already above CL; board stray not in specs)."
|
||||
),
|
||||
why="Series combination of load caps exceeds specified CL without needing stray.",
|
||||
recommendation="Reduce load caps or confirm the datasheet CL value.",
|
||||
reference="netlist topology",
|
||||
rule_id="PE-XTAL-002",
|
||||
pins=[ref, c1.reference, c2.reference],
|
||||
))
|
||||
elif series < cl * 0.5:
|
||||
findings.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="clock",
|
||||
source="crystal_cl_check",
|
||||
status="INFO",
|
||||
finding=(
|
||||
f"{ref} CL={_fmt_f(cl)}; series of {c1.reference}/{c2.reference} "
|
||||
f"≈{_fmt_f(series)} (stray unknown — verify against datasheet)."
|
||||
),
|
||||
why="Without stray capacitance in specs, effective CL cannot be fully checked.",
|
||||
recommendation="Confirm Cstray or populate load_capacitance / stray in crystal specs.",
|
||||
reference="netlist topology",
|
||||
rule_id="PE-XTAL-003",
|
||||
pins=[ref, c1.reference, c2.reference],
|
||||
))
|
||||
continue
|
||||
|
||||
if c_eff > cl * 1.25 or c_eff < cl * 0.75:
|
||||
findings.append(Finding(
|
||||
designator=ref,
|
||||
mpn=comp.mpn or "",
|
||||
aspect="clock",
|
||||
source="crystal_cl_check",
|
||||
status="WARNING",
|
||||
finding=(
|
||||
f"{ref} CL={_fmt_f(cl)}; C_eff≈{_fmt_f(c_eff)} "
|
||||
f"(series {_fmt_f(series)} + stray {_fmt_f(stray)}) "
|
||||
f"from {c1.reference}/{c2.reference}."
|
||||
),
|
||||
why="Effective load capacitance should stay near the crystal's specified CL.",
|
||||
recommendation="Adjust C1/C2 so C_eff ≈ CL.",
|
||||
reference="netlist topology",
|
||||
rule_id="PE-XTAL-002",
|
||||
pins=[ref, c1.reference, c2.reference],
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
def _load_caps_for_crystal(graph: DesignGraph, crystal: Component) -> list[Component]:
|
||||
caps: dict[str, Component] = {}
|
||||
for net in crystal.pins.values():
|
||||
if not net or _is_ground_net(graph, net):
|
||||
continue
|
||||
for cref in graph.capacitors_on_net(net):
|
||||
cap = graph.components.get(cref)
|
||||
if cap:
|
||||
caps[cref] = cap
|
||||
return list(caps.values())
|
||||
|
||||
|
||||
def _stray_farads(comp: Component) -> float | None:
|
||||
specs = comp.specs
|
||||
if not isinstance(specs, SimpleComponentSpecs):
|
||||
return None
|
||||
for key in _STRAY_KEYS:
|
||||
raw = specs.values.get(key)
|
||||
if raw is None:
|
||||
continue
|
||||
try:
|
||||
v = float(raw)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if v >= 0:
|
||||
return v
|
||||
return None
|
||||
|
||||
|
||||
def _fmt_f(farads: float) -> str:
|
||||
if farads >= 1e-6:
|
||||
return f"{farads * 1e6:.3g}µF"
|
||||
if farads >= 1e-9:
|
||||
return f"{farads * 1e9:.3g}nF"
|
||||
return f"{farads * 1e12:.3g}pF"
|
||||
Reference in New Issue
Block a user