ShiboSoftwareDev/muse-m5stack-sticks3-replacement

A 4-layer ESP32-S3 carrier board with USB-C 5V input, AMS1117 3.3V regulation, FFC interface, reset/boot/user buttons, status LED, and passives for compact display/control applications.

Version
0.0.1
License
unset
Stars
0

src/MuseGadgetBoard.tsx

import { Fragment } from "react"
import { AFC01_S24FCA_00 } from "../imports/AFC01_S24FCA_00"
import { AMS1117_3_3 } from "../imports/AMS1117_3_3"
import { ESP32_S3_WROOM_1_N16R8 } from "../imports/ESP32_S3_WROOM_1_N16R8"
import { TS_1187A_B_A_B } from "../imports/TS_1187A_B_A_B"
import { TYPE_C_6P } from "../imports/TYPE_C_6P"
import { createJlcFourLayerAutorouter } from "./jlc-autorouter"
import type { GadgetVariant } from "./variants"

const resistorParts = {
  r1k: {
    value: "1k",
    lcsc: "C21190",
    mpn: "0603WAF1001T5E",
  },
  r5k1: {
    value: "5.1k",
    lcsc: "C23186",
    mpn: "0603WAF5101T5E",
  },
  r10k: {
    value: "10k",
    lcsc: "C2906982",
    mpn: "FRC0603F1002TS",
  },
} as const

const capacitorParts = {
  c100n: {
    value: "100nF",
    lcsc: "C14663",
    mpn: "CC0603KRX7R9BB104",
    footprint: "0603",
  },
  c1u: {
    value: "1uF",
    lcsc: "C15849",
    mpn: "CL10A105KB8NNNC",
    footprint: "0603",
  },
  c10u: {
    value: "10uF",
    lcsc: "C15850",
    mpn: "CL21A106KAYNNNE",
    footprint: "0805",
  },
} as const

const interfaceNames: Record<GadgetVariant["interfaceKind"], string[]> = {
  status: [],
  "spi-display": [
    "SPI_SCLK",
    "SPI_MOSI",
    "DISPLAY_CS",
    "DISPLAY_DC",
    "DISPLAY_RST",
    "BACKLIGHT_PWM",
    "TOUCH_SDA",
    "TOUCH_SCL",
  ],
  epaper: [
    "EPD_SCLK",
    "EPD_MOSI",
    "EPD_CS",
    "EPD_DC",
    "EPD_RST",
    "EPD_BUSY",
    "FRONTLIGHT_PWM",
    "I2C_SDA",
  ],
  voice: [
    "I2S_BCLK",
    "I2S_LRCLK",
    "I2S_DOUT",
    "I2S_DIN",
    "AUDIO_SDA",
    "AUDIO_SCL",
    "AMP_ENABLE_PTT",
    "ROTARY_AB",
  ],
  watch: [
    "SPI_SCLK",
    "SPI_MOSI",
    "DISPLAY_CS",
    "DISPLAY_DC",
    "DISPLAY_RST",
    "BACKLIGHT_PWM",
    "TOUCH_SDA",
    "TOUCH_SCL",
  ],
}

const gpioConnections = {
  IO4: "net.IFACE_1",
  IO5: "net.IFACE_2",
  IO6: "net.IFACE_3",
  IO7: "net.IFACE_4",
  IO15: "net.IFACE_5",
  IO16: "net.IFACE_6",
  IO17: "net.IFACE_7",
  IO18: "net.IFACE_8",
} as const

const interfaceFanoutPinLabels = {
  pin1: ["pin1"],
  pin2: ["pin2"],
  pin3: ["pin3"],
  pin4: ["pin4"],
  pin5: ["pin5"],
  pin6: ["pin6"],
  pin7: ["pin7"],
  pin8: ["pin8"],
  pin9: ["pin9"],
  pin10: ["pin10"],
} as const

const interfaceFanoutRoutes = [
  { pin: "pin1", y: 4.75, destination: ".U1 > .3V3" },
  { pin: "pin2", y: 4.25, destination: ".U1 > .GND1" },
  { pin: "pin3", y: 3.75, destination: ".U1 > .IO4" },
  { pin: "pin4", y: 3.25, destination: ".U1 > .IO5" },
  { pin: "pin5", y: 2.75, destination: ".U1 > .IO6" },
  { pin: "pin6", y: 2.25, destination: ".U1 > .IO7" },
  { pin: "pin7", y: 1.75, destination: ".U1 > .IO15" },
  { pin: "pin8", y: 1.25, destination: ".U1 > .IO16" },
  { pin: "pin9", y: 0.75, destination: ".U1 > .IO17" },
  { pin: "pin10", y: 0.25, destination: ".U1 > .IO18" },
] as const

type Passive = {
  name: string
  part: keyof typeof resistorParts | keyof typeof capacitorParts
  a: string
  b: string
  pcbX: number
  pcbY: number
  rotation?: number
  schX: number
  schY: number
}

export function MuseGadgetBoard({ variant }: { variant: GadgetVariant }) {
  const { width, height } = variant
  const left = -width / 2
  const right = width / 2
  const top = height / 2
  const bottom = -height / 2
  const espX = 4
  const espY = top - 16.85
  const usbX = left + 4.8
  const ffcX = -15
  const hasInterface = variant.interfaceKind !== "status"
  const statusLedX = hasInterface ? -5 : left + 15
  const statusResistorX = hasInterface ? -8 : left + 10.5
  const statusY = hasInterface ? bottom + 8 : 1
  const ifaceNames = interfaceNames[variant.interfaceKind]

  const resistors: Passive[] = [
    { name: "R_CC1", part: "r5k1", a: "USB_CC1", b: "GND", pcbX: left + 10.5, pcbY: -4.0, rotation: 90, schX: -11, schY: 7.5 },
    { name: "R_CC2", part: "r5k1", a: "USB_CC2", b: "GND", pcbX: left + 12.2, pcbY: -4.0, rotation: 90, schX: -8, schY: 7.5 },
    { name: "R_EN", part: "r10k", a: "VCC3V3", b: "ESP_EN", pcbX: hasInterface ? -7.5 : -8.5, pcbY: hasInterface ? espY + 4.9 : 4.5, rotation: hasInterface ? 90 : 0, schX: 5, schY: 8 },
    { name: "R_BOOT", part: "r10k", a: "VCC3V3", b: "ESP_GPIO0", pcbX: right - 12.5, pcbY: -10.5, rotation: 90, schX: 8, schY: 5 },
    { name: "R_LED", part: "r1k", a: "VCC3V3", b: "STATUS_LED_A", pcbX: statusResistorX, pcbY: statusY, rotation: 0, schX: -3, schY: -6 },
  ]
  if (variant.userButtons >= 1) {
    resistors.push({ name: "R_USER1", part: "r10k", a: "VCC3V3", b: "USER1", pcbX: right - 8.5, pcbY: 3.0, rotation: 90, schX: 8, schY: 1 })
  }
  if (variant.userButtons >= 2) {
    resistors.push({ name: "R_USER2", part: "r10k", a: "VCC3V3", b: "USER2", pcbX: right - 13, pcbY: 15.0, rotation: 90, schX: 8, schY: -2 })
  }

  const capacitors: Passive[] = [
    { name: "C_LDO_IN", part: "c10u", a: "VBUS5V", b: "GND", pcbX: left + 5.5, pcbY: top - 14.0, rotation: 90, schX: -8, schY: 3 },
    { name: "C_LDO_OUT", part: "c10u", a: "VCC3V3", b: "GND", pcbX: left + 15, pcbY: top - 6.0, rotation: 90, schX: -4, schY: 3 },
    { name: "C_ESP_BULK", part: "c10u", a: "VCC3V3", b: "GND", pcbX: hasInterface ? -7.5 : -8.5, pcbY: hasInterface ? espY + 8.3 : 8.0, rotation: hasInterface ? 90 : 0, schX: 1, schY: 3 },
    { name: "C_ESP_HF", part: "c100n", a: "VCC3V3", b: "GND", pcbX: hasInterface ? -10.7 : -8.5, pcbY: hasInterface ? espY + 8.3 : 6.3, rotation: hasInterface ? 90 : 0, schX: 4, schY: 3 },
    { name: "C_EN", part: "c1u", a: "ESP_EN", b: "GND", pcbX: hasInterface ? -10.7 : -8.5, pcbY: hasInterface ? espY + 4.9 : 2.8, rotation: hasInterface ? 90 : 0, schX: 7, schY: 8 },
  ]

  return (
    <board
      name={variant.boardName}
      title={variant.title}
      width={`${width}mm`}
      height={`${height}mm`}
      layers={4}
      autorouter={{
        local: true,
        groupMode: "subcircuit",
        algorithmFn: createJlcFourLayerAutorouter,
      }}
      autorouterEffortLevel="2x"
      schTraceAutoLabelEnabled
      minTraceWidth="0.15mm"
      minViaHoleDiameter="0.3mm"
      minViaPadDiameter="0.45mm"
      minBoardEdgeClearance="0.25mm"
    >
      <schematicsheet name="SYSTEM" displayName={`${variant.title} — system`} sheetIndex={0}>
        <schematicsection name="POWER" displayName="USB-C POWER + 3.3 V REGULATOR" />
        <schematicsection name="MCU" displayName="ESP32-S3 + CONTROLS" />
        {hasInterface && <schematicsection name="INTERFACE" displayName={`${variant.role.toUpperCase()} — 24-PIN INTERFACE`} />}
      </schematicsheet>

      <copperpour
        name="GND_PLANE"
        layer="inner1"
        connectsTo="net.GND"
        unbroken
        clearance="0.2mm"
        outline={[
          { x: left + 0.3, y: bottom + 0.3 },
          { x: right - 0.3, y: bottom + 0.3 },
          { x: right - 0.3, y: top - 8 },
          { x: left + 0.3, y: top - 8 },
        ]}
      />
      <copperpour
        name="VCC3V3_PLANE"
        layer="inner2"
        connectsTo="net.VCC3V3"
        unbroken
        clearance="0.2mm"
        outline={[
          { x: left + 0.3, y: bottom + 0.3 },
          { x: right - 0.3, y: bottom + 0.3 },
          { x: right - 0.3, y: top - 8 },
          { x: left + 0.3, y: top - 8 },
        ]}
      />

      {[
        [left + 2.8, top - 2.8],
        [right - 2.8, top - 2.8],
        [left + 2.8, bottom + 2.8],
        [right - 2.8, bottom + 2.8],
      ].map(([x, y], index) => (
        <Fragment key={`mount-${index + 1}`}>
          <hole name={`H${index + 1}`} pcbX={x} pcbY={y} diameter="2.2mm" />
          <keepout shape="circle" layers={["top", "inner1", "inner2", "bottom"]} radius="2mm" pcbX={x} pcbY={y} />
        </Fragment>
      ))}

      <TYPE_C_6P
        name="U_USB"
        displayName="USB-C 5 V POWER"
        layer="top"
        pcbX={usbX}
        pcbY={-1}
        pcbRotation={-90}
        schX={-12}
        schY={6}
        schSheetName="SYSTEM"
        schSectionName="POWER"
        connections={{
          EH: "net.GND",
          GND1: "net.GND",
          GND2: "net.GND",
          VBUS1: "net.VBUS5V",
          VBUS2: "net.VBUS5V",
          CC1: "net.USB_CC1",
          CC2: "net.USB_CC2",
        }}
      />
      <AMS1117_3_3
        name="U_3V3"
        displayName="AMS1117-3.3"
        layer="top"
        pcbX={left + 10}
        pcbY={top - 6}
        pcbRotation={90}
        schX={-6}
        schY={6}
        schSheetName="SYSTEM"
        schSectionName="POWER"
        connections={{ GND: "net.GND", VIN: "net.VBUS5V", VOUT1: "net.VCC3V3", VOUT2: "net.VCC3V3" }}
      />

      <ESP32_S3_WROOM_1_N16R8
        name="U1"
        displayName="ESP32-S3-WROOM-1-N16R8"
        layer="top"
        pcbX={espX}
        pcbY={espY}
        pcbRotation={0}
        schX={3}
        schY={6}
        schHeight={4.2}
        schSheetName="SYSTEM"
        schSectionName="MCU"
        connections={{
          GND1: "net.GND",
          GND2: "net.GND",
          GND3: "net.GND",
          "3V3": "net.VCC3V3",
          EN: "net.ESP_EN",
          IO0: "net.ESP_GPIO0",
          IO21: "net.STATUS_LED_K",
          ...(variant.userButtons >= 1 ? { IO47: "net.USER1" } : {}),
          ...(variant.userButtons >= 2 ? { IO48: "net.USER2" } : {}),
          RXD0: "net.UART_RX",
          TXD0: "net.UART_TX",
          ...(hasInterface ? gpioConnections : {}),
        }}
      />

      <TS_1187A_B_A_B
        name="SW_RESET"
        displayName="RESET"
        layer="top"
        pcbX={right - 5.2}
        pcbY={-5.5}
        schX={8}
        schY={8}
        schSheetName="SYSTEM"
        schSectionName="MCU"
        connections={{ A: "net.ESP_EN", C: "net.ESP_EN", B: "net.GND", D: "net.GND" }}
      />
      <TS_1187A_B_A_B
        name="SW_BOOT"
        displayName="BOOT"
        layer="top"
        pcbX={right - 5.2}
        pcbY={-12.5}
        schX={8}
        schY={5}
        schSheetName="SYSTEM"
        schSectionName="MCU"
        connections={{ A: "net.ESP_GPIO0", C: "net.ESP_GPIO0", B: "net.GND", D: "net.GND" }}
      />
      {variant.userButtons >= 1 && (
        <TS_1187A_B_A_B
          name="SW_USER1"
          displayName={variant.interfaceKind === "voice" || variant.interfaceKind === "watch" ? "PUSH TO TALK" : "USER 1"}
          layer="top"
          pcbX={right - 5.2}
          pcbY={8.5}
          schX={8}
          schY={1}
          schSheetName="SYSTEM"
          schSectionName="MCU"
          connections={{ A: "net.USER1", C: "net.USER1", B: "net.GND", D: "net.GND" }}
        />
      )}
      {variant.userButtons >= 2 && (
        <TS_1187A_B_A_B
          name="SW_USER2"
          displayName="USER 2"
          layer="bottom"
          pcbX={right - 5.2}
          pcbY={8.5}
          schX={8}
          schY={-2}
          schSheetName="SYSTEM"
          schSectionName="MCU"
          connections={{ A: "net.USER2", C: "net.USER2", B: "net.GND", D: "net.GND" }}
        />
      )}

      <led
        name="D_STATUS"
        displayName="STATUS RED"
        color="red"
        footprint="0603"
        supplierPartNumbers={{ jlcpcb: ["C2286"] }}
        manufacturerPartNumber="KT-0603R"
        layer="top"
        pcbX={statusLedX}
        pcbY={statusY}
        schX={0}
        schY={-6}
        schSheetName="SYSTEM"
        schSectionName="MCU"
        connections={{ anode: "net.STATUS_LED_A", cathode: "net.STATUS_LED_K" }}
      />

      {resistors.map(({ part, a, b, rotation = 0, ...props }) => {
        const p = resistorParts[part as keyof typeof resistorParts]
        return (
          <resistor
            key={props.name}
            resistance={p.value}
            footprint="0603"
            supplierPartNumbers={{ jlcpcb: [p.lcsc] }}
            manufacturerPartNumber={p.mpn}
            layer="top"
            pcbRotation={rotation}
            schSheetName="SYSTEM"
            schSectionName={props.name.startsWith("R_CC") ? "POWER" : "MCU"}
            connections={{ pin1: `net.${a}`, pin2: `net.${b}` }}
            {...props}
          />
        )
      })}
      {capacitors.map(({ part, a, b, rotation = 0, ...props }) => {
        const p = capacitorParts[part as keyof typeof capacitorParts]
        return (
          <capacitor
            key={props.name}
            capacitance={p.value}
            footprint={p.footprint}
            supplierPartNumbers={{ jlcpcb: [p.lcsc] }}
            manufacturerPartNumber={p.mpn}
            layer="top"
            pcbRotation={rotation}
            schSheetName="SYSTEM"
            schSectionName={props.name.startsWith("C_LDO") ? "POWER" : "MCU"}
            connections={{ pin1: `net.${a}`, pin2: `net.${b}` }}
            {...props}
          />
        )
      })}

      {hasInterface && (
        <Fragment>
          <AFC01_S24FCA_00
            name="U_INTERFACE"
            displayName={`${variant.role} FFC`}
            layer="top"
            pcbX={ffcX}
            pcbY={-1}
            pcbRotation={-90}
            schX={-1}
            schY={-5}
            schHeight={3.2}
            schSheetName="SYSTEM"
            schSectionName="INTERFACE"
          />
          <chip
            name="INTERFACE_FANOUT"
            displayName="FFC fanout vias (DNP)"
            doNotPlace
            pcbX={0}
            pcbY={0}
            schX={3}
            schY={-5}
            schSheetName="SYSTEM"
            schSectionName="INTERFACE"
            pinLabels={interfaceFanoutPinLabels}
            pinAttributes={{
              pin1: { requiresPower: true },
              pin2: { requiresGround: true },
            }}
            footprint={
              <footprint>
                {interfaceFanoutRoutes.map(({ pin, y }, index) => (
                  <Fragment key={`interface-fanout-via-${pin}`}>
                    <platedhole
                      portHints={[pin]}
                      shape="circle"
                      holeDiameter="0.3mm"
                      outerDiameter="0.45mm"
                      pcbX={index % 2 === 0 ? -12.2 : -11.0}
                      pcbY={y}
                    />
                  </Fragment>
                ))}
              </footprint>
            }
          />
          {interfaceFanoutRoutes.map(({ pin, destination }) => (
            <Fragment key={`interface-fanout-${pin}`}>
              <trace
                name={`TRACE_INTERFACE_${pin}_ESCAPE`}
                from={`.U_INTERFACE > .${pin}`}
                to={`.INTERFACE_FANOUT > .${pin}`}
                thickness="0.15mm"
                pcbPath={[`.INTERFACE_FANOUT > .${pin}`]}
              />
              <trace
                name={`TRACE_INTERFACE_${pin}_RUN`}
                from={`.INTERFACE_FANOUT > .${pin}`}
                to={destination}
                thickness="0.15mm"
              />
            </Fragment>
          ))}
        </Fragment>
      )}

      {[
        ["TP_TX", "UART_TX", hasInterface ? right - 20 : left + 7, bottom + 7],
        ["TP_RX", "UART_RX", hasInterface ? right - 16 : left + 11, bottom + 7],
        ["TP_3V3", "VCC3V3", hasInterface ? right - 12 : left + 15, bottom + 7],
        ["TP_GND", "GND", hasInterface ? right - 8 : left + 19, bottom + 7],
      ].map(([name, net, x, y]) => (
        <testpoint
          key={String(name)}
          name={String(name)}
          footprintVariant="pad"
          padShape="circle"
          padDiameter="1.3mm"
          layer="bottom"
          pcbX={Number(x)}
          pcbY={Number(y)}
          schX={-9 + Number(x) * 0.01}
          schY={-7}
          schSheetName="SYSTEM"
          schSectionName="MCU"
          connections={{ pin1: `net.${net}` }}
        />
      ))}

      {/* ESP exposed-pad thermal/stitch vias; all vias on every board use the
          specified 0.30 mm drill and 0.45 mm land. */}
      {[
        [espX - 2.20015, espY + 0.62485],
        [espX - 0.8001, espY + 0.62485],
        [espX - 2.20015, espY + 2.0249],
        [espX - 0.8001, espY + 2.0249],
      ].map(([x, y], index) => (
        <Fragment key={`epad-via-${index}`}>
          <via name={`VIA_ESP_GND_${index + 1}`} connectsTo="net.GND" fromLayer="top" toLayer="bottom" pcbX={x} pcbY={y} outerDiameter="0.45mm" holeDiameter="0.3mm" />
        </Fragment>
      ))}
      <via name="VIA_GND_USB" connectsTo="net.GND" fromLayer="top" toLayer="bottom" pcbX={left + 9} pcbY={-5} outerDiameter="0.45mm" holeDiameter="0.3mm" />
      <via name="VIA_GND_LDO" connectsTo="net.GND" fromLayer="top" toLayer="bottom" pcbX={left + 10} pcbY={top - 12} outerDiameter="0.45mm" holeDiameter="0.3mm" />

      {ifaceNames.map((label, index) => (
        <Fragment key={label}>
          <silkscreentext
            text={`${index + 3}:${label}`}
            layer="bottom"
            pcbX={left + 7 + (index % 2) * 13}
            pcbY={top - 4 - Math.floor(index / 2) * 2}
            fontSize="0.65mm"
            anchorAlignment="center_left"
          />
        </Fragment>
      ))}
      <silkscreentext text={variant.boardName} layer="bottom" pcbX={0} pcbY={bottom + 2.2} fontSize="0.8mm" anchorAlignment="center" />
      <silkscreentext text="4L | VIA 0.30/0.45 mm" layer="bottom" pcbX={0} pcbY={bottom + 3.5} fontSize="0.6mm" anchorAlignment="center" />
    </board>
  )
}