Gate SI layout_rules to the quoted bus, not EN RC onto USB.
PE-SI-009 no longer treats ESP32 CHIP_PU/EN RC as a USB series R. USB without a library Z number stays PE-SI-010 measurement-only. Pintable skill 1.13.0 requires net_class on SI kinds (usb2/usb3/MDI/RGMII/DDR3).
This commit is contained in:
@@ -48,6 +48,20 @@ def has_si_layout_rule(raw: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def has_ungated_si_rule(raw: object) -> bool:
|
||||
"""True when an SI kind has no net_class — it must not paint USB/DDR/PHY."""
|
||||
if not isinstance(raw, list):
|
||||
return False
|
||||
for row in raw:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if str(row.get("kind") or "").strip() not in SI_KINDS:
|
||||
continue
|
||||
if not str(row.get("net_class") or "").strip():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def needs_layout_rules_refresh(
|
||||
data: dict,
|
||||
*,
|
||||
@@ -61,6 +75,9 @@ def needs_layout_rules_refresh(
|
||||
From skill 1.11.0, SI kinds are first-class. An older JSON that only
|
||||
has decoupling/thermal rules is stale and must be re-extracted so
|
||||
ImpedenceFinder checks are not starved.
|
||||
|
||||
From skill 1.13.0, SI kinds without ``net_class`` are ungated (EN RC
|
||||
was painted onto USB). Re-extract those.
|
||||
"""
|
||||
ver = str(data.get("model_version") or "0.0.0")
|
||||
if not min_scan_version or min_scan_version == "0.0.0":
|
||||
@@ -76,7 +93,19 @@ def needs_layout_rules_refresh(
|
||||
except Exception:
|
||||
need_si = False
|
||||
if need_si:
|
||||
return not has_si_layout_rule(data.get("layout_rules"))
|
||||
try:
|
||||
need_bus = Version(min_scan_version) >= Version("1.13.0")
|
||||
except Exception:
|
||||
need_bus = False
|
||||
if need_bus and has_ungated_si_rule(data.get("layout_rules")):
|
||||
return True
|
||||
if has_si_layout_rule(data.get("layout_rules")):
|
||||
return False
|
||||
try:
|
||||
extracted_before_si = Version(ver) < Version("1.11.0")
|
||||
except Exception:
|
||||
extracted_before_si = True
|
||||
return extracted_before_si
|
||||
return not has_any_layout_rule(data.get("layout_rules"))
|
||||
|
||||
|
||||
|
||||
+219
-71
@@ -1,8 +1,9 @@
|
||||
"""SI checks: datasheet layout_rules vs board + ImpedenceFinder.
|
||||
|
||||
USB / HDMI / PCIe / Ethernet / LVDS / DDR (and any net_class on an
|
||||
``impedance`` / ``length_match`` / … rule) are checked. I2C, GPIO, EN,
|
||||
analog REGN, USB CC are not 50 Ω pairs. No invented USB/IEC Z0.
|
||||
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 Ω.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -30,14 +31,71 @@ _SKIP_RE = re.compile(
|
||||
)
|
||||
|
||||
_HS_CLASS_RE = (
|
||||
("usb", re.compile(r"USB", re.I)),
|
||||
("usb3", re.compile(
|
||||
r"USB3|SSTX|SSRX|USB[_]?SS|SS[_]?T[XR]|SS[_]?RX", re.I,
|
||||
)),
|
||||
("hdmi", re.compile(r"HDMI", re.I)),
|
||||
("pcie", re.compile(r"PCIE|PEX_", re.I)),
|
||||
("ethernet", re.compile(r"(?:^|[_/])(ETH|MDI|TRD[0-3])", re.I)),
|
||||
("sgmii", re.compile(r"SGMII", re.I)),
|
||||
("rgmii", re.compile(r"RGMII|(?:^|[_/])GMII", re.I)),
|
||||
("eth_mdi", re.compile(
|
||||
r"(?:^|[_/])(MDI|TRD[0-3]|TCT|RCT)|"
|
||||
r"ETH.?(?:TD|RD|TX|RX|TP)[+\-_PN0-3]|1000BASE|RJ45",
|
||||
re.I,
|
||||
)),
|
||||
("lvds", re.compile(r"LVDS", re.I)),
|
||||
("ddr", re.compile(r"DDR|DQS|(?:^|[_/])DQ\d+", re.I)),
|
||||
("ddr3_clk", re.compile(r"DDR3?.*CK|(?:^|[_/])CK[_]?[PN](?:$|[_/])", re.I)),
|
||||
("ddr3_dqs", re.compile(r"DQS", re.I)),
|
||||
("ddr3_dq", re.compile(r"(?:^|[_/])DQ\d+|DDR3?.*DQ\d+", re.I)),
|
||||
("ddr3_addr", re.compile(
|
||||
r"DDR3?.*(?:A\d+|ADDR|BA\d+|RAS|CAS|WE|ODT|CKE|(?:^|[_/])CS)",
|
||||
re.I,
|
||||
)),
|
||||
)
|
||||
|
||||
_STRAP_RULE_RE = re.compile(
|
||||
r"(?:^|[\s_/.\-])(EN|CHIP_PU|CHIP_EN|ENABLE|NRST|RST|RESET|STRAP|"
|
||||
r"ILIM|BOOT|CHIP_PU)\b|"
|
||||
r"RC\s*(?:delay|filter|network)|"
|
||||
r"10\s*k\s*(?:[Ωohm]|ohm).{0,32}1\s*[µu]F|"
|
||||
r"1\s*[µu]F.{0,32}10\s*k",
|
||||
re.I,
|
||||
)
|
||||
|
||||
_BUS_TOKEN_EXPAND: dict[str, frozenset[str]] = {
|
||||
"usb": frozenset({"usb2"}),
|
||||
"usb2": frozenset({"usb2"}),
|
||||
"usb_2": frozenset({"usb2"}),
|
||||
"usb2_0": frozenset({"usb2"}),
|
||||
"hs_usb": frozenset({"usb2"}),
|
||||
"usb3": frozenset({"usb3"}),
|
||||
"usb_3": frozenset({"usb3"}),
|
||||
"usb3_0": frozenset({"usb3"}),
|
||||
"usb3_1": frozenset({"usb3"}),
|
||||
"superspeed": frozenset({"usb3"}),
|
||||
"ss": frozenset({"usb3"}),
|
||||
"ethernet": frozenset({"eth_mdi"}),
|
||||
"eth": frozenset({"eth_mdi"}),
|
||||
"eth_mdi": frozenset({"eth_mdi"}),
|
||||
"mdi": frozenset({"eth_mdi"}),
|
||||
"rj45": frozenset({"eth_mdi"}),
|
||||
"magnetics": frozenset({"eth_mdi"}),
|
||||
"rgmii": frozenset({"rgmii"}),
|
||||
"gmii": frozenset({"rgmii"}),
|
||||
"mac_phy": frozenset({"rgmii", "sgmii"}),
|
||||
"mac": frozenset({"rgmii", "sgmii"}),
|
||||
"sgmii": frozenset({"sgmii"}),
|
||||
"ddr": frozenset({"ddr3_clk", "ddr3_dqs", "ddr3_dq", "ddr3_addr"}),
|
||||
"ddr3": frozenset({"ddr3_clk", "ddr3_dqs", "ddr3_dq", "ddr3_addr"}),
|
||||
"ddr3_clk": frozenset({"ddr3_clk"}),
|
||||
"ddr3_dqs": frozenset({"ddr3_dqs"}),
|
||||
"ddr3_dq": frozenset({"ddr3_dq"}),
|
||||
"ddr3_addr": frozenset({"ddr3_addr"}),
|
||||
"hdmi": frozenset({"hdmi"}),
|
||||
"pcie": frozenset({"pcie"}),
|
||||
"lvds": frozenset({"lvds"}),
|
||||
}
|
||||
|
||||
_SI_KINDS = frozenset({
|
||||
"impedance", "length_match", "max_length", "spacing",
|
||||
"ref_plane", "si_via", "layer", "series_resistor", "return_path", "si",
|
||||
@@ -84,16 +142,97 @@ def bus_class(net: str) -> str | None:
|
||||
if skip_si_net(net):
|
||||
return None
|
||||
leaf = _leaf(net)
|
||||
if "USB" in leaf.upper() and re.search(r"(D\+|D-|DP|DM)", leaf, re.I):
|
||||
return "usb"
|
||||
u = leaf.upper()
|
||||
if re.search(r"USB3|SSTX|SSRX|USB[_]?SS|SS[_]?T[XR]", u):
|
||||
return "usb3"
|
||||
if "USB" in u and re.search(r"(D\+|D-|DP|DM)", leaf, re.I):
|
||||
return "usb2"
|
||||
if re.search(r"(?:ETH|MAC).*(TXD|RXD|TXC|RXC|TX_CLK|RX_CLK|TX_CTL|RX_CTL|TXEN|RXDV|GTX)", u):
|
||||
return "rgmii"
|
||||
if re.search(r"(?:^|[_/])ETH(?:$|[_/])", u) and re.search(r"[+\-]|_P$|_N$|_P/|_N/", leaf):
|
||||
return "eth_mdi"
|
||||
for cls, cre in _HS_CLASS_RE:
|
||||
if cls == "usb":
|
||||
continue
|
||||
if cre.search(leaf):
|
||||
return cls
|
||||
if re.search(r"DDR", u):
|
||||
return "ddr3_dq"
|
||||
return None
|
||||
|
||||
|
||||
def _norm_bus_token(raw: str) -> str:
|
||||
t = re.sub(r"[^a-z0-9]+", "_", (raw or "").strip().lower()).strip("_")
|
||||
t = t.replace("usb_2_0", "usb2").replace("usb2_0", "usb2")
|
||||
t = t.replace("usb_3_1", "usb3").replace("usb_3_0", "usb3").replace("usb3_0", "usb3")
|
||||
return t
|
||||
|
||||
|
||||
def _expand_bus_token(raw: str) -> frozenset[str]:
|
||||
t = _norm_bus_token(raw)
|
||||
if not t:
|
||||
return frozenset()
|
||||
if t in _BUS_TOKEN_EXPAND:
|
||||
return _BUS_TOKEN_EXPAND[t]
|
||||
for key, buses in _BUS_TOKEN_EXPAND.items():
|
||||
if t == key or t.startswith(key + "_") or key.startswith(t + "_"):
|
||||
return buses
|
||||
return frozenset({t})
|
||||
|
||||
|
||||
def _is_strap_si_rule(rule: dict) -> bool:
|
||||
"""EN / CHIP_PU RC, strap, ILIM — not a USB/DDR/PHY series R."""
|
||||
blob = " ".join(
|
||||
str(rule.get(k) or "") for k in ("pin", "net_class", "note", "parameter")
|
||||
)
|
||||
if _STRAP_RULE_RE.search(blob):
|
||||
return True
|
||||
kind = str(rule.get("kind") or "")
|
||||
if kind == "series_resistor" and re.search(r"[µu]F", blob, re.I):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _rule_target_buses(rule: dict) -> frozenset[str]:
|
||||
nc = str(rule.get("net_class") or "").strip()
|
||||
note = str(rule.get("note") or "")
|
||||
pin = str(rule.get("pin") or "")
|
||||
found: set[str] = set()
|
||||
if nc:
|
||||
found |= set(_expand_bus_token(nc))
|
||||
blob = f"{nc} {note} {pin}"
|
||||
scans: tuple[tuple[str, str], ...] = (
|
||||
(r"super\s*speed|usb\s*3|sstx|ssrx", "usb3"),
|
||||
(r"rgmii|gtx_clk|tx_ctl|rx_ctl", "rgmii"),
|
||||
(r"sgmii", "sgmii"),
|
||||
(r"mdi|rj-?45|magnetics|trd[0-3]|1000\s*base", "eth_mdi"),
|
||||
(r"ddr3|\bddr\b", "ddr3"),
|
||||
(r"hdmi", "hdmi"),
|
||||
(r"pcie|pci[\s-]*express", "pcie"),
|
||||
(r"lvds", "lvds"),
|
||||
(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"),
|
||||
)
|
||||
for pat, token in scans:
|
||||
if re.search(pat, blob, re.I):
|
||||
found |= set(_expand_bus_token(token))
|
||||
if "usb3" in found:
|
||||
found.discard("usb2")
|
||||
return frozenset(found)
|
||||
|
||||
|
||||
def _bus_in_targets(bc: str, targets: frozenset[str]) -> bool:
|
||||
if not bc or not targets:
|
||||
return False
|
||||
if bc in targets:
|
||||
return True
|
||||
for t in targets:
|
||||
if bc.startswith(t + "_") or t.startswith(bc + "_"):
|
||||
return True
|
||||
if t == "ddr3" and bc.startswith("ddr3"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_hs_net(net: str) -> bool:
|
||||
return bus_class(net) is not None
|
||||
|
||||
@@ -166,27 +305,34 @@ def _collect_rules(graph: DesignGraph, constraints_map: dict) -> list[tuple[str,
|
||||
|
||||
|
||||
def _rule_nets(layout: LayoutGraph, rule: dict, graph: DesignGraph) -> list[str]:
|
||||
if _is_strap_si_rule(rule):
|
||||
return []
|
||||
names = {s.net for s in layout.segments if s.net}
|
||||
nc = str(rule.get("net_class") or "").strip().lower()
|
||||
pin = str(rule.get("pin") or "").strip()
|
||||
ic = rule.get("_ic")
|
||||
ic_nets: set[str] = rule.get("_ic_nets") or set()
|
||||
targets = _rule_target_buses(rule)
|
||||
picked: list[str] = []
|
||||
for net in sorted(names):
|
||||
if skip_si_net(net):
|
||||
continue
|
||||
bc = bus_class(net)
|
||||
if not bc:
|
||||
continue
|
||||
on_ic = net in ic_nets or any(kicad_nets_match(net, n) for n in ic_nets)
|
||||
if not on_ic:
|
||||
continue
|
||||
if pin and ic:
|
||||
sch = graph.pin_net(ic, pin) if hasattr(graph, "pin_net") else None
|
||||
if sch and not kicad_nets_match(sch, net):
|
||||
if pin.upper() not in _leaf(net).upper():
|
||||
continue
|
||||
if nc:
|
||||
bc = bus_class(net) or ""
|
||||
if nc not in bc and nc not in _leaf(net).lower():
|
||||
if targets:
|
||||
if not _bus_in_targets(bc, targets):
|
||||
continue
|
||||
else:
|
||||
on_ic = net in ic_nets or any(kicad_nets_match(net, n) for n in ic_nets)
|
||||
if not on_ic or not _is_hs_net(net):
|
||||
# No bus on the quote: pin-scoped only. Never paint USB/DDR/PHY.
|
||||
if not pin:
|
||||
continue
|
||||
picked.append(net)
|
||||
return picked
|
||||
@@ -337,6 +483,7 @@ def check_si(
|
||||
findings: list[Finding] = []
|
||||
rules = _collect_rules(graph, constraints_map)
|
||||
seen: set[tuple] = set()
|
||||
z_covered: set[str] = set()
|
||||
|
||||
for _ref, rule in rules:
|
||||
kind = _param(rule)
|
||||
@@ -385,6 +532,9 @@ def check_si(
|
||||
rec = f"Adjust {net} geometry toward the datasheet Z, then re-run PCB review."
|
||||
if window is None and nom is None:
|
||||
continue
|
||||
bc = bus_class(net)
|
||||
if bc:
|
||||
z_covered.add(bc)
|
||||
if avg is None:
|
||||
findings.append(_si_finding(
|
||||
rule_id="PE-SI-002", net=net, mpn=mpn, designator=ic,
|
||||
@@ -619,59 +769,57 @@ def check_si(
|
||||
))
|
||||
|
||||
hs_present = [s.net for s in layout.segments if s.net and _is_hs_net(s.net)]
|
||||
si_kinds = {
|
||||
"impedance", "zdiff", "z0", "length_match", "skew", "max_length",
|
||||
"spacing", "ref_plane", "layer", "si_via", "series_resistor", "return_path",
|
||||
}
|
||||
if hs_present and not any(_param(r) in si_kinds for _i, r in rules):
|
||||
shown: set[str] = set()
|
||||
for net in hs_present:
|
||||
bc = bus_class(net)
|
||||
if skip_si_net(net) or not bc or bc in shown:
|
||||
continue
|
||||
shown.add(bc)
|
||||
zrow = _z_row(rows, net)
|
||||
partner = _partner_on_board(layout, net, zrow)
|
||||
avg = _z_field(zrow, "z0_avg_ohms", "z0_ohm", "mean_z0", "z0")
|
||||
zmin = _z_field(zrow, "z0_min_ohms")
|
||||
zmax = _z_field(zrow, "z0_max_ohms")
|
||||
la = _z_field(zrow, "length_mm") or net_length_mm(layout, net)
|
||||
lb = 0.0
|
||||
if partner:
|
||||
zb = _z_row(rows, partner)
|
||||
lb = _z_field(zb, "length_mm") or net_length_mm(layout, partner)
|
||||
skew = abs(la - lb) if partner else 0.0
|
||||
rec = (
|
||||
"Re-run schematic review so pintable extract ≥ 1.12.0 fills "
|
||||
"layout_rules (Z0/skew/spacing). Do not assume 90 Ω. PCB does "
|
||||
"not re-read the PDF."
|
||||
)
|
||||
findings.append(Finding(
|
||||
designator="layout",
|
||||
mpn="",
|
||||
aspect="si",
|
||||
finding=(
|
||||
f"Unverified: {bc} {net}"
|
||||
+ (f"/{partner}" if partner else "")
|
||||
+ f" ImpedenceFinder Zavg={avg} Ω (min {zmin}, max {zmax}); "
|
||||
f"skew={skew:.2f} mm — library has no SI FACT."
|
||||
),
|
||||
facts=(
|
||||
f"avg={avg} min={zmin} max={zmax} Ω; "
|
||||
f"L={la:.2f}/{lb:.2f} mm; topologies={zrow.get('topologies') if zrow else None}."
|
||||
),
|
||||
requirement="Datasheet layout_rules impedance/length_match (none on file).",
|
||||
inference="Not USB/IEC 90 Ω folklore; CC/GPIO/I2C are not this check.",
|
||||
why="Insufficient library SI numbers.",
|
||||
status="INFO",
|
||||
recommendation=rec,
|
||||
action=rec,
|
||||
source="si_check",
|
||||
rule_id="PE-SI-010",
|
||||
finding_class="INFO",
|
||||
provenance="TYPICAL",
|
||||
evidence_status="INSUFFICIENT",
|
||||
net=net,
|
||||
pins=[],
|
||||
))
|
||||
z_target = frozenset(z_covered)
|
||||
shown: set[str] = set()
|
||||
for net in hs_present:
|
||||
bc = bus_class(net)
|
||||
if skip_si_net(net) or not bc or bc in shown:
|
||||
continue
|
||||
if bc in z_covered or _bus_in_targets(bc, z_target):
|
||||
continue
|
||||
shown.add(bc)
|
||||
zrow = _z_row(rows, net)
|
||||
partner = _partner_on_board(layout, net, zrow)
|
||||
avg = _z_field(zrow, "z0_avg_ohms", "z0_ohm", "mean_z0", "z0")
|
||||
zmin = _z_field(zrow, "z0_min_ohms")
|
||||
zmax = _z_field(zrow, "z0_max_ohms")
|
||||
la = _z_field(zrow, "length_mm") or net_length_mm(layout, net)
|
||||
lb = 0.0
|
||||
if partner:
|
||||
zb = _z_row(rows, partner)
|
||||
lb = _z_field(zb, "length_mm") or net_length_mm(layout, partner)
|
||||
skew = abs(la - lb) if partner else 0.0
|
||||
rec = (
|
||||
"Re-run schematic review so pintable extract ≥ 1.13.0 fills "
|
||||
"layout_rules impedance for this bus (net_class required). Do not "
|
||||
"assume 90 Ω. PCB does not re-read the PDF."
|
||||
)
|
||||
findings.append(Finding(
|
||||
designator="layout",
|
||||
mpn="",
|
||||
aspect="si",
|
||||
finding=(
|
||||
f"Unverified: {bc} {net}"
|
||||
+ (f"/{partner}" if partner else "")
|
||||
+ f" ImpedenceFinder Zavg={avg} Ω (min {zmin}, max {zmax}); "
|
||||
f"skew={skew:.2f} mm — library has no impedance FACT for this bus."
|
||||
),
|
||||
facts=(
|
||||
f"avg={avg} min={zmin} max={zmax} Ω; "
|
||||
f"L={la:.2f}/{lb:.2f} mm; topologies={zrow.get('topologies') if zrow else None}."
|
||||
),
|
||||
requirement="Datasheet layout_rules impedance on this bus (none on file).",
|
||||
inference="Not USB/IEC 90 Ω folklore; strap/EN RC and CC/GPIO/I2C are not this check.",
|
||||
why="Insufficient library SI numbers for this bus.",
|
||||
status="INFO",
|
||||
recommendation=rec,
|
||||
action=rec,
|
||||
source="si_check",
|
||||
rule_id="PE-SI-010",
|
||||
finding_class="INFO",
|
||||
provenance="TYPICAL",
|
||||
evidence_status="INSUFFICIENT",
|
||||
net=net,
|
||||
pins=[],
|
||||
))
|
||||
return findings
|
||||
|
||||
@@ -144,7 +144,9 @@ PINTABLE_TOOL = {
|
||||
"number — never invent 3 mm/JEDEC), same_layer (bool), min_via_count, "
|
||||
"max_via_count, z0_ohm, zdiff_ohm, tolerance_pct, z_min_ohm, z_max_ohm, "
|
||||
"topology, min_spacing_mm, value_ohms, ref_plane, parameter, "
|
||||
"net_class, note, source_page. Empty array if the PDF has no layout guidance."
|
||||
"net_class (required for SI kinds: usb2 | usb3 | eth_mdi | rgmii | "
|
||||
"sgmii | ddr3 | hdmi | pcie | lvds — never map EN/CHIP_PU RC onto "
|
||||
"USB), note, source_page. Empty array if the PDF has no layout guidance."
|
||||
),
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"default_model_version": "1.12.0",
|
||||
"default_model_version": "1.13.0",
|
||||
"extract-pintable": {
|
||||
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
|
||||
"latest_version": "1784798970179642",
|
||||
|
||||
@@ -97,9 +97,26 @@ AI PCB: skip IC senza pintable (`run schematic review first`).
|
||||
| PE-ESD-001 | ESD connettore/USB | REVIEW se manca parte ESD |
|
||||
| PE-RET-001 | return HS senza via GND | REVIEW, non ERROR |
|
||||
|
||||
**B — SI (shipped, gated):** ImpedenceFinder is **in the checks**, not only the impedance tab dump. Each datasheet SI requirement is one finding: PASS / FAIL / MARGIN + FACT / REQUIREMENT / source. Buses: USB D+/D−, HDMI, PCIe, Ethernet, LVDS, DDR. **Skip** I2C, GPIO, EN, analog REGN, USB CC (not 90 Ω pairs). No invented 50/90 Ω.
|
||||
**B — SI (shipped, gated):** ImpedenceFinder is **in the checks**, not only the impedance tab dump. Each datasheet SI requirement is one finding: PASS / FAIL / MARGIN + FACT / REQUIREMENT / source. A rule applies only to the **quoted bus**. **Skip** I2C, GPIO, EN, analog REGN, USB CC, strap/EN RC (not USB/DDR/PHY pairs). No invented 50/90 Ω.
|
||||
|
||||
Emmaforo USB verdict: if library has e.g. 90 Ω ±10% (81–99), Zavg ~88 Ω **PASS** on average and **MARGIN/FAIL** if min (~46 Ω) is outside the window; ~2.5 mm intra-pair skew **FAIL** only when `length_match` mm exists and 2.5 > limit. Without library Z, **PE-SI-010** INFO (insufficient) — not a 90 Ω ERROR.
|
||||
### Coverage matrix (bus × requirement)
|
||||
|
||||
Gate: library `layout_rules` kind + `net_class`/quote on that bus. Empty cell = PE-SI-010 INFO (measurement only) if the bus is on the board; never a folklore 90 Ω ERROR.
|
||||
|
||||
| Bus | Z (002) | skew (001) | max L (003) | spacing (004) | ref/layer (005) | vias (006) | return (008) | series R (009) | no-Z (010) |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| USB2 D+/D− | gated | gated | gated | gated | gated | gated | gated | gated; **not** EN RC | measurement-only if no Z FACT |
|
||||
| USB3 SuperSpeed | gated | gated | gated | gated | gated | gated | gated | gated | per-bus |
|
||||
| ETH MDI / PHY+RJ45 | gated | gated | gated | gated | gated | gated | gated | gated | per-bus |
|
||||
| RGMII MAC–PHY | gated | gated | gated | gated | gated | gated | gated | gated | per-bus |
|
||||
| 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 |
|
||||
| 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.
|
||||
|
||||
Pintable skill **1.13.0** (`net_class` required on SI kinds). Older extracts with ungated `series_resistor` refresh on schematic re-run (`si_extract_needed.json` on PCB).
|
||||
|
||||
**B — still missing (true leftovers):**
|
||||
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
What's new in Periscope.
|
||||
|
||||
## 2.37.0 — 2026-09-20 — SI bus gate (EN RC ≠ USB series R)
|
||||
|
||||
Datasheet SI facts apply only to the bus in the quote. ESP32 EN RC 10 kΩ / 1 µF is not a USB D+/D− series resistor. USB without a library Z number stays measurement-only (88 Ω), not a 90 Ω FAIL. Coverage: PHY+RJ45 MDI, MAC–PHY RGMII/SGMII, DDR3, USB-C SuperSpeed.
|
||||
|
||||
- [Fixed] `PE-SI-009` (and other SI kinds) require `net_class` / quote bus match; strap/EN RC is skipped.
|
||||
- [Changed] `bus_class`: `usb2` / `usb3` / `eth_mdi` / `rgmii` / `sgmii` / `ddr3_*`.
|
||||
- [Changed] `PE-SI-010` is per bus with no impedance FACT (not silenced by an unrelated SI kind).
|
||||
- [Changed] Pintable skill `1.13.0`: SI `net_class` required; EN/CHIP_PU RC is not `series_resistor`.
|
||||
|
||||
## 2.36.0 — 2026-09-20 — Fase A: operator identity + PinScope fence
|
||||
|
||||
This instance is operated by Michele Bigi. Faradworks, Inc. is not the operator. PinScope identifiers are dual-read only. PCB BOM/SPOF/EMI/θJA slices from 2.35.0 stay on this branch.
|
||||
|
||||
@@ -72,6 +72,10 @@ You **must** look for layout guidance. Emit `layout_rules` as a list. Use `[]` o
|
||||
|
||||
Do **not** emit impedance/50 Ω rules for I2C, GPIO, EN, analog REGN, or USB CC. Do **not** invent USB 90 Ω unless **this** datasheet states a number.
|
||||
|
||||
`net_class` is **required** for every SI kind (`impedance`, `length_match`, `max_length`, `spacing`, `ref_plane`, `si_via`, `layer`, `series_resistor`, `return_path`, `si`). Use one of: `usb2`, `usb3`, `eth_mdi`, `rgmii`, `sgmii`, `ddr3`, `hdmi`, `pcie`, `lvds`. PCB review will not map a rule onto another bus.
|
||||
|
||||
`series_resistor` is a termination / series R **on that HS net** (e.g. USB 22 Ω, RGMII 22 Ω). It is **not** CHIP_PU / EN / RESET RC (10 kΩ + 1 µF), ILIM, or a strap divider — omit those or use `decoupling_proximity` / leave them to timing checks.
|
||||
|
||||
#### Fields
|
||||
- `pin` — number or name as printed (`"5"`, `"VIN"`, `"VDD"`, `"EP"`)
|
||||
- `cap_value_hint` — only if shown (`"100nF"`, `"10µF"`)
|
||||
@@ -81,7 +85,8 @@ Do **not** emit impedance/50 Ω rules for I2C, GPIO, EN, analog REGN, or USB CC.
|
||||
- **Never invent** JEDEC, USB, IPC, or “standard 3 mm / 5 mm” distances
|
||||
- `same_layer` — `true`/`false` only if text says same side / opposite side of the board; else null
|
||||
- `min_via_count` — integer only if stated (“at least 4 vias”)
|
||||
- `net_class` / `note` — short quote of the guidance
|
||||
- `net_class` — **required for SI kinds**: `usb2` | `usb3` | `eth_mdi` | `rgmii` | `sgmii` | `ddr3` | `hdmi` | `pcie` | `lvds`. Must match the quoted bus (PHY+RJ45 = `eth_mdi`, MAC–PHY = `rgmii`/`sgmii`, USB-C SuperSpeed = `usb3`, USB D+/D− = `usb2`). Never leave SI `net_class` empty.
|
||||
- `note` — short quote of the guidance
|
||||
- `source_page` — 1-based page of the guidance (required when you emit a rule)
|
||||
|
||||
#### Examples
|
||||
@@ -129,6 +134,8 @@ Thermal vias:
|
||||
- Do not invent land-pattern pad sizes from the mechanical drawing alone
|
||||
- Do not emit `length_match` or `impedance` for USB/HDMI/PCIe unless **this** datasheet states a skew/Z number
|
||||
- Do not treat I2C, GPIO, EN, analog, or USB-CC as 50 Ω / 90 Ω pairs
|
||||
- Do not emit `series_resistor` for EN / CHIP_PU / RESET RC, ILIM, or strap networks
|
||||
- Do not emit an SI kind without `net_class` naming the quoted bus
|
||||
- Do not use kinds outside the closed set
|
||||
- One rule per distinct pin/guidance; prefer supply pins that show caps in the application figure
|
||||
|
||||
|
||||
@@ -154,6 +154,27 @@ def validate(data: dict) -> list[str]:
|
||||
errors.append(
|
||||
f"layout_rules[{i}].same_layer must be a boolean or null"
|
||||
)
|
||||
si_kinds = {
|
||||
"length_match", "impedance", "max_length", "spacing",
|
||||
"ref_plane", "si_via", "layer", "series_resistor",
|
||||
"return_path", "si",
|
||||
}
|
||||
nc = row.get("net_class")
|
||||
if kind in si_kinds and not (isinstance(nc, str) and nc.strip()):
|
||||
errors.append(
|
||||
f"layout_rules[{i}] SI kind {kind!r} requires net_class "
|
||||
f"(usb2|usb3|eth_mdi|rgmii|sgmii|ddr3|hdmi|pcie|lvds)"
|
||||
)
|
||||
note = str(row.get("note") or "")
|
||||
pin = str(row.get("pin") or "")
|
||||
if kind == "series_resistor" and (
|
||||
re.search(r"[µu]F", note, re.I)
|
||||
or re.search(r"\b(EN|CHIP_PU|CHIP_EN|STRAP|ILIM)\b", f"{note} {pin}", re.I)
|
||||
):
|
||||
errors.append(
|
||||
f"layout_rules[{i}] series_resistor is HS termination, "
|
||||
f"not EN/CHIP_PU RC or strap"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@@ -48,10 +48,35 @@ def test_si_refresh_when_old_extract_has_only_decoupling():
|
||||
)
|
||||
assert not needs_layout_rules_refresh(
|
||||
{
|
||||
"model_version": "1.9.0",
|
||||
"layout_rules": [{"kind": "decoupling_proximity", "max_distance_mm": None}],
|
||||
"model_version": "1.12.0",
|
||||
"layout_rules": [{"kind": "series_resistor", "value_ohms": 10000}],
|
||||
},
|
||||
min_scan_version="1.10.0",
|
||||
min_scan_version="1.12.0",
|
||||
)
|
||||
assert needs_layout_rules_refresh(
|
||||
{
|
||||
"model_version": "1.12.0",
|
||||
"layout_rules": [{
|
||||
"kind": "series_resistor",
|
||||
"value_ohms": 10000,
|
||||
"note": "EN RC 10 kΩ / 1 µF",
|
||||
}],
|
||||
},
|
||||
min_scan_version="1.13.0",
|
||||
)
|
||||
assert not needs_layout_rules_refresh(
|
||||
{
|
||||
"model_version": "1.12.0",
|
||||
"layout_rules": [{"kind": "impedance", "net_class": "usb2", "zdiff_ohm": 90}],
|
||||
},
|
||||
min_scan_version="1.13.0",
|
||||
)
|
||||
assert not needs_layout_rules_refresh(
|
||||
{
|
||||
"model_version": "1.12.0",
|
||||
"layout_rules": [],
|
||||
},
|
||||
min_scan_version="1.13.0",
|
||||
)
|
||||
|
||||
|
||||
|
||||
+208
-2
@@ -69,7 +69,13 @@ def test_skip_i2c_gpio_cc_regn():
|
||||
assert skip_si_net("ESP32_EN")
|
||||
assert not skip_si_net("USB_D+")
|
||||
assert not skip_si_net("USB_DP")
|
||||
assert bus_class("USB_D+") == "usb"
|
||||
assert bus_class("USB_D+") == "usb2"
|
||||
assert bus_class("USB_SSTX_P") == "usb3"
|
||||
assert bus_class("ETH_TX+") == "eth_mdi"
|
||||
assert bus_class("RGMII_TXD0") == "rgmii"
|
||||
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("USB_CC1") is None
|
||||
assert bus_class("I2C_SCL") is None
|
||||
|
||||
@@ -170,7 +176,10 @@ def test_usb_without_library_z_is_insufficient_not_90ohm_fail():
|
||||
f = next(x for x in findings if x.rule_id == "PE-SI-010")
|
||||
assert f.evidence_status == "INSUFFICIENT"
|
||||
assert f.status == "INFO"
|
||||
assert "90" not in (f.requirement or "") or "folklore" in (f.inference or "").lower() or True
|
||||
assert "90" not in (f.requirement or "")
|
||||
blob = f"{f.finding} {f.requirement} {f.inference}"
|
||||
assert "FAIL" not in f.finding
|
||||
assert "90 Ω" not in blob or "folklore" in (f.inference or "").lower()
|
||||
assert "CC" not in (f.net or "")
|
||||
|
||||
|
||||
@@ -215,3 +224,200 @@ def test_i2c_not_checked_as_50_ohm():
|
||||
])
|
||||
assert all("I2C" not in (f.net or "") for f in findings)
|
||||
assert all("SDA" not in f.finding for f in findings)
|
||||
|
||||
|
||||
def test_en_rc_series_resistor_does_not_paint_usb():
|
||||
"""PE-SI-009 must not use ESP32 EN RC 10 kΩ / 1 µF as a USB series R."""
|
||||
cons = ComponentConstraints(
|
||||
mpn="ESP32",
|
||||
pintable=[Pin(number="1", name="D+"), Pin(number="3", name="EN")],
|
||||
absolute_maximum_ratings=[],
|
||||
rules=[],
|
||||
layout_rules=[{
|
||||
"kind": "series_resistor",
|
||||
"pin": "EN",
|
||||
"value_ohms": 10000,
|
||||
"note": "EN RC 10 kΩ / 1 µF",
|
||||
"source_page": 28,
|
||||
}],
|
||||
)
|
||||
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows())
|
||||
si009 = [f for f in findings if f.rule_id == "PE-SI-009"]
|
||||
assert si009 == []
|
||||
zfail = [f for f in findings if f.rule_id == "PE-SI-002"]
|
||||
assert zfail == []
|
||||
si010 = [f for f in findings if f.rule_id == "PE-SI-010"]
|
||||
assert si010
|
||||
assert si010[0].evidence_status == "INSUFFICIENT"
|
||||
assert "88" in (si010[0].finding or "")
|
||||
assert "90" not in (si010[0].requirement or "")
|
||||
|
||||
|
||||
def _phy_graph() -> DesignGraph:
|
||||
pins = {
|
||||
"1": "ETH_TX+", "2": "ETH_TX-",
|
||||
"3": "RGMII_TXD0", "4": "RGMII_TXD1",
|
||||
"5": "USB_SSTX_P", "6": "USB_SSTX_N",
|
||||
"7": "DDR3_DQ0", "8": "DDR3_DQ1",
|
||||
"9": "DDR3_DQS0_P", "10": "DDR3_DQS0_N",
|
||||
"11": "USB_D+", "12": "USB_D-",
|
||||
}
|
||||
nets = {
|
||||
n: Net(name=n, net_type=NetType.SIGNAL, pins=[
|
||||
PinConnection(component_ref="U1", pin_number=p),
|
||||
])
|
||||
for p, n in pins.items()
|
||||
}
|
||||
return DesignGraph(
|
||||
components={
|
||||
"U1": Component(
|
||||
reference="U1", value="PHY", footprint="",
|
||||
component_type=ComponentType.IC, mpn="PHY",
|
||||
pins=pins,
|
||||
),
|
||||
},
|
||||
nets=nets,
|
||||
)
|
||||
|
||||
|
||||
def _phy_layout() -> LayoutGraph:
|
||||
segs = [
|
||||
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="ETH_TX+"),
|
||||
LayoutSegment(start=(0, 0.4), end=(10.2, 0.4), width=0.2, layer="F.Cu", net="ETH_TX-"),
|
||||
LayoutSegment(start=(0, 2), end=(8, 2), width=0.15, layer="F.Cu", net="RGMII_TXD0"),
|
||||
LayoutSegment(start=(0, 2.4), end=(8.4, 2.4), width=0.15, layer="F.Cu", net="RGMII_TXD1"),
|
||||
LayoutSegment(start=(0, 4), end=(20, 4), width=0.12, layer="F.Cu", net="USB_SSTX_P"),
|
||||
LayoutSegment(start=(0, 4.3), end=(20.5, 4.3), width=0.12, layer="F.Cu", net="USB_SSTX_N"),
|
||||
LayoutSegment(start=(0, 6), end=(15, 6), width=0.1, layer="F.Cu", net="DDR3_DQ0"),
|
||||
LayoutSegment(start=(0, 6.2), end=(15.1, 6.2), width=0.1, layer="F.Cu", net="DDR3_DQ1"),
|
||||
LayoutSegment(start=(0, 7), end=(12, 7), width=0.1, layer="F.Cu", net="DDR3_DQS0_P"),
|
||||
LayoutSegment(start=(0, 7.2), end=(12.3, 7.2), width=0.1, layer="F.Cu", net="DDR3_DQS0_N"),
|
||||
LayoutSegment(start=(0, 9), end=(10, 9), width=0.2, layer="F.Cu", net="USB_D+"),
|
||||
LayoutSegment(start=(0, 9.4), end=(12.5, 9.4), width=0.2, layer="F.Cu", net="USB_D-"),
|
||||
]
|
||||
return LayoutGraph(segments=segs, vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)])
|
||||
|
||||
|
||||
def _z(name, partner, avg, length, **kw):
|
||||
row = {
|
||||
"net_name": name,
|
||||
"partner_net_name": partner,
|
||||
"is_differential": True,
|
||||
"z0_avg_ohms": avg,
|
||||
"z0_min_ohms": avg - 2,
|
||||
"z0_max_ohms": avg + 2,
|
||||
"length_mm": length,
|
||||
"topologies": ["MICROSTRIP"],
|
||||
"flags": (),
|
||||
}
|
||||
row.update(kw)
|
||||
return row
|
||||
|
||||
|
||||
def test_coverage_mdi_rgmii_ddr3_usb3_gated_not_cross_mapped():
|
||||
cons = ComponentConstraints(
|
||||
mpn="PHY",
|
||||
pintable=[Pin(number="1", name="TX+")],
|
||||
absolute_maximum_ratings=[],
|
||||
rules=[],
|
||||
layout_rules=[
|
||||
{
|
||||
"kind": "impedance",
|
||||
"net_class": "eth_mdi",
|
||||
"zdiff_ohm": 100,
|
||||
"tolerance_pct": 10,
|
||||
"note": "MDI 100 Ω to RJ45",
|
||||
"source_page": 40,
|
||||
},
|
||||
{
|
||||
"kind": "max_length",
|
||||
"net_class": "rgmii",
|
||||
"max_distance_mm": 20,
|
||||
"note": "RGMII MAC–PHY trace < 20 mm",
|
||||
"source_page": 41,
|
||||
},
|
||||
{
|
||||
"kind": "length_match",
|
||||
"net_class": "ddr3",
|
||||
"max_distance_mm": 1.0,
|
||||
"note": "DDR3 DQS intra-pair",
|
||||
"source_page": 42,
|
||||
},
|
||||
{
|
||||
"kind": "impedance",
|
||||
"net_class": "usb3",
|
||||
"zdiff_ohm": 90,
|
||||
"tolerance_pct": 10,
|
||||
"note": "USB-C SuperSpeed 90 Ω",
|
||||
"source_page": 43,
|
||||
},
|
||||
{
|
||||
"kind": "series_resistor",
|
||||
"net_class": "rgmii",
|
||||
"value_ohms": 22,
|
||||
"note": "RGMII series 22 Ω",
|
||||
"source_page": 41,
|
||||
},
|
||||
],
|
||||
)
|
||||
rows = [
|
||||
_z("ETH_TX+", "ETH_TX-", 95.0, 10.0),
|
||||
_z("ETH_TX-", "ETH_TX+", 95.0, 10.2),
|
||||
_z("USB_SSTX_P", "USB_SSTX_N", 88.0, 20.0),
|
||||
_z("USB_SSTX_N", "USB_SSTX_P", 88.0, 20.5),
|
||||
_z("DDR3_DQS0_P", "DDR3_DQS0_N", 50.0, 12.0),
|
||||
_z("DDR3_DQS0_N", "DDR3_DQS0_P", 50.0, 12.3),
|
||||
_z("USB_D+", "USB_D-", 88.0, 10.0),
|
||||
_z("USB_D-", "USB_D+", 88.0, 12.5),
|
||||
{"net_name": "RGMII_TXD0", "z0_avg_ohms": 48.0, "length_mm": 8.0, "topologies": ["MICROSTRIP"]},
|
||||
{"net_name": "RGMII_TXD1", "z0_avg_ohms": 48.0, "length_mm": 8.4, "topologies": ["MICROSTRIP"]},
|
||||
{"net_name": "DDR3_DQ0", "z0_avg_ohms": 50.0, "length_mm": 15.0, "topologies": ["MICROSTRIP"]},
|
||||
]
|
||||
findings = check_si(_phy_graph(), {"PHY": cons}, _phy_layout(), rows)
|
||||
by = {}
|
||||
for f in findings:
|
||||
by.setdefault(f.rule_id, []).append(f)
|
||||
|
||||
mdi_z = [f for f in by.get("PE-SI-002", []) if f.net and "ETH" in f.net]
|
||||
assert mdi_z and mdi_z[0].finding.startswith("PASS:")
|
||||
usb3_z = [f for f in by.get("PE-SI-002", []) if f.net and "SSTX" in f.net]
|
||||
assert usb3_z and usb3_z[0].finding.startswith("PASS:")
|
||||
usb2_z = [f for f in by.get("PE-SI-002", []) if f.net and "USB_D" in (f.net or "")]
|
||||
assert usb2_z == []
|
||||
|
||||
rgmii_len = [f for f in by.get("PE-SI-003", []) if f.net and "RGMII" in f.net]
|
||||
assert rgmii_len and rgmii_len[0].finding.startswith("PASS:")
|
||||
assert not any(f.net and "ETH_TX" in f.net for f in by.get("PE-SI-003", []))
|
||||
|
||||
dqs = [f for f in by.get("PE-SI-001", []) if f.net and "DQS" in f.net]
|
||||
assert dqs and dqs[0].finding.startswith("PASS:")
|
||||
|
||||
si009 = by.get("PE-SI-009", [])
|
||||
assert si009
|
||||
assert all(f.net and "RGMII" in f.net for f in si009)
|
||||
assert not any(f.net and "USB" in f.net for f in si009)
|
||||
|
||||
si010 = by.get("PE-SI-010", [])
|
||||
nets010 = {f.net for f in si010}
|
||||
assert any(n and "USB_D" in n for n in nets010)
|
||||
assert not any(n and "ETH_TX" in n for n in nets010)
|
||||
assert not any(n and "SSTX" in n for n in nets010)
|
||||
|
||||
|
||||
def test_empty_net_class_impedance_does_not_paint_usb():
|
||||
cons = ComponentConstraints(
|
||||
mpn="PHY",
|
||||
pintable=[Pin(number="1", name="D+")],
|
||||
absolute_maximum_ratings=[],
|
||||
rules=[],
|
||||
layout_rules=[{
|
||||
"kind": "impedance",
|
||||
"z0_ohm": 50,
|
||||
"tolerance_pct": 10,
|
||||
"note": "generic 50 Ω with no bus",
|
||||
"source_page": 1,
|
||||
}],
|
||||
)
|
||||
findings = check_si(_usb_graph(), {"PHY": cons}, _usb_layout(), _if_rows())
|
||||
assert all(f.rule_id != "PE-SI-002" for f in findings)
|
||||
assert any(f.rule_id == "PE-SI-010" for f in findings)
|
||||
|
||||
Reference in New Issue
Block a user