Skip module-die and truncated QFN pintable false PE-BOM hits (2.63.3).
WROOM lands are not the ESP32 die table (U11). QFN-48 pads 25–48 are not extras when extraction stopped at 1–24 (U12). VQFN is the QFN family (U19 24-QFN 4×4). Real extra signal pads still ERROR.
This commit is contained in:
@@ -566,7 +566,9 @@ Skip se manca un lato (BOM, footprint, o numero datasheet).
|
|||||||
|
|
||||||
#### `PE-BOM-010` famiglia package
|
#### `PE-BOM-010` famiglia package
|
||||||
|
|
||||||
- Test: `tests/test_pcb_plan_closeout.py` `test_package_family_mismatch_is_pe_bom_010`.
|
VQFN/WQFN/HVQFN = QFN. LQFP ≠ QFN.
|
||||||
|
|
||||||
|
- Test: `tests/pcb/test_pcb_plan_closeout.py` `test_package_family_mismatch_is_pe_bom_010`; `tests/pcb/test_bom_011_named_join.py` `test_qfn_vs_vqfn_same_24_qfn_land_not_bom_010`.
|
||||||
|
|
||||||
#### `PE-BOM-011` pintable vs pad, join per nome
|
#### `PE-BOM-011` pintable vs pad, join per nome
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,17 @@ _FAMILIES = (
|
|||||||
"WLCSP", "BGA", "LGA", "CSP", "SC-70", "SC70",
|
"WLCSP", "BGA", "LGA", "CSP", "SC-70", "SC70",
|
||||||
"TO-252", "TO252", "TO-263", "QFP",
|
"TO-252", "TO252", "TO-263", "QFP",
|
||||||
)
|
)
|
||||||
|
# Same land family: VQFN/WQFN/HVQFN are QFN (24-QFN 4×4 ≠ a different package).
|
||||||
|
_FAMILY_CANON = {
|
||||||
|
"VQFN": "QFN",
|
||||||
|
"WQFN": "QFN",
|
||||||
|
"HVQFN": "QFN",
|
||||||
|
}
|
||||||
|
_QFN_PIN_N = re.compile(r"(?:HVQFN|VQFN|WQFN|QFN)[_-](\d+)\b", re.I)
|
||||||
|
_MODULE_FP = re.compile(
|
||||||
|
r"WROOM|WROVER|ESP-WROOM|RF[_-]?MODULE|CASTELLAT",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
_EP_PAD = re.compile(
|
_EP_PAD = re.compile(
|
||||||
r"^(?:EP|EXP|EPAD|PAD|TAB|TH|THERMAL|DIEPAD|THERMAL[\s_]?PAD)(?:[_-]?\d+)?$",
|
r"^(?:EP|EXP|EPAD|PAD|TAB|TH|THERMAL|DIEPAD|THERMAL[\s_]?PAD)(?:[_-]?\d+)?$",
|
||||||
re.I,
|
re.I,
|
||||||
@@ -66,7 +77,7 @@ def _family(blob: str) -> str | None:
|
|||||||
return None
|
return None
|
||||||
for fam in _FAMILIES:
|
for fam in _FAMILIES:
|
||||||
if fam.replace("-", "") in u.replace("-", ""):
|
if fam.replace("-", "") in u.replace("-", ""):
|
||||||
return fam
|
return _FAMILY_CANON.get(fam, fam)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -107,6 +118,66 @@ def _is_shield_land(number: str, pinfunction: str = "") -> bool:
|
|||||||
return bool(_SH_PAD.match(s) or (pf and _SH_PAD.match(pf)))
|
return bool(_SH_PAD.match(s) or (pf and _SH_PAD.match(pf)))
|
||||||
|
|
||||||
|
|
||||||
|
def _cad_blob(*parts: str) -> str:
|
||||||
|
return " ".join(p for p in parts if (p or "").strip())
|
||||||
|
|
||||||
|
|
||||||
|
def _is_module_land(blob: str) -> bool:
|
||||||
|
"""WROOM / RF module footprint — not the silicon die pintable."""
|
||||||
|
return bool(_MODULE_FP.search(blob or ""))
|
||||||
|
|
||||||
|
|
||||||
|
def _declared_io_pins(pkg: PackageInfo | None, *blobs: str) -> int | None:
|
||||||
|
"""Largest QFN-n / pin_count seen in CAD or package_info. Not a pad census."""
|
||||||
|
n: list[int] = []
|
||||||
|
if pkg and pkg.pin_count:
|
||||||
|
try:
|
||||||
|
n.append(int(pkg.pin_count))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
for b in blobs:
|
||||||
|
for m in _QFN_PIN_N.finditer(b or ""):
|
||||||
|
n.append(int(m.group(1)))
|
||||||
|
return max(n) if n else None
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_explainable_extras(
|
||||||
|
extra: list[str] | set[str],
|
||||||
|
*,
|
||||||
|
missing: list[str] | set[str],
|
||||||
|
is_module: bool,
|
||||||
|
declared: int | None,
|
||||||
|
) -> tuple[list[str], str]:
|
||||||
|
"""Module lands beyond the die, or QFN-n pads the pintable never listed.
|
||||||
|
|
||||||
|
Missing datasheet pins are never dropped. Pad ≠ via.
|
||||||
|
"""
|
||||||
|
leftover: list[str] = []
|
||||||
|
reasons: list[str] = []
|
||||||
|
miss = {str(x) for x in missing}
|
||||||
|
if miss:
|
||||||
|
return sorted(extra), ""
|
||||||
|
extras = [str(x) for x in extra]
|
||||||
|
if not extras:
|
||||||
|
return [], ""
|
||||||
|
if is_module:
|
||||||
|
reasons.append("module-footprint-vs-die-pintable")
|
||||||
|
return [], reasons[0]
|
||||||
|
if declared:
|
||||||
|
kept: list[str] = []
|
||||||
|
dropped = False
|
||||||
|
for e in extras:
|
||||||
|
if e.isdigit() and int(e) <= declared:
|
||||||
|
dropped = True
|
||||||
|
continue
|
||||||
|
kept.append(e)
|
||||||
|
if dropped and not kept:
|
||||||
|
reasons.append(f"pintable-truncated-vs-declared-{declared}")
|
||||||
|
return [], reasons[0]
|
||||||
|
extras = kept
|
||||||
|
return sorted(extras), (reasons[0] if reasons else "")
|
||||||
|
|
||||||
|
|
||||||
def _ds_join_keys(cons: ComponentConstraints | None) -> set[str]:
|
def _ds_join_keys(cons: ComponentConstraints | None) -> set[str]:
|
||||||
"""Pintable pin **numbers** (A5, 17, EP). Names like P1 are not pad ids."""
|
"""Pintable pin **numbers** (A5, 17, EP). Names like P1 are not pad ids."""
|
||||||
out: set[str] = set()
|
out: set[str] = set()
|
||||||
@@ -296,6 +367,7 @@ def _mismatch(
|
|||||||
ref: str, mpn: str, rule_id: str, finding: str, facts: str,
|
ref: str, mpn: str, rule_id: str, finding: str, facts: str,
|
||||||
requirement: str, rec: str, status: str = "ERROR",
|
requirement: str, rec: str, status: str = "ERROR",
|
||||||
inference: str = "", net: str | None = None,
|
inference: str = "", net: str | None = None,
|
||||||
|
evidence_status: str = "SUFFICIENT",
|
||||||
) -> Finding:
|
) -> Finding:
|
||||||
return Finding(
|
return Finding(
|
||||||
designator=ref,
|
designator=ref,
|
||||||
@@ -311,7 +383,7 @@ def _mismatch(
|
|||||||
action=rec,
|
action=rec,
|
||||||
source="bom_pcb_check",
|
source="bom_pcb_check",
|
||||||
rule_id=rule_id,
|
rule_id=rule_id,
|
||||||
evidence_status="SUFFICIENT",
|
evidence_status=evidence_status,
|
||||||
net=net,
|
net=net,
|
||||||
pins=[ref],
|
pins=[ref],
|
||||||
)
|
)
|
||||||
@@ -380,10 +452,13 @@ def _pin_delta(
|
|||||||
facts: str,
|
facts: str,
|
||||||
requirement: str,
|
requirement: str,
|
||||||
rec: str,
|
rec: str,
|
||||||
|
status: str = "ERROR",
|
||||||
|
evidence_status: str = "SUFFICIENT",
|
||||||
) -> Finding:
|
) -> Finding:
|
||||||
return _mismatch(
|
return _mismatch(
|
||||||
ref=ref, mpn=mpn, rule_id="PE-BOM-011",
|
ref=ref, mpn=mpn, rule_id="PE-BOM-011",
|
||||||
finding=finding, facts=facts, requirement=requirement, rec=rec,
|
finding=finding, facts=facts, requirement=requirement, rec=rec,
|
||||||
|
status=status, evidence_status=evidence_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -402,21 +477,50 @@ def _pinout_findings(
|
|||||||
mpn = comp.mpn or ""
|
mpn = comp.mpn or ""
|
||||||
pin_count = pkg.pin_count if pkg else None
|
pin_count = pkg.pin_count if pkg else None
|
||||||
pcb_ids = _pads_as_ids(fp_layout)
|
pcb_ids = _pads_as_ids(fp_layout)
|
||||||
pcb = _join_keys_from_ids(pcb_ids, pin_count=pin_count, ds_keys=ds)
|
|
||||||
fp_name = ""
|
fp_name = ""
|
||||||
if fp_layout is not None:
|
if fp_layout is not None:
|
||||||
fp_name = (fp_layout.footprint or "").strip()
|
fp_name = (fp_layout.footprint or "").strip()
|
||||||
if not fp_name:
|
if not fp_name:
|
||||||
fp_name = (comp.footprint or "").strip()
|
fp_name = (comp.footprint or "").strip()
|
||||||
|
cad = _cad_blob(
|
||||||
|
fp_name, comp.footprint or "", mpn, comp.value or "",
|
||||||
|
)
|
||||||
|
is_module = _is_module_land(cad)
|
||||||
|
declared = _declared_io_pins(pkg, cad)
|
||||||
|
ep_count = declared or pin_count
|
||||||
|
pcb = _join_keys_from_ids(pcb_ids, pin_count=ep_count, ds_keys=ds)
|
||||||
lib, lib_path = _lib_join_keys(
|
lib, lib_path = _lib_join_keys(
|
||||||
fp_name, pin_count=pin_count, ds_keys=ds, search_dirs=footprint_dirs,
|
fp_name, pin_count=ep_count, ds_keys=ds, search_dirs=footprint_dirs,
|
||||||
)
|
)
|
||||||
sch = _join_keys_from_ids(
|
sch = _join_keys_from_ids(
|
||||||
[(str(n), "") for n in comp.pins.keys()],
|
[(str(n), "") for n in comp.pins.keys()],
|
||||||
pin_count=pin_count,
|
pin_count=ep_count,
|
||||||
ds_keys=ds,
|
ds_keys=ds,
|
||||||
)
|
)
|
||||||
out: list[Finding] = []
|
out: list[Finding] = []
|
||||||
|
|
||||||
|
def _join_issue(
|
||||||
|
*,
|
||||||
|
miss: list[str],
|
||||||
|
extra: list[str],
|
||||||
|
finding: str,
|
||||||
|
facts: str,
|
||||||
|
requirement: str,
|
||||||
|
rec: str,
|
||||||
|
) -> Finding | None:
|
||||||
|
extra, _why = _drop_explainable_extras(
|
||||||
|
extra, missing=miss, is_module=is_module, declared=declared,
|
||||||
|
)
|
||||||
|
if not miss and not extra:
|
||||||
|
return None
|
||||||
|
return _pin_delta(
|
||||||
|
ref=ref, mpn=mpn,
|
||||||
|
finding=finding,
|
||||||
|
facts=facts,
|
||||||
|
requirement=requirement,
|
||||||
|
rec=rec,
|
||||||
|
)
|
||||||
|
|
||||||
if lib is not None:
|
if lib is not None:
|
||||||
miss_lib = sorted(ds - lib)
|
miss_lib = sorted(ds - lib)
|
||||||
extra_lib = sorted(lib - ds)
|
extra_lib = sorted(lib - ds)
|
||||||
@@ -425,8 +529,8 @@ def _pinout_findings(
|
|||||||
f"Change {ref}'s KiCad footprint library so pad names match "
|
f"Change {ref}'s KiCad footprint library so pad names match "
|
||||||
f"the datasheet pintable (not a PCB pad-count guess)."
|
f"the datasheet pintable (not a PCB pad-count guess)."
|
||||||
)
|
)
|
||||||
out.append(_pin_delta(
|
fnd = _join_issue(
|
||||||
ref=ref, mpn=mpn,
|
miss=miss_lib, extra=extra_lib,
|
||||||
finding=(
|
finding=(
|
||||||
f"{ref} datasheet pintable vs library footprint differ "
|
f"{ref} datasheet pintable vs library footprint differ "
|
||||||
f"(missing on lib {miss_lib}, extra on lib {extra_lib})."
|
f"(missing on lib {miss_lib}, extra on lib {extra_lib})."
|
||||||
@@ -440,7 +544,9 @@ def _pinout_findings(
|
|||||||
"pintable by pin name. pin_count is not authority."
|
"pintable by pin name. pin_count is not authority."
|
||||||
),
|
),
|
||||||
rec=rec,
|
rec=rec,
|
||||||
))
|
)
|
||||||
|
if fnd:
|
||||||
|
out.append(fnd)
|
||||||
miss_emb = sorted(lib - pcb) if pcb else sorted(lib)
|
miss_emb = sorted(lib - pcb) if pcb else sorted(lib)
|
||||||
extra_emb = sorted(pcb - lib) if pcb else []
|
extra_emb = sorted(pcb - lib) if pcb else []
|
||||||
if pcb and (miss_emb or extra_emb):
|
if pcb and (miss_emb or extra_emb):
|
||||||
@@ -448,8 +554,8 @@ def _pinout_findings(
|
|||||||
f"Replace {ref}'s embedded PCB footprint so it matches the "
|
f"Replace {ref}'s embedded PCB footprint so it matches the "
|
||||||
f"named library footprint {fp_name}."
|
f"named library footprint {fp_name}."
|
||||||
)
|
)
|
||||||
out.append(_pin_delta(
|
fnd = _join_issue(
|
||||||
ref=ref, mpn=mpn,
|
miss=miss_emb, extra=extra_emb,
|
||||||
finding=(
|
finding=(
|
||||||
f"{ref} library footprint vs PCB embedded pads differ "
|
f"{ref} library footprint vs PCB embedded pads differ "
|
||||||
f"(missing on PCB {miss_emb}, extra on PCB {extra_emb})."
|
f"(missing on PCB {miss_emb}, extra on PCB {extra_emb})."
|
||||||
@@ -460,7 +566,9 @@ def _pinout_findings(
|
|||||||
"by pin name."
|
"by pin name."
|
||||||
),
|
),
|
||||||
rec=rec,
|
rec=rec,
|
||||||
))
|
)
|
||||||
|
if fnd:
|
||||||
|
out.append(fnd)
|
||||||
return out
|
return out
|
||||||
if pcb:
|
if pcb:
|
||||||
miss = sorted(ds - pcb)
|
miss = sorted(ds - pcb)
|
||||||
@@ -470,8 +578,8 @@ def _pinout_findings(
|
|||||||
f"Fix {ref} pinout: PCB pad names vs datasheet pintable. "
|
f"Fix {ref} pinout: PCB pad names vs datasheet pintable. "
|
||||||
f"Missing on PCB: {miss or '—'}; extra on PCB: {extra or '—'}."
|
f"Missing on PCB: {miss or '—'}; extra on PCB: {extra or '—'}."
|
||||||
)
|
)
|
||||||
out.append(_pin_delta(
|
fnd = _join_issue(
|
||||||
ref=ref, mpn=mpn,
|
miss=miss, extra=extra,
|
||||||
finding=(
|
finding=(
|
||||||
f"{ref} pintable vs PCB pads differ "
|
f"{ref} pintable vs PCB pads differ "
|
||||||
f"(missing {miss}, extra {extra})."
|
f"(missing {miss}, extra {extra})."
|
||||||
@@ -485,7 +593,9 @@ def _pinout_findings(
|
|||||||
"Do not compare pin_count to pad cardinality."
|
"Do not compare pin_count to pad cardinality."
|
||||||
),
|
),
|
||||||
rec=rec,
|
rec=rec,
|
||||||
))
|
)
|
||||||
|
if fnd:
|
||||||
|
out.append(fnd)
|
||||||
return out
|
return out
|
||||||
if sch and ds != sch:
|
if sch and ds != sch:
|
||||||
miss = sorted(ds - sch)
|
miss = sorted(ds - sch)
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
What's new in Periscope.
|
What's new in Periscope.
|
||||||
|
|
||||||
|
## 2.63.3 — 2026-09-22 — Module die, truncated QFN pintable, QFN=VQFN
|
||||||
|
|
||||||
|
HubAudio software leftovers on PE-BOM-010/011: a WROOM module is not the ESP32 die pintable; a QFN-48 land is not wrong because extraction stopped at pins 1–24; VQFN-24 4×4 is the same QFN family as datasheet QFN-24. Extra signal pads above the declared QFN-n still ERROR. Pad ≠ via. Kelvin unchanged.
|
||||||
|
|
||||||
|
- [Fixed] PE-BOM-011 skips die-pintable extras on WROOM/WROVER module footprints (U11).
|
||||||
|
- [Fixed] PE-BOM-011 skips QFN-n pads the pintable never listed (U12 25–48).
|
||||||
|
- [Fixed] PE-BOM-010 treats VQFN/WQFN/HVQFN as QFN (U19).
|
||||||
|
|
||||||
## 2.63.2 — 2026-09-22 — PE-BOM-011 joins pins by name, not pin_count
|
## 2.63.2 — 2026-09-22 — PE-BOM-011 joins pins by name, not pin_count
|
||||||
|
|
||||||
Datasheet `pin_count` and PCB pad cardinality are incommensurable (USB-C 16P vs SH/mount/EP). PE-BOM-011 joins the pintable × KiCad `.kicad_mod` × embedded PCB pads by **pin name**. USB-C A5/B5 are CC pins, not EP. Missing pintable is a skip, never ERROR on `pin_count=16`. The library footprint is checked against the datasheet; lib vs embedded PCB is a separate mismatch. AI still extracts the table and runs review/class/SI — it does not count pads. Pad ≠ via.
|
Datasheet `pin_count` and PCB pad cardinality are incommensurable (USB-C 16P vs SH/mount/EP). PE-BOM-011 joins the pintable × KiCad `.kicad_mod` × embedded PCB pads by **pin name**. USB-C A5/B5 are CC pins, not EP. Missing pintable is a skip, never ERROR on `pin_count=16`. The library footprint is checked against the datasheet; lib vs embedded PCB is a separate mismatch. AI still extracts the table and runs review/class/SI — it does not count pads. Pad ≠ via.
|
||||||
|
|||||||
@@ -277,3 +277,247 @@ def test_lib_vs_embedded_pcb_is_separate_finding(tmp_path: Path):
|
|||||||
if f.rule_id == "PE-BOM-011"
|
if f.rule_id == "PE-BOM-011"
|
||||||
]
|
]
|
||||||
assert any("embedded" in f.finding.lower() or "PCB" in f.finding for f in findings)
|
assert any("embedded" in f.finding.lower() or "PCB" in f.finding for f in findings)
|
||||||
|
|
||||||
|
|
||||||
|
def _ic(
|
||||||
|
ref: str,
|
||||||
|
mpn: str,
|
||||||
|
footprint: str,
|
||||||
|
n_sch: int,
|
||||||
|
) -> Component:
|
||||||
|
return Component(
|
||||||
|
reference=ref, value=mpn, footprint=footprint,
|
||||||
|
component_type=ComponentType.IC, mpn=mpn,
|
||||||
|
pins={str(i): "SIG" for i in range(1, n_sch + 1)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wroom_module_pads_vs_die_pintable_not_bom_011():
|
||||||
|
"""U11: WROOM land ≠ ESP32 die 61-pin pintable."""
|
||||||
|
cons = ComponentConstraints(
|
||||||
|
mpn="ESP32-S31-WROOM-3",
|
||||||
|
package_info=PackageInfo(
|
||||||
|
base_family="ESP", package="QFN-56", pin_count=61,
|
||||||
|
),
|
||||||
|
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 62)],
|
||||||
|
absolute_maximum_ratings=[],
|
||||||
|
rules=[],
|
||||||
|
)
|
||||||
|
pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 100)]
|
||||||
|
graph = DesignGraph(
|
||||||
|
components={
|
||||||
|
"U11": _ic(
|
||||||
|
"U11", "ESP32-S31-WROOM-3",
|
||||||
|
"MIKILAB_ESP32_S31_WROOM_3:ESP32-S31-WROOM-3", 41,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
layout = LayoutGraph(
|
||||||
|
footprints={
|
||||||
|
"U11": LayoutFootprint(
|
||||||
|
reference="U11",
|
||||||
|
footprint="MIKILAB_ESP32_S31_WROOM_3:ESP32-S31-WROOM-3",
|
||||||
|
x=0, y=0, pads=pads,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ids = _ids(check_bom_pcb_datasheet(graph, {"ESP32-S31-WROOM-3": cons}, layout))
|
||||||
|
assert "PE-BOM-011" not in ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_wroom_missing_die_pin_still_fires():
|
||||||
|
cons = ComponentConstraints(
|
||||||
|
mpn="ESP32-S31-WROOM-3",
|
||||||
|
package_info=PackageInfo(
|
||||||
|
base_family="ESP", package="QFN-56", pin_count=61,
|
||||||
|
),
|
||||||
|
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 62)],
|
||||||
|
absolute_maximum_ratings=[],
|
||||||
|
rules=[],
|
||||||
|
)
|
||||||
|
pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 60)]
|
||||||
|
graph = DesignGraph(
|
||||||
|
components={
|
||||||
|
"U11": _ic(
|
||||||
|
"U11", "ESP32-S31-WROOM-3",
|
||||||
|
"MIKILAB_ESP32_S31_WROOM_3:ESP32-S31-WROOM-3", 40,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
layout = LayoutGraph(
|
||||||
|
footprints={
|
||||||
|
"U11": LayoutFootprint(
|
||||||
|
reference="U11",
|
||||||
|
footprint="MIKILAB_ESP32_S31_WROOM_3:ESP32-S31-WROOM-3",
|
||||||
|
x=0, y=0, pads=pads,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert "PE-BOM-011" in _ids(
|
||||||
|
check_bom_pcb_datasheet(graph, {"ESP32-S31-WROOM-3": cons}, layout),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_qfn48_truncated_pintable_24_not_extra_error():
|
||||||
|
"""U12: pintable 1–24 vs QFN-48 lands is extraction, not a wrong footprint."""
|
||||||
|
cons = ComponentConstraints(
|
||||||
|
mpn="Si4684-A10-GM",
|
||||||
|
package_info=PackageInfo(
|
||||||
|
base_family="Skyworks", package="QFN-48", pin_count=24,
|
||||||
|
),
|
||||||
|
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 25)],
|
||||||
|
absolute_maximum_ratings=[],
|
||||||
|
rules=[],
|
||||||
|
)
|
||||||
|
pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 49)]
|
||||||
|
pads.append(LayoutPad(number="49", x=0, y=0, net="GND", pinfunction="EP"))
|
||||||
|
graph = DesignGraph(
|
||||||
|
components={
|
||||||
|
"U12": _ic(
|
||||||
|
"U12", "Si4684-A10-GM",
|
||||||
|
"Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm_ThermalVias",
|
||||||
|
48,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
layout = LayoutGraph(
|
||||||
|
footprints={
|
||||||
|
"U12": LayoutFootprint(
|
||||||
|
reference="U12",
|
||||||
|
footprint="Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm_ThermalVias",
|
||||||
|
x=0, y=0, pads=pads,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert "PE-BOM-011" not in _ids(
|
||||||
|
check_bom_pcb_datasheet(graph, {"Si4684-A10-GM": cons}, layout),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_qfn24_extra_pad_30_still_fires():
|
||||||
|
cons = ComponentConstraints(
|
||||||
|
mpn="IC24",
|
||||||
|
package_info=PackageInfo(base_family="QFN", package="QFN-24", pin_count=24),
|
||||||
|
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 25)],
|
||||||
|
absolute_maximum_ratings=[],
|
||||||
|
rules=[],
|
||||||
|
)
|
||||||
|
pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 25)]
|
||||||
|
pads.append(LayoutPad(number="30", x=1, y=0, net="SIG"))
|
||||||
|
graph = DesignGraph(
|
||||||
|
components={"U1": _ic("U1", "IC24", "Package_DFN_QFN:QFN-24-1EP", 24)},
|
||||||
|
)
|
||||||
|
layout = LayoutGraph(
|
||||||
|
footprints={
|
||||||
|
"U1": LayoutFootprint(
|
||||||
|
reference="U1", footprint="Package_DFN_QFN:QFN-24-1EP",
|
||||||
|
x=0, y=0, pads=pads,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert "PE-BOM-011" in _ids(
|
||||||
|
check_bom_pcb_datasheet(graph, {"IC24": cons}, layout),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_qfn_vs_vqfn_same_24_qfn_land_not_bom_010():
|
||||||
|
"""U19: datasheet QFN vs CAD VQFN-24 4×4 is the same land family."""
|
||||||
|
cons = ComponentConstraints(
|
||||||
|
mpn="LAN8720A",
|
||||||
|
package_info=PackageInfo(
|
||||||
|
base_family="SMS", package="QFN-24", pin_count=24,
|
||||||
|
),
|
||||||
|
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 25)],
|
||||||
|
absolute_maximum_ratings=[],
|
||||||
|
rules=[],
|
||||||
|
)
|
||||||
|
pads = [LayoutPad(number=str(i), x=0, y=0, net="SIG") for i in range(1, 25)]
|
||||||
|
pads.append(LayoutPad(number="25", x=0, y=0, net="GND", pinfunction="EP"))
|
||||||
|
graph = DesignGraph(
|
||||||
|
components={
|
||||||
|
"U19": _ic(
|
||||||
|
"U19", "LAN8720A",
|
||||||
|
"Package_DFN_QFN:VQFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias",
|
||||||
|
24,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
schematic_fields={
|
||||||
|
"U19": {
|
||||||
|
"footprint": (
|
||||||
|
"Package_DFN_QFN:VQFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bom_fields={"U19": {"footprint": "VQFN-24"}},
|
||||||
|
)
|
||||||
|
layout = LayoutGraph(
|
||||||
|
footprints={
|
||||||
|
"U19": LayoutFootprint(
|
||||||
|
reference="U19",
|
||||||
|
footprint=(
|
||||||
|
"Package_DFN_QFN:VQFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias"
|
||||||
|
),
|
||||||
|
x=0, y=0, pads=pads,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ids = _ids(check_bom_pcb_datasheet(graph, {"LAN8720A": cons}, layout))
|
||||||
|
assert "PE-BOM-010" not in ids
|
||||||
|
assert "PE-BOM-011" not in ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_hubaudio_u11_u12_u19_software_not_error():
|
||||||
|
if not _HUB_PCB.is_file():
|
||||||
|
return
|
||||||
|
layout = parse_kicad_pcb(_HUB_PCB)
|
||||||
|
slim = LayoutGraph(
|
||||||
|
footprints={k: layout.footprints[k] for k in ("U11", "U12", "U19")
|
||||||
|
if k in layout.footprints},
|
||||||
|
)
|
||||||
|
graph = DesignGraph(
|
||||||
|
components={
|
||||||
|
"U11": _ic(
|
||||||
|
"U11", "ESP32-S31-WROOM-3",
|
||||||
|
"MIKILAB_ESP32_S31_WROOM_3:ESP32-S31-WROOM-3", 61,
|
||||||
|
),
|
||||||
|
"U12": _ic(
|
||||||
|
"U12", "Si4684-A10-GM",
|
||||||
|
"Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm_ThermalVias",
|
||||||
|
48,
|
||||||
|
),
|
||||||
|
"U19": _ic(
|
||||||
|
"U19", "LAN8720A",
|
||||||
|
"Package_DFN_QFN:VQFN-24-1EP_4x4mm_P0.5mm_EP2.5x2.5mm_ThermalVias",
|
||||||
|
24,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
cmap = {
|
||||||
|
"ESP32-S31-WROOM-3": ComponentConstraints(
|
||||||
|
mpn="ESP32-S31-WROOM-3",
|
||||||
|
package_info=PackageInfo(base_family="ESP", package="QFN-56", pin_count=61),
|
||||||
|
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 62)],
|
||||||
|
absolute_maximum_ratings=[],
|
||||||
|
rules=[],
|
||||||
|
),
|
||||||
|
"Si4684-A10-GM": ComponentConstraints(
|
||||||
|
mpn="Si4684-A10-GM",
|
||||||
|
package_info=PackageInfo(base_family="Sky", package="QFN-48", pin_count=24),
|
||||||
|
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 25)],
|
||||||
|
absolute_maximum_ratings=[],
|
||||||
|
rules=[],
|
||||||
|
),
|
||||||
|
"LAN8720A": ComponentConstraints(
|
||||||
|
mpn="LAN8720A",
|
||||||
|
package_info=PackageInfo(base_family="SMS", package="QFN-24", pin_count=24),
|
||||||
|
pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 25)],
|
||||||
|
absolute_maximum_ratings=[],
|
||||||
|
rules=[],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
findings = check_bom_pcb_datasheet(graph, cmap, slim)
|
||||||
|
err = [
|
||||||
|
f for f in findings
|
||||||
|
if f.rule_id in {"PE-BOM-010", "PE-BOM-011"} and f.status == "ERROR"
|
||||||
|
]
|
||||||
|
assert err == [], [f.finding for f in err]
|
||||||
|
|||||||
Reference in New Issue
Block a user