Phase A: stub, return-split, and BOM termination on buses present on the graph; DDR/CPU/FPGA class certifiers only when that device exists. Skip without evidence. Via, pad, track, and zone stay distinct. Not DRC or FEM.
399 lines
15 KiB
Python
399 lines
15 KiB
Python
"""Phase A: HF pair integrity + CPU/FPGA/DDR class — only if present.
|
|
|
|
Via ≠ pad ≠ track ≠ zone. No invented Z/I/mm. Skip when evidence is missing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from backend.periscopex.finding_engine import complete_finding, lookup_rule
|
|
from backend.periscopex.hf_bus_class import check_memory_fpga_classes
|
|
from backend.periscopex.hf_line_check import check_hf_lines
|
|
from backend.periscopex.models import (
|
|
Component,
|
|
ComponentConstraints,
|
|
ComponentType,
|
|
DesignGraph,
|
|
LayoutDielectric,
|
|
LayoutFootprint,
|
|
LayoutGraph,
|
|
LayoutPad,
|
|
LayoutSegment,
|
|
LayoutStackup,
|
|
LayoutVia,
|
|
LayoutZone,
|
|
Net,
|
|
NetType,
|
|
Pin,
|
|
PinConnection,
|
|
ResistorSpecs,
|
|
)
|
|
from backend.periscopex.pcb_checks import run_pcb_checks
|
|
from backend.periscopex.si_check import bus_class, check_si, skip_si_net
|
|
|
|
|
|
def _ic(ref: str, pins: dict[str, str], *, mpn: str = "PHY", subtype: str | None = None) -> Component:
|
|
return Component(
|
|
reference=ref, value=mpn, footprint="",
|
|
component_type=ComponentType.IC, mpn=mpn,
|
|
component_subtype=subtype, pins=pins,
|
|
)
|
|
|
|
|
|
def _net(name: str, *pairs: tuple[str, str], ntype: NetType = NetType.SIGNAL) -> Net:
|
|
return Net(
|
|
name=name, net_type=ntype,
|
|
pins=[PinConnection(component_ref=r, pin_number=p) for r, p in pairs],
|
|
)
|
|
|
|
|
|
def _usb_graph() -> DesignGraph:
|
|
return DesignGraph(
|
|
components={
|
|
"U1": _ic("U1", {"1": "USB_D+", "2": "USB_D-"}),
|
|
"J2": Component(
|
|
reference="J2", value="USB_C", footprint="",
|
|
component_type=ComponentType.CONNECTOR, pins={"A6": "USB_D+", "A7": "USB_D-"},
|
|
),
|
|
},
|
|
nets={
|
|
"USB_D+": _net("USB_D+", ("U1", "1"), ("J2", "A6")),
|
|
"USB_D-": _net("USB_D-", ("U1", "2"), ("J2", "A7")),
|
|
},
|
|
)
|
|
|
|
|
|
def _fp_with_pads(ref: str, pads: list[LayoutPad], x: float = 0.0, y: float = 0.0) -> LayoutFootprint:
|
|
return LayoutFootprint(reference=ref, footprint="P", x=x, y=y, pads=pads)
|
|
|
|
|
|
def test_bus_class_mipi_hf_clk_not_xtal():
|
|
assert bus_class("MIPI_D0_P") == "mipi"
|
|
assert bus_class("CSI_CLK_N") == "mipi"
|
|
assert bus_class("DSI_D1_P") == "mipi"
|
|
assert bus_class("GTX_CLK") == "hf_clk"
|
|
assert bus_class("PCIE_REFCLK") == "pcie"
|
|
assert bus_class("CLK_P") == "hf_clk"
|
|
assert bus_class("XTAL_IN") is None
|
|
assert bus_class("OSCIN") is None
|
|
assert bus_class("HFXIN") is None
|
|
assert skip_si_net("USB_CC1")
|
|
assert bus_class("USB_D+") == "usb2"
|
|
|
|
|
|
def test_no_layout_skips_hf_geometry():
|
|
assert check_hf_lines(_usb_graph(), {}, None) == []
|
|
assert check_hf_lines(_usb_graph(), {}, LayoutGraph()) == []
|
|
|
|
|
|
def test_pad_to_pad_usb_has_no_stub():
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"U1": _fp_with_pads("U1", [
|
|
LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+"),
|
|
LayoutPad(number="2", x=0.0, y=0.4, net="USB_D-"),
|
|
]),
|
|
"J2": _fp_with_pads("J2", [
|
|
LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+"),
|
|
LayoutPad(number="A7", x=10.0, y=0.4, net="USB_D-"),
|
|
], x=10.0),
|
|
},
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
LayoutSegment(start=(0, 0.4), end=(10, 0.4), width=0.2, layer="F.Cu", net="USB_D-"),
|
|
],
|
|
)
|
|
findings = check_hf_lines(_usb_graph(), {}, layout)
|
|
assert [f for f in findings if f.rule_id == "PE-SI-007"] == []
|
|
|
|
|
|
def test_via_is_not_a_stub():
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
|
"J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0),
|
|
},
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(5, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
LayoutSegment(start=(5, 0), end=(10, 0), width=0.2, layer="B.Cu", net="USB_D+"),
|
|
],
|
|
vias=[LayoutVia(x=5.0, y=0.0, net="USB_D+", drill=0.3)],
|
|
)
|
|
findings = check_hf_lines(_usb_graph(), {}, layout)
|
|
assert [f for f in findings if f.rule_id == "PE-SI-007"] == []
|
|
assert all(not isinstance(v, LayoutPad) for v in layout.vias)
|
|
|
|
|
|
def test_zone_is_not_a_track_stub():
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
|
},
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(4, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
],
|
|
zones=[LayoutZone(
|
|
net="USB_D+", layer="F.Cu",
|
|
outlines=[[(3.5, -1.0), (8.0, -1.0), (8.0, 1.0), (3.5, 1.0)]],
|
|
)],
|
|
)
|
|
findings = check_hf_lines(_usb_graph(), {}, layout)
|
|
assert [f for f in findings if f.rule_id == "PE-SI-007"] == []
|
|
|
|
|
|
def test_dangling_track_stub_vs_datasheet_mm_is_fail():
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
|
"J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0),
|
|
},
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
LayoutSegment(start=(10, 0), end=(10, 3), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
],
|
|
)
|
|
cons = ComponentConstraints(
|
|
mpn="PHY", pintable=[Pin(number="1", name="D+")],
|
|
absolute_maximum_ratings=[], rules=[],
|
|
layout_rules=[{
|
|
"kind": "stub", "net_class": "usb2", "max_distance_mm": 1.0,
|
|
"note": "USB stub < 1 mm", "source_page": 12,
|
|
}],
|
|
)
|
|
findings = check_hf_lines(_usb_graph(), {"PHY": cons}, layout)
|
|
stubs = [f for f in findings if f.rule_id == "PE-SI-007"]
|
|
assert len(stubs) == 1
|
|
assert stubs[0].finding.startswith("FAIL:")
|
|
assert stubs[0].facts.startswith("stub_mm=")
|
|
assert "1" in stubs[0].requirement
|
|
complete_finding(stubs[0])
|
|
assert stubs[0].status == "WARNING"
|
|
assert stubs[0].finding_class != "RULE" or stubs[0].provenance != "MANDATORY"
|
|
assert stubs[0].evidence_status == "SUFFICIENT"
|
|
|
|
|
|
def test_measured_stub_without_datasheet_mm_is_insufficient():
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"U1": _fp_with_pads("U1", [LayoutPad(number="1", x=0.0, y=0.0, net="USB_D+")]),
|
|
"J2": _fp_with_pads("J2", [LayoutPad(number="A6", x=10.0, y=0.0, net="USB_D+")], x=10.0),
|
|
},
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
LayoutSegment(start=(5, 0), end=(5, 4), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
],
|
|
)
|
|
findings = check_hf_lines(_usb_graph(), {}, layout)
|
|
stubs = [f for f in findings if f.rule_id == "PE-SI-007"]
|
|
assert len(stubs) == 1
|
|
assert stubs[0].status == "INFO"
|
|
assert stubs[0].evidence_status == "INSUFFICIENT"
|
|
assert "90" not in (stubs[0].requirement or "")
|
|
assert "FAIL" not in stubs[0].finding
|
|
|
|
|
|
def test_split_under_pair_needs_stackup_and_zone():
|
|
segs = [
|
|
LayoutSegment(start=(0, 0), end=(20, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
]
|
|
bare = LayoutGraph(segments=segs)
|
|
assert [f for f in check_hf_lines(_usb_graph(), {}, bare) if f.rule_id == "PE-SI-011"] == []
|
|
|
|
stack = LayoutStackup(
|
|
copper_layers=["F.Cu", "B.Cu"],
|
|
dielectrics=[LayoutDielectric(name="dielectric_1", er=4.5, height_mm=0.2)],
|
|
copper_thickness_mm=0.035,
|
|
)
|
|
no_zone = LayoutGraph(segments=segs, stackup=stack)
|
|
assert [f for f in check_hf_lines(_usb_graph(), {}, no_zone) if f.rule_id == "PE-SI-011"] == []
|
|
|
|
covered = LayoutGraph(
|
|
segments=segs, stackup=stack,
|
|
zones=[LayoutZone(
|
|
net="GND", layer="B.Cu",
|
|
outlines=[[(-1, -2), (21, -2), (21, 2), (-1, 2)]],
|
|
)],
|
|
)
|
|
assert [f for f in check_hf_lines(_usb_graph(), {}, covered) if f.rule_id == "PE-SI-011"] == []
|
|
|
|
split = LayoutGraph(
|
|
segments=segs, stackup=stack,
|
|
zones=[LayoutZone(
|
|
net="GND", layer="B.Cu",
|
|
outlines=[[(0, -2), (5, -2), (5, 2), (0, 2)]],
|
|
)],
|
|
)
|
|
hits = [f for f in check_hf_lines(_usb_graph(), {}, split) if f.rule_id == "PE-SI-011"]
|
|
assert len(hits) == 1
|
|
assert hits[0].status == "WARNING"
|
|
assert hits[0].finding_class == "REVIEW"
|
|
assert hits[0].facts
|
|
assert hits[0].requirement
|
|
assert hits[0].inference
|
|
complete_finding(hits[0])
|
|
assert hits[0].status != "ERROR"
|
|
|
|
|
|
def test_bom_termination_is_fact_missing_is_skip():
|
|
layout = LayoutGraph(
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
],
|
|
)
|
|
g = _usb_graph()
|
|
assert [f for f in check_hf_lines(g, {}, layout) if f.rule_id == "PE-SI-012"] == []
|
|
|
|
g.components["R22"] = Component(
|
|
reference="R22", value="22R", footprint="",
|
|
component_type=ComponentType.RESISTOR,
|
|
pins={"1": "USB_D+", "2": "USB_D+_PHY"},
|
|
specs=ResistorSpecs(value_ohms=22.0, value_formatted="22"),
|
|
)
|
|
g.nets["USB_D+_PHY"] = _net("USB_D+_PHY", ("R22", "2"))
|
|
g.nets["USB_D+"].pins.append(PinConnection(component_ref="R22", pin_number="1"))
|
|
found = [f for f in check_hf_lines(g, {}, layout) if f.rule_id == "PE-SI-012"]
|
|
assert len(found) == 1
|
|
assert found[0].status == "INFO"
|
|
assert found[0].evidence_status == "SUFFICIENT"
|
|
assert "R22" in found[0].facts
|
|
assert "22" in found[0].facts
|
|
assert "50 Ω" not in found[0].requirement
|
|
|
|
|
|
def test_no_ddr_cpu_fpga_on_usb_only_graph():
|
|
findings = check_memory_fpga_classes(_usb_graph())
|
|
assert findings == []
|
|
ids = {f.rule_id for f in run_pcb_checks(_usb_graph(), {}, None)}
|
|
assert not any(str(i).startswith(("PE-DDR", "PE-CPU", "PE-FPGA")) for i in ids)
|
|
|
|
|
|
def test_ddr_present_class_and_nets_skip_invented_vtt():
|
|
pins = {str(i + 1): f"DDR3_DQ{i}" for i in range(8)}
|
|
pins["20"] = "DDR3_DQS0_P"
|
|
pins["21"] = "DDR3_DQS0_N"
|
|
pins["22"] = "DDR3_CK_P"
|
|
pins["23"] = "DDR3_CK_N"
|
|
pins["24"] = "DDR3_A0"
|
|
comps = {"U10": _ic("U10", pins, mpn="MT41K256M16TW", subtype="ic.memory.ddr")}
|
|
nets = {n: _net(n, ("U10", p)) for p, n in pins.items()}
|
|
g = DesignGraph(components=comps, nets=nets)
|
|
findings = check_memory_fpga_classes(g)
|
|
for f in findings:
|
|
complete_finding(f)
|
|
assert [f.rule_id for f in findings if f.rule_id == "PE-DDR-001"]
|
|
assert [f for f in findings if f.rule_id == "PE-DDR-001"][0].status == "INFO"
|
|
assert [f for f in findings if f.rule_id == "PE-DDR-002"][0].status == "INFO"
|
|
assert not any(f.rule_id == "PE-DDR-003" for f in findings)
|
|
assert not any((f.rule_id or "").startswith("PE-CPU") for f in findings)
|
|
assert not any((f.rule_id or "").startswith("PE-FPGA") for f in findings)
|
|
|
|
g.nets["DDR_VTT"] = _net("DDR_VTT", ("U10", "30"), ntype=NetType.POWER)
|
|
g.components["U10"].pins["30"] = "DDR_VTT"
|
|
with_vtt = check_memory_fpga_classes(g)
|
|
assert [f for f in with_vtt if f.rule_id == "PE-DDR-003"][0].status == "INFO"
|
|
|
|
|
|
def test_cpu_parallel_bus_only_when_data_addr_control_exist():
|
|
mcu_pins = {str(i + 1): f"MCU_D{i}" for i in range(8)}
|
|
for i in range(8):
|
|
mcu_pins[str(20 + i)] = f"MCU_A{i}"
|
|
mcu_pins["40"] = "MCU_NWE"
|
|
mcu_pins["41"] = "MCU_NOE"
|
|
mcu_pins["42"] = "MCU_NCS"
|
|
sram_pins = dict(mcu_pins)
|
|
comps = {
|
|
"U1": _ic("U1", mcu_pins, mpn="STM32F407", subtype="ic.mcu"),
|
|
"U2": _ic("U2", sram_pins, mpn="IS61WV51216", subtype="ic.memory.sram"),
|
|
}
|
|
nets = {}
|
|
for p, n in mcu_pins.items():
|
|
nets[n] = _net(n, ("U1", p), ("U2", p))
|
|
g = DesignGraph(components=comps, nets=nets)
|
|
findings = check_memory_fpga_classes(g)
|
|
ids = {f.rule_id for f in findings}
|
|
assert "PE-CPU-001" in ids
|
|
assert "PE-CPU-002" in ids
|
|
assert "PE-CPU-003" in ids
|
|
assert "PE-CPU-004" in ids
|
|
assert not any(str(i).startswith("PE-DDR") for i in ids)
|
|
assert not any(str(i).startswith("PE-FPGA") for i in ids)
|
|
|
|
gpio = DesignGraph(
|
|
components={"U1": _ic("U1", {"1": "GPIO0", "2": "GPIO1"}, mpn="MSPM0", subtype="ic.mcu")},
|
|
nets={
|
|
"GPIO0": _net("GPIO0", ("U1", "1")),
|
|
"GPIO1": _net("GPIO1", ("U1", "2")),
|
|
},
|
|
)
|
|
assert check_memory_fpga_classes(gpio) == []
|
|
|
|
|
|
def test_fpga_only_when_present_config_flash_optional():
|
|
fpga_pins = {"1": "IO_L1P", "2": "IO_L1N", "3": "VCCINT", "4": "VCCIO"}
|
|
g = DesignGraph(
|
|
components={"U5": _ic("U5", fpga_pins, mpn="XC7A35T", subtype="ic.fpga")},
|
|
nets={
|
|
"IO_L1P": _net("IO_L1P", ("U5", "1")),
|
|
"IO_L1N": _net("IO_L1N", ("U5", "2")),
|
|
"VCCINT": _net("VCCINT", ("U5", "3"), ntype=NetType.POWER),
|
|
"VCCIO": _net("VCCIO", ("U5", "4"), ntype=NetType.POWER),
|
|
},
|
|
)
|
|
findings = check_memory_fpga_classes(g)
|
|
ids = {f.rule_id for f in findings}
|
|
assert "PE-FPGA-001" in ids
|
|
assert "PE-FPGA-003" in ids
|
|
assert "PE-FPGA-002" not in ids
|
|
assert not any(str(i).startswith("PE-DDR") for i in ids)
|
|
assert not any(str(i).startswith("PE-CPU") for i in ids)
|
|
|
|
g.components["U6"] = _ic("U6", {"1": "FPGA_CS", "2": "FPGA_MOSI"}, mpn="W25Q64", subtype="ic.memory.flash")
|
|
g.nets["FPGA_CS"] = _net("FPGA_CS", ("U6", "1"), ("U5", "10"))
|
|
g.components["U5"].pins["10"] = "FPGA_CS"
|
|
with_flash = check_memory_fpga_classes(g)
|
|
assert any(f.rule_id == "PE-FPGA-002" for f in with_flash)
|
|
|
|
|
|
def test_rule_catalog_hf_ids():
|
|
for rid, domain in (
|
|
("PE-SI-007", "pcb"),
|
|
("PE-SI-011", "pcb"),
|
|
("PE-SI-012", "pcb"),
|
|
("PE-DDR-001", "shared"),
|
|
("PE-DDR-002", "shared"),
|
|
("PE-DDR-003", "shared"),
|
|
("PE-CPU-001", "shared"),
|
|
("PE-CPU-002", "shared"),
|
|
("PE-CPU-003", "shared"),
|
|
("PE-CPU-004", "shared"),
|
|
("PE-FPGA-001", "shared"),
|
|
("PE-FPGA-002", "shared"),
|
|
("PE-FPGA-003", "shared"),
|
|
):
|
|
rec = lookup_rule(rid)
|
|
assert rec is not None, rid
|
|
assert rec.domain == domain
|
|
|
|
|
|
def test_existing_si_usb_not_polluted_by_hf_geometry():
|
|
layout = LayoutGraph(
|
|
segments=[
|
|
LayoutSegment(start=(0, 0), end=(10, 0), width=0.2, layer="F.Cu", net="USB_D+"),
|
|
LayoutSegment(start=(0, 0.4), end=(12.5, 0.4), width=0.2, layer="F.Cu", net="USB_D-"),
|
|
],
|
|
vias=[LayoutVia(x=1, y=0.2, net="GND", drill=0.3)],
|
|
)
|
|
findings = check_si(_usb_graph(), {}, layout, [
|
|
{
|
|
"net_name": "USB_D+", "partner_net_name": "USB_D-",
|
|
"z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0,
|
|
"length_mm": 10.0, "topologies": ["MICROSTRIP"],
|
|
},
|
|
{
|
|
"net_name": "USB_D-", "partner_net_name": "USB_D+",
|
|
"z0_avg_ohms": 88.0, "z0_min_ohms": 46.0, "z0_max_ohms": 92.0,
|
|
"length_mm": 12.5, "topologies": ["MICROSTRIP"],
|
|
},
|
|
])
|
|
assert any(f.rule_id == "PE-SI-010" for f in findings)
|
|
assert all(f.rule_id != "PE-SI-002" for f in findings)
|