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
|
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(
|
def needs_layout_rules_refresh(
|
||||||
data: dict,
|
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
|
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
|
has decoupling/thermal rules is stale and must be re-extracted so
|
||||||
ImpedenceFinder checks are not starved.
|
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")
|
ver = str(data.get("model_version") or "0.0.0")
|
||||||
if not min_scan_version or min_scan_version == "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:
|
except Exception:
|
||||||
need_si = False
|
need_si = False
|
||||||
if need_si:
|
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"))
|
return not has_any_layout_rule(data.get("layout_rules"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+176
-28
@@ -1,8 +1,9 @@
|
|||||||
"""SI checks: datasheet layout_rules vs board + ImpedenceFinder.
|
"""SI checks: datasheet layout_rules vs board + ImpedenceFinder.
|
||||||
|
|
||||||
USB / HDMI / PCIe / Ethernet / LVDS / DDR (and any net_class on an
|
USB2 / USB3 SuperSpeed / Ethernet MDI / RGMII / SGMII / DDR3 / HDMI /
|
||||||
``impedance`` / ``length_match`` / … rule) are checked. I2C, GPIO, EN,
|
PCIe / LVDS are checked only against a rule whose quote or ``net_class``
|
||||||
analog REGN, USB CC are not 50 Ω pairs. No invented USB/IEC Z0.
|
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
|
from __future__ import annotations
|
||||||
@@ -30,14 +31,71 @@ _SKIP_RE = re.compile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
_HS_CLASS_RE = (
|
_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)),
|
("hdmi", re.compile(r"HDMI", re.I)),
|
||||||
("pcie", re.compile(r"PCIE|PEX_", 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)),
|
("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({
|
_SI_KINDS = frozenset({
|
||||||
"impedance", "length_match", "max_length", "spacing",
|
"impedance", "length_match", "max_length", "spacing",
|
||||||
"ref_plane", "si_via", "layer", "series_resistor", "return_path", "si",
|
"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):
|
if skip_si_net(net):
|
||||||
return None
|
return None
|
||||||
leaf = _leaf(net)
|
leaf = _leaf(net)
|
||||||
if "USB" in leaf.upper() and re.search(r"(D\+|D-|DP|DM)", leaf, re.I):
|
u = leaf.upper()
|
||||||
return "usb"
|
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:
|
for cls, cre in _HS_CLASS_RE:
|
||||||
if cls == "usb":
|
|
||||||
continue
|
|
||||||
if cre.search(leaf):
|
if cre.search(leaf):
|
||||||
return cls
|
return cls
|
||||||
|
if re.search(r"DDR", u):
|
||||||
|
return "ddr3_dq"
|
||||||
return None
|
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:
|
def _is_hs_net(net: str) -> bool:
|
||||||
return bus_class(net) is not None
|
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]:
|
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}
|
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()
|
pin = str(rule.get("pin") or "").strip()
|
||||||
ic = rule.get("_ic")
|
ic = rule.get("_ic")
|
||||||
ic_nets: set[str] = rule.get("_ic_nets") or set()
|
ic_nets: set[str] = rule.get("_ic_nets") or set()
|
||||||
|
targets = _rule_target_buses(rule)
|
||||||
picked: list[str] = []
|
picked: list[str] = []
|
||||||
for net in sorted(names):
|
for net in sorted(names):
|
||||||
if skip_si_net(net):
|
if skip_si_net(net):
|
||||||
continue
|
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:
|
if pin and ic:
|
||||||
sch = graph.pin_net(ic, pin) if hasattr(graph, "pin_net") else None
|
sch = graph.pin_net(ic, pin) if hasattr(graph, "pin_net") else None
|
||||||
if sch and not kicad_nets_match(sch, net):
|
if sch and not kicad_nets_match(sch, net):
|
||||||
if pin.upper() not in _leaf(net).upper():
|
if pin.upper() not in _leaf(net).upper():
|
||||||
continue
|
continue
|
||||||
if nc:
|
if targets:
|
||||||
bc = bus_class(net) or ""
|
if not _bus_in_targets(bc, targets):
|
||||||
if nc not in bc and nc not in _leaf(net).lower():
|
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
on_ic = net in ic_nets or any(kicad_nets_match(net, n) for n in ic_nets)
|
# No bus on the quote: pin-scoped only. Never paint USB/DDR/PHY.
|
||||||
if not on_ic or not _is_hs_net(net):
|
if not pin:
|
||||||
continue
|
continue
|
||||||
picked.append(net)
|
picked.append(net)
|
||||||
return picked
|
return picked
|
||||||
@@ -337,6 +483,7 @@ def check_si(
|
|||||||
findings: list[Finding] = []
|
findings: list[Finding] = []
|
||||||
rules = _collect_rules(graph, constraints_map)
|
rules = _collect_rules(graph, constraints_map)
|
||||||
seen: set[tuple] = set()
|
seen: set[tuple] = set()
|
||||||
|
z_covered: set[str] = set()
|
||||||
|
|
||||||
for _ref, rule in rules:
|
for _ref, rule in rules:
|
||||||
kind = _param(rule)
|
kind = _param(rule)
|
||||||
@@ -385,6 +532,9 @@ def check_si(
|
|||||||
rec = f"Adjust {net} geometry toward the datasheet Z, then re-run PCB review."
|
rec = f"Adjust {net} geometry toward the datasheet Z, then re-run PCB review."
|
||||||
if window is None and nom is None:
|
if window is None and nom is None:
|
||||||
continue
|
continue
|
||||||
|
bc = bus_class(net)
|
||||||
|
if bc:
|
||||||
|
z_covered.add(bc)
|
||||||
if avg is None:
|
if avg is None:
|
||||||
findings.append(_si_finding(
|
findings.append(_si_finding(
|
||||||
rule_id="PE-SI-002", net=net, mpn=mpn, designator=ic,
|
rule_id="PE-SI-002", net=net, mpn=mpn, designator=ic,
|
||||||
@@ -619,16 +769,14 @@ def check_si(
|
|||||||
))
|
))
|
||||||
|
|
||||||
hs_present = [s.net for s in layout.segments if s.net and _is_hs_net(s.net)]
|
hs_present = [s.net for s in layout.segments if s.net and _is_hs_net(s.net)]
|
||||||
si_kinds = {
|
z_target = frozenset(z_covered)
|
||||||
"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()
|
shown: set[str] = set()
|
||||||
for net in hs_present:
|
for net in hs_present:
|
||||||
bc = bus_class(net)
|
bc = bus_class(net)
|
||||||
if skip_si_net(net) or not bc or bc in shown:
|
if skip_si_net(net) or not bc or bc in shown:
|
||||||
continue
|
continue
|
||||||
|
if bc in z_covered or _bus_in_targets(bc, z_target):
|
||||||
|
continue
|
||||||
shown.add(bc)
|
shown.add(bc)
|
||||||
zrow = _z_row(rows, net)
|
zrow = _z_row(rows, net)
|
||||||
partner = _partner_on_board(layout, net, zrow)
|
partner = _partner_on_board(layout, net, zrow)
|
||||||
@@ -642,9 +790,9 @@ def check_si(
|
|||||||
lb = _z_field(zb, "length_mm") or net_length_mm(layout, partner)
|
lb = _z_field(zb, "length_mm") or net_length_mm(layout, partner)
|
||||||
skew = abs(la - lb) if partner else 0.0
|
skew = abs(la - lb) if partner else 0.0
|
||||||
rec = (
|
rec = (
|
||||||
"Re-run schematic review so pintable extract ≥ 1.12.0 fills "
|
"Re-run schematic review so pintable extract ≥ 1.13.0 fills "
|
||||||
"layout_rules (Z0/skew/spacing). Do not assume 90 Ω. PCB does "
|
"layout_rules impedance for this bus (net_class required). Do not "
|
||||||
"not re-read the PDF."
|
"assume 90 Ω. PCB does not re-read the PDF."
|
||||||
)
|
)
|
||||||
findings.append(Finding(
|
findings.append(Finding(
|
||||||
designator="layout",
|
designator="layout",
|
||||||
@@ -654,15 +802,15 @@ def check_si(
|
|||||||
f"Unverified: {bc} {net}"
|
f"Unverified: {bc} {net}"
|
||||||
+ (f"/{partner}" if partner else "")
|
+ (f"/{partner}" if partner else "")
|
||||||
+ f" ImpedenceFinder Zavg={avg} Ω (min {zmin}, max {zmax}); "
|
+ f" ImpedenceFinder Zavg={avg} Ω (min {zmin}, max {zmax}); "
|
||||||
f"skew={skew:.2f} mm — library has no SI FACT."
|
f"skew={skew:.2f} mm — library has no impedance FACT for this bus."
|
||||||
),
|
),
|
||||||
facts=(
|
facts=(
|
||||||
f"avg={avg} min={zmin} max={zmax} Ω; "
|
f"avg={avg} min={zmin} max={zmax} Ω; "
|
||||||
f"L={la:.2f}/{lb:.2f} mm; topologies={zrow.get('topologies') if zrow else None}."
|
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).",
|
requirement="Datasheet layout_rules impedance on this bus (none on file).",
|
||||||
inference="Not USB/IEC 90 Ω folklore; CC/GPIO/I2C are not this check.",
|
inference="Not USB/IEC 90 Ω folklore; strap/EN RC and CC/GPIO/I2C are not this check.",
|
||||||
why="Insufficient library SI numbers.",
|
why="Insufficient library SI numbers for this bus.",
|
||||||
status="INFO",
|
status="INFO",
|
||||||
recommendation=rec,
|
recommendation=rec,
|
||||||
action=rec,
|
action=rec,
|
||||||
|
|||||||
@@ -144,7 +144,9 @@ PINTABLE_TOOL = {
|
|||||||
"number — never invent 3 mm/JEDEC), same_layer (bool), min_via_count, "
|
"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, "
|
"max_via_count, z0_ohm, zdiff_ohm, tolerance_pct, z_min_ohm, z_max_ohm, "
|
||||||
"topology, min_spacing_mm, value_ohms, ref_plane, parameter, "
|
"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": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"default_model_version": "1.12.0",
|
"default_model_version": "1.13.0",
|
||||||
"extract-pintable": {
|
"extract-pintable": {
|
||||||
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
|
"skill_id": "skill_01VMWPZuvuZAe4LmLbmsNWNY",
|
||||||
"latest_version": "1784798970179642",
|
"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-ESD-001 | ESD connettore/USB | REVIEW se manca parte ESD |
|
||||||
| PE-RET-001 | return HS senza via GND | REVIEW, non ERROR |
|
| 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):**
|
**B — still missing (true leftovers):**
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
What's new in Periscope.
|
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
|
## 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.
|
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.
|
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
|
#### Fields
|
||||||
- `pin` — number or name as printed (`"5"`, `"VIN"`, `"VDD"`, `"EP"`)
|
- `pin` — number or name as printed (`"5"`, `"VIN"`, `"VDD"`, `"EP"`)
|
||||||
- `cap_value_hint` — only if shown (`"100nF"`, `"10µF"`)
|
- `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
|
- **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
|
- `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”)
|
- `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)
|
- `source_page` — 1-based page of the guidance (required when you emit a rule)
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
@@ -129,6 +134,8 @@ Thermal vias:
|
|||||||
- Do not invent land-pattern pad sizes from the mechanical drawing alone
|
- 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 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 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
|
- Do not use kinds outside the closed set
|
||||||
- One rule per distinct pin/guidance; prefer supply pins that show caps in the application figure
|
- 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(
|
errors.append(
|
||||||
f"layout_rules[{i}].same_layer must be a boolean or null"
|
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
|
return errors
|
||||||
|
|
||||||
|
|||||||
@@ -48,10 +48,35 @@ def test_si_refresh_when_old_extract_has_only_decoupling():
|
|||||||
)
|
)
|
||||||
assert not needs_layout_rules_refresh(
|
assert not needs_layout_rules_refresh(
|
||||||
{
|
{
|
||||||
"model_version": "1.9.0",
|
"model_version": "1.12.0",
|
||||||
"layout_rules": [{"kind": "decoupling_proximity", "max_distance_mm": None}],
|
"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 skip_si_net("ESP32_EN")
|
||||||
assert not skip_si_net("USB_D+")
|
assert not skip_si_net("USB_D+")
|
||||||
assert not skip_si_net("USB_DP")
|
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("USB_CC1") is None
|
||||||
assert bus_class("I2C_SCL") 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")
|
f = next(x for x in findings if x.rule_id == "PE-SI-010")
|
||||||
assert f.evidence_status == "INSUFFICIENT"
|
assert f.evidence_status == "INSUFFICIENT"
|
||||||
assert f.status == "INFO"
|
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 "")
|
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("I2C" not in (f.net or "") for f in findings)
|
||||||
assert all("SDA" not in f.finding 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