ShiboSoftwareDev/mspm0g3507-usb-c-dev-board

The circuit assembles a USB-C port with integrated ESD protection, power regulation, UART interface, and microcontroller (MSPM0G3507) with supporting components for USB data lines, CC termination, I2C, SPI, reset circuitry, and indicator LEDs.

Version
1.4.1
License
unset
Stars
0

scripts/audit-gerbers.mjs

import { readFileSync } from "node:fs"
import { execFileSync } from "node:child_process"

const zipPath = process.argv[2] ?? "outputs/MSPM0G3507_USB_C_JLCPCB_Gerbers.zip"
const circuitPath = process.argv[3] ?? "dist/index/circuit.json"
const circuit = JSON.parse(readFileSync(circuitPath, "utf8"))
const failures = []
const assert = (condition, message) => {
  if (!condition) failures.push(message)
}
const zipText = (name) => execFileSync("unzip", ["-p", zipPath, name], { encoding: "utf8" })
const fileNames = execFileSync("unzip", ["-Z1", zipPath], { encoding: "utf8" }).trim().split("\n")

const expectedFiles = [
  "F_Cu.gbr",
  "B_Cu.gbr",
  "F_Mask.gbr",
  "B_Mask.gbr",
  "F_Paste.gbr",
  "B_Paste.gbr",
  "F_SilkScreen.gbr",
  "B_SilkScreen.gbr",
  "Edge_Cuts.gbr",
  "F_Fab.gbr",
  "drill-L1-L2.drl",
  "drill_npth.drl",
  "bom.csv",
  "pick_and_place.csv",
]
for (const fileName of expectedFiles) assert(fileNames.includes(fileName), `ZIP is missing ${fileName}`)
assert(
  !fileNames.some((name) => /inner|In\d_Cu|drill-L(?!1-L2)/i.test(name)),
  "ZIP contains an inner-layer or non-through drill file",
)

const countFlashes = (gerber) => (gerber.match(/D03\*/g) ?? []).length
const edgeCuts = zipText("Edge_Cuts.gbr")
const outlineCoordinates = [...edgeCuts.matchAll(/X(-?\d+)Y(-?\d+)/g)].map((match) => ({
  x: Number(match[1]) / 1_000_000,
  y: Number(match[2]) / 1_000_000,
}))
const outlineBounds = {
  minX: Math.min(...outlineCoordinates.map(({ x }) => x)),
  maxX: Math.max(...outlineCoordinates.map(({ x }) => x)),
  minY: Math.min(...outlineCoordinates.map(({ y }) => y)),
  maxY: Math.max(...outlineCoordinates.map(({ y }) => y)),
}
const gerberBoardSize = {
  width: outlineBounds.maxX - outlineBounds.minX,
  height: outlineBounds.maxY - outlineBounds.minY,
}
assert(
  Math.abs(gerberBoardSize.width - 90) < 0.001 && Math.abs(gerberBoardSize.height - 21) < 0.001,
  `Gerber outline bounds are ${gerberBoardSize.width} mm by ${gerberBoardSize.height} mm instead of 90 mm by 21 mm`,
)
const hasOutlinePoint = (x, y) => outlineCoordinates.some(
  (point) => Math.abs(point.x - x) < 0.001 && Math.abs(point.y - y) < 0.001,
)
for (const [x, y] of [
  [-45, 10.5], [45, 10.5], [45, -10.5], [-45, -10.5],
]) {
  assert(hasOutlinePoint(x, y), `Gerber outline is missing rectangular vertex (${x}, ${y}) mm`)
}
const topPasteFlashes = countFlashes(zipText("F_Paste.gbr"))
const bottomPasteFlashes = countFlashes(zipText("B_Paste.gbr"))
const circuitPasteCount = circuit.filter(
  (element) => element.type === "pcb_solder_paste" && element.layer === "top",
).length
assert(
  topPasteFlashes === circuitPasteCount,
  `Top paste has ${topPasteFlashes} flashes; Circuit JSON has ${circuitPasteCount}`,
)
assert(bottomPasteFlashes === 0, `Bottom paste contains ${bottomPasteFlashes} flashes`)

const parseExcellon = (text) => {
  const tools = new Map()
  let activeTool = null
  let pendingSlotStart = null
  for (const line of text.split(/\r?\n/)) {
    const definition = line.match(/^T(\d+)C([\d.]+)/)
    if (definition) {
      tools.set(definition[1], { diameter: Number(definition[2]), hits: 0, slots: [] })
      continue
    }
    const selection = line.match(/^T(\d+)$/)
    if (selection) {
      activeTool = tools.get(selection[1]) ?? null
      pendingSlotStart = null
      continue
    }
    if (!activeTool) continue
    const point = line.match(/^X(-?[\d.]+)Y(-?[\d.]+)$/)
    if (point) {
      pendingSlotStart = { x: Number(point[1]), y: Number(point[2]) }
      activeTool.hits += 1
      continue
    }
    const slotEnd = line.match(/^G85X(-?[\d.]+)Y(-?[\d.]+)$/)
    if (slotEnd && pendingSlotStart) {
      activeTool.slots.push({
        start: pendingSlotStart,
        end: { x: Number(slotEnd[1]), y: Number(slotEnd[2]) },
      })
      pendingSlotStart = null
    }
  }
  return [...tools.values()]
}

const platedTools = parseExcellon(zipText("drill-L1-L2.drl"))
const nonPlatedTools = parseExcellon(zipText("drill_npth.drl"))
const topCopper = zipText("F_Cu.gbr")
const bottomCopper = zipText("B_Cu.gbr")
const findTool = (tools, diameter) =>
  tools.find((tool) => Math.abs(tool.diameter - diameter) < 0.00001)
const shieldTool = findTool(platedTools, 0.799998)
const groveTool = findTool(platedTools, 0.849986)
const headerTool = findTool(platedTools, 1.05)
const viaTool = findTool(platedTools, 0.3)
const usbLocatingTool = findTool(nonPlatedTools, 0.599999)
const mountingTool = findTool(nonPlatedTools, 2.7)
const circuitViaCount = circuit.filter((element) => element.type === "pcb_via").length

assert(viaTool?.hits === circuitViaCount, `Through-drill file has ${viaTool?.hits ?? 0} via hits; Circuit JSON has ${circuitViaCount}`)
assert(/^%ADD\d+C,0\.450000\*%$/m.test(topCopper), "Top copper lacks the 0.45 mm via-pad aperture")
assert(/^%ADD\d+C,0\.450000\*%$/m.test(bottomCopper), "Bottom copper lacks the 0.45 mm via-pad aperture")
assert(headerTool?.hits === 40, `Expected 40 header drills, found ${headerTool?.hits ?? 0}`)
assert(groveTool?.hits === 4, `Expected four Grove drills, found ${groveTool?.hits ?? 0}`)
assert(shieldTool?.slots.length === 4, `Expected four USB-C shield slots, found ${shieldTool?.slots.length ?? 0}`)
assert(usbLocatingTool?.hits === 2, `Expected two USB-C locating holes, found ${usbLocatingTool?.hits ?? 0}`)
assert(mountingTool?.hits === 4, `Expected four M2.5 mounting holes, found ${mountingTool?.hits ?? 0}`)
for (const [index, slot] of (shieldTool?.slots ?? []).entries()) {
  const dx = Math.abs(slot.end.x - slot.start.x)
  const dy = Math.abs(slot.end.y - slot.start.y)
  assert(dx >= 0.599 && dy < 0.001, `USB-C shield slot ${index + 1} is not horizontally oriented after board rotation`)
}

const bom = zipText("bom.csv").trim().split(/\r?\n/)
const cpl = zipText("pick_and_place.csv").trim().split(/\r?\n/)
assert(bom[0]?.includes("Manufacturer Part Number"), "Embedded BOM lacks the manufacturer-part-number column")
assert(bom[0]?.includes("JLCPCB Part #"), "Embedded BOM lacks the JLCPCB part-number column")
assert(bom.length === 39, `Embedded BOM has ${bom.length - 1} placements instead of 38`)
assert(cpl.length === 39, `Embedded CPL has ${cpl.length - 1} placements instead of 38`)
for (const [designator, jlc, mpn] of [
  ["U_MCU", "C22362630", "MSPM0G3507SPTR"],
  ["U_USB_UART", "C84681", "CH340C"],
  ["D_USER_RGB", "C3029072", "SZYY1615RGB-A 4pin"],
  ["R_RGB_RED", "C25104", "0402WGF3300TCE"],
  ["J_GROVE_I2C", "C7429580", "ZX-HY2.0-4PZZ"],
  ["J_I2C_JST_SH", "C160404", "SM04B-SRSS-TB(LF)(SN)"],
  ["J_SPI_DISPLAY", "C160407", "SM08B-SRSS-TB(LF)(SN)"],
  ["J_TOP", "C42431804", "PH2.54-1X20P-H25"],
  ["J_BOTTOM", "C42431804", "PH2.54-1X20P-H25"],
]) {
  assert(
    bom.some((line) => line.startsWith(`${designator},`) && line.includes(`,${jlc},${mpn}`)),
    `Embedded BOM is missing ${designator} / ${jlc} / ${mpn}`,
  )
}
assert(!bom.some((line) => line.includes("MSP430F5503")), "Embedded BOM contains stale MSP430 data")
assert(cpl.some((line) => /^U_MCU,-3\.500,0\.000,top,0$/.test(line)), "Embedded CPL has the wrong MCU placement")
assert(cpl.some((line) => /^U_USB_UART,-25\.900,0\.000,top,270$/.test(line)), "Embedded CPL has the wrong CH340C placement")
assert(cpl.some((line) => /^J_TOP,0\.000,8\.890,top,0$/.test(line)), "Embedded CPL has the wrong top-header placement")
assert(cpl.some((line) => /^J_BOTTOM,0\.000,-8\.890,top,0$/.test(line)), "Embedded CPL has the wrong bottom-header placement")
assert(cpl.some((line) => /^J_SPI_DISPLAY,41\.850,4\.500,top,90$/.test(line)), "Embedded CPL has the wrong SPI connector placement")
assert(cpl.some((line) => /^J_I2C_JST_SH,42\.000,-5\.100,top,90$/.test(line)), "Embedded CPL has the wrong JST-SH connector placement")
assert(cpl.some((line) => /^J_GROVE_I2C,33\.500,-7\.200,top,0$/.test(line)), "Embedded CPL has the wrong Grove connector placement")

console.log(JSON.stringify({
  files: fileNames.length,
  boardMm: [gerberBoardSize.width, gerberBoardSize.height],
  copperLayers: ["top", "bottom"],
  drill: {
    throughVias: viaTool?.hits,
    viaHoleDiameterMm: 0.3,
    viaPadDiameterMm: 0.45,
    headerHoles: headerTool?.hits,
    groveHoles: groveTool?.hits,
    usbShieldSlots: shieldTool?.slots.length,
    usbShieldSlotsOrientation: "horizontal, matching rotated PCB footprint",
    usbLocatingHoles: usbLocatingTool?.hits,
    mountingHoles: mountingTool?.hits,
  },
  pasteFlashes: { top: topPasteFlashes, bottom: bottomPasteFlashes },
  assemblyPlacements: { bom: bom.length - 1, cpl: cpl.length - 1 },
  failures,
}, null, 2))

if (failures.length > 0) process.exitCode = 1