Introduce an editable KiCad 10 schematic and routed two-layer 82 × 80 mm layout with local provisional footprints, validation tooling, and component research. Keep the RS-232 male-module mapping and mechanical clearances explicitly provisional pending hardware verification.
95 lines
5.2 KiB
Python
95 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Export and validate the checked-in schematic; never regenerates the design."""
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import xml.etree.ElementTree as ET
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
OUT = ROOT / "validation"
|
|
SCHEMATIC = ROOT / "serial-carrier.kicad_sch"
|
|
|
|
def run(*args):
|
|
subprocess.run(args, check=True, cwd=ROOT)
|
|
|
|
run("kicad-cli", "sch", "export", "netlist", "--format", "kicadxml", "-o", str(OUT / "serial-carrier.xml"), str(SCHEMATIC))
|
|
run("kicad-cli", "sch", "erc", "--format", "json", "--exit-code-violations", "-o", str(OUT / "erc.json"), str(SCHEMATIC))
|
|
root = ET.parse(OUT / "serial-carrier.xml").getroot()
|
|
nets = {net.get("name").removeprefix("/"): net.findall("node") for net in root.findall("nets/net")}
|
|
members = {name: sorted((node.get("ref"), node.get("pin")) for node in nodes) for name, nodes in nets.items()}
|
|
components = {comp.get("ref"): comp for comp in root.findall("components/comp")}
|
|
assert set(components) == {"U1", "U2", "DS1", "SW1", "SW2", "SW3", "R1", "R2", "R3"}
|
|
|
|
# Independent firmware-to-module contract, not imported from the schematic generator.
|
|
contracts = [
|
|
("RS_TX", "RS232_TX_GPIO", "TX", "5"),
|
|
("RS_RX", "RS232_RX_GPIO", "RX", "4"),
|
|
("RS_RTS", "RS232_RTS_GPIO", "RTS", "8"),
|
|
("RS_CTS", "RS232_CTS_GPIO", "CTS", "9"),
|
|
("RS_DTR", "RS232_DTR_GPIO", "DTR", "6"),
|
|
("RS_DSR", "RS232_DSR_GPIO", "DSR", "7"),
|
|
("RS_DCD", "RS232_DCD_GPIO", "DCD", "3"),
|
|
("RS_RI", "RS232_RI_GPIO", "RI", "10"),
|
|
("RS_VALID", "RS232_VALID_GPIO", "VLD", "11"),
|
|
("RS_OFF_N", "RS232_FORCE_OFF_N_GPIO", "OFF_N", "12"),
|
|
("OLED_SDA", "LOCAL_UI_DISPLAY_SDA_GPIO", "SDA", "4"),
|
|
("OLED_SCL", "LOCAL_UI_DISPLAY_SCL_GPIO", "SCL", "3"),
|
|
("BTN_PREVIOUS", "LOCAL_UI_BUTTON_PREVIOUS_GPIO", None, None),
|
|
("BTN_SELECT", "LOCAL_UI_BUTTON_SELECT_GPIO", None, None),
|
|
("BTN_NEXT", "LOCAL_UI_BUTTON_NEXT_GPIO", None, None),
|
|
]
|
|
firmware = ROOT.parents[1].joinpath("src/board_pins.h").read_text()
|
|
for net, macro, peripheral_name, peripheral_pin in contracts:
|
|
match = re.search(r"^#define\s+" + re.escape(macro) + r"\s+GPIO_NUM_(\d+)\s*$", firmware, re.MULTILINE)
|
|
assert match, macro
|
|
gpio = "GPIO" + match.group(1)
|
|
mcu = [node for node in nets[net] if node.get("ref") == "U1"]
|
|
assert len(mcu) == 1 and mcu[0].get("pinfunction") == gpio + "_" + mcu[0].get("pin"), (net, gpio)
|
|
if peripheral_name:
|
|
ref = "DS1" if net.startswith("OLED_") else "U2"
|
|
node = [node for node in nets[net] if node.get("ref") == ref]
|
|
assert len(node) == 1 and node[0].get("pin") == peripheral_pin
|
|
assert node[0].get("pinfunction") == peripheral_name + "_" + peripheral_pin
|
|
assert len(nets[net]) == 2, (net, members[net])
|
|
|
|
for i, net in enumerate(("BTN_PREVIOUS", "BTN_SELECT", "BTN_NEXT"), 1):
|
|
assert len(nets[net]) == 3
|
|
assert (f"SW{i}", "1") in members[net] and (f"R{i}", "2") in members[net]
|
|
assert components[f"SW{i}"].findtext("value") == "B3F-1000"
|
|
assert components[f"R{i}"].findtext("value") == "2.2k 1%"
|
|
assert members["+3V3"] == sorted([("U1","1"),("U1","2"),("U2","1"),("DS1","2"),("R1","1"),("R2","1"),("R3","1")])
|
|
assert members["GND"] == sorted([("U1","22"),("U1","23"),("U1","43"),("U1","44"),("U2","2"),("DS1","1"),("SW1","2"),("SW2","2"),("SW3","2")])
|
|
active = {name for name in nets if not name.startswith("unconnected-")}
|
|
assert active == {"+3V3", "GND"} | {row[0] for row in contracts}
|
|
for name, nodes in nets.items():
|
|
if name.startswith("unconnected-"):
|
|
assert len(nodes) == 1 and nodes[0].get("ref") == "U1"
|
|
# All 44 MCU header pins must occur exactly once, including deliberate no-connects.
|
|
assert sorted(int(node.get("pin")) for nodes in nets.values() for node in nodes if node.get("ref") == "U1") == list(range(1,45))
|
|
# USB/UART0, 5 V and PSRAM-conflicting pins must never be carrier nets.
|
|
for name in active:
|
|
assert not any(node.get("ref") == "U1" and node.get("pin") in ("21","24","25","33","34","35","41","42") for node in nets[name])
|
|
# U2's physical male footprint must stay explicitly unresolved in this draft.
|
|
assert not components["U2"].findtext("footprint")
|
|
expected = json.loads((OUT / "expected-nets.json").read_text())
|
|
assert set(expected) == active
|
|
for net, nodes in expected.items():
|
|
assert members[net] == sorted(tuple(node) for node in nodes), net
|
|
report = json.loads((OUT / "erc.json").read_text())
|
|
assert not any(sheet["violations"] for sheet in report["sheets"])
|
|
run("kicad-cli", "sch", "export", "pdf", "-o", str(ROOT / "serial-carrier.pdf"), str(SCHEMATIC))
|
|
run("kicad-cli", "sch", "export", "svg", "-o", str(OUT) + "/", str(SCHEMATIC))
|
|
summary = "\n".join([
|
|
"Native schematic validation passed.",
|
|
"KiCad ERC: zero reported errors/warnings; no project exclusions added.",
|
|
"17 connected nets, 9 components; exact net memberships verified.",
|
|
"15 firmware GPIO definitions independently checked against exported pin functions.",
|
|
"All 44 MCU header pins accounted for; USB/UART0/5V/PSRAM pins unused on carrier.",
|
|
"PDF and SVG exported successfully.",
|
|
"Not validated: male-module physical mapping, power budget/path, OLED pull-ups,",
|
|
"selected connector fit, PCB DRC/layout, procurement, or assembled hardware.",
|
|
]) + "\n"
|
|
(OUT / "summary.txt").write_text(summary)
|
|
print(summary)
|