Files
periscope/backend/periscopex/timing_check.py
T
michele 342792df2d Ship Fase B slices: hierarchy, derating bands, timing, PI, ESD/return.
ERROR stays only for RULE+MANDATORY. Hierarchy walks component→pin→net→block.
Derating is PASS/MARGIN/RISK from Vop/Vrated. Timing and PI skip without numbers.
ESD and HS return path are REVIEW, not RULE ERROR.
2026-09-20 09:35:04 +02:00

186 lines
7.0 KiB
Python

"""Reset RC and strap dividers — only when R, C, and datasheet times/levels exist."""
from __future__ import annotations
from backend.periscopex.models import (
CapacitorSpecs,
ComponentConstraints,
ComponentType,
DesignGraph,
Finding,
ResistorSpecs,
)
from backend.periscopex.passive_rail_check import (
_is_reset_pin,
_pin_name_tokens,
_resistor_to_ground,
_resistor_to_power,
)
from backend.periscopex.validate import _match_constraints
_T_RESET_KEYS = ("t_reset_min_s", "t_reset_min_ms", "reset_delay_ms", "t_por_ms")
_VIH_KEYS = ("vih", "vih_min_v", "v_ih_min", "vih_min")
_VIL_KEYS = ("vil", "vil_max_v", "v_il_max", "vil_max")
_STRAP_RE = __import__("re").compile(
r"(?:PSEL|BOOT|STRAP|VSENSE|VSET|CFG)",
__import__("re").I,
)
def _specs_map(comp) -> dict:
specs = getattr(comp, "specs", None)
if specs is None:
return {}
vals = getattr(specs, "values", None)
return dict(vals) if isinstance(vals, dict) else {}
def _first_num(values: dict, keys: tuple[str, ...]) -> float | None:
for k in keys:
v = values.get(k)
if isinstance(v, bool) or v is None:
continue
try:
n = float(v)
except (TypeError, ValueError):
continue
if k.endswith("_ms"):
n = n / 1000.0
return n
return None
def _r_on_net(graph: DesignGraph, net: str) -> list[float]:
out: list[float] = []
for ref in graph.components_on_net(net):
comp = graph.components.get(ref)
if not comp or comp.component_type != ComponentType.RESISTOR:
continue
if isinstance(comp.specs, ResistorSpecs) and comp.specs.value_ohms > 0:
out.append(float(comp.specs.value_ohms))
return out
def _c_on_net(graph: DesignGraph, net: str) -> list[float]:
out: list[float] = []
for ref in graph.components_on_net(net):
comp = graph.components.get(ref)
if not comp or comp.component_type != ComponentType.CAPACITOR:
continue
if isinstance(comp.specs, CapacitorSpecs) and comp.specs.value_farads > 0:
out.append(float(comp.specs.value_farads))
return out
def check_timing(
graph: DesignGraph,
constraints_map: dict[str, ComponentConstraints] | None = None,
) -> list[Finding]:
"""PE-TIM-001 RC vs t_reset; PE-TIM-002 strap vs Vih/Vil. Skip without numbers."""
cmap = constraints_map or {}
out: list[Finding] = []
seen: set[str] = set()
for ref, comp in sorted(graph.components.items()):
if comp.component_type != ComponentType.IC:
continue
cons = _match_constraints(comp.mpn or comp.value, cmap)
values = _specs_map(comp)
t_min = _first_num(values, _T_RESET_KEYS)
vih = _first_num(values, _VIH_KEYS)
vil = _first_num(values, _VIL_KEYS)
for pin_num, net in sorted(comp.pins.items(), key=lambda x: str(x[0])):
if not net or net in seen:
continue
if _is_reset_pin(graph, cons, pin_num, net) and t_min is not None:
rs = _r_on_net(graph, net)
cs = _c_on_net(graph, net)
if not rs or not cs:
continue
tau = min(rs) * min(cs)
seen.add(net)
if tau + 1e-18 >= t_min:
continue
rec = (
f"Increase R or C on {net} so τ=RC ≥ {t_min:g} s "
f"(measured τ={tau:g} s)."
)
out.append(Finding(
designator=ref,
mpn=comp.mpn or "",
aspect="timing",
finding=(
f"{ref} reset net {net} has τ=RC={tau:g} s "
f"below t_reset={t_min:g} s."
),
facts=f"R={min(rs):g} Ω, C={min(cs):g} F, τ={tau:g} s.",
requirement=f"Datasheet t_reset_min={t_min:g} s.",
inference="τ=R·C compared to t_reset; no 10 ms default.",
why="Reset delay only when R, C, and t_reset are numeric.",
status="ERROR",
recommendation=rec,
action=rec,
source="timing_check",
rule_id="PE-TIM-001",
calculation=f"τ=R·C={tau:g}; t_min={t_min:g}",
evidence_status="SUFFICIENT",
net=net,
pins=[f"{ref}.{pin_num}"],
))
continue
tokens = _pin_name_tokens(cons, pin_num) or [net, str(pin_num)]
if not any(_STRAP_RE.search(t or "") for t in tokens):
continue
if vih is None and vil is None:
continue
if not (_resistor_to_power(graph, net) and _resistor_to_ground(graph, net)):
continue
rs = _r_on_net(graph, net)
if len(rs) < 2:
continue
rhi, rlo = max(rs), min(rs)
vrail = None
nobj = graph.nets.get(net)
# Strap mid-point: need rail voltage on the pull-up net.
for rref in graph.components_on_net(net):
rcomp = graph.components.get(rref)
if not rcomp or rcomp.component_type != ComponentType.RESISTOR:
continue
for pnet in rcomp.pins.values():
other = graph.nets.get(pnet)
if other and other.voltage and other.voltage > 0:
vrail = float(other.voltage)
if vrail is None:
continue
vstrap = vrail * rlo / (rhi + rlo)
seen.add(net)
fail = (vih is not None and vstrap < vih) or (vil is not None and vstrap > vil and vih is None)
if not fail:
continue
rec = f"Adjust the strap divider on {net} so Vstrap meets Vih/Vil."
out.append(Finding(
designator=ref,
mpn=comp.mpn or "",
aspect="timing",
finding=(
f"{ref} strap {net} V={vstrap:g} V from {rhi:g}/{rlo:g} Ω "
f"on {vrail:g} V rail."
),
facts=f"Vstrap={vstrap:g} V; Rhi={rhi:g}; Rlo={rlo:g}; Vrail={vrail:g}.",
requirement=(
f"Vih={vih if vih is not None else '—'} V, "
f"Vil={vil if vil is not None else '—'} V from specs."
),
inference="Divider ratio vs Vih/Vil; no 50% default.",
why="Strap level only when divider ohms and Vih/Vil exist.",
status="ERROR",
recommendation=rec,
action=rec,
source="timing_check",
rule_id="PE-TIM-002",
calculation=f"V=Vrail·Rlo/(Rhi+Rlo)={vstrap:g}",
evidence_status="SUFFICIENT",
net=net,
pins=[f"{ref}.{pin_num}"],
))
return out