pixalynx/pixal-gps

These files define two separate printed circuit boards: a small, two-layer battery cartridge with protection circuitry and contact pads for a pouch cell, and a larger, two-layer wireless charging dock featuring USB-C input, a resonant coil, and wireless power transmission components.

Version
0.1.2
License
unset
Stars
0

lib/footprints.tsx

import { Children, Fragment, type ReactElement } from "react"

/** Land-pattern generators. Every dimension is in millimetres and is annotated
 * with the manufacturer document it was read from (see docs/sources.md).
 * Pin numbering follows the manufacturer's pin table; pin 1 is top-left in the
 * unrotated footprint and numbering proceeds counter-clockwise for QFN/LGA
 * packages (left side downwards, bottom left-to-right, right upwards, top
 * right-to-left), which is the JEDEC convention used by Nordic, TI and ST. */

export const pad = (pin: number | string, x: number, y: number, w: number, h: number) => (
  <Fragment key={`p${pin}-${x}-${y}`}>
    <smtpad portHints={[typeof pin === "number" ? `pin${pin}` : pin]} pcbX={x} pcbY={y} width={w} height={h} shape="rect" />
  </Fragment>
)
export const circlePad = (pin: number | string, x: number, y: number, d: number) => (
  <Fragment key={`c${pin}-${x}-${y}`}>
    <smtpad portHints={[typeof pin === "number" ? `pin${pin}` : pin]} pcbX={x} pcbY={y} radius={d / 2} shape="circle" />
  </Fragment>
)
export const via = (pin: number | string, x: number, y: number, drill: number, padD: number) => (
  <Fragment key={`v${pin}-${x}-${y}`}>
    <platedhole portHints={[typeof pin === "number" ? `pin${pin}` : pin]} pcbX={x} pcbY={y} holeDiameter={drill} outerDiameter={padD} shape="circle" />
  </Fragment>
)
export const outline = (w: number, h: number, stroke = 0.12) => (
  <silkscreenpath route={[{ x: -w / 2, y: -h / 2 }, { x: w / 2, y: -h / 2 }, { x: w / 2, y: h / 2 }, { x: -w / 2, y: h / 2 }, { x: -w / 2, y: -h / 2 }]} strokeWidth={stroke} />
)
export const courtyard = (w: number, h: number) => (
  <courtyardoutline outline={[{ x: -w / 2, y: h / 2 }, { x: w / 2, y: h / 2 }, { x: w / 2, y: -h / 2 }, { x: -w / 2, y: -h / 2 }, { x: -w / 2, y: h / 2 }]} />
)
const pin1Mark = (x: number, y: number) => <silkscreencircle pcbX={x} pcbY={y} radius={0.15} strokeWidth={0.1} />

/** Generic QFN/DFN land pattern.
 * @param nx pins per horizontal side (top/bottom); @param ny pins per vertical side (left/right)
 * @param pitch pin pitch; @param span centre-to-centre distance of opposite land rows
 * @param landL land length (radial); @param landW land width (tangential)
 * @param ep exposed-pad copper size (0 = none); @param epVias grid of ground vias in the EP
 */
export function Qfn(p: { nx: number; ny: number; pitch: number; spanX: number; spanY?: number; landL: number; landW: number; ep?: number | [number, number]; epVias?: { n: number; pitch: number; drill: number; pad: number }; body: [number, number]; epPin?: number }) {
  const { nx, ny, pitch, spanX, landL, landW } = p
  const spanY = p.spanY ?? spanX
  const total = 2 * nx + 2 * ny
  const epPin = p.epPin ?? total + 1
  const pads: ReactElement[] = []
  let n = 1
  // left side, top to bottom
  for (let i = 0; i < ny; i++) pads.push(pad(n++, -spanX / 2, ((ny - 1) / 2 - i) * pitch, landL, landW))
  // bottom, left to right
  for (let i = 0; i < nx; i++) pads.push(pad(n++, (i - (nx - 1) / 2) * pitch, -spanY / 2, landW, landL))
  // right, bottom to top
  for (let i = 0; i < ny; i++) pads.push(pad(n++, spanX / 2, (i - (ny - 1) / 2) * pitch, landL, landW))
  // top, right to left
  for (let i = 0; i < nx; i++) pads.push(pad(n++, ((nx - 1) / 2 - i) * pitch, spanY / 2, landW, landL))
  const [epW, epH] = p.ep ? (Array.isArray(p.ep) ? p.ep : [p.ep, p.ep]) : [0, 0]
  const vias: ReactElement[] = []
  if (p.epVias && epW) {
    const { n: g, pitch: vp, drill, pad: vpad } = p.epVias
    for (let i = 0; i < g; i++) for (let j = 0; j < g; j++)
      vias.push(via(epPin, (i - (g - 1) / 2) * vp, (j - (g - 1) / 2) * vp, drill, vpad))
  }
  return (
    <footprint>
      {Children.toArray(pads)}
      {epW > 0 && pad(epPin, 0, 0, epW, epH)}
      {Children.toArray(vias)}
      {outline(p.body[0], p.body[1])}
      {pin1Mark(-p.body[0] / 2 - 0.45, p.body[1] / 2 - 0.1)}
      {courtyard(spanX + landL + 0.3, spanY + landL + 0.3)}
    </footprint>
  )
}

/** Nordic nPM1300 QFN32 5x5 mm, 0.5 mm pitch. Nordic reference layout v1.2 Gerbers
 * (Altium Nordic_QFN50P500X500X90-33N): 0.30 x 0.851 mm lands, row centres +/-2.451 mm,
 * exposed pad 3.5 mm (D2/E2 nominal 3.4-3.6), 9 ground vias 0.305 mm drill / 0.61 mm pad on
 * a 1.2 mm grid, mask opening 3.653 mm. Pin 1 top-left, numbering counter-clockwise (JLC
 * C7501206 footprint agrees on land order). */
export const Npm1300Qfn32 = () => (
  <Qfn nx={8} ny={8} pitch={0.5} spanX={4.902} landL={0.85} landW={0.30} ep={3.5} epVias={{ n: 3, pitch: 1.2, drill: 0.305, pad: 0.61 }} body={[5, 5]} />
)

/** TI BQ51013B VQFN-20 (RHL) 3.5 x 4.5 mm, 0.5 mm pitch: 8 lands on each long (4.5 mm) side,
 * 2 lands on each short side; thermal pad. Dimensions from TI RHL0020A land pattern
 * (see docs/sources.md, wireless-charging.md). Pin 1 = AC1 at the top-left corner of the
 * long side in the datasheet top view; here the long sides are horizontal. */
export const Bq51013bRhl = (p: { landL: number; landW: number; spanLong: number; spanShort: number; ep: [number, number] }) => {
  // TI numbers pins 1..8 along the bottom? No: RHL pin 1 is at the top-left of the package
  // when the 4.5 mm sides are vertical. We build it with the 4.5 mm sides vertical (ny=8, nx=2).
  return <Qfn nx={2} ny={8} pitch={0.5} spanX={p.spanShort} spanY={p.spanLong} landL={p.landL} landW={p.landW} ep={p.ep} epVias={{ n: 2, pitch: 1.0, drill: 0.25, pad: 0.5 }} body={[3.5, 4.5]} />
}

/** Nordic nRF9151 LGA-113, 12.1 x 11.1 x 1.2 mm. 80 perimeter lands on a 0.5 mm pitch
 * (20 per side; the four corner lands are 0.7 mm squares), 24 small reserved lands and
 * 9 large ground lands inside. Geometry as JLC/EasyEDA C22397843 (JLCPCB assembles from
 * this footprint) cross-checked against the Nordic product specification recommended
 * land pattern (docs/sources.md). Pin numbering follows Nordic: pin 1 is the top-left
 * corner land and numbering proceeds counter-clockwise around the perimeter (1-80), then
 * the inner lands 81-113. */
export function Nrf9151Lga113(p: { perimW?: number; perimL?: number; cornerSq?: number; innerSq?: number; gndSq?: number; edgeX?: number; edgeY?: number; cornerX?: number; cornerY?: number }) {
  const perimL = p.perimL ?? 0.45, perimW = p.perimW ?? 0.30, corner = p.cornerSq ?? 0.7, inner = p.innerSq ?? 0.2, gnd = p.gndSq ?? 1.6
  const edgeX = p.edgeX ?? 5.625, edgeY = p.edgeY ?? 5.125, cornerX = p.cornerX ?? 5.5, cornerY = p.cornerY ?? 5.0
  const pads: ReactElement[] = []
  let n = 1
  // Left column: pin 1 corner (top-left), pins 2-19 on x=-edgeX from y=+4.25 downwards, pin 20 corner (bottom-left)
  pads.push(pad(n++, -cornerX, cornerY, corner, corner))
  for (let i = 0; i < 18; i++) pads.push(pad(n++, -edgeX, 4.25 - i * 0.5, perimL, perimW))
  pads.push(pad(n++, -cornerX, -cornerY, corner, corner))
  // Bottom row: pins 21-40 on y=-edgeY from x=-4.75 to +4.75
  for (let i = 0; i < 20; i++) pads.push(pad(n++, -4.75 + i * 0.5, -edgeY, perimW, perimL))
  // Right column: pin 41 corner (bottom-right), 42-59 upwards, pin 60 corner (top-right)
  pads.push(pad(n++, cornerX, -cornerY, corner, corner))
  for (let i = 0; i < 18; i++) pads.push(pad(n++, edgeX, -4.25 + i * 0.5, perimL, perimW))
  pads.push(pad(n++, cornerX, cornerY, corner, corner))
  // Top row: pins 61-80 on y=+edgeY from x=+4.75 to -4.75
  for (let i = 0; i < 20; i++) pads.push(pad(n++, 4.75 - i * 0.5, edgeY, perimW, perimL))
  // Inner reserved lands 81-104 (0.2 mm squares): left column 81-89 (x=-4.10, y 3.40..-3.40 step 0.55/0.85 irregular
  // as EasyEDA/Nordic), centre rows 90-92 (y=-2.15) and 102-104 (y=+2.15), right column 93-101.
  const colY = [3.4, 2.85, 2.3, 0.55, 0, -0.55, -2.3, -2.85, -3.4]
  colY.forEach((y) => pads.push(pad(n++, -4.1, y, inner, inner)))            // 81-89
  ;[-0.55, 0, 0.55].forEach((x) => pads.push(pad(n++, x, -2.15, inner, inner))) // 90-92
  colY.slice().reverse().forEach((y) => pads.push(pad(n++, 4.1, y, inner, inner))) // 93-101 (bottom to top)
  ;[0.55, 0, -0.55].forEach((x) => pads.push(pad(n++, x, 2.15, inner, inner)))  // 102-104
  // Ground lands 105-113 (1.6 mm squares; 108 and 112 are 1.6 x 1.95)
  const gndLands: [number, number, number, number][] = [
    [-2.85, 2.85, gnd, gnd], [-2.85, 0, gnd, gnd], [-2.85, -2.85, gnd, gnd], [0, -3.575, gnd, 1.95],
    [2.85, -2.85, gnd, gnd], [2.85, 0, gnd, gnd], [2.85, 2.85, gnd, gnd], [0, 3.575, gnd, 1.95], [0, 0, gnd, gnd],
  ]
  gndLands.forEach(([x, y, w, h]) => pads.push(pad(n++, x, y, w, h)))
  // Ground vias in the large ground lands (one 0.3/0.6 via each; the SiP's GND lands
  // are the only path from the L1 pads to the L2 plane under the package).
  const gndVias = gndLands.map(([x, y], i) => via(105 + i, x, y, 0.3, 0.6))
  return (
    <footprint>
      {Children.toArray(pads)}
      {Children.toArray(gndVias)}
      {outline(12.1, 11.1)}
      {pin1Mark(-6.55, 5.55)}
      {courtyard(12.5, 11.5)}
    </footprint>
  )
}

/** Hirose U.FL-R-SMT-1 (Hirose drawing, matches JLC C88374): signal land 1.0 x 1.05 mm at
 * y=-1.05, two ground lands 1.05 x 2.2 mm at x=+/-1.475, y=+0.475. Cable exits along +y
 * from the receptacle; the signal pad is the "1" pin. Body 2.6 x 2.6 mm. */
export const UflRsmt = () => (
  <footprint insertionDirection="from_above">
    {pad(1, 0, -1.05, 1.0, 1.05)}
    {pad(2, -1.475, 0.475, 1.05, 2.2)}
    {pad(3, 1.475, 0.475, 1.05, 2.2)}
    {outline(2.6, 2.6)}
    {courtyard(4.2, 3.3)}
  </footprint>
)

/** ST LIS2DW12 LGA-12 2.0 x 2.0 x 0.7 mm, 0.5 mm pitch: 4 lands on the top/bottom edges
 * (x = +/-0.25, +/-0.75) and 2 on each side (y = +/-0.25). Land 0.28 x 0.50 mm, centres
 * 1.75 mm apart (JLC C189624; ST DS11473 Figure 12 gives the same 1.75 mm land rows). Pin 1
 * is the bottom-left land in ST's top view (marked corner), numbering counter-clockwise:
 * 1-4 bottom (left to right), 5-6 right (bottom to top), 7-10 top (right to left), 11-12 left
 * (top to bottom). */
export const Lga12_2x2 = () => (
  <footprint>
    {[-0.75, -0.25, 0.25, 0.75].map((x, i) => pad(1 + i, x, -0.875, 0.28, 0.5))}
    {pad(5, 0.875, -0.25, 0.5, 0.28)}
    {pad(6, 0.875, 0.25, 0.5, 0.28)}
    {[0.75, 0.25, -0.25, -0.75].map((x, i) => pad(7 + i, x, 0.875, 0.28, 0.5))}
    {pad(11, -0.875, 0.25, 0.5, 0.28)}
    {pad(12, -0.875, -0.25, 0.5, 0.28)}
    {outline(2, 2)}
    {pin1Mark(-1.35, -1.0)}
    {courtyard(2.5, 2.5)}
  </footprint>
)

/** 3215 (3.2 x 1.5 mm) two-pad 32.768 kHz crystal, e.g. Epson FC-135 / Seiko SC-32S.
 * Epson recommended land: 1.4 x 1.2 mm lands, 2.2 mm apart (centres +/-1.1). */
export const Crystal3215 = () => (
  <footprint>
    {pad(1, -1.1, 0, 1.4, 1.2)}
    {pad(2, 1.1, 0, 1.4, 1.2)}
    {outline(3.2, 1.5)}
    {courtyard(4.0, 2.0)}
  </footprint>
)

/** TI DRL (SOT-553) 5-lead, 1.6 x 1.2 mm body: lands 0.5 x 0.29 mm on a 0.5 mm pitch, rows
 * 1.4 mm apart (JLC C470828 / TI DRL0005A). Pin 1 top-left; pins 1-3 down the left, pin 4
 * bottom-right, pin 5 top-right. */
export const Sot553 = () => (
  <footprint>
    {pad(1, -0.7, 0.5, 0.5, 0.29)}
    {pad(2, -0.7, 0, 0.5, 0.29)}
    {pad(3, -0.7, -0.5, 0.5, 0.29)}
    {pad(4, 0.7, -0.5, 0.5, 0.29)}
    {pad(5, 0.7, 0.5, 0.5, 0.29)}
    {outline(1.6, 1.2)}
    {pin1Mark(-1.2, 0.8)}
    {courtyard(2.4, 1.9)}
  </footprint>
)

/** TI WSON-6 1.5 x 1.5 mm (DSE), 0.5 mm pitch: 0.75 x 0.28 mm lands, rows 1.27 mm apart
 * (JLC C183096). Pin 1 top-left, 1-3 down the left, 4-6 up the right. */
export const Wson6_1p5 = () => (
  <footprint>
    {pad(1, -0.636, 0.5, 0.75, 0.28)}
    {pad(2, -0.636, 0, 0.75, 0.28)}
    {pad(3, -0.636, -0.5, 0.75, 0.28)}
    {pad(4, 0.636, -0.5, 0.75, 0.28)}
    {pad(5, 0.636, 0, 0.75, 0.28)}
    {pad(6, 0.636, 0.5, 0.75, 0.28)}
    {outline(1.5, 1.5)}
    {pin1Mark(-1.15, 0.8)}
    {courtyard(2.3, 2.1)}
  </footprint>
)

/** Round ENIG contact pad for a spring-loaded (pogo) contact or test probe. */
export const ContactPad = (d = 2.0) => (
  <footprint>
    {circlePad(1, 0, 0, d)}
    <silkscreencircle pcbX={0} pcbY={0} radius={d / 2 + 0.25} strokeWidth={0.1} />
  </footprint>
)

/** Single-pad SMD spring-loaded pin such as BWCD-L3.5W2.0H1.0 (JLC C2826547): 2.0 mm base,
 * one 2.2 mm round land (EasyEDA), 3.5 mm free height, 1.0 mm stroke. */
export const PogoPinSmd = () => (
  <footprint insertionDirection="from_above">
    {circlePad(1, 0, 0, 2.2)}
    <silkscreencircle pcbX={0} pcbY={0} radius={1.4} strokeWidth={0.1} />
    {courtyard(2.6, 2.6)}
  </footprint>
)

/** 2.0 x 1.6 mm (0806 / 2016 metric) power inductor such as Murata DFE201612E / Samsung
 * CIGT201610: two 0.8 x 1.6 mm lands on 1.9 mm centres (Murata DFE201612E recommended pattern
 * 0.8 x 1.6, gap 1.1). */
export const Ind0806 = () => (
  <footprint>
    {pad(1, -0.95, 0, 0.8, 1.6)}
    {pad(2, 0.95, 0, 0.8, 1.6)}
    {outline(2.0, 1.6)}
    {courtyard(2.9, 2.0)}
  </footprint>
)