Match exposed pads by land, cite ESD, follow CC to Rd (2.84.0).
PE-BOM-011 treats EP/EPAD/THERMAL PAD and the footprint exposed land as one pad when both sides have one. PE-ESD-001 quotes a datasheet only with part and section, and does not warn on a 3V3 jack rail, uncitable VBUS, or optical S/PDIF. CC Rd is the packed 5.1 kΩ on the Type-C CC pin net. Ethernet MDI pair Z is PASS, FAIL, or ERROR against the cited 10/100 or gigabit clause.
This commit is contained in:
@@ -0,0 +1,371 @@
|
||||
"""HubAudio false findings: exposed-pad names, ESD cites, CC-via-net, Ethernet Z."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.periscopex.af_trace_check import check_af_traces
|
||||
from backend.periscopex.bom_pcb_check import check_bom_pcb_datasheet
|
||||
from backend.periscopex.esd_return_check import check_esd
|
||||
from backend.periscopex.models import (
|
||||
Component,
|
||||
ComponentConstraints,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
LayoutFootprint,
|
||||
LayoutGraph,
|
||||
LayoutPad,
|
||||
LayoutSegment,
|
||||
Net,
|
||||
NetType,
|
||||
PackageInfo,
|
||||
Pin,
|
||||
PinConnection,
|
||||
)
|
||||
from backend.periscopex.protocol_l0 import _run_l0_check, l0_findings
|
||||
from backend.periscopex.protocol_recognize import PhysicalBusInstance
|
||||
|
||||
|
||||
def _ic(ref: str, mpn: str, pins: dict[str, str], *, footprint: str = "") -> Component:
|
||||
return Component(
|
||||
reference=ref, value=mpn, footprint=footprint,
|
||||
component_type=ComponentType.IC, mpn=mpn, pins=pins,
|
||||
)
|
||||
|
||||
|
||||
def _net(name: str, *pairs: tuple[str, str]) -> Net:
|
||||
return Net(
|
||||
name=name, net_type=NetType.SIGNAL,
|
||||
pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs],
|
||||
)
|
||||
|
||||
|
||||
def _cons(mpn: str, numbers: list[str], pin_count: int) -> ComponentConstraints:
|
||||
return ComponentConstraints(
|
||||
mpn=mpn,
|
||||
package_info=PackageInfo(base_family="QFN", package=f"QFN-{pin_count}", pin_count=pin_count),
|
||||
pintable=[Pin(number=n, name=n) for n in numbers],
|
||||
absolute_maximum_ratings=[],
|
||||
rules=[],
|
||||
)
|
||||
|
||||
|
||||
def _layout(ref: str, pads: list[tuple[str, str]], *, footprint: str) -> LayoutGraph:
|
||||
return LayoutGraph(
|
||||
footprints={
|
||||
ref: LayoutFootprint(
|
||||
reference=ref, footprint=footprint, x=0, y=0,
|
||||
pads=[
|
||||
LayoutPad(number=n, x=0, y=0, net="GND", pinfunction=pf)
|
||||
for n, pf in pads
|
||||
],
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _bom(ref: str, mpn: str, numbers: list[str], pads: list[tuple[str, str]], pin_count: int):
|
||||
graph = DesignGraph(components={
|
||||
ref: _ic(ref, mpn, {n: "N" for n, _pf in pads if n}, footprint="Lib:Part"),
|
||||
})
|
||||
# Schematic pins follow the pintable numbers that are real contacts.
|
||||
graph.components[ref].pins = {n: "N" for n in numbers if n not in {"EP", "THERMAL PAD"}}
|
||||
layout = _layout(ref, pads, footprint="Lib:Part")
|
||||
findings = check_bom_pcb_datasheet(graph, {mpn: _cons(mpn, numbers, pin_count)}, layout)
|
||||
return [f for f in findings if f.rule_id == "PE-BOM-011"]
|
||||
|
||||
|
||||
def test_u16_ep_matches_pad_41():
|
||||
"""AXP2101: pintable EP, footprint pad 41 / EP_41."""
|
||||
hits = _bom(
|
||||
"U16", "AXP2101", ["1", "EP"],
|
||||
[("1", "CHGLED_1"), ("41", "EP_41")],
|
||||
40,
|
||||
)
|
||||
assert hits == []
|
||||
|
||||
|
||||
def test_u18_ep_matches_pad_89():
|
||||
"""ADAU1467: pintable EP, footprint pad 89 / EP_89."""
|
||||
hits = _bom(
|
||||
"U18", "ADAU1467", ["1", "EP"],
|
||||
[("1", "DGND_1_1"), ("89", "EP_89")],
|
||||
88,
|
||||
)
|
||||
assert hits == []
|
||||
|
||||
|
||||
def test_u19_ep_matches_pad_25_vss():
|
||||
"""LAN8720A: pintable EP, VQFN-24 land is pad 25 / VSS_25."""
|
||||
hits = _bom(
|
||||
"U19", "LAN8720A", ["1", "EP"],
|
||||
[("1", "VDD2A_1"), ("25", "VSS_25")],
|
||||
24,
|
||||
)
|
||||
assert hits == []
|
||||
|
||||
|
||||
def test_u5_thermal_pad_matches_epad_29():
|
||||
"""TAC5212: pintable THERMAL PAD, footprint pad 29 / EPAD_29."""
|
||||
hits = _bom(
|
||||
"U5", "XTAC5212", ["1", "THERMAL PAD"],
|
||||
[("1", "DREG_1"), ("29", "EPAD_29")],
|
||||
24,
|
||||
)
|
||||
assert hits == []
|
||||
|
||||
|
||||
def test_exposed_pad_absent_from_footprint_stays_error():
|
||||
hits = _bom(
|
||||
"U9", "BARE", ["1", "EP"],
|
||||
[("1", "PIN_1")],
|
||||
24,
|
||||
)
|
||||
assert hits
|
||||
assert any("EP" in (f.finding or "") for f in hits)
|
||||
|
||||
|
||||
def _esd_graph(net: str, *, j_fp: str = "", j_value: str = "JACK") -> DesignGraph:
|
||||
return DesignGraph(
|
||||
components={
|
||||
"J1": Component(
|
||||
reference="J1", value=j_value, footprint=j_fp,
|
||||
component_type=ComponentType.CONNECTOR, mpn="",
|
||||
pins={"13": net},
|
||||
),
|
||||
"U19": _ic("U19", "LAN8720A", {"1": net}),
|
||||
},
|
||||
nets={net: _net(net, ("J1", "13"), ("U19", "1"))},
|
||||
)
|
||||
|
||||
|
||||
def test_esd_does_not_say_datasheet_specified_without_a_cite():
|
||||
findings = check_esd(_esd_graph("USB_DP"))
|
||||
assert findings and findings[0].rule_id == "PE-ESD-001"
|
||||
blob = f"{findings[0].action} {findings[0].recommendation}"
|
||||
assert "datasheet-specified" not in blob
|
||||
|
||||
|
||||
def test_esd_quotes_datasheet_when_part_and_section_exist():
|
||||
cons = ComponentConstraints(
|
||||
mpn="LAN8720A",
|
||||
pintable=[],
|
||||
absolute_maximum_ratings=[],
|
||||
rules=[],
|
||||
layout_rules=[{
|
||||
"kind": "esd",
|
||||
"document": "LAN8720A datasheet",
|
||||
"section": "3.2",
|
||||
"page": "12",
|
||||
"note": "ESD on USB_DP",
|
||||
}],
|
||||
)
|
||||
findings = check_esd(_esd_graph("USB_DP"), {"LAN8720A": cons})
|
||||
assert findings
|
||||
blob = f"{findings[0].action} {findings[0].finding}"
|
||||
assert "datasheet-specified" in blob
|
||||
assert "LAN8720A datasheet" in blob
|
||||
assert "3.2" in blob
|
||||
|
||||
|
||||
def test_3v3_ethernet_rail_is_not_an_esd_warning():
|
||||
assert check_esd(_esd_graph("3V3_ETHERNET")) == []
|
||||
|
||||
|
||||
def test_vbus_without_cite_is_not_an_esd_warning():
|
||||
graph = _esd_graph("VBUS")
|
||||
graph.components["U16"] = _ic("U16", "AXP2101", {"37": "VBUS"})
|
||||
graph.components["J2"] = Component(
|
||||
reference="J2", value="USB_C", footprint="Connector_USB:USB_C_Receptacle",
|
||||
component_type=ComponentType.CONNECTOR,
|
||||
pins={"A4": "VBUS"},
|
||||
)
|
||||
graph.nets = {"VBUS": _net("VBUS", ("J2", "A4"), ("U16", "37"))}
|
||||
findings = check_esd(graph)
|
||||
assert findings == []
|
||||
blob = " ".join(f.action or "" for f in findings)
|
||||
assert "datasheet-specified" not in blob
|
||||
|
||||
|
||||
def test_optical_spdif_is_not_an_esd_warning():
|
||||
findings = check_esd(_esd_graph(
|
||||
"SPIF_IN",
|
||||
j_fp="OptoDevice:Broadcom_AFBR-16xxZ_Horizontal",
|
||||
j_value="TOSLINK",
|
||||
))
|
||||
assert findings == []
|
||||
|
||||
|
||||
def test_unverified_spdif_warns_it_was_not_seen_as_optical():
|
||||
findings = check_esd(_esd_graph("SPIF_OUT", j_fp="Connector:PinHeader"))
|
||||
assert findings
|
||||
assert findings[0].status == "WARNING"
|
||||
assert "not verified as optical" in (findings[0].finding or "")
|
||||
assert "datasheet-specified" not in (findings[0].action or "")
|
||||
|
||||
|
||||
def _cc_inst() -> PhysicalBusInstance:
|
||||
return PhysicalBusInstance(
|
||||
instance_id="usb-c-usb2-receptacle:J2",
|
||||
logical_protocol_id="usb2-hs",
|
||||
physical_interface_id="usb-c-usb2-receptacle",
|
||||
pcb_relevant="YES",
|
||||
confidence=1.0,
|
||||
evidence_kind="pinout",
|
||||
recognition_status="RECOGNIZED",
|
||||
host_ref="J2",
|
||||
nets=["Net-(J2-A5)", "Net-(J2-B5)"],
|
||||
)
|
||||
|
||||
|
||||
def _cc_graph(*, both: bool) -> DesignGraph:
|
||||
comps = {
|
||||
"J2": Component(
|
||||
reference="J2",
|
||||
value="USB_C_Receptacle_USB2.0_16P",
|
||||
footprint="Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12",
|
||||
component_type=ComponentType.CONNECTOR,
|
||||
pins={"A5": "Net-(J2-A5)", "B5": "Net-(J2-B5)", "A4": "VBUS"},
|
||||
),
|
||||
"R1": Component(
|
||||
reference="R1", value="5.1k", footprint="R_0603",
|
||||
component_type=ComponentType.RESISTOR,
|
||||
pins={"1": "Net-(J2-A5)", "2": "GND"},
|
||||
),
|
||||
}
|
||||
nets = {
|
||||
"Net-(J2-A5)": _net("Net-(J2-A5)", ("J2", "A5"), ("R1", "1")),
|
||||
"Net-(J2-B5)": _net("Net-(J2-B5)", ("J2", "B5")),
|
||||
"GND": _net("GND", ("R1", "2")),
|
||||
}
|
||||
if both:
|
||||
comps["R2"] = Component(
|
||||
reference="R2", value="5.1k", footprint="R_0603",
|
||||
component_type=ComponentType.RESISTOR,
|
||||
pins={"1": "Net-(J2-B5)", "2": "GND"},
|
||||
)
|
||||
nets["Net-(J2-B5)"] = _net("Net-(J2-B5)", ("J2", "B5"), ("R2", "1"))
|
||||
nets["GND"] = _net("GND", ("R1", "2"), ("R2", "2"))
|
||||
return DesignGraph(components=comps, nets=nets)
|
||||
|
||||
|
||||
def test_cc_rd_present_on_unnamed_nets_does_not_fail():
|
||||
row = _run_l0_check(_cc_graph(both=True), _cc_inst(), "cc_rd_rp", "MANDATORY")
|
||||
assert row.result == "PASS"
|
||||
assert "5.1" in row.notes
|
||||
assert "CC1/CC2 net not present" not in row.notes
|
||||
findings = l0_findings(_cc_inst(), [row])
|
||||
assert not any(f.rule_id == "PE-PRT-L0-001" for f in findings)
|
||||
|
||||
|
||||
def test_cc_rd_missing_on_pin_net_fails():
|
||||
row = _run_l0_check(_cc_graph(both=False), _cc_inst(), "cc_rd_rp", "MANDATORY")
|
||||
assert row.result == "FAIL"
|
||||
assert "B5" in row.notes
|
||||
assert "CC1/CC2 net not present" not in row.notes
|
||||
assert "5.1" in row.notes
|
||||
findings = l0_findings(_cc_inst(), [row])
|
||||
assert any(f.rule_id == "PE-PRT-L0-001" and f.status == "ERROR" for f in findings)
|
||||
|
||||
|
||||
def _eth_graph(mpn: str, *, value: str = "") -> DesignGraph:
|
||||
return DesignGraph(
|
||||
components={
|
||||
"J1": Component(
|
||||
reference="J1", value="RJ45",
|
||||
footprint="Connector_RJ:RJ45_Abracon_ARJP11A-MA_Horizontal",
|
||||
component_type=ComponentType.CONNECTOR,
|
||||
pins={"9": "RJ45_TXP", "10": "RJ45_TXN"},
|
||||
),
|
||||
"U19": _ic("U19", mpn, {"21": "RJ45_TXP", "20": "RJ45_TXN"}, footprint="VQFN-24"),
|
||||
},
|
||||
nets={
|
||||
"RJ45_TXP": _net("RJ45_TXP", ("J1", "9"), ("U19", "21")),
|
||||
"RJ45_TXN": _net("RJ45_TXN", ("J1", "10"), ("U19", "20")),
|
||||
},
|
||||
) if not value else DesignGraph(
|
||||
components={
|
||||
"J1": Component(
|
||||
reference="J1", value="RJ45",
|
||||
footprint="Connector_RJ:RJ45",
|
||||
component_type=ComponentType.CONNECTOR,
|
||||
pins={"9": "RJ45_TXP", "10": "RJ45_TXN"},
|
||||
),
|
||||
"U19": Component(
|
||||
reference="U19", value=value, footprint="",
|
||||
component_type=ComponentType.IC, mpn=mpn,
|
||||
pins={"21": "RJ45_TXP", "20": "RJ45_TXN"},
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"RJ45_TXP": _net("RJ45_TXP", ("J1", "9"), ("U19", "21")),
|
||||
"RJ45_TXN": _net("RJ45_TXN", ("J1", "10"), ("U19", "20")),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _eth_layout() -> LayoutGraph:
|
||||
return LayoutGraph(
|
||||
segments=[
|
||||
LayoutSegment(start=(0, 0), end=(20, 0), width=0.2, layer="F.Cu", net="RJ45_TXP"),
|
||||
LayoutSegment(start=(0, 0.2), end=(20, 0.2), width=0.2, layer="F.Cu", net="RJ45_TXN"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _eth_findings(graph: DesignGraph, monkeypatch, z: float | None):
|
||||
if z is not None:
|
||||
monkeypatch.setattr(
|
||||
"backend.periscopex.af_trace_check._pair_z_ohm",
|
||||
lambda layout, unit: z,
|
||||
)
|
||||
return [f for f in check_af_traces(graph, {}, _eth_layout()) if f.rule_id == "PE-AF-002"]
|
||||
|
||||
|
||||
def test_lan8720_pair_without_z_is_error_with_100base_cite(monkeypatch):
|
||||
findings = _eth_findings(_eth_graph("LAN8720A"), monkeypatch, None)
|
||||
assert len(findings) == 1
|
||||
f = findings[0]
|
||||
assert f.status == "ERROR"
|
||||
blob = f"{f.finding} {f.action} {f.requirement}"
|
||||
assert blob.startswith("ERROR:") or "ERROR:" in f.finding
|
||||
assert "100" in blob and "25.4.9" in blob
|
||||
assert "stackup" in blob
|
||||
assert "Fornire" not in blob
|
||||
assert "tr" not in blob.lower()
|
||||
assert "90" not in blob
|
||||
assert "Clause 40" not in blob
|
||||
assert "1000BASE" not in blob
|
||||
assert "PASS" not in f.finding
|
||||
|
||||
|
||||
def test_lan8720_pair_z_inside_cite_is_pass(monkeypatch):
|
||||
findings = _eth_findings(_eth_graph("LAN8720A"), monkeypatch, 100.0)
|
||||
assert len(findings) == 1
|
||||
assert findings[0].finding.startswith("PASS:")
|
||||
assert findings[0].status == "INFO"
|
||||
assert "RJ45_TXP" in findings[0].finding and "RJ45_TXN" in findings[0].finding
|
||||
assert "25.4.9" in findings[0].finding
|
||||
|
||||
|
||||
def test_lan8720_pair_z_outside_cite_is_fail(monkeypatch):
|
||||
findings = _eth_findings(_eth_graph("LAN8720A"), monkeypatch, 90.0)
|
||||
assert len(findings) == 1
|
||||
assert findings[0].finding.startswith("FAIL:")
|
||||
assert findings[0].status == "ERROR"
|
||||
assert "PASS" not in findings[0].finding
|
||||
assert "25.4.9" in findings[0].finding
|
||||
|
||||
|
||||
def test_gigabit_phy_uses_clause_40_not_100base(monkeypatch):
|
||||
findings = _eth_findings(
|
||||
_eth_graph("RTL8211F", value="1000BASE-T"),
|
||||
monkeypatch,
|
||||
None,
|
||||
)
|
||||
assert len(findings) == 1
|
||||
blob = findings[0].finding
|
||||
assert findings[0].status == "ERROR"
|
||||
assert "Clause 40" in blob
|
||||
assert "25.4.9" not in blob
|
||||
assert "PASS" not in blob
|
||||
Reference in New Issue
Block a user