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",
|
||||
|
||||
Reference in New Issue
Block a user