Add HF line and bus analysis to run_pcb_checks (2.62.0).
Phase A: stub, return-split, and BOM termination on buses present on the graph; DDR/CPU/FPGA class certifiers only when that device exists. Skip without evidence. Via, pad, track, and zone stay distinct. Not DRC or FEM.
This commit is contained in:
@@ -152,6 +152,12 @@ def _seed() -> None:
|
||||
requirement="Series resistor on the HS net vs datasheet ohms.")
|
||||
_add("PE-SI-010", "TYPICAL", "INFO", domain="pcb",
|
||||
requirement="HS bus measured; library has no SI FACT — not 90 Ω folklore.")
|
||||
_add("PE-SI-007", "RECOMMENDED", "RISK", domain="pcb",
|
||||
requirement="Dangling track stub vs datasheet millimetres; pad/via/zone are not stubs.")
|
||||
_add("PE-SI-011", "TYPICAL", "REVIEW", domain="pcb",
|
||||
requirement="Continuous GND pour under an HF pair when that pour exists (not IEC).")
|
||||
_add("PE-SI-012", "TYPICAL", "INFO", domain="pcb",
|
||||
requirement="BOM termination on an HF net is reported; missing terminator is a skip.")
|
||||
_add("PE-PLC-004", "RECOMMENDED", "REVIEW", domain="pcb",
|
||||
requirement="Keepout from layout_rules — not a DRC for pad copper.")
|
||||
_add("PE-VIA-001", "TYPICAL", "INFO", domain="pcb",
|
||||
@@ -224,6 +230,26 @@ def _seed() -> None:
|
||||
requirement="PoE class/type from 802.3af/at/bt or Class n — never invented.")
|
||||
_add("PE-POE-003", "MANDATORY", "RULE", domain="shared",
|
||||
requirement="PoE requires magnetics/isolation (MagJack or LAN transformer).")
|
||||
_add("PE-DDR-001", "TYPICAL", "INFO", domain="shared",
|
||||
requirement="DDR class from a DRAM on this graph — never invented.")
|
||||
_add("PE-DDR-002", "MANDATORY", "RULE", domain="shared",
|
||||
requirement="DDR needs CK, DQS, and ≥8 DQ nets when the DRAM is fitted.")
|
||||
_add("PE-DDR-003", "TYPICAL", "INFO", domain="shared",
|
||||
requirement="VTT/VREF reported only when those nets exist.")
|
||||
_add("PE-CPU-001", "TYPICAL", "INFO", domain="shared",
|
||||
requirement="CPU data/address/control bus only if that parallel bus is on the graph.")
|
||||
_add("PE-CPU-002", "TYPICAL", "INFO", domain="shared",
|
||||
requirement="Parallel data bus ≥8 D/DQ/AD nets.")
|
||||
_add("PE-CPU-003", "TYPICAL", "INFO", domain="shared",
|
||||
requirement="Address or muxed AD nets when that CPU bus exists.")
|
||||
_add("PE-CPU-004", "TYPICAL", "INFO", domain="shared",
|
||||
requirement="CS/WE/OE (or FMC equivalents) on the CPU parallel bus.")
|
||||
_add("PE-FPGA-001", "TYPICAL", "INFO", domain="shared",
|
||||
requirement="FPGA class only when an FPGA is fitted.")
|
||||
_add("PE-FPGA-002", "TYPICAL", "INFO", domain="shared",
|
||||
requirement="FPGA config flash reported when a flash IC is on the graph.")
|
||||
_add("PE-FPGA-003", "TYPICAL", "INFO", domain="shared",
|
||||
requirement="FPGA core/IO rails reported only when VCCINT/VCCIO (named) exist.")
|
||||
|
||||
|
||||
_seed()
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
"""DDR / CPU parallel / FPGA class certifiers — only if that device is on the graph.
|
||||
|
||||
Not SI Z0. Not invented VTT/DDR/FPGA. Same FACT/REQUIREMENT/INFERENCE contract
|
||||
as USB-C / Ethernet / PoE (2.61.2).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from backend.periscopex.models import Component, ComponentType, DesignGraph, Finding
|
||||
from backend.periscopex.pcb_net_match import normalize_kicad_hierarchy_net
|
||||
|
||||
SOURCE = "hf_bus_class"
|
||||
|
||||
_MIN_PARALLEL_BITS = 8
|
||||
|
||||
_DDR_MPN_RE = re.compile(
|
||||
r"DDR[234]|MT41|MT47|MT48|K4B|K4A|IS43TR|IS42S|W971|W972|EDB\d|H5TQ|H5TC",
|
||||
re.I,
|
||||
)
|
||||
_SRAM_MPN_RE = re.compile(r"IS61|IS62|CY7C|AS6C|\bSRAM\b", re.I)
|
||||
_FPGA_MPN_RE = re.compile(
|
||||
r"XC[2367]|XCKU|XCZU|LFE5|ICE40|EP[1-4]C|10CL|10M\d|ECP5|"
|
||||
r"ARTIX|KINTEX|SPARTAN|CYCLONE|MAX.?10|POLARFIRE|\bLATTICE\b",
|
||||
re.I,
|
||||
)
|
||||
_FLASH_MPN_RE = re.compile(r"W25|S25|M25|MX25|GD25|AT25|SST25|N25Q", re.I)
|
||||
_DATA_RE = re.compile(r"(?:^|[_/.])(?:DQ|AD|DATA|FMC_D|D)(\d+)(?:$|[_/.])", re.I)
|
||||
_ADDR_RE = re.compile(r"(?:^|[_/.])(?:ADDR|FMC_A|BA|A)(\d+)(?:$|[_/.])", re.I)
|
||||
_CTRL_RE = re.compile(
|
||||
r"(?:^|[_/.])(?:N?WE|N?OE|N?CS|N?LB|N?UB|RAS|CAS|CKE|ODT|"
|
||||
r"FMC_NWE|FMC_NOE|FMC_NE\d|NE\d)(?:$|[_/.])",
|
||||
re.I,
|
||||
)
|
||||
_CK_RE = re.compile(r"(?:DDR.*)?(?:CK|CLK)[_]?[PN](?:$|[_/.])|(?:^|[_/.])CK[_]?[PN]", re.I)
|
||||
_DQS_RE = re.compile(r"DQS", re.I)
|
||||
_VTT_RE = re.compile(r"VTT|VREF", re.I)
|
||||
_FPGA_IO_RE = re.compile(r"VCCINT|VCCIO|VCCAUX|VCORE|VCCBRAM", re.I)
|
||||
|
||||
|
||||
def check_memory_fpga_classes(graph: DesignGraph) -> list[Finding]:
|
||||
"""Certify DDR / CPU parallel / FPGA only when that part exists on this graph."""
|
||||
out: list[Finding] = []
|
||||
out.extend(_check_ddr(graph))
|
||||
out.extend(_check_cpu(graph))
|
||||
out.extend(_check_fpga(graph))
|
||||
return out
|
||||
|
||||
|
||||
def _leaf(net: str) -> str:
|
||||
n = normalize_kicad_hierarchy_net(net)
|
||||
return n.split("/")[-1] if n else ""
|
||||
|
||||
|
||||
def _blob(comp: Component) -> str:
|
||||
return " ".join(str(x or "") for x in (comp.mpn, comp.value, comp.component_subtype))
|
||||
|
||||
|
||||
def _nets_of(comp: Component) -> list[str]:
|
||||
return [n for n in comp.pins.values() if n]
|
||||
|
||||
|
||||
def _indices(nets: list[str], cre: re.Pattern[str]) -> set[int]:
|
||||
found: set[int] = set()
|
||||
for n in nets:
|
||||
m = cre.search(_leaf(n))
|
||||
if m:
|
||||
found.add(int(m.group(1)))
|
||||
return found
|
||||
|
||||
|
||||
def _has_ctrl(nets: list[str]) -> list[str]:
|
||||
return sorted({_leaf(n) for n in nets if _CTRL_RE.search(_leaf(n))})
|
||||
|
||||
|
||||
def _is_ddr_ic(comp: Component) -> bool:
|
||||
if comp.component_type != ComponentType.IC:
|
||||
return False
|
||||
sub = (comp.component_subtype or "").lower()
|
||||
if sub == "ic.memory.ddr" or "ddr" in sub:
|
||||
return True
|
||||
return bool(_DDR_MPN_RE.search(_blob(comp)))
|
||||
|
||||
|
||||
def _is_fpga_ic(comp: Component) -> bool:
|
||||
if comp.component_type != ComponentType.IC:
|
||||
return False
|
||||
sub = (comp.component_subtype or "").lower()
|
||||
if sub == "ic.fpga" or sub.startswith("ic.fpga."):
|
||||
return True
|
||||
return bool(_FPGA_MPN_RE.search(_blob(comp)))
|
||||
|
||||
|
||||
def _is_sram_ic(comp: Component) -> bool:
|
||||
if comp.component_type != ComponentType.IC:
|
||||
return False
|
||||
sub = (comp.component_subtype or "").lower()
|
||||
if sub == "ic.memory.sram":
|
||||
return True
|
||||
return bool(_SRAM_MPN_RE.search(_blob(comp)))
|
||||
|
||||
|
||||
def _is_flash_ic(comp: Component) -> bool:
|
||||
if comp.component_type != ComponentType.IC:
|
||||
return False
|
||||
sub = (comp.component_subtype or "").lower()
|
||||
if sub == "ic.memory.flash":
|
||||
return True
|
||||
return bool(_FLASH_MPN_RE.search(_blob(comp)))
|
||||
|
||||
|
||||
def _is_mcu(comp: Component) -> bool:
|
||||
if comp.component_type != ComponentType.IC:
|
||||
return False
|
||||
if _is_ddr_ic(comp) or _is_fpga_ic(comp) or _is_sram_ic(comp) or _is_flash_ic(comp):
|
||||
return False
|
||||
sub = (comp.component_subtype or "").lower()
|
||||
return sub == "ic.mcu" or sub.startswith("ic.cpu") or "mcu" in (comp.value or "").lower()
|
||||
|
||||
|
||||
def _parallel_groups(nets: list[str]) -> tuple[set[int], set[int], list[str]]:
|
||||
data = _indices(nets, _DATA_RE)
|
||||
addr = _indices(nets, _ADDR_RE)
|
||||
return data, addr, _has_ctrl(nets)
|
||||
|
||||
|
||||
def _check_ddr(graph: DesignGraph) -> list[Finding]:
|
||||
chips = [c for c in graph.components.values() if _is_ddr_ic(c)]
|
||||
if not chips:
|
||||
return []
|
||||
out: list[Finding] = []
|
||||
for comp in sorted(chips, key=lambda c: c.reference):
|
||||
nets = _nets_of(comp)
|
||||
leaves = [_leaf(n) for n in nets]
|
||||
dq = _indices(nets, _DATA_RE)
|
||||
has_dqs = any(_DQS_RE.search(x) for x in leaves)
|
||||
has_ck = any(_CK_RE.search(x) for x in leaves)
|
||||
klass = "DDR"
|
||||
blob = _blob(comp).upper()
|
||||
if "DDR4" in blob:
|
||||
klass = "DDR4"
|
||||
elif "DDR3" in blob or "MT41" in blob or "K4B" in blob:
|
||||
klass = "DDR3"
|
||||
elif "DDR2" in blob:
|
||||
klass = "DDR2"
|
||||
out.append(_info(
|
||||
comp.reference, "PE-DDR-001",
|
||||
f"{comp.reference} class {klass} (device on this graph).",
|
||||
f"mpn={comp.mpn!r}; subtype={comp.component_subtype!r}; dq_bits={len(dq)}.",
|
||||
"DDR class from MPN/subtype on a DRAM that is actually fitted.",
|
||||
mpn=comp.mpn or "",
|
||||
))
|
||||
if len(dq) >= _MIN_PARALLEL_BITS and has_dqs and has_ck:
|
||||
out.append(_info(
|
||||
comp.reference, "PE-DDR-002",
|
||||
f"{comp.reference} has CK, DQS, and {len(dq)} DQ nets.",
|
||||
f"ck={has_ck}; dqs={has_dqs}; dq_indices={sorted(dq)[:16]}.",
|
||||
"DDR needs CK, DQS, and a data bus on the netlist.",
|
||||
mpn=comp.mpn or "",
|
||||
))
|
||||
else:
|
||||
out.append(_insufficient(
|
||||
comp.reference, "PE-DDR-002",
|
||||
f"{comp.reference} is DDR; CK/DQS/DQ bus is not fully evidenced.",
|
||||
f"ck={has_ck}; dqs={has_dqs}; dq_bits={len(dq)} (need ≥{_MIN_PARALLEL_BITS}).",
|
||||
"Do not invent DDR pinout; need CK, DQS, and ≥8 DQ nets.",
|
||||
mpn=comp.mpn or "",
|
||||
))
|
||||
vtt = [n for n in nets if _VTT_RE.search(_leaf(n))]
|
||||
extra = [n for n in graph.nets if _VTT_RE.search(_leaf(n))]
|
||||
names = sorted({_leaf(n) for n in vtt + extra})
|
||||
if names:
|
||||
out.append(_info(
|
||||
comp.reference, "PE-DDR-003",
|
||||
f"{comp.reference} VTT/VREF nets present: {', '.join(names)}.",
|
||||
f"vtt_vref={names}.",
|
||||
"VTT/VREF is reported only when those nets exist — never invented.",
|
||||
mpn=comp.mpn or "",
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def _cpu_host(graph: DesignGraph) -> Component | None:
|
||||
for comp in sorted(graph.components.values(), key=lambda c: c.reference):
|
||||
if not (_is_mcu(comp) or _is_fpga_ic(comp)):
|
||||
continue
|
||||
data, addr, ctrl = _parallel_groups(_nets_of(comp))
|
||||
if len(data) >= _MIN_PARALLEL_BITS and ctrl and (addr or _muxed(data, addr)):
|
||||
if _is_fpga_ic(comp) and not _is_mcu(comp):
|
||||
continue
|
||||
return comp
|
||||
for comp in sorted(graph.components.values(), key=lambda c: c.reference):
|
||||
if not _is_sram_ic(comp):
|
||||
continue
|
||||
data, addr, ctrl = _parallel_groups(_nets_of(comp))
|
||||
if len(data) >= _MIN_PARALLEL_BITS and ctrl:
|
||||
return comp
|
||||
return None
|
||||
|
||||
|
||||
def _muxed(data: set[int], addr: set[int]) -> bool:
|
||||
return bool(data) and data == addr
|
||||
|
||||
|
||||
def _check_cpu(graph: DesignGraph) -> list[Finding]:
|
||||
host = _cpu_host(graph)
|
||||
if host is None:
|
||||
return []
|
||||
if _is_ddr_ic(host):
|
||||
return []
|
||||
nets = _nets_of(host)
|
||||
data, addr, ctrl = _parallel_groups(nets)
|
||||
if _is_fpga_ic(host) and not _is_mcu(host):
|
||||
return []
|
||||
kind = "mux AD" if _muxed(data, addr) else "parallel"
|
||||
if any("FMC" in _leaf(n).upper() for n in nets):
|
||||
kind = "FMC"
|
||||
elif _is_sram_ic(host) or any(_is_sram_ic(c) for c in graph.components.values()):
|
||||
kind = "SRAM"
|
||||
out = [_info(
|
||||
host.reference, "PE-CPU-001",
|
||||
f"{host.reference} class CPU {kind} bus (present on this graph).",
|
||||
f"data_bits={len(data)}; addr_bits={len(addr)}; ctrl={ctrl}.",
|
||||
"CPU data/address/control is certified only when that bus exists.",
|
||||
mpn=host.mpn or "",
|
||||
)]
|
||||
out.append(_info(
|
||||
host.reference, "PE-CPU-002",
|
||||
f"{host.reference} data bus {len(data)} bits.",
|
||||
f"dq_indices={sorted(data)[:16]}.",
|
||||
"Parallel data bus needs ≥8 D/DQ/AD nets.",
|
||||
mpn=host.mpn or "",
|
||||
))
|
||||
if addr and not _muxed(data, addr):
|
||||
out.append(_info(
|
||||
host.reference, "PE-CPU-003",
|
||||
f"{host.reference} address bus {len(addr)} bits.",
|
||||
f"a_indices={sorted(addr)[:16]}.",
|
||||
"Address bus reported when A/ADDR nets exist (mux AD is not a second bus).",
|
||||
mpn=host.mpn or "",
|
||||
))
|
||||
elif _muxed(data, addr):
|
||||
out.append(_info(
|
||||
host.reference, "PE-CPU-003",
|
||||
f"{host.reference} multiplexed AD bus ({len(data)} bits).",
|
||||
f"ad_indices={sorted(data)[:16]}.",
|
||||
"Muxed AD is one bus, not invented separate D and A.",
|
||||
mpn=host.mpn or "",
|
||||
))
|
||||
else:
|
||||
return out
|
||||
if ctrl:
|
||||
out.append(_info(
|
||||
host.reference, "PE-CPU-004",
|
||||
f"{host.reference} control: {', '.join(ctrl)}.",
|
||||
f"ctrl={ctrl}.",
|
||||
"CS/WE/OE (or FMC equivalents) from net names on this bus.",
|
||||
mpn=host.mpn or "",
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def _check_fpga(graph: DesignGraph) -> list[Finding]:
|
||||
fpgas = [c for c in graph.components.values() if _is_fpga_ic(c)]
|
||||
if not fpgas:
|
||||
return []
|
||||
flashes = [c for c in graph.components.values() if _is_flash_ic(c)]
|
||||
out: list[Finding] = []
|
||||
for comp in sorted(fpgas, key=lambda c: c.reference):
|
||||
out.append(_info(
|
||||
comp.reference, "PE-FPGA-001",
|
||||
f"{comp.reference} class FPGA (device on this graph).",
|
||||
f"mpn={comp.mpn!r}; subtype={comp.component_subtype!r}.",
|
||||
"FPGA analysis runs only when an FPGA is fitted — never invented.",
|
||||
mpn=comp.mpn or "",
|
||||
))
|
||||
if flashes:
|
||||
names = ", ".join(sorted(c.reference for c in flashes))
|
||||
out.append(_info(
|
||||
comp.reference, "PE-FPGA-002",
|
||||
f"{comp.reference} config flash present: {names}.",
|
||||
f"flash={[c.reference + '=' + (c.mpn or '') for c in flashes]}.",
|
||||
"Config flash is reported when a flash IC is on the graph.",
|
||||
mpn=comp.mpn or "",
|
||||
))
|
||||
rails = sorted({
|
||||
_leaf(n) for n in _nets_of(comp) if _FPGA_IO_RE.search(_leaf(n))
|
||||
})
|
||||
if rails:
|
||||
out.append(_info(
|
||||
comp.reference, "PE-FPGA-003",
|
||||
f"{comp.reference} FPGA rails: {', '.join(rails)}.",
|
||||
f"rails={rails}.",
|
||||
"Core/IO supplies reported only when VCCINT/VCCIO (or named equivalent) exist.",
|
||||
mpn=comp.mpn or "",
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def _info(
|
||||
designator: str, rule_id: str, finding: str, facts: str, requirement: str,
|
||||
*, mpn: str = "",
|
||||
) -> Finding:
|
||||
rec = "No connection-integrity change required for this check."
|
||||
return Finding(
|
||||
designator=designator, mpn=mpn, aspect="interface_class",
|
||||
finding=finding, why=requirement, facts=facts, requirement=requirement,
|
||||
inference="Class/integrity FACT from netlist — not SI Z0 and not invented I/Z/mm.",
|
||||
status="INFO", recommendation=rec, action=rec, source=SOURCE,
|
||||
rule_id=rule_id, finding_class="INFO", provenance="TYPICAL",
|
||||
evidence_status="SUFFICIENT", confidence=0.9, pins=[],
|
||||
)
|
||||
|
||||
|
||||
def _insufficient(
|
||||
designator: str, rule_id: str, finding: str, facts: str, requirement: str,
|
||||
*, mpn: str = "",
|
||||
) -> Finding:
|
||||
rec = "Add BOM/net evidence, then re-run. Do not invent DDR/CPU/FPGA buses."
|
||||
return Finding(
|
||||
designator=designator, mpn=mpn, aspect="interface_class",
|
||||
finding=finding, why=requirement, facts=facts, requirement=requirement,
|
||||
inference="Insufficient evidence — not inventing DDR pinout, VTT, Z, I, or mm.",
|
||||
status="INFO", recommendation=rec, action=rec, source=SOURCE,
|
||||
rule_id=rule_id, finding_class="INFO", provenance="TYPICAL",
|
||||
evidence_status="INSUFFICIENT", confidence=0.3, pins=[],
|
||||
)
|
||||
@@ -0,0 +1,384 @@
|
||||
"""HF pair geometry: stubs, reference split, BOM termination.
|
||||
|
||||
Track ≠ via ≠ pad ≠ zone. Skip without evidence. No invented 50/90 Ω or mm.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections import defaultdict
|
||||
|
||||
from backend.periscopex.constraints_lookup import match_constraints as _match_constraints
|
||||
from backend.periscopex.models import (
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
Finding,
|
||||
LayoutGraph,
|
||||
LayoutSegment,
|
||||
ResistorSpecs,
|
||||
)
|
||||
from backend.periscopex.pcb_net_match import kicad_nets_match, refs_on_matched_net
|
||||
from backend.periscopex.pcb_power_thermal import _is_gnd_name
|
||||
from backend.periscopex.placement_check import _in_poly
|
||||
from backend.periscopex.si_check import (
|
||||
_bus_in_targets,
|
||||
_expand_bus_token,
|
||||
_num,
|
||||
_quote,
|
||||
bus_class,
|
||||
partner_net,
|
||||
skip_si_net,
|
||||
)
|
||||
|
||||
SOURCE = "hf_line_check"
|
||||
|
||||
# Join tolerance for collinear KiCad segment ends — not an SI length limit.
|
||||
ENDPOINT_SNAP_MM = 0.05
|
||||
COORD_QUANT_MM = 0.01
|
||||
|
||||
|
||||
def check_hf_lines(
|
||||
graph: DesignGraph,
|
||||
constraints_map: dict,
|
||||
layout: LayoutGraph | None,
|
||||
) -> list[Finding]:
|
||||
if layout is None or not layout.segments:
|
||||
return []
|
||||
hs = _hs_nets(layout)
|
||||
if not hs:
|
||||
return []
|
||||
out: list[Finding] = []
|
||||
out.extend(_stub_findings(graph, constraints_map, layout, hs))
|
||||
out.extend(_split_findings(layout, hs))
|
||||
out.extend(_termination_findings(graph, hs))
|
||||
return out
|
||||
|
||||
|
||||
def _hs_nets(layout: LayoutGraph) -> list[str]:
|
||||
names = sorted({s.net for s in layout.segments if s.net})
|
||||
return [n for n in names if bus_class(n) and not skip_si_net(n)]
|
||||
|
||||
|
||||
def _qxy(x: float, y: float) -> tuple[float, float]:
|
||||
q = COORD_QUANT_MM
|
||||
return (round(x / q) * q, round(y / q) * q)
|
||||
|
||||
|
||||
def _edge_key(a: tuple[float, float], b: tuple[float, float]) -> tuple:
|
||||
return tuple(sorted((a, b)))
|
||||
|
||||
|
||||
def _track_graph(layout: LayoutGraph, net: str) -> tuple[
|
||||
dict[tuple[float, float], set[tuple[float, float]]],
|
||||
dict[tuple, float],
|
||||
]:
|
||||
nbrs: dict[tuple[float, float], set[tuple[float, float]]] = defaultdict(set)
|
||||
elen: dict[tuple, float] = {}
|
||||
for s in layout.segments:
|
||||
if not s.net or not kicad_nets_match(s.net, net):
|
||||
continue
|
||||
a = _qxy(*s.start)
|
||||
b = _qxy(*s.end)
|
||||
if a == b:
|
||||
continue
|
||||
nbrs[a].add(b)
|
||||
nbrs[b].add(a)
|
||||
elen[_edge_key(a, b)] = math.hypot(s.end[0] - s.start[0], s.end[1] - s.start[1])
|
||||
return nbrs, elen
|
||||
|
||||
|
||||
def _snap(pt: tuple[float, float], nodes: list[tuple[float, float]]) -> tuple[float, float] | None:
|
||||
px, py = _qxy(*pt)
|
||||
best = None
|
||||
best_d = ENDPOINT_SNAP_MM
|
||||
for n in nodes:
|
||||
d = math.hypot(n[0] - px, n[1] - py)
|
||||
if d <= best_d:
|
||||
best_d = d
|
||||
best = n
|
||||
return best
|
||||
|
||||
|
||||
def _terminals(layout: LayoutGraph, net: str, nodes: list[tuple[float, float]]) -> set[tuple[float, float]]:
|
||||
found: set[tuple[float, float]] = set()
|
||||
for fp in layout.footprints.values():
|
||||
for pad in fp.pads:
|
||||
if pad.net and kicad_nets_match(pad.net, net):
|
||||
hit = _snap((pad.x, pad.y), nodes)
|
||||
if hit:
|
||||
found.add(hit)
|
||||
for v in layout.vias:
|
||||
if v.net and kicad_nets_match(v.net, net):
|
||||
hit = _snap((v.x, v.y), nodes)
|
||||
if hit:
|
||||
found.add(hit)
|
||||
for z in layout.zones:
|
||||
if not z.net or not kicad_nets_match(z.net, net):
|
||||
continue
|
||||
for outline in z.outlines:
|
||||
if len(outline) < 3:
|
||||
continue
|
||||
for node in nodes:
|
||||
if _in_poly(node[0], node[1], outline):
|
||||
found.add(node)
|
||||
return found
|
||||
|
||||
|
||||
def stub_lengths_mm(layout: LayoutGraph, net: str) -> list[float]:
|
||||
"""Dangling track branches on ``net``. Pads, vias, and same-net zones end a stub."""
|
||||
nbrs, elen = _track_graph(layout, net)
|
||||
if not nbrs:
|
||||
return []
|
||||
nodes = list(nbrs)
|
||||
terms = _terminals(layout, net, nodes)
|
||||
stubs: list[float] = []
|
||||
seen: set[tuple[float, float]] = set()
|
||||
for node, friends in nbrs.items():
|
||||
if len(friends) != 1 or node in terms or node in seen:
|
||||
continue
|
||||
prev = None
|
||||
cur = node
|
||||
total = 0.0
|
||||
for _ in range(len(nbrs) + 1):
|
||||
nxts = [n for n in nbrs[cur] if n != prev]
|
||||
if not nxts:
|
||||
break
|
||||
nxt = nxts[0]
|
||||
total += elen.get(_edge_key(cur, nxt), 0.0)
|
||||
prev, cur = cur, nxt
|
||||
if cur in terms or len(nbrs[cur]) != 2:
|
||||
break
|
||||
if total > COORD_QUANT_MM:
|
||||
stubs.append(total)
|
||||
seen.add(node)
|
||||
return stubs
|
||||
|
||||
|
||||
def _stub_limit(graph: DesignGraph, constraints_map: dict, bus: str) -> tuple[float | None, str]:
|
||||
for _ref, comp in sorted(graph.components.items()):
|
||||
if comp.component_type != ComponentType.IC:
|
||||
continue
|
||||
cons = _match_constraints(comp.mpn or comp.value, constraints_map)
|
||||
if not cons:
|
||||
continue
|
||||
for rule in cons.layout_rules or []:
|
||||
kind = str(rule.get("kind") or "")
|
||||
if kind not in {"stub", "max_stub"}:
|
||||
continue
|
||||
targets = _expand_bus_token(str(rule.get("net_class") or ""))
|
||||
if targets and not _bus_in_targets(bus, targets):
|
||||
continue
|
||||
lim = _num(rule.get("max_distance_mm"))
|
||||
if lim is None:
|
||||
continue
|
||||
return lim, _quote(rule)
|
||||
return None, ""
|
||||
|
||||
|
||||
def _pair_seen(net: str, seen: set[tuple[str, str]]) -> bool:
|
||||
p = partner_net(net)
|
||||
key = tuple(sorted((net, p or "")))
|
||||
if key in seen:
|
||||
return True
|
||||
seen.add(key) # type: ignore[arg-type]
|
||||
return False
|
||||
|
||||
|
||||
def _stub_findings(
|
||||
graph: DesignGraph,
|
||||
constraints_map: dict,
|
||||
layout: LayoutGraph,
|
||||
hs: list[str],
|
||||
) -> list[Finding]:
|
||||
out: list[Finding] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
for net in hs:
|
||||
if _pair_seen(net, seen):
|
||||
continue
|
||||
lengths = stub_lengths_mm(layout, net)
|
||||
p = partner_net(net)
|
||||
if p:
|
||||
lengths.extend(stub_lengths_mm(layout, p))
|
||||
if not lengths:
|
||||
continue
|
||||
stub_mm = max(lengths)
|
||||
bc = bus_class(net) or "hs"
|
||||
lim, quote = _stub_limit(graph, constraints_map, bc)
|
||||
facts = f"stub_mm={stub_mm:.3f}; branches={ [round(x, 3) for x in lengths] }."
|
||||
if lim is None:
|
||||
rec = (
|
||||
"Re-extract layout_rules stub millimetres from the datasheet. "
|
||||
"Do not assume a default stub length."
|
||||
)
|
||||
out.append(Finding(
|
||||
designator="layout", mpn="", aspect="si",
|
||||
finding=(
|
||||
f"Unverified: {bc} {net} stub {stub_mm:.2f} mm — "
|
||||
"library has no stub millimetre FACT."
|
||||
),
|
||||
facts=facts,
|
||||
requirement="Datasheet layout_rules stub max_distance_mm on this bus (none on file).",
|
||||
inference="Not an invented stub limit; pads/vias/zones are not stubs.",
|
||||
why="Insufficient library stub millimetres for this bus.",
|
||||
status="INFO", recommendation=rec, action=rec, source=SOURCE,
|
||||
rule_id="PE-SI-007", finding_class="INFO", provenance="TYPICAL",
|
||||
evidence_status="INSUFFICIENT", net=net, pins=[],
|
||||
))
|
||||
continue
|
||||
fail = stub_mm > lim + 1e-9
|
||||
rec = "Shorten the dangling copper on the pair." if fail else "Stub length is within the datasheet millimetre."
|
||||
out.append(Finding(
|
||||
designator="layout", mpn="", aspect="si",
|
||||
finding=(
|
||||
f"{'FAIL' if fail else 'PASS'}: {net} stub {stub_mm:.2f} mm "
|
||||
f"(max {lim:g} mm)."
|
||||
),
|
||||
facts=facts,
|
||||
requirement=f"stub max_distance_mm={lim:g} ({quote or 'layout_rules'})",
|
||||
inference="FAIL vs datasheet stub millimetre." if fail else "",
|
||||
why="Stub is dangling track, not a via or pad.",
|
||||
status="WARNING" if fail else "INFO",
|
||||
recommendation=rec, action=rec, source=SOURCE,
|
||||
rule_id="PE-SI-007",
|
||||
finding_class="RISK" if fail else "INFO",
|
||||
provenance="RECOMMENDED" if fail else "TYPICAL",
|
||||
evidence_status="SUFFICIENT", net=net, pins=[],
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def _ref_layer(layout: LayoutGraph, track_layer: str) -> str | None:
|
||||
stack = layout.stackup
|
||||
if stack is None or not stack.copper_layers:
|
||||
return None
|
||||
layers = stack.copper_layers
|
||||
want = track_layer
|
||||
idx = None
|
||||
for i, name in enumerate(layers):
|
||||
if name == want or name.lower() == want.lower():
|
||||
idx = i
|
||||
break
|
||||
if idx is None:
|
||||
return None
|
||||
if idx + 1 < len(layers):
|
||||
return layers[idx + 1]
|
||||
if idx - 1 >= 0:
|
||||
return layers[idx - 1]
|
||||
return None
|
||||
|
||||
|
||||
def _seg_samples(seg: LayoutSegment) -> list[tuple[float, float]]:
|
||||
return [
|
||||
seg.start,
|
||||
((seg.start[0] + seg.end[0]) / 2.0, (seg.start[1] + seg.end[1]) / 2.0),
|
||||
seg.end,
|
||||
]
|
||||
|
||||
|
||||
def _point_in_gnd_zone(layout: LayoutGraph, x: float, y: float, layer: str) -> bool:
|
||||
for z in layout.zones:
|
||||
if z.keepout or not z.net or not _is_gnd_name(z.net):
|
||||
continue
|
||||
if z.layer and z.layer.lower() != layer.lower():
|
||||
continue
|
||||
for outline in z.outlines:
|
||||
if len(outline) >= 3 and _in_poly(x, y, outline):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _split_findings(layout: LayoutGraph, hs: list[str]) -> list[Finding]:
|
||||
if layout.stackup is None:
|
||||
return []
|
||||
gnd_layers = {
|
||||
z.layer for z in layout.zones
|
||||
if z.net and _is_gnd_name(z.net) and z.layer and not z.keepout and z.outlines
|
||||
}
|
||||
if not gnd_layers:
|
||||
return []
|
||||
out: list[Finding] = []
|
||||
seen: set[tuple[str, str]] = set()
|
||||
for net in hs:
|
||||
if _pair_seen(net, seen):
|
||||
continue
|
||||
segs = [s for s in layout.segments if s.net and kicad_nets_match(s.net, net)]
|
||||
exposed = 0
|
||||
checked = 0
|
||||
ref = None
|
||||
for s in segs:
|
||||
if not s.layer:
|
||||
continue
|
||||
ref = _ref_layer(layout, s.layer)
|
||||
if ref is None or ref not in gnd_layers:
|
||||
continue
|
||||
for x, y in _seg_samples(s):
|
||||
checked += 1
|
||||
if not _point_in_gnd_zone(layout, x, y, ref):
|
||||
exposed += 1
|
||||
if checked == 0 or exposed == 0:
|
||||
continue
|
||||
rec = f"Restore a continuous GND reference under {net}, then re-run PCB review."
|
||||
out.append(Finding(
|
||||
designator="layout", mpn="", aspect="si",
|
||||
finding=(
|
||||
f"{net} crosses a gap in the GND pour on reference layer "
|
||||
f"{ref} ({exposed}/{checked} samples off pour)."
|
||||
),
|
||||
facts=(
|
||||
f"ref_layer={ref}; samples={checked}; off_pour={exposed}; "
|
||||
f"gnd_zone_layers={sorted(gnd_layers)}."
|
||||
),
|
||||
requirement="Continuous reference pour under an HF pair (typical, not IEC).",
|
||||
inference="REVIEW — no creepage/IEC millimetre in context.",
|
||||
why="Split under the line is geometry FACT; via/pad/track stay distinct.",
|
||||
status="WARNING", recommendation=rec, action=rec, source=SOURCE,
|
||||
rule_id="PE-SI-011", finding_class="REVIEW", provenance="TYPICAL",
|
||||
evidence_status="SUFFICIENT", net=net, pins=[],
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def _termination_findings(graph: DesignGraph, hs: list[str]) -> list[Finding]:
|
||||
out: list[Finding] = []
|
||||
seen: set[str] = set()
|
||||
hs_set = set(hs)
|
||||
for net in hs:
|
||||
if skip_si_net(net):
|
||||
continue
|
||||
for ref in refs_on_matched_net(graph, net):
|
||||
if ref in seen:
|
||||
continue
|
||||
c = graph.components.get(ref)
|
||||
if not c or c.component_type != ComponentType.RESISTOR:
|
||||
continue
|
||||
pins = [n for n in c.pins.values() if n]
|
||||
if len(pins) < 2:
|
||||
continue
|
||||
others = [n for n in pins if not kicad_nets_match(n, net)]
|
||||
if not others:
|
||||
continue
|
||||
other = others[0]
|
||||
series = (not _is_gnd_name(other)) and (not skip_si_net(other) or other in hs_set)
|
||||
parallel = _is_gnd_name(other)
|
||||
if not series and not parallel:
|
||||
continue
|
||||
seen.add(ref)
|
||||
ohms = None
|
||||
if isinstance(c.specs, ResistorSpecs) and c.specs.value_ohms:
|
||||
ohms = float(c.specs.value_ohms)
|
||||
kind = "parallel-to-GND" if parallel else "series"
|
||||
rec = "No termination change required; this is a BOM FACT."
|
||||
facts = f"{ref} {kind} on {net}; other={other}; ohms={ohms}."
|
||||
out.append(Finding(
|
||||
designator=ref, mpn=c.mpn or "", aspect="si",
|
||||
finding=f"{net} has {kind} termination {ref}"
|
||||
+ (f" ({ohms:g} Ω)" if ohms is not None else "") + ".",
|
||||
facts=facts,
|
||||
requirement="Report matching parts that exist on the HF net; do not invent a terminator.",
|
||||
inference="BOM FACT — missing terminator is a skip, not a folklore FAIL.",
|
||||
why="Termination is certified only when the resistor is on the netlist.",
|
||||
status="INFO", recommendation=rec, action=rec, source=SOURCE,
|
||||
rule_id="PE-SI-012", finding_class="INFO", provenance="TYPICAL",
|
||||
evidence_status="SUFFICIENT", net=net, pins=[ref],
|
||||
))
|
||||
return out
|
||||
@@ -29,6 +29,8 @@ from backend.periscopex.pcb_power_thermal import (
|
||||
from backend.periscopex.pi_check import check_power_integrity
|
||||
from backend.periscopex.placement_check import check_placement
|
||||
from backend.periscopex.interface_class_check import SOURCE as IF_SOURCE, check_interface_classes
|
||||
from backend.periscopex.hf_bus_class import check_memory_fpga_classes
|
||||
from backend.periscopex.hf_line_check import check_hf_lines
|
||||
from backend.periscopex.si_check import check_si
|
||||
from backend.periscopex.spof_check import check_spof
|
||||
from backend.periscopex.timing_check import check_timing
|
||||
@@ -270,6 +272,7 @@ def run_pcb_checks(
|
||||
("pcb_net_match", lambda: check_pcb_net_match(graph, layout)),
|
||||
("placement_check", lambda: check_placement(graph, constraints_map, layout)),
|
||||
("si_check", lambda: check_si(graph, constraints_map, layout, impedance_nets)),
|
||||
("hf_line", lambda: check_hf_lines(graph, constraints_map, layout)),
|
||||
("pcb_derating", lambda: check_pcb_derating(graph)),
|
||||
("pcb_power_traces", lambda: check_pcb_power_traces(graph, constraints_map, layout)),
|
||||
("pcb_via_current", lambda: check_pcb_via_current(graph, constraints_map, layout)),
|
||||
@@ -286,6 +289,7 @@ def run_pcb_checks(
|
||||
("emi_check", lambda: check_emi(graph, constraints_map, layout)),
|
||||
("pcb_junction_temp", lambda: check_pcb_junction_temp(graph, constraints_map, layout)),
|
||||
("interface_class", lambda: check_interface_classes(graph)),
|
||||
("hf_bus_class", lambda: check_memory_fpga_classes(graph)),
|
||||
):
|
||||
try:
|
||||
out.extend(fn())
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""SI checks: datasheet layout_rules vs board + ImpedenceFinder.
|
||||
|
||||
USB2 / USB3 SuperSpeed / Ethernet MDI / RGMII / SGMII / DDR3 / HDMI /
|
||||
PCIe / LVDS are checked only against a rule whose quote or ``net_class``
|
||||
names that bus. I2C, GPIO, EN, analog REGN, USB CC, and strap/EN RC are
|
||||
not HS pairs. No invented USB/IEC 90 Ω.
|
||||
PCIe / LVDS / MIPI / HF clock are checked only against a rule whose quote
|
||||
or ``net_class`` names that bus. I2C, GPIO, EN, analog REGN, USB CC,
|
||||
strap/EN RC, and crystal XTAL nets are not HS pairs. No invented USB/IEC 90 Ω.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -30,6 +30,11 @@ _SKIP_RE = re.compile(
|
||||
re.I,
|
||||
)
|
||||
|
||||
_XTAL_RE = re.compile(
|
||||
r"XTAL|OSCIN|OSCOUT|HFXIN|LFXIN|\bXIN\b|\bXOUT\b",
|
||||
re.I,
|
||||
)
|
||||
|
||||
_HS_CLASS_RE = (
|
||||
("usb3", re.compile(
|
||||
r"USB3|SSTX|SSRX|USB[_]?SS|SS[_]?T[XR]|SS[_]?RX", re.I,
|
||||
@@ -43,6 +48,7 @@ _HS_CLASS_RE = (
|
||||
r"ETH.?(?:TD|RD|TX|RX|TP)[+\-_PN0-3]|1000BASE|RJ45",
|
||||
re.I,
|
||||
)),
|
||||
("mipi", re.compile(r"MIPI|\bDSI\b|\bCSI\b|CSI[_-]|DSI[_-]", re.I)),
|
||||
("lvds", re.compile(r"LVDS", re.I)),
|
||||
("ddr3_clk", re.compile(r"DDR3?.*CK|(?:^|[_/])CK[_]?[PN](?:$|[_/])", re.I)),
|
||||
("ddr3_dqs", re.compile(r"DQS", re.I)),
|
||||
@@ -51,6 +57,10 @@ _HS_CLASS_RE = (
|
||||
r"DDR3?.*(?:A\d+|ADDR|BA\d+|RAS|CAS|WE|ODT|CKE|(?:^|[_/])CS)",
|
||||
re.I,
|
||||
)),
|
||||
("hf_clk", re.compile(
|
||||
r"GTX_CLK|(?:^|[_/])CLK[_]?[PN](?:$|[_/])|(?:^|[_/])MCLK[_]?[PN]",
|
||||
re.I,
|
||||
)),
|
||||
)
|
||||
|
||||
_STRAP_RULE_RE = re.compile(
|
||||
@@ -94,6 +104,11 @@ _BUS_TOKEN_EXPAND: dict[str, frozenset[str]] = {
|
||||
"hdmi": frozenset({"hdmi"}),
|
||||
"pcie": frozenset({"pcie"}),
|
||||
"lvds": frozenset({"lvds"}),
|
||||
"mipi": frozenset({"mipi"}),
|
||||
"dsi": frozenset({"mipi"}),
|
||||
"csi": frozenset({"mipi"}),
|
||||
"hf_clk": frozenset({"hf_clk"}),
|
||||
"clock": frozenset({"hf_clk"}),
|
||||
}
|
||||
|
||||
_SI_KINDS = frozenset({
|
||||
@@ -151,6 +166,8 @@ def bus_class(net: str) -> str | None:
|
||||
if skip_si_net(net):
|
||||
return None
|
||||
leaf = _leaf(net)
|
||||
if _XTAL_RE.search(leaf):
|
||||
return None
|
||||
u = leaf.upper()
|
||||
if re.search(r"USB3|SSTX|SSRX|USB[_]?SS|SS[_]?T[XR]", u):
|
||||
return "usb3"
|
||||
@@ -217,6 +234,8 @@ def _rule_target_buses(rule: dict) -> frozenset[str]:
|
||||
(r"hdmi", "hdmi"),
|
||||
(r"pcie|pci[\s-]*express", "pcie"),
|
||||
(r"lvds", "lvds"),
|
||||
(r"mipi|\bdsi\b|\bcsi\b", "mipi"),
|
||||
(r"gtx_clk|hf\s*clock|ref\s*clk", "hf_clk"),
|
||||
(r"usb\s*2|d\s*\+|d\s*−|d\s*-|dp\s*/\s*dm|dp/dm", "usb2"),
|
||||
(r"\busb\b", "usb2"),
|
||||
(r"\bethernet\b|\beth\b", "eth_mdi"),
|
||||
|
||||
@@ -18,6 +18,8 @@ Classe dichiarata o inferita (FACT/REQUIREMENT da BOM, net, footprint, sheet). S
|
||||
| PE-ETH-001…004 | c’è un RJ45 | 10/100 vs GbE, coppie TX/RX o 4 MDI, magnetics se GbE, Bob Smith REVIEW |
|
||||
| PE-POE-001…003 | evidenza PoE sul jack | classe/tipo se citati; isolamento/magnetics; **skip** se RJ45 nudo |
|
||||
| PE-DDR-* | solo se c’è un dispositivo DDR | non inventato su HubAudio (nessun DDR in BOM) |
|
||||
| PE-CPU-* | solo se c’è un bus parallelo D/A/controllo | SRAM/NOR/FMC/mux AD; niente bus inventato |
|
||||
| PE-FPGA-* | solo se c’è un FPGA | bank/IO rails e flash di config se presenti; niente FPGA inventata |
|
||||
|
||||
Motore: `periscopex/interface_class_check.py` (grafo, non geometria). Schema `MODE=run` e PCB `MODE=pcb`. L’IA spiega i FACT, non certifica. Nessun I inventato: PE-PWR **skip** se il datasheet non riporta I_load/Imax/I_abs.
|
||||
|
||||
|
||||
@@ -57,7 +57,10 @@ Mutex vs analisi e vs placement. SSE `pcb_*`. IDs `PCB-{ref}-{001}`. KiCad `/net
|
||||
| PE-SI-001 | `length_match` mm on USB/HDMI/PCIe/ETH/LVDS/DDR nets (not I2C/GPIO/CC) |
|
||||
| PE-SI-002 | ImpedenceFinder Zavg/min/max vs `impedance` window |
|
||||
| PE-SI-003…009 | max length, spacing, ref plane, vias, return, series R — each a datasheet number |
|
||||
| PE-SI-007 | dangling **track** stub vs datasheet mm; via ≠ pad ≠ zone; INFO if no mm |
|
||||
| PE-SI-010 | HS bus measured, library has no SI FACT (not 90 Ω folklore) |
|
||||
| PE-SI-011 | GND-pour gap under HF pair when stackup + pour exist (REVIEW, not IEC) |
|
||||
| PE-SI-012 | BOM terminator on HF net (FACT); skip if none |
|
||||
| PE-PWR-001 | I_load/Imax/I_abs + width; **skip** se manca I (no 500 mA, no INFO); IPC solo con thickness e Tjmax |
|
||||
| PE-DRT-001 | Vop e Vrated, Vop > Vr → RULE ERROR |
|
||||
| PE-DRT-002 | 0.8 < Vop/Vr ≤ 1 → RISK MARGIN |
|
||||
@@ -115,6 +118,8 @@ Gate: library `layout_rules` kind + `net_class`/quote on that bus. Empty cell =
|
||||
| SGMII | gated | gated | gated | gated | gated | gated | gated | gated | per-bus |
|
||||
| DDR3 CLK/DQS/DQ/ADDR | gated | gated | gated | gated | gated | gated | gated | gated | per-subclass if no Z |
|
||||
| HDMI / PCIe / LVDS | gated | gated | gated | gated | gated | gated | gated | gated | per-bus |
|
||||
| MIPI DSI/CSI | gated | gated | gated | gated | gated | gated | gated | gated | per-bus |
|
||||
| HF clock (not XTAL) | gated | gated | gated | gated | gated | gated | gated | gated | per-bus |
|
||||
| I2C / GPIO / EN / CC | skip | skip | skip | skip | skip | skip | skip | skip | skip |
|
||||
|
||||
Emmaforo USB: extract with **no Z number** → Zavg ~88 Ω is **PE-SI-010** INFO (insufficient), **not** a 90 Ω FAIL. If library later has 90 Ω ±10% (81–99), Zavg ~88 Ω **PASS** on average and **MARGIN/FAIL** if min is outside the window; ~2.5 mm intra-pair skew **FAIL** only when `length_match` mm exists. PE-SI-009 must not fire on USB from ESP32 EN RC 10 kΩ / 1 µF p.28.
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
What's new in Periscope.
|
||||
|
||||
## 2.62.0 — 2026-09-21 — HF lines/buses in PCB checks (Phase A)
|
||||
|
||||
USB / Eth / DDR / MIPI / PCIe / HDMI / LVDS / HF clock in `run_pcb_checks` at the same finding quality as 2.61.2. Pair integrity: stub (track only; via ≠ pad ≠ zone), reference-split under the line, BOM termination. CPU data/address/control and FPGA only if that device is on the graph. Skip if no evidence. Not DRC. Not FEM. Datasheet millimetres only; no invented Z/I.
|
||||
|
||||
- [New] `PE-SI-007` dangling track stub vs datasheet mm (INFO if no mm).
|
||||
- [New] `PE-SI-011` GND-pour gap under an HF pair (REVIEW; skip without stackup/zone).
|
||||
- [New] `PE-SI-012` BOM series/parallel terminator FACT (skip if none).
|
||||
- [New] `PE-DDR-001`…`003` DRAM class / CK-DQS-DQ / VTT only if DRAM present.
|
||||
- [New] `PE-CPU-001`…`004` parallel CPU bus only if D/A/control exist.
|
||||
- [New] `PE-FPGA-001`…`003` FPGA class / config flash / VCCINT·VCCIO only if FPGA present.
|
||||
- [Changed] `bus_class` adds MIPI and leftover HF clocks; XTAL/OSCIN stay out.
|
||||
|
||||
## 2.61.2 — 2026-09-21 — Certifiers only for connectors on this board
|
||||
|
||||
USB-C / RJ45 / PoE / DDR3 are the method, not a catalog. If this graph has a USB-C receptacle, run USB-C questions. If it has RJ45, run Ethernet. PoE only with PoE evidence — never because RJ45 exists. No connector → no that certifier (not N/A). HubAudio has USB-C USB2 + PoE 10/100 MagJack and no DDR: no DDR findings. Skip-I (2.61.1) unchanged. No FEM, no via IPC, no invented I/Z/PoE/SuperSpeed.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "periscope-web",
|
||||
"version": "2.61.2",
|
||||
"version": "2.62.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"sync-version": "node scripts/sync-version.mjs",
|
||||
|
||||
@@ -18,9 +18,14 @@ export function isLayoutFinding(f: {
|
||||
f.source === "bom_pcb_check" ||
|
||||
f.source === "spof_check" ||
|
||||
f.source === "emi_check" ||
|
||||
f.source === "hf_line_check" ||
|
||||
f.source === "hf_bus_class" ||
|
||||
rid.startsWith("PE-PLC") ||
|
||||
rid.startsWith("PE-LAY") ||
|
||||
rid.startsWith("PE-SI") ||
|
||||
rid.startsWith("PE-DDR") ||
|
||||
rid.startsWith("PE-CPU") ||
|
||||
rid.startsWith("PE-FPGA") ||
|
||||
rid.startsWith("PE-DRT") ||
|
||||
rid.startsWith("PE-PWR") ||
|
||||
rid.startsWith("PE-THM") ||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
/** Stamped from content/changelog.md by scripts/sync-version.mjs. */
|
||||
export const APP_VERSION = "2.61.0";
|
||||
export const APP_VERSION = "2.62.0";
|
||||
export const APP_VERSION_DATE = "2026-09-21";
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
"ic.memory.eeprom": {
|
||||
"description": "EEPROM"
|
||||
},
|
||||
"ic.memory.ddr": {
|
||||
"description": "DDR SDRAM",
|
||||
"example_mpn": "MT41K256M16TW"
|
||||
},
|
||||
"ic.memory.sram": {
|
||||
"description": "Parallel SRAM",
|
||||
"example_mpn": "IS61WV51216"
|
||||
},
|
||||
"ic.fpga": {
|
||||
"description": "Field-programmable gate array",
|
||||
"example_mpn": "XC7A35T"
|
||||
},
|
||||
"ic.logic.buffer": {
|
||||
"description": "Buffer or line driver"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
"""Phase A: HF pair integrity + CPU/FPGA/DDR class — only if present.
|
||||
|
||||
Via ≠ pad ≠ track ≠ zone. No invented Z/I/mm. Skip when evidence is missing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.periscopex.finding_engine import complete_finding, lookup_rule
|
||||
from backend.periscopex.hf_bus_class import check_memory_fpga_classes
|
||||
from backend.periscopex.hf_line_check import check_hf_lines
|
||||
from backend.periscopex.models import (
|
||||
Component,
|
||||
ComponentConstraints,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
LayoutDielectric,
|
||||
LayoutFootprint,
|
||||
LayoutGraph,
|
||||
LayoutPad,
|
||||
LayoutSegment,
|
||||
LayoutStackup,
|
||||
LayoutVia,
|
||||
LayoutZone,
|
||||
Net,
|
||||
NetType,
|
||||
Pin,
|
||||
PinConnection,
|
||||
ResistorSpecs,
|
||||
)
|
||||
from backend.periscopex.pcb_checks import run_pcb_checks
|
||||
from backend.periscopex.si_check import bus_class, check_si, skip_si_net
|
||||
|
||||
|
||||
def _ic(ref: str, pins: dict[str, str], *, mpn: str = "PHY", subtype: str | None = None) -> Component:
|
||||
return Component(
|
||||
reference=ref, value=mpn, footprint="",
|
||||
component_type=ComponentType.IC, mpn=mpn,
|
||||
component_subtype=subtype, pins=pins,
|
||||
)
|
||||
|
||||
|
||||
def _net(name: str, *pairs: tuple[str, str], ntype: NetType = NetType.SIGNAL) -> Net:
|
||||
return Net(
|
||||
name=name, net_type=ntype,
|
||||
pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs],
|
||||
)
|
||||
|
||||
|
||||
def _usb_graph() -> DesignGraph:
|
||||
return DesignGraph(
|
||||
components={
|
||||
"U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}),
|
||||
"J2": Component(
|
||||
reference="J2", value="USB_C", footprint="",
|
||||
component_type=ComponentType.CONNECTOR, pins={"A6": "USB_D+", "A7": "USB_D-"},
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "A6")),
|
||||
"USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "A7")),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _fp_with_pads(ref: str, pads: list[LayoutPad], x: float = 0.0, y: float = 0.0) -> LayoutFootprint:
|
||||
return LayoutFootprint(reference=ref, footprint="P", x=x, y=y, pads=pads)
|
||||
|
||||
|
||||
def test_bus_class_mipi_hf_clk_not_xtal():
|
||||
assert bus_class("MIPI_D0_P") == "mipi"
|
||||
assert bus_class("CSI_CLK_N") == "mipi"
|
||||
assert bus_class("DSI_D1_P") == "mipi"
|
||||
assert bus_class("GTX_CLK") == "hf_clk"
|
||||
assert bus_class("PCIE_REFCLK") == "pcie"
|
||||
assert bus_class("CLK_P") == "hf_clk"
|
||||
assert bus_class("XTAL_IN") is None
|
||||
assert bus_class("OSCIN") is None
|
||||
assert bus_class("HFXIN") is None
|
||||
assert skip_si_net("USB_CC1")
|
||||
assert bus_class("USB_D+") == "usb2"
|
||||
|
||||
|
||||
def test_no_layout_skips_hf_geometry():
|
||||
assert check_hf_lines(_usb_graph(), {}, None) == []
|
||||
assert check_hf_lines(_usb_graph(), {}, LayoutGraph()) == []
|
||||
|
||||
|
||||
def test_pad_to_pad_usb_has_no_stub():
|
||||
layout = LayoutGraph(
|
||||
footprints={
|
||||
"U1": _fp_with_pads("U1", [
|
||||
LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+"),
|
||||
LayoutPad(number="2", x=0.0, y=0.4, net="USB_D-"),
|
||||
]),
|
||||
"J2": _fp_with_pads("J2", [
|
||||
LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+"),
|
||||
LayoutPad(number="A7", x=10.0, y=0.4, net="USB_D-"),
|
||||
], x=10.0),
|
||||
},
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
LayoutSegment(start=(0, 0.4), end=(10, 0.4), width=0.2, layer="F.Cu", net="USB_D-"),
|
||||
],
|
||||
)
|
||||
findings = check_hf_lines(_usb_graph(), {}, layout)
|
||||
assert [f for f in findings if f.rule_id == "PE-SI-007"] == []
|
||||
|
||||
|
||||
def test_via_is_not_a_stub():
|
||||
layout = LayoutGraph(
|
||||
footprints={
|
||||
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
||||
"J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0),
|
||||
},
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(5, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
LayoutSegment(start=(5, 0), end=(10, 0), width=0.2, layer="B.Cu", net="USB_D+"),
|
||||
],
|
||||
vias=[LayoutVia(x=5.0, y=0.0, net="USB_D+", drill=0.3)],
|
||||
)
|
||||
findings = check_hf_lines(_usb_graph(), {}, layout)
|
||||
assert [f for f in findings if f.rule_id == "PE-SI-007"] == []
|
||||
assert all(not isinstance(v, LayoutPad) for v in layout.vias)
|
||||
|
||||
|
||||
def test_zone_is_not_a_track_stub():
|
||||
layout = LayoutGraph(
|
||||
footprints={
|
||||
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
||||
},
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(4, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
],
|
||||
zones=[LayoutZone(
|
||||
net="USB_D+", layer="F.Cu",
|
||||
outlines=[[(3.5, -1.0), (8.0, -1.0), (8.0, 1.0), (3.5, 1.0)]],
|
||||
)],
|
||||
)
|
||||
findings = check_hf_lines(_usb_graph(), {}, layout)
|
||||
assert [f for f in findings if f.rule_id == "PE-SI-007"] == []
|
||||
|
||||
|
||||
def test_dangling_track_stub_vs_datasheet_mm_is_fail():
|
||||
layout = LayoutGraph(
|
||||
footprints={
|
||||
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
||||
"J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0),
|
||||
},
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
LayoutSegment(start=(10, 0), end=(10, 3), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
],
|
||||
)
|
||||
cons = ComponentConstraints(
|
||||
mpn="PHY", pintable=[Pin(number="1", name="D+")],
|
||||
absolute_maximum_ratings=[], rules=[],
|
||||
layout_rules=[{
|
||||
"kind": "stub", "net_class": "usb2", "max_distance_mm": 1.0,
|
||||
"note": "USB stub < 1 mm", "source_page": 12,
|
||||
}],
|
||||
)
|
||||
findings = check_hf_lines(_usb_graph(), {"PHY": cons}, layout)
|
||||
stubs = [f for f in findings if f.rule_id == "PE-SI-007"]
|
||||
assert len(stubs) == 1
|
||||
assert stubs[0].finding.startswith("FAIL:")
|
||||
assert stubs[0].facts.startswith("stub_mm=")
|
||||
assert "1" in stubs[0].requirement
|
||||
complete_finding(stubs[0])
|
||||
assert stubs[0].status == "WARNING"
|
||||
assert stubs[0].finding_class != "RULE" or stubs[0].provenance != "MANDATORY"
|
||||
assert stubs[0].evidence_status == "SUFFICIENT"
|
||||
|
||||
|
||||
def test_measured_stub_without_datasheet_mm_is_insufficient():
|
||||
layout = LayoutGraph(
|
||||
footprints={
|
||||
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
||||
"J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0),
|
||||
},
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
LayoutSegment(start=(5, 0), end=(5, 4), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
],
|
||||
)
|
||||
findings = check_hf_lines(_usb_graph(), {}, layout)
|
||||
stubs = [f for f in findings if f.rule_id == "PE-SI-007"]
|
||||
assert len(stubs) == 1
|
||||
assert stubs[0].status == "INFO"
|
||||
assert stubs[0].evidence_status == "INSUFFICIENT"
|
||||
assert "90" not in (stubs[0].requirement or "")
|
||||
assert "FAIL" not in stubs[0].finding
|
||||
|
||||
|
||||
def test_split_under_pair_needs_stackup_and_zone():
|
||||
segs = [
|
||||
LayoutSegment(start=(0, 0), end=(20, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
]
|
||||
bare = LayoutGraph(segments=segs)
|
||||
assert [f for f in check_hf_lines(_usb_graph(), {}, bare) if f.rule_id == "PE-SI-011"] == []
|
||||
|
||||
stack = LayoutStackup(
|
||||
copper_layers=["F.Cu", "B.Cu"],
|
||||
dielectrics=[LayoutDielectric(name="dielectric_1", er=4.5, height_mm=0.2)],
|
||||
copper_thickness_mm=0.035,
|
||||
)
|
||||
no_zone = LayoutGraph(segments=segs, stackup=stack)
|
||||
assert [f for f in check_hf_lines(_usb_graph(), {}, no_zone) if f.rule_id == "PE-SI-011"] == []
|
||||
|
||||
covered = LayoutGraph(
|
||||
segments=segs, stackup=stack,
|
||||
zones=[LayoutZone(
|
||||
net="GND", layer="B.Cu",
|
||||
outlines=[[(-1, -2), (21, -2), (21, 2), (-1, 2)]],
|
||||
)],
|
||||
)
|
||||
assert [f for f in check_hf_lines(_usb_graph(), {}, covered) if f.rule_id == "PE-SI-011"] == []
|
||||
|
||||
split = LayoutGraph(
|
||||
segments=segs, stackup=stack,
|
||||
zones=[LayoutZone(
|
||||
net="GND", layer="B.Cu",
|
||||
outlines=[[(0, -2), (5, -2), (5, 2), (0, 2)]],
|
||||
)],
|
||||
)
|
||||
hits = [f for f in check_hf_lines(_usb_graph(), {}, split) if f.rule_id == "PE-SI-011"]
|
||||
assert len(hits) == 1
|
||||
assert hits[0].status == "WARNING"
|
||||
assert hits[0].finding_class == "REVIEW"
|
||||
assert hits[0].facts
|
||||
assert hits[0].requirement
|
||||
assert hits[0].inference
|
||||
complete_finding(hits[0])
|
||||
assert hits[0].status != "ERROR"
|
||||
|
||||
|
||||
def test_bom_termination_is_fact_missing_is_skip():
|
||||
layout = LayoutGraph(
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
],
|
||||
)
|
||||
g = _usb_graph()
|
||||
assert [f for f in check_hf_lines(g, {}, layout) if f.rule_id == "PE-SI-012"] == []
|
||||
|
||||
g.components["R22"] = Component(
|
||||
reference="R22", value="22R", footprint="",
|
||||
component_type=ComponentType.RESISTOR,
|
||||
pins={"1": "USB_D+", "2": "USB_D+_PHY"},
|
||||
specs=ResistorSpecs(value_ohms=22.0, value_formatted="22"),
|
||||
)
|
||||
g.nets["USB_D+_PHY"] = _net("USB_D+_PHY", ("R22", "2"))
|
||||
g.nets["USB_D+"].pins.append(PinConnection(component_ref="R22", pin_number="1"))
|
||||
found = [f for f in check_hf_lines(g, {}, layout) if f.rule_id == "PE-SI-012"]
|
||||
assert len(found) == 1
|
||||
assert found[0].status == "INFO"
|
||||
assert found[0].evidence_status == "SUFFICIENT"
|
||||
assert "R22" in found[0].facts
|
||||
assert "22" in found[0].facts
|
||||
assert "50 Ω" not in found[0].requirement
|
||||
|
||||
|
||||
def test_no_ddr_cpu_fpga_on_usb_only_graph():
|
||||
findings = check_memory_fpga_classes(_usb_graph())
|
||||
assert findings == []
|
||||
ids = {f.rule_id for f in run_pcb_checks(_usb_graph(), {}, None)}
|
||||
assert not any(str(i).startswith(("PE-DDR", "PE-CPU", "PE-FPGA")) for i in ids)
|
||||
|
||||
|
||||
def test_ddr_present_class_and_nets_skip_invented_vtt():
|
||||
pins = {str(i + 1): f"DDR3_DQ{i}" for i in range(8)}
|
||||
pins["20"] = "DDR3_DQS0_P"
|
||||
pins["21"] = "DDR3_DQS0_N"
|
||||
pins["22"] = "DDR3_CK_P"
|
||||
pins["23"] = "DDR3_CK_N"
|
||||
pins["24"] = "DDR3_A0"
|
||||
comps = {"U10": _ic("U10", pins, mpn="MT41K256M16TW", subtype="ic.memory.ddr")}
|
||||
nets = {n: _net(n, ("U10", p)) for p, n in pins.items()}
|
||||
g = DesignGraph(components=comps, nets=nets)
|
||||
findings = check_memory_fpga_classes(g)
|
||||
for f in findings:
|
||||
complete_finding(f)
|
||||
assert [f.rule_id for f in findings if f.rule_id == "PE-DDR-001"]
|
||||
assert [f for f in findings if f.rule_id == "PE-DDR-001"][0].status == "INFO"
|
||||
assert [f for f in findings if f.rule_id == "PE-DDR-002"][0].status == "INFO"
|
||||
assert not any(f.rule_id == "PE-DDR-003" for f in findings)
|
||||
assert not any((f.rule_id or "").startswith("PE-CPU") for f in findings)
|
||||
assert not any((f.rule_id or "").startswith("PE-FPGA") for f in findings)
|
||||
|
||||
g.nets["DDR_VTT"] = _net("DDR_VTT", ("U10", "30"), ntype=NetType.POWER)
|
||||
g.components["U10"].pins["30"] = "DDR_VTT"
|
||||
with_vtt = check_memory_fpga_classes(g)
|
||||
assert [f for f in with_vtt if f.rule_id == "PE-DDR-003"][0].status == "INFO"
|
||||
|
||||
|
||||
def test_cpu_parallel_bus_only_when_data_addr_control_exist():
|
||||
mcu_pins = {str(i + 1): f"MCU_D{i}" for i in range(8)}
|
||||
for i in range(8):
|
||||
mcu_pins[str(20 + i)] = f"MCU_A{i}"
|
||||
mcu_pins["40"] = "MCU_NWE"
|
||||
mcu_pins["41"] = "MCU_NOE"
|
||||
mcu_pins["42"] = "MCU_NCS"
|
||||
sram_pins = dict(mcu_pins)
|
||||
comps = {
|
||||
"U1": _ic("U1", mcu_pins, mpn="STM32F407", subtype="ic.mcu"),
|
||||
"U2": _ic("U2", sram_pins, mpn="IS61WV51216", subtype="ic.memory.sram"),
|
||||
}
|
||||
nets = {}
|
||||
for p, n in mcu_pins.items():
|
||||
nets[n] = _net(n, ("U1", p), ("U2", p))
|
||||
g = DesignGraph(components=comps, nets=nets)
|
||||
findings = check_memory_fpga_classes(g)
|
||||
ids = {f.rule_id for f in findings}
|
||||
assert "PE-CPU-001" in ids
|
||||
assert "PE-CPU-002" in ids
|
||||
assert "PE-CPU-003" in ids
|
||||
assert "PE-CPU-004" in ids
|
||||
assert not any(str(i).startswith("PE-DDR") for i in ids)
|
||||
assert not any(str(i).startswith("PE-FPGA") for i in ids)
|
||||
|
||||
gpio = DesignGraph(
|
||||
components={"U1": _ic("U1", {"1": "GPIO0", "2": "GPIO1"}, mpn="MSPM0", subtype="ic.mcu")},
|
||||
nets={
|
||||
"GPIO0": _net("GPIO0", ("U1", "1")),
|
||||
"GPIO1": _net("GPIO1", ("U1", "2")),
|
||||
},
|
||||
)
|
||||
assert check_memory_fpga_classes(gpio) == []
|
||||
|
||||
|
||||
def test_fpga_only_when_present_config_flash_optional():
|
||||
fpga_pins = {"1": "IO_L1P", "2": "IO_L1N", "3": "VCCINT", "4": "VCCIO"}
|
||||
g = DesignGraph(
|
||||
components={"U5": _ic("U5", fpga_pins, mpn="XC7A35T", subtype="ic.fpga")},
|
||||
nets={
|
||||
"IO_L1P": _net("IO_L1P", ("U5", "1")),
|
||||
"IO_L1N": _net("IO_L1N", ("U5", "2")),
|
||||
"VCCINT": _net("VCCINT", ("U5", "3"), ntype=NetType.POWER),
|
||||
"VCCIO": _net("VCCIO", ("U5", "4"), ntype=NetType.POWER),
|
||||
},
|
||||
)
|
||||
findings = check_memory_fpga_classes(g)
|
||||
ids = {f.rule_id for f in findings}
|
||||
assert "PE-FPGA-001" in ids
|
||||
assert "PE-FPGA-003" in ids
|
||||
assert "PE-FPGA-002" not in ids
|
||||
assert not any(str(i).startswith("PE-DDR") for i in ids)
|
||||
assert not any(str(i).startswith("PE-CPU") for i in ids)
|
||||
|
||||
g.components["U6"] = _ic("U6", {"1": "FPGA_CS", "2": "FPGA_MOSI"}, mpn="W25Q64", subtype="ic.memory.flash")
|
||||
g.nets["FPGA_CS"] = _net("FPGA_CS", ("U6", "1"), ("U5", "10"))
|
||||
g.components["U5"].pins["10"] = "FPGA_CS"
|
||||
with_flash = check_memory_fpga_classes(g)
|
||||
assert any(f.rule_id == "PE-FPGA-002" for f in with_flash)
|
||||
|
||||
|
||||
def test_rule_catalog_hf_ids():
|
||||
for rid, domain in (
|
||||
("PE-SI-007", "pcb"),
|
||||
("PE-SI-011", "pcb"),
|
||||
("PE-SI-012", "pcb"),
|
||||
("PE-DDR-001", "shared"),
|
||||
("PE-DDR-002", "shared"),
|
||||
("PE-DDR-003", "shared"),
|
||||
("PE-CPU-001", "shared"),
|
||||
("PE-CPU-002", "shared"),
|
||||
("PE-CPU-003", "shared"),
|
||||
("PE-CPU-004", "shared"),
|
||||
("PE-FPGA-001", "shared"),
|
||||
("PE-FPGA-002", "shared"),
|
||||
("PE-FPGA-003", "shared"),
|
||||
):
|
||||
rec = lookup_rule(rid)
|
||||
assert rec is not None, rid
|
||||
assert rec.domain == domain
|
||||
|
||||
|
||||
def test_existing_si_usb_not_polluted_by_hf_geometry():
|
||||
layout = LayoutGraph(
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
LayoutSegment(start=(0, 0.4), end=(12.5, 0.4), width=0.2, layer="F.Cu", net="USB_D-"),
|
||||
],
|
||||
vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)],
|
||||
)
|
||||
findings = check_si(_usb_graph(), {}, layout, [
|
||||
{
|
||||
"net_name": "USB_D+", "partner_net_name": "USB_D-",
|
||||
"z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0,
|
||||
"length_mm": 10.0, "topologies": ["MICROSTRIP"],
|
||||
},
|
||||
{
|
||||
"net_name": "USB_D-", "partner_net_name": "USB_D+",
|
||||
"z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0,
|
||||
"length_mm": 12.5, "topologies": ["MICROSTRIP"],
|
||||
},
|
||||
])
|
||||
assert any(f.rule_id == "PE-SI-010" for f in findings)
|
||||
assert all(f.rule_id != "PE-SI-002" for f in findings)
|
||||
@@ -559,7 +559,9 @@ def test_run_pcb_checks_skips_absent_interfaces():
|
||||
assert "PE-USBC-001" not in ids
|
||||
assert "PE-ETH-001" not in ids
|
||||
assert "PE-POE-001" not in ids
|
||||
if_f = [f for f in findings if (f.rule_id or "").startswith(("PE-USBC", "PE-ETH", "PE-POE", "PE-DDR"))]
|
||||
if_f = [f for f in findings if (f.rule_id or "").startswith(
|
||||
("PE-USBC", "PE-ETH", "PE-POE", "PE-DDR", "PE-CPU", "PE-FPGA")
|
||||
)]
|
||||
assert if_f == []
|
||||
|
||||
|
||||
|
||||
@@ -6,14 +6,17 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC = ROOT / "periscope" / "src" / "frontend" / "src" / "components/upload/file-upload-zone.tsx"
|
||||
SKIP = ROOT / "periscope" / "src" / "frontend" / "src" / "lib" / "kicad-project-files.ts"
|
||||
|
||||
|
||||
def test_upload_zone_is_src():
|
||||
text = SRC.read_text(encoding="utf-8")
|
||||
skip = SKIP.read_text(encoding="utf-8")
|
||||
assert "Native Periscope overlay" not in text[:400]
|
||||
assert "export function FileUploadZone" in text
|
||||
assert "webkitGetAsEntry" in text
|
||||
assert "webkitRelativePath" in text
|
||||
assert "-backups" in text
|
||||
assert "skipWalkDirName" in text
|
||||
assert "-backups" in skip
|
||||
assert "border-dashed" in text
|
||||
assert "border-emerald-500/40" in text
|
||||
|
||||
@@ -78,6 +78,9 @@ def test_skip_i2c_gpio_cc_regn():
|
||||
assert bus_class("SGMII_TX_P") == "sgmii"
|
||||
assert bus_class("DDR3_DQ0") == "ddr3_dq"
|
||||
assert bus_class("DDR3_DQS0_P") == "ddr3_dqs"
|
||||
assert bus_class("MIPI_D0_P") == "mipi"
|
||||
assert bus_class("GTX_CLK") == "hf_clk"
|
||||
assert bus_class("XTAL_IN") is None
|
||||
assert bus_class("USB_CC1") is None
|
||||
assert bus_class("I2C_SCL") is None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user