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,
  };
}