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