ShiboSoftwareDev/mspm0g3507-gameboy-handheld
An AO3400A N-channel MOSFET for power switching and an AP2112K-3.3TRG1 LDO regulator providing a regulated 3.3 V output, both in compact SMD packages.
- Version
- 1.2.1
- License
- unset
- Stars
- 0
scripts/audit-fabrication.mjs
import { readFileSync } from "node:fs"
import {
runAllNetlistChecks,
runAllPinSpecificationChecks,
runAllPlacementChecks,
runAllRoutingChecks,
} from "@tscircuit/checks"
const circuitPath = process.argv[2] ?? "dist/index/circuit.json"
const circuit = JSON.parse(readFileSync(circuitPath, "utf8"))
const source = readFileSync("index.circuit.tsx", "utf8")
const failures = []
const elements = (type) => circuit.filter((element) => element.type === type)
const assert = (condition, message) => {
if (!condition) failures.push(message)
}
const approximately = (actual, expected, tolerance = 0.001) =>
Math.abs(actual - expected) <= tolerance
const sourceComponents = elements("source_component")
const sourcePorts = elements("source_port")
const sourceNets = elements("source_net")
const sourceTraces = elements("source_trace")
const pcbComponents = elements("pcb_component")
const pcbPorts = elements("pcb_port")
const pcbTraces = elements("pcb_trace")
const vias = elements("pcb_via")
const componentByName = new Map(sourceComponents.map((component) => [component.name, component]))
const netByName = new Map(sourceNets.map((net) => [net.name, net]))
const componentBySourceId = new Map(sourceComponents.map((component) => [component.source_component_id, component]))
const pcbComponentBySourceId = new Map(pcbComponents.map((component) => [component.source_component_id, component]))
const portsByComponentId = new Map()
for (const port of sourcePorts) {
const ports = portsByComponentId.get(port.source_component_id) ?? []
ports.push(port)
portsByComponentId.set(port.source_component_id, ports)
}
const pcbTracesBySourceId = new Map()
for (const trace of pcbTraces) {
const traces = pcbTracesBySourceId.get(trace.source_trace_id) ?? []
traces.push(trace)
pcbTracesBySourceId.set(trace.source_trace_id, traces)
}
const viaCountByTraceId = new Map()
for (const via of vias) {
viaCountByTraceId.set(via.pcb_trace_id, (viaCountByTraceId.get(via.pcb_trace_id) ?? 0) + 1)
}
const sourcePort = (componentName, portName) => {
const component = componentByName.get(componentName)
return (portsByComponentId.get(component?.source_component_id) ?? []).find(
(port) => port.name === portName || port.port_hints?.includes(portName),
)
}
const assertPortNet = (componentName, portName, netName) => {
const port = sourcePort(componentName, portName)
const net = netByName.get(netName)
assert(Boolean(port), `${componentName}.${portName} is missing`)
assert(Boolean(net), `net.${netName} is missing`)
assert(
port?.subcircuit_connectivity_map_key === net?.subcircuit_connectivity_map_key,
`${componentName}.${portName} is not on net.${netName}`,
)
}
const assertPortsConnected = (componentA, portA, componentB, portB) => {
const first = sourcePort(componentA, portA)
const second = sourcePort(componentB, portB)
assert(Boolean(first), `${componentA}.${portA} is missing`)
assert(Boolean(second), `${componentB}.${portB} is missing`)
assert(
first?.subcircuit_connectivity_map_key === second?.subcircuit_connectivity_map_key,
`${componentA}.${portA} is not connected to ${componentB}.${portB}`,
)
}
const traceLength = (trace) => {
let length = 0
for (let index = 1; index < trace.route.length; index += 1) {
length += Math.hypot(
trace.route[index].x - trace.route[index - 1].x,
trace.route[index].y - trace.route[index - 1].y,
)
}
return length
}
const traceStats = (name) => {
const sourceTrace = sourceTraces.find((trace) => trace.name === name)
assert(Boolean(sourceTrace), `Missing named source trace ${name}`)
let traces = pcbTracesBySourceId.get(sourceTrace?.source_trace_id) ?? []
if (traces.length === 0 && sourceTrace?.subcircuit_connectivity_map_key) {
traces = sourceTraces
.filter(
(candidate) =>
candidate.subcircuit_connectivity_map_key === sourceTrace.subcircuit_connectivity_map_key,
)
.flatMap((candidate) => pcbTracesBySourceId.get(candidate.source_trace_id) ?? [])
}
assert(traces.length > 0, `${name} has no routed PCB trace`)
return {
length: traces.reduce((sum, trace) => sum + traceLength(trace), 0),
vias: traces.reduce((sum, trace) => sum + (viaCountByTraceId.get(trace.pcb_trace_id) ?? 0), 0),
}
}
const checkResults = {
netlist: await runAllNetlistChecks(circuit),
pinSpecification: await runAllPinSpecificationChecks(circuit),
placement: await runAllPlacementChecks(circuit),
routing: await runAllRoutingChecks(circuit),
}
for (const [name, results] of Object.entries(checkResults)) {
assert(
results.length === 0,
`${name} checks returned ${results.length}: ${results.map((result) => result.message).join(" | ")}`,
)
}
const board = elements("pcb_board")[0]
assert(elements("pcb_board").length === 1, "Expected exactly one PCB")
assert(
approximately(board?.width, 72) && approximately(board?.height, 112),
`PCB is ${board?.width} mm by ${board?.height} mm instead of 72 mm by 112 mm`,
)
assert(board?.num_layers === 4, `Expected a four-layer PCB, found ${board?.num_layers} layers`)
assert(sourceComponents.length === 74, `Expected 74 source components, found ${sourceComponents.length}`)
assert(pcbComponents.length === 74, `Expected 74 placed PCB components, found ${pcbComponents.length}`)
const testPoints = sourceComponents.filter((component) => component.ftype === "simple_test_point")
const assemblyComponents = sourceComponents.filter((component) => component.ftype !== "simple_test_point")
assert(testPoints.length === 5, `Expected five service test points, found ${testPoints.length}`)
for (const component of assemblyComponents) {
assert(Boolean(component.manufacturer_part_number), `${component.name} has no manufacturer part number`)
assert(Boolean(component.supplier_part_numbers?.jlcpcb?.[0]), `${component.name} has no JLCPCB part number`)
}
const courtyards = [
...elements("pcb_courtyard_outline"),
...elements("pcb_courtyard_rect"),
...elements("pcb_courtyard_circle"),
]
const courtyardComponentIds = new Set(courtyards.map((courtyard) => courtyard.pcb_component_id))
for (const component of assemblyComponents) {
const pcbComponent = pcbComponentBySourceId.get(component.source_component_id)
assert(Boolean(pcbComponent), `${component.name} has no PCB component`)
assert(courtyardComponentIds.has(pcbComponent?.pcb_component_id), `${component.name} has no courtyard`)
assert(pcbComponent?.layer === "top", `${component.name} is not on the top layer`)
}
for (const via of vias) {
assert(approximately(via.hole_diameter, 0.3), `${via.pcb_via_id} has a non-0.30 mm drill`)
assert(approximately(via.outer_diameter, 0.45), `${via.pcb_via_id} has a non-0.45 mm pad`)
assert(via.layers?.includes("top") && via.layers?.includes("bottom"), `${via.pcb_via_id} is not a through-via`)
}
const controls = [
["SW_UP", "PA2", "BTN_UP"],
["SW_DOWN", "PA3", "BTN_DOWN"],
["SW_LEFT", "PA4", "BTN_LEFT"],
["SW_RIGHT", "PA5", "BTN_RIGHT"],
["SW_A", "PA6", "BTN_A"],
["SW_B", "PA7", "BTN_B"],
["SW_START", "PA12", "BTN_START"],
["SW_SELECT", "PA13", "BTN_SELECT"],
]
for (const [switchName, mcuPort, net] of controls) {
assert(componentByName.get(switchName)?.ftype === "simple_push_button", `${switchName} is not a pushbutton`)
if (["SW_UP", "SW_LEFT", "SW_START"].includes(switchName)) {
assertPortsConnected(switchName, "pin1", "U_MCU", mcuPort)
} else {
assertPortNet(switchName, "pin1", net)
assertPortNet("U_MCU", mcuPort, net)
}
assertPortNet(switchName, "pin2", "GND_CONTROLS")
}
const displayPins = {
IM0: "GND_DISPLAY",
IM1: "VDD_3V3",
IM2: "VDD_3V3",
IM3: "GND_DISPLAY",
RESET: "DISPLAY_RESET_N",
SDI: "DISPLAY_MOSI",
RD: "VDD_3V3",
WRX_D_CX: "DISPLAY_DC",
D_CX_SCL: "DISPLAY_SCK",
CSX: "DISPLAY_CS_N",
VDDI1: "VDD_3V3",
VDDI2: "VDD_3V3",
VCI: "VDD_3V3",
GND1: "GND_DISPLAY",
GND2: "GND_DISPLAY",
GND3: "GND_DISPLAY",
GND4: "GND_DISPLAY",
}
for (const [pin, net] of Object.entries(displayPins)) assertPortNet("J_DISPLAY_FPC", pin, net)
assertPortNet("R_DISPLAY_5V_LINK", "pin1", "DISPLAY_5V")
assertPortsConnected("J_DISPLAY_FPC", "LEDA", "R_DISPLAY_5V_LINK", "pin2")
for (const index of [1, 2, 3, 4]) {
assertPortsConnected("J_DISPLAY_FPC", `LEDK${index}`, `R_BACKLIGHT_K${index}`, "pin1")
assertPortNet(`R_BACKLIGHT_K${index}`, "pin2", "DISPLAY_BACKLIGHT_SINK")
assert(
componentByName.get(`R_BACKLIGHT_K${index}`)?.resistance === 100,
`R_BACKLIGHT_K${index} is not 100 ohms`,
)
}
assertPortNet("U_BACKLIGHT_SWITCH", "G", "DISPLAY_BACKLIGHT_PWM")
assertPortNet("U_BACKLIGHT_SWITCH", "D", "DISPLAY_BACKLIGHT_SINK")
assertPortNet("U_BACKLIGHT_SWITCH", "S", "GND_DISPLAY")
assertPortNet("R_BACKLIGHT_GATE_PULLDOWN", "pin1", "DISPLAY_BACKLIGHT_PWM")
assertPortNet("R_BACKLIGHT_GATE_PULLDOWN", "pin2", "GND_DISPLAY")
assert(componentByName.get("J_DISPLAY_FPC")?.supplier_part_numbers?.jlcpcb?.[0] === "C2911826", "Wrong display FPC connector")
assert(componentByName.get("U_BACKLIGHT_SWITCH")?.supplier_part_numbers?.jlcpcb?.[0] === "C20917", "Wrong backlight MOSFET")
for (const [switchName] of controls) {
assert(componentByName.get(switchName)?.supplier_part_numbers?.jlcpcb?.[0] === "C10852", `${switchName} is not the larger C10852 switch`)
}
for (const [pin, net] of Object.entries({
pin1: "GND_IO",
pin2: "VDD_3V3",
pin3: "AUDIO_PWM",
pin4: "AUDIO_ENABLE",
})) assertPortNet("J_AUDIO_MODULE", pin, net)
for (const [testPoint, net] of [
["TP_GND", "GND_MCU"],
["TP_3V3", "VDD_3V3"],
["TP_RESET", "MCU_RESET_N"],
]) assertPortNet(testPoint, "pin1", net)
assertPortsConnected("TP_SWDIO", "pin1", "U_MCU", "PA19")
assertPortsConnected("TP_SWCLK", "pin1", "U_MCU", "PA20")
const expectedPowerParts = {
J_BATTERY: "C160353",
U_BATTERY_CHARGER: "C15464",
SW_POWER: "C431540",
U_3V3_BUCK_BOOST: "C15516",
L_3V3_BUCK_BOOST: "C167745",
U_BACKLIGHT_BOOST: "C59567",
L_BACKLIGHT_BOOST: "C167745",
}
for (const [name, lcsc] of Object.entries(expectedPowerParts)) {
assert(componentByName.get(name)?.supplier_part_numbers?.jlcpcb?.[0] === lcsc, `${name} is not ${lcsc}`)
}
assertPortNet("J_BATTERY", "pin1", "BATTERY_POSITIVE")
assertPortNet("J_BATTERY", "pin2", "BATTERY_NTC")
assertPortNet("J_BATTERY", "pin3", "GND_POWER")
for (const pin of ["BAT1", "BAT2"]) assertPortNet("U_BATTERY_CHARGER", pin, "BATTERY_POSITIVE")
assertPortNet("U_BATTERY_CHARGER", "IN", "VBUS_5V")
assertPortNet("U_BATTERY_CHARGER", "TS", "BATTERY_NTC")
for (const pin of ["OUT1", "OUT2", "EN1"]) assertPortNet("U_BATTERY_CHARGER", pin, "VSYS_BAT")
for (const pin of ["EN2", "N_CE", "SYSOFF", "VSS", "EP"]) assertPortNet("U_BATTERY_CHARGER", pin, "GND_POWER")
assert(componentByName.get("R_CHARGE_CURRENT")?.resistance === 3300, "Charge-current resistor is not 3.3 kΩ")
assert(componentByName.get("R_INPUT_CURRENT_LIMIT")?.resistance === 3300, "Input-current-limit resistor is not 3.3 kΩ")
assert(componentByName.get("R_CHARGE_TIMER")?.resistance === 47000, "Charge-timer resistor is not 47 kΩ")
assertPortNet("SW_POWER", "pin1", "GND_POWER")
assertPortNet("SW_POWER", "pin2", "POWER_ENABLE")
assertPortNet("SW_POWER", "pin3", "VSYS_BAT")
for (const pin of ["VIN", "VINA"]) assertPortNet("U_3V3_BUCK_BOOST", pin, "VSYS_BAT")
for (const pin of ["VOUT", "FB"]) assertPortNet("U_3V3_BUCK_BOOST", pin, "VDD_3V3")
for (const pin of ["pin7", "PGND", "GND", "EP"]) assertPortNet("U_3V3_BUCK_BOOST", pin, "GND_POWER")
assertPortNet("U_BACKLIGHT_BOOST", "VIN", "VSYS_BAT")
assertPortNet("U_BACKLIGHT_BOOST", "EN", "POWER_ENABLE")
for (const pin of ["VOUT", "FB"]) assertPortNet("U_BACKLIGHT_BOOST", pin, "DISPLAY_5V")
for (const pin of ["GND", "PP"]) assertPortNet("U_BACKLIGHT_BOOST", pin, "GND_POWER")
for (const [name, domain] of Object.entries({
R_GND_USB_LINK: "GND_USB",
R_GND_POWER_LINK: "GND_POWER",
R_GND_MCU_LINK: "GND_MCU",
R_GND_CONTROLS_LINK: "GND_CONTROLS",
R_GND_DISPLAY_LINK: "GND_DISPLAY",
R_GND_IO_LINK: "GND_IO",
})) {
assertPortNet(name, "pin1", domain)
assertPortNet(name, "pin2", "GND")
assert(componentByName.get(name)?.resistance === 0, `${name} is not a zero-ohm link`)
}
const usbTraceNames = [
"USB_DP_FLIP_A",
"USB_DP_FLIP_B",
"USB_DM_FLIP_A",
"USB_DM_FLIP_B",
"USB_DM_TO_ESD",
"USB_DP_PROTECTED",
"USB_DM_PROTECTED",
]
const usbTraceStats = Object.fromEntries(usbTraceNames.map((name) => [name, traceStats(name)]))
for (const [name, stats] of Object.entries(usbTraceStats)) {
assert(stats.vias <= 2, `${name} contains ${stats.vias} vias (limit: 2)`)
}
const vcore = traceStats("VCORE_DECOUPLE")
assert(vcore.vias === 0, `VCORE_DECOUPLE contains ${vcore.vias} vias`)
assert(vcore.length <= 4, `VCORE_DECOUPLE is ${vcore.length.toFixed(3)} mm (limit: 4 mm)`)
for (const name of ["REGULATOR_3V3_SWITCH_NODE_A", "REGULATOR_3V3_SWITCH_NODE_B", "BACKLIGHT_5V_SWITCH_NODE"]) {
const stats = traceStats(name)
assert(stats.vias === 0, `${name} contains ${stats.vias} vias`)
}
const mountingHoles = elements("pcb_hole").filter(
(hole) => hole.pcb_component_id === null && approximately(hole.hole_diameter, 2.7),
)
const fiducials = elements("pcb_smtpad").filter(
(pad) => pad.pcb_component_id === null && pad.shape === "circle" && approximately(pad.radius, 0.5),
)
assert(mountingHoles.length === 4, `Expected four mounting holes, found ${mountingHoles.length}`)
assert(fiducials.length === 3, `Expected three fiducials, found ${fiducials.length}`)
const silk = new Set(elements("pcb_silkscreen_text").map((text) => text.text))
for (const label of ["UP", "DOWN", "LEFT", "RIGHT", "A", "B", "START", "SELECT", "MSPM0 POCKET"]) {
assert(silk.has(label), `Missing control silkscreen label: ${label}`)
}
const pasteLayers = [...new Set(elements("pcb_solder_paste").map((paste) => paste.layer))]
assert(pasteLayers.length === 1 && pasteLayers[0] === "top", `Unexpected paste layers: ${pasteLayers.join(", ")}`)
assert(elements("schematic_sheet").length === 8, "Expected eight schematic sheets")
assert(
new Set(elements("schematic_sheet").map((sheet) => sheet.name)).size === 8,
"Schematic sheets do not have eight unique names",
)
assert(elements("schematic_element_outside_sheet_warning").length === 0, "Schematic element outside sheet")
assert(circuit.filter((element) => element.type.endsWith("_error")).length === 0, "Circuit JSON contains embedded errors")
assert(!/<pcbtrace\b/i.test(source), "Manual PCB trace found in source")
assert(!/<via\b/i.test(source), "Manual via found in source")
console.log(JSON.stringify({
checks: Object.fromEntries(Object.entries(checkResults).map(([name, results]) => [name, results.length])),
boardMm: [board?.width, board?.height],
layers: board?.num_layers,
sourceComponents: sourceComponents.length,
assemblyComponents: assemblyComponents.length,
controls: controls.length,
testPoints: testPoints.length,
pcbTraces: pcbTraces.length,
throughVias: vias.length,
usb: Object.fromEntries(Object.entries(usbTraceStats).map(([name, stats]) => [name, { vias: stats.vias, lengthMm: Number(stats.length.toFixed(3)) }])),
vcore: { vias: vcore.vias, lengthMm: Number(vcore.length.toFixed(3)) },
mountingHoles: mountingHoles.length,
fiducials: fiducials.length,
pasteLayers,
failures,
}, null, 2))
if (failures.length > 0) process.exitCode = 1