tscircuit/standard-jst-programmer
26 × 42 mm USB-C RP2040 programmer with JST and TC2030 IDC SWD, NRST, selectable sensed 5 V / 3.3 V power, RGB status, and importable JST SH/Tag-Connect targets. Top-side assembly.
- Version
- 0.8.0
- License
- unset
- Stars
- 0
scripts/check-circuits.ts
import { runAllRoutingChecks } from "@tscircuit/checks";
import { strict as assert } from "node:assert";
import { readFileSync } from "node:fs";
const load = (name: string) =>
JSON.parse(
readFileSync(`dist/circuits/${name}/circuit.json`, "utf8"),
) as any[];
for (const variant of ["upward", "side", "programmer"]) {
const circuit = load(variant);
// Run checks directly: asynchronous renderer failures must not look like clean DRC.
const routingIssues = await runAllRoutingChecks(circuit);
assert.equal(
routingIssues.filter((e) => e.type.endsWith("_error")).length,
0,
`${variant}: independent routing DRC`,
);
assert.equal(
circuit.filter((e) => e.type.endsWith("_error")).length,
0,
`${variant}: build/DRC errors`,
);
for (const [name, expected] of [
["J1", ["SWCLK", "GND", "SWDIO"]],
["J2", ["VOUT", "GND"]],
["J3", ["VOUT", "SWDIO", "GND", "SWCLK", "NRST"]],
["J5", variant === "programmer" ? ["TX", "GND", "RX"] : ["RX", "GND", "TX"]],
] as const) {
const j = circuit.find(
(e) => e.type === "source_component" && e.name === name,
);
const ports = circuit.filter(
(e) =>
e.type === "source_port" &&
e.source_component_id === j.source_component_id,
);
assert.equal(
ports.length,
expected.length,
`${variant} ${name}: electrical pin count`,
);
expected.forEach((label, i) =>
assert(
ports.find(
(p) => p.pin_number === i + 1 && p.port_hints.includes(label),
),
`${variant}: pin ${i + 1} = ${label}`,
),
);
const pc = circuit.find(
(e) =>
e.type === "pcb_component" &&
e.source_component_id === j.source_component_id,
);
const cad = circuit.find(
(e) =>
e.type === "cad_component" &&
e.pcb_component_id === pc.pcb_component_id,
);
const part =
name === "J3"
? variant === "upward"
? "C160391"
: "C136657"
: (name === "J1" || name === "J5")
? variant === "upward"
? "C160389"
: "C160403"
: variant === "upward"
? "C160388"
: "C160402";
assert.deepEqual(
j.supplier_part_numbers.jlcpcb,
[part],
`${variant}: JLCPCB part number`,
);
assert(
cad?.model_obj_url?.includes(`${part}.obj`),
`${variant}: matching 3D model`,
);
assert(
cad?.model_step_url?.includes(`${part}.step`),
`${variant}: matching STEP model`,
);
const pads = circuit.filter(
(e) =>
e.type === "pcb_smtpad" && e.pcb_component_id === pc.pcb_component_id,
);
assert.equal(
pads.length,
expected.length + 2,
`${variant}: contacts plus two mounting tabs`,
);
const signalPads = pads.filter((p) => p.pcb_port_id);
assert.equal(signalPads.length, expected.length);
for (const pad of signalPads)
assert(Math.abs(Math.min(pad.width, pad.height) - 0.6) < 0.001);
}
}
const circuit = load("programmer");
const components = circuit.filter((e) => e.type === "source_component");
const port = (name: string, label: string) => {
const component = components.find((e) => e.name === name);
return circuit.find(
(e) =>
e.type === "source_port" &&
e.source_component_id === component.source_component_id &&
e.port_hints.includes(label),
).source_port_id;
};
// Union ports and named nets through wires only; resistors and the selector do not short nets.
const parent = new Map<string, string>();
const root = (s: string): string => (parent.has(s) ? root(parent.get(s)!) : s);
for (const trace of circuit.filter((e) => e.type === "source_trace")) {
const nodes = [
...trace.connected_source_port_ids,
...trace.connected_source_net_ids,
];
nodes.slice(1).forEach((n) => {
const a = root(nodes[0]),
b = root(n);
if (a !== b) parent.set(b, a);
});
}
const connected = (a: string, b: string) => assert.equal(root(a), root(b));
connected(port("U1", "GPIO2"), port("R_CLK", "pin1"));
connected(port("U1", "GPIO3"), port("R_DIO", "pin1"));
connected(port("R_CLK", "pin2"), port("J1", "SWCLK"));
connected(port("R_DIO", "pin2"), port("J1", "SWDIO"));
connected(port("J1", "GND"), port("U1", "GND"));
connected(port("J2", "GND"), port("U1", "GND"));
connected(port("SW_PWR", "pin2"), port("R_SHUNT", "pin1"));
connected(port("R_SHUNT", "pin2"), port("J2", "VOUT"));
connected(port("SW_PWR", "pin3"), port("J_USB", "A4B9"));
connected(port("SW_PWR", "pin1"), port("U3", "VOUT"));
const powerNodes = [
port("SW_PWR", "pin1"),
port("SW_PWR", "pin2"),
port("SW_PWR", "pin3"),
port("J2", "GND"),
].map(root);
assert.equal(
new Set(powerNodes).size,
4,
"Selector throws, common, and ground must be separate copper nets",
);
for (const selected of [0, 2]) {
const state = (n: string) =>
root(n) === powerNodes[selected] ? powerNodes[1] : root(n);
assert.equal(
state(port("R_SHUNT", "pin1")),
state(port("SW_PWR", selected === 0 ? "pin1" : "pin3")),
);
assert.notEqual(
state(port("SW_PWR", "pin1")),
state(port("SW_PWR", "pin3")),
"Neither selector position shorts the supply rails",
);
}
for (const name of ["R_CLK", "R_DIO"])
assert.equal(
components.find((e) => e.name === name).resistance,
100,
"100 ohm source termination",
);
assert(
!components.some((e) => ["R_RST", "R_PULLUP", "JP_PWR"].includes(e.name)),
"Old reset and jumper circuit removed",
);
assert.deepEqual(
components.find((e) => e.name === "SW_PWR").supplier_part_numbers.jlcpcb,
["C221660"],
);
assert(
readFileSync("firmware/board_standard_jst_config.h", "utf8").includes(
"#define PROBE_PIN_RESET 1",
),
);
assert(
readFileSync("firmware/openocd.cfg", "utf8").includes("reset_config none"),
);
assert(
circuit.some((e) => e.type === "pcb_trace"),
"Programmer must have routed copper",
);
// Discrete support circuit: verify silicon pins, supplies, USB-C and external QSPI flash.
const mcu = components.find((e) => e.name === "U1");
assert.equal(
circuit.filter(
(e) =>
e.type === "source_port" &&
e.source_component_id === mcu.source_component_id,
).length,
57,
"U1 must be the bare 57-pad RP2040, not a module",
);
for (const pin of [
"IOVDD1",
"IOVDD2",
"IOVDD3",
"IOVDD4",
"IOVDD5",
"IOVDD6",
"USB_VDD",
"VREG_IN",
])
connected(port("U1", pin), port("U3", "VOUT"));
for (const pin of ["DVDD1", "DVDD2"])
connected(port("U1", pin), port("U1", "VREG_VOUT"));
assert.notEqual(
root(port("U1", "VREG_VOUT")),
root(port("U3", "VOUT")),
"1.1 V core and 3.3 V must remain separate",
);
connected(port("U1", "TESTEN"), port("U1", "GND"));
connected(port("U2", "VCC"), port("U3", "VOUT"));
connected(port("U2", "GND"), port("U1", "GND"));
connected(port("U2", "EP"), port("U1", "GND"));
for (const [a, b] of [
["QSPI_SS", "CS"],
["QSPI_SCLK", "CLK"],
["QSPI_SD0", "pin5"],
["QSPI_SD1", "pin2"],
["QSPI_SD2", "pin3"],
["QSPI_SD3", "pin7"],
])
connected(port("U1", a!), port("U2", b!));
for (const [usb, resistor, mcuPin] of [
["A7", "R_USB1", "USB_DM"],
["B7", "R_USB1", "USB_DM"],
["A6", "R_USB2", "USB_DP"],
["B6", "R_USB2", "USB_DP"],
]) {
connected(port("J_USB", usb!), port(resistor!, "pin1"));
connected(port(resistor!, "pin2"), port("U1", mcuPin!));
}
connected(port("J_USB", "A5"), port("R_CC1", "pin1"));
connected(port("J_USB", "B5"), port("R_CC2", "pin1"));
for (const name of ["R_CC1", "R_CC2"])
connected(port(name, "pin2"), port("U1", "GND"));
connected(port("J_USB", "A4B9"), port("J_USB", "B4A9"));
connected(port("U3", "VIN"), port("C_REG_IN", "pin1"));
connected(port("U3", "VOUT"), port("C_REG_OUT", "pin1"));
connected(port("U1", "VREG_IN"), port("C_VREG_IN", "pin1"));
connected(port("U1", "VREG_VOUT"), port("C_CORE", "pin1"));
for (let i = 1; i <= 6; i++)
connected(port("U1", `IOVDD${i}`), port(`C_IOVDD${i}`, "pin1"));
connected(port("U1", "QSPI_SS"), port("R_BOOT_SER", "pin2"));
connected(port("R_BOOT_SER", "pin1"), port("SW_BOOT", "pin1"));
assert.notEqual(
root(port("SW_BOOT", "pin1")),
root(port("U1", "QSPI_SS")),
"BOOTSEL must include the 1k series resistor",
);
console.log(
"Validated all three circuits: JST pinouts, bare RP2040 supplies/flash/USB/boot, SWD, selectable power isolation and DRC.",
);
const preview = JSON.parse(
readFileSync("dist/preview/circuit.json", "utf8"),
) as any[];
assert.equal(
(await runAllRoutingChecks(preview)).filter((e) => e.type.endsWith("_error"))
.length,
0,
"preview: independent routing DRC",
);
assert.equal(
preview.filter((e) => e.type === "pcb_board").length,
1,
"preview: standalone programmer board",
);
assert.equal(
preview.filter(
(e) =>
e.type === "cad_component" &&
/C160389|C160403|C160388|C160402|C160391|C136657/.test(
e.model_obj_url ?? "",
),
).length,
4,
"preview: all four programmer JST models",
);
assert.equal(
preview.filter((e) => e.type.endsWith("_error")).length,
0,
"preview: no build errors",
);
console.log("Programmer preview and JST CAD checks passed");
const compact = load("programmer");
// Ground vias must escape the crystal solder lands rather than wick solder.
for (const design of [compact, preview]) {
const crystalSourceIds = new Set(design.filter((e) =>
e.type === "source_component" && e.name === "Y1",
).map((e) => e.source_component_id));
const crystalPcbIds = new Set(design.filter((e) =>
e.type === "pcb_component" && crystalSourceIds.has(e.source_component_id),
).map((e) => e.pcb_component_id));
const crystalPads = design.filter((e) =>
e.type === "pcb_smtpad" && crystalPcbIds.has(e.pcb_component_id),
);
assert.equal(crystalPads.length, 4, "crystal has four solder lands");
for (const via of design.filter((e) => e.type === "pcb_via")) {
for (const pad of crystalPads) {
const distance = Math.hypot(
Math.max(Math.abs(via.x - pad.x) - pad.width / 2, 0),
Math.max(Math.abs(via.y - pad.y) - pad.height / 2, 0),
);
assert(distance - via.outer_diameter / 2 > 0,
"crystal via copper must not overlap solder lands");
}
}
}
const compactBoard = compact.find((e) => e.type === "pcb_board");
assert.equal(compactBoard.width, 26);
assert.equal(compactBoard.height, 42);
assert.equal(compactBoard.num_layers, 4);
assert(
compact.filter((e) => e.type === "pcb_via").every((via) =>
["top", "inner1", "inner2", "bottom"].every((layer) => via.layers.includes(layer)),
),
"all programmer vias must be through vias across all four copper layers",
);
assert(
compact
.filter((e) => e.type === "pcb_component")
.every((e) => e.layer === "top"),
"single-sided component assembly",
);
assert(
!compact.some(
(e) => e.type === "source_component" && e.name.startsWith("TP_"),
),
"no test points",
);
const pcbFor = (name: string) => {
const source = compact.find(
(e) => e.type === "source_component" && e.name === name,
);
return compact.find(
(e) =>
e.type === "pcb_component" &&
e.source_component_id === source.source_component_id,
);
};
assert(
pcbFor("J_USB").center.y > 14 && pcbFor("J1").center.y < -15,
"opposite connector edges",
);
for (const label of [
"1:CLK 2:GND",
"3:DIO",
"1:VOUT",
"2:GND",
"1:VOUT 2:DIO 3:GND",
"4:CLK 5:NRST",
"5V",
"3V3",
"IO:3V3",
])
assert(
compact.some(
(e) =>
e.type === "pcb_silkscreen_text" &&
e.text === label &&
e.layer === "top",
),
`JST legend: ${label}`,
);
console.log(
"Compact board dimensions, top-side assembly, opposite connectors, and pinout legend verified",
);
// Five-pin extension shares one SWD target and one sensed power domain.
for (const label of ["SWCLK", "SWDIO", "GND"])
connected(port("J3", label), port("J1", label));
connected(port("J3", "VOUT"), port("J2", "VOUT"));
connected(port("J3", "NRST"), port("R_NRST", "pin2"));
connected(port("R_NRST", "pin1"), port("U1", "GPIO1"));
assert.notEqual(root(port("J3", "NRST")), root(port("U1", "RUN")));
assert.notEqual(root(port("J3", "NRST")), root(port("J3", "VOUT")));
// All supplied target current must cross the shunt; monitor supply is upstream.
assert.equal(components.find((e) => e.name === "R_SHUNT").resistance, 0.1);
assert.notEqual(root(port("R_SHUNT", "pin1")), root(port("R_SHUNT", "pin2")));
connected(port("U_SENSE", "IN_POS"), port("R_SHUNT", "pin1"));
connected(port("U_SENSE", "IN_NEG"), port("R_SHUNT", "pin2"));
connected(port("U_SENSE", "VS"), port("U3", "VOUT"));
for (const label of ["GND", "A0", "A1"])
connected(port("U_SENSE", label), port("U1", "GND"));
connected(port("U_SENSE", "SDA"), port("U1", "GPIO18"));
connected(port("U_SENSE", "SCL"), port("U1", "GPIO19"));
for (const [r, l] of [
["R_SDA", "SDA"],
["R_SCL", "SCL"],
]) {
connected(port(r!, "pin1"), port("U_SENSE", l!));
connected(port(r!, "pin2"), port("U3", "VOUT"));
}
connected(port("U1", "GPIO25"), port("U_RGB", "A"));
connected(port("U_RGB", "Y"), port("R_RGB", "pin1"));
connected(port("R_RGB", "pin2"), port("D_RGB", "DI"));
connected(port("U_RGB", "N_OE"), port("U1", "GND"));
connected(port("D_RGB", "GND"), port("U1", "GND"));
for (const [name, label] of [
["D_RGB", "VDD"],
["U_RGB", "VCC"],
])
connected(port(name!, label!), port("J_USB", "A4B9"));
assert.notEqual(
root(port("D_RGB", "VDD")),
root(port("J2", "VOUT")),
"Probe LED current excluded from measurement",
);
for (const [name, cid] of [
["D_RGB", "C41413180"],
["U_SENSE", "C87469"],
["U_RGB", "C350557"],
])
assert.deepEqual(
components.find((e) => e.name === name).supplier_part_numbers.jlcpcb,
[cid],
);
assert.equal(
components.find((e) => e.name === "D_RGB").manufacturer_part_number,
"XL-1615RGBC-2812B-S",
);
console.log(
"NRST, shared five-pin interface, current sensing and buffered RGB verified",
);
// The TC2030 cable is straight-through; header and target use the same six-pin map.
const tagLabels = ["V3_3", "SWDIO", "NRST", "SWCLK", "GND", "V5"];
for (const variant of ["programmer", "upward", "side"]) {
const cj = load(variant);
const sc = cj.find((e) => e.type === "source_component" && e.name === "J4");
const pc = cj.find(
(e) =>
e.type === "pcb_component" &&
e.source_component_id === sc.source_component_id,
);
const ports = cj.filter(
(e) =>
e.type === "source_port" &&
e.source_component_id === sc.source_component_id,
);
assert.equal(ports.length, 6);
tagLabels.forEach((label, i) =>
assert(
ports.some((p) => p.pin_number === i + 1 && p.port_hints.includes(label)),
),
);
const pads = cj.filter(
(e) =>
e.type === "pcb_smtpad" && e.pcb_component_id === pc.pcb_component_id,
);
assert.equal(pads.length, 6);
if (variant === "programmer") {
assert.deepEqual(sc.supplier_part_numbers.jlcpcb, ["C3324375"]);
assert.equal(sc.manufacturer_part_number, "FTSH-103-01-L-DV-TR");
assert(
cj.some(
(e) =>
e.type === "cad_component" &&
e.pcb_component_id === pc.pcb_component_id,
),
);
// Check actual copper endpoints, not only a router's connectsTo metadata.
for (const pad of pads) {
const pp = cj.find(
(e) => e.type === "pcb_port" && e.pcb_port_id === pad.pcb_port_id,
);
const sp = ports.find((p) => p.source_port_id === pp.source_port_id);
const endpoints = cj
.filter((e) => e.type === "pcb_trace")
.flatMap((t) => [t.route[0], t.route.at(-1)]);
const touched = endpoints.some(
(p) =>
p?.route_type === "wire" &&
p.layer === "top" &&
Math.abs(p.x - pad.x) <= pad.width / 2 + 0.001 &&
Math.abs(p.y - pad.y) <= pad.height / 2 + 0.001,
);
assert.equal(
touched,
true,
`IDC pin ${sp.pin_number}: actual routed endpoint`,
);
}
} else {
assert.equal(pc.do_not_place, true, "Bare-pad target is not assembled");
assert(
pads.every(
(p) => p.shape === "circle" && Math.abs(p.radius - 0.3937) < 0.00001,
),
);
assert(
!cj.some(
(e) =>
e.type === "pcb_solder_paste" &&
pads.some((p) => e.pcb_smtpad_id === p.pcb_smtpad_id),
),
"No paste on spring contacts",
);
const holes = cj.filter(
(e) =>
e.type === "pcb_hole" && e.pcb_component_id === pc.pcb_component_id,
);
assert.equal(holes.length, 3, "Three unplated alignment holes");
assert(holes.every((h) => Math.abs(h.hole_diameter - 0.9906) < 0.00001));
assert(
!cj.some(
(e) =>
e.type === "cad_component" &&
e.pcb_component_id === pc.pcb_component_id,
),
);
}
}
for (const label of ["SWDIO", "NRST", "SWCLK", "GND"])
connected(port("J4", label), port("J3", label));
connected(port("J4", "V3_3"), port("U3", "VOUT"));
connected(port("J4", "V5"), port("J_USB", "A4B9"));
for (const rail of ["V3_3", "V5"]) {
assert.notEqual(root(port("J4", rail)), root(port("J3", "VOUT")),
`J4 ${rail} bypasses selected/current-sensed VOUT`);
}
assert.notEqual(root(port("J4", "V3_3")), root(port("J4", "V5")));
for (const text of ["1 3V3 / 2 DIO", "5 GND / 6 5V"]) {
assert(circuit.some((e) => e.type === "pcb_silkscreen_text" && e.text === text),
`J4 fixed-rail silkscreen: ${text}`);
}
console.log("IDC physical connections and TC2030 bare-pad target verified");
assert(
pcbFor("J4").center.y < -5,
"IDC header stays beside the lower target connectors",
);
assert(
pcbFor("SW_RUN").center.y > 14,
"RUN button moved away from the target cables",
);
// The registry preview must show the same copper as the fabrication board.
assert.deepEqual(
preview.filter((e: any) => e.type === "pcb_copper_pour"),
circuit.filter((e: any) => e.type === "pcb_copper_pour"),
"registry preview ground pour matches the standalone fabrication board",
);
assert.equal(preview.filter((e: any) => e.type.endsWith("_error")).length, 0);
// UART is a separate 3.3 V interface, with source termination at the MCU.
connected(port("U1", "GPIO8"), port("R_UART_TX", "pin1"));
connected(port("U1", "GPIO9"), port("R_UART_RX", "pin1"));
connected(port("R_UART_TX", "pin2"), port("J5", "TX"));
connected(port("R_UART_RX", "pin2"), port("J5", "RX"));
connected(port("J5", "GND"), port("U1", "GND"));
for (const name of ["R_UART_TX", "R_UART_RX"])
assert.equal(components.find(e => e.name === name).resistance, 100);
for (const a of ["TX", "RX", "GND"])
for (const b of ["TX", "RX", "GND"])
if (a !== b) assert.notEqual(root(port("J5", a)), root(port("J5", b)));
for (const variant of ["side", "upward", "programmer"]) {
const cj = load(variant);
const j = cj.find(e => e.type === "source_component" && e.name === "J5");
const pc = cj.find(e => e.type === "pcb_component" && e.source_component_id === j.source_component_id);
const pads = cj.filter(e => e.type === "pcb_smtpad" && e.pcb_component_id === pc.pcb_component_id && e.pcb_port_id);
for (const text of ["TX", "RX", "GND"]) {
const sp = cj.find(e => e.type === "source_port" && e.source_component_id === j.source_component_id && e.port_hints.includes(text));
const pp = cj.find(e => e.type === "pcb_port" && e.source_port_id === sp.source_port_id);
const pad = pads.find(e => e.pcb_port_id === pp.pcb_port_id);
const label = cj.find(e => e.type === "pcb_silkscreen_text" && (variant === "programmer" || e.pcb_component_id === pc.pcb_component_id) && e.text === text);
assert(label, `${variant}: UART ${text} is printed beside its connector pad`);
// Programmer host faces left; its pin labels follow pad Y coordinates.
if (variant === "programmer") assert(Math.abs(label.anchor_position.y - pad.y) < 0.001);
else assert(Math.abs(label.anchor_position.x - pad.x) < 0.001);
}
}
assert(circuit.some(e => e.type === "pcb_silkscreen_text" && e.text === "UART"));
console.log("UART pinout, source termination, isolation, and rotated footprint pin labels verified");
// UART routing must terminate at real copper and keep vias out of solder lands.
const uartSourceTraceIds = new Set(circuit.filter(e => e.type === "source_trace" &&
e.connected_source_port_ids.some((id: string) =>
[port("R_UART_TX", "pin1"), port("R_UART_TX", "pin2"),
port("R_UART_RX", "pin1"), port("R_UART_RX", "pin2")].includes(id),
),
).map(e => e.source_trace_id));
const uartTraces = circuit.filter(e => e.type === "pcb_trace" && uartSourceTraceIds.has(e.source_trace_id));
assert.equal(uartTraces.length, 4, "all UART signal segments have copper");
const uartPcbTraceIds = new Set(uartTraces.map(e => e.pcb_trace_id));
for (const via of circuit.filter(e => e.type === "pcb_via" && uartPcbTraceIds.has(e.pcb_trace_id))) {
for (const pad of circuit.filter(e => e.type === "pcb_smtpad" && e.width && e.height)) {
const distance = Math.hypot(
Math.max(Math.abs(via.x - pad.x) - pad.width / 2, 0),
Math.max(Math.abs(via.y - pad.y) - pad.height / 2, 0),
);
assert(distance - via.outer_diameter / 2 >= 0.2 - 0.001,
"UART through vias have at least 0.2 mm clearance from all SMD lands");
}
}
for (const trace of uartTraces) {
for (const id of circuit.find(e => e.type === "source_trace" && e.source_trace_id === trace.source_trace_id).connected_source_port_ids) {
const pp = circuit.find(e => e.type === "pcb_port" && e.source_port_id === id);
const pad = circuit.find(e => e.type === "pcb_smtpad" && e.pcb_port_id === pp.pcb_port_id);
assert([trace.route[0], trace.route.at(-1)].some(p =>
p.route_type === "wire" && p.layer === pad.layer &&
Math.abs(p.x - pad.x) <= pad.width / 2 && Math.abs(p.y - pad.y) <= pad.height / 2,
), "UART signal endpoint reaches its physical pad");
}
}
console.log("UART routed pad endpoints and solder-land via clearance verified");