Add KiCad module carrier PCB draft
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.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user