astra-abse/t113-s3
Allwinner T113-S3 / JLCPCB C5197687: native saved fanout, LCD top, storage right, 127 perimeter exits, 30 decoupling capacitors, connected GND planes; clean DRC and shorts, all 29 vias connected.
- Version
- 1.2.0
- License
- MIT
- Stars
- 0
scripts/connectivity.ts
import assert from "node:assert/strict";
import type { AnyCircuitElement } from "circuit-json";
import { elements } from "./audit";
import { extractCopper, gap } from "./copper-clearance";
import { DECOUPLING, capacitorName } from "../src/decoupling";
/** Physical copper graph, using intended source nets and the emitted pour holes.
* A pour is one connected polygon; vias join every layer they physically span.
*/
export function auditConnectivity(json: AnyCircuitElement[]) {
const copper = extractCopper(json),
parents = copper.map((_, i) => i);
const root = (i: number): number =>
parents[i] === i ? i : (parents[i] = root(parents[i]!));
const viaContacts = new Map<string, Set<string>>();
for (let i = 0; i < copper.length; i++)
for (let j = i + 1; j < copper.length; j++) {
const a = copper[i]!,
b = copper[j]!;
if (a.net !== b.net || (a.kind === "polygon" && b.kind === "polygon"))
continue;
const layers = a.layers.filter((l) => b.layers.includes(l));
if (!layers.length || gap(a, b) > 1e-6) continue;
parents[root(i)] = root(j);
for (const [v, other] of [
[a, b],
[b, a],
])
if (v!.id.startsWith("pcb_via_") && other!.kind !== "circle") {
const contacts = viaContacts.get(v!.id) ?? new Set<string>();
layers.forEach((l) => contacts.add(l));
viaContacts.set(v!.id, contacts);
}
}
const sourceComponents = elements(json, "source_component");
const sourcePorts = elements(json, "source_port");
const pcbPorts = elements(json, "pcb_port");
const padIndex = (name: string, pin: number) => {
const component = sourceComponents.find((c) => c.name === name);
assert.ok(component, `Missing component ${name}`);
const port = sourcePorts.find(
(p) =>
p.source_component_id === component.source_component_id &&
p.pin_number === pin,
);
assert.ok(port, `Missing ${name}.${pin}`);
const pcb = pcbPorts.find((p) => p.source_port_id === port.source_port_id);
const pad = elements(json, "pcb_smtpad").find(
(p) => p.pcb_port_id === pcb?.pcb_port_id,
);
assert.ok(pad, `Missing pad ${name}.${pin}`);
return copper.findIndex((c) => c.id === pad.pcb_smtpad_id);
};
const groundRoot = root(padIndex("U1", 129));
assert.equal(
root(padIndex("U1", 91)),
groundRoot,
"AGND and EPAD must share physical ground copper",
);
for (const c of DECOUPLING) {
assert.equal(
root(padIndex(capacitorName(c), 1)),
root(padIndex("U1", c.pin)),
`${capacitorName(c)} power pad disconnected`,
);
assert.equal(
root(padIndex(capacitorName(c), 2)),
groundRoot,
`${capacitorName(c)} ground pad disconnected`,
);
}
for (let i = 0; i < copper.length; i++) {
const c = copper[i]!;
if (c.kind === "polygon")
assert.equal(root(i), groundRoot, `Floating ground pour ${c.id}`);
if (c.id.startsWith("pcb_via_"))
assert.ok(
(viaContacts.get(c.id)?.size ?? 0) >= 2,
`Floating via ${c.id}: needs copper on two layers`,
);
}
return {
capacitors: DECOUPLING.length,
connectedGroundPads: DECOUPLING.length + 2,
connectedVias: viaContacts.size,
groundPours: elements(json, "pcb_copper_pour").length,
};
}