Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pinrow6_rows2_cols5_p2.54mm_py5.08mm_missing(3,4,8,9)
smdpinheader6
tssop20_p0.5mm
sot23
utdfn4
qfn24_w6_h6_p0.8mm_thermalpad_startingpin(topside,rightpin)_ccw
qfn64_thermalpad6.3mmx6.3mm_thermalvias4x4_thermalviapitch1mm_thermalviaid0.3048mm_thermalviaod0.6096mm
axial_p0.2in
Expand Down Expand Up @@ -80,6 +81,25 @@ fp.string("dip4_w7.62mm") // same as fp.dip(4).w(7.62)
fp.string("dip4_w0.3in") // same as fp.dip(4).w("0.3in")
```

### UTDFN-4-EP (1x1mm)

`utdfn4`, `utdfn`, and `UTDFN-4-EP(1x1)` generate the SGM2036
UTDFN-1x1-4L land pattern from SGMICRO drawing TX00066.000. The footprint
has four corner pads and a 0.48mm square exposed pad rotated 45 degrees.
Pin 1 is the longer lower-left pad in the manufacturer's top view; the
remaining pads are numbered counter-clockwise. The exposed pad uses the
`thermalpad` port hint.

```ts
fp.string("UTDFN-4-EP(1x1)").circuitJson()
fp().utdfn(4).circuitJson()
fp().utdfn().ep(false).circuitJson() // explicitly omit the exposed pad
```

These dimensions target SGM2036, including the SGM2036-1.2YUDH4G/TR listed
under this catalog package. Other manufacturers' 1x1mm packages can have
different land patterns; check the actual part's datasheet.

### Pin 1 location

Every footprint accepts a `pin1location(side,alignment)` modifier that rotates
Expand Down
1 change: 1 addition & 0 deletions src/fn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export { sot886 } from "./sot886"
export { sot23 } from "./sot23"
export { sot25 } from "./sot25"
export { dfn } from "./dfn"
export { utdfn } from "./utdfn"
export { do219ad } from "./do219ad"
export { pinrow } from "./pinrow"
export { headermodule } from "./headermodule"
Expand Down
79 changes: 79 additions & 0 deletions src/fn/utdfn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { AnyCircuitElement, PcbCourtyardRect } from "circuit-json"
import { z } from "zod"
import { polygonpad } from "../helpers/polygonpad"
import { silkscreenpath } from "../helpers/silkscreenpath"
import { silkscreenRef } from "../helpers/silkscreenRef"
import { base_def } from "../helpers/zod/base_def"

export const utdfn_def = base_def.extend({
fn: z.literal("utdfn"),
num_pins: z.literal(4).default(4),
ep: z.boolean().default(true).describe("include the exposed thermal pad"),
})

/**
* SGM2036 UTDFN-1x1-4L, catalogued as UTDFN-4-EP(1x1).
* SGMICRO package drawing TX00066.000, recommended land pattern (top view):
* https://www.sg-micro.com/rect/assets/efa85993-263c-41aa-9274-b488f59f85d5/SGM2036.pdf
* Different manufacturers' 1x1mm packages are not necessarily land-compatible.
*/
export const utdfn = (rawParams: z.input<typeof utdfn_def>) => {
const parameters = utdfn_def.parse(rawParams)
const circuitJson: AnyCircuitElement[] = []

// Top view: pin 1 lower left, then counter-clockwise. Pin 1 is longer
// and has a full-width diagonal; the other three have 0.18mm chamfers.
const quadrants = [
[-1, -1],
[1, -1],
[1, 1],
[-1, 1],
] as const
for (const [index, [sx, sy]] of quadrants.entries()) {
const innerY = sy * (index === 0 ? 0.18 : 0.25)
const cut = index === 0 ? 0.25 : 0.18
const points = [
{ x: sx * 0.2, y: innerY + sy * cut },
{ x: sx * 0.2, y: sy * 0.65 },
{ x: sx * 0.45, y: sy * 0.65 },
{ x: sx * 0.45, y: innerY },
]
if (index !== 0) points.push({ x: sx * (0.2 + cut), y: innerY })
circuitJson.push(polygonpad(index + 1, points))
}

if (parameters.ep) {
// 0.48mm is the side length, not the axis-aligned bounding-box width.
const radius = 0.48 / Math.sqrt(2)
circuitJson.push(
polygonpad(
["thermalpad"],
[
{ x: 0, y: radius },
{ x: radius, y: 0 },
{ x: 0, y: -radius },
{ x: -radius, y: 0 },
],
),
)
}

for (const [index, [sx, sy]] of quadrants.entries()) {
const route = [{ x: sx * 0.65, y: sy * 0.64 }]
if (index !== 0) route.push({ x: sx * 0.65, y: sy * 0.8 })
route.push({ x: sx * 0.49, y: sy * 0.8 })
circuitJson.push(silkscreenpath(route))
}
circuitJson.push(silkscreenRef(0, 1.2, 0.3))
const courtyard: PcbCourtyardRect = {
type: "pcb_courtyard_rect",
pcb_courtyard_rect_id: "",
pcb_component_id: "",
center: { x: 0, y: 0 },
width: 1.5,
height: 1.8,
layer: "top",
}
circuitJson.push(courtyard)
return { circuitJson, parameters }
}
2 changes: 2 additions & 0 deletions src/footprinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ export type Footprinter = {
) => FootprinterParamsBuilder<
"p" | "rowspan" | "pl" | "pw" | "ep" | "epw" | "eph" | "w" | "h"
>
utdfn: (num_pins?: number) => FootprinterParamsBuilder<"ep">
vssop: (
num_pins?: number,
) => FootprinterParamsBuilder<
Expand Down Expand Up @@ -636,6 +637,7 @@ const normalizeDefinition = (def: string): string => {
.replace(/^do-219ad(?=_|$)/i, "do219ad")
.replace(/^sod-323he(?=_|$)/i, "sod323he")
.replace(/^pinheader(?=[\d_]|$)/i, "pinrow")
.replace(/^utdfn-4(?:-ep\(1x1\))?(?=_|$)/i, "utdfn4")
.replace(/^d2pak(\d+)(?=_|$)/i, "d2pak_$1")
.replace(/^to-252(?:-(\d+))?(?=_|$)/i, (_, pins) =>
pins ? `to252_${pins}` : "to252",
Expand Down
1 change: 1 addition & 0 deletions tests/__snapshots__/utdfn4_no_ep.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/__snapshots__/utdfn4_sgm2036.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions tests/utdfn.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { expect, test } from "bun:test"
import { convertCircuitJsonToPcbSvg } from "circuit-to-svg"
import { fp } from "../src/footprinter"

const getPads = (definition = "utdfn4") =>
fp
.string(definition)
.circuitJson()
.filter((item) => item.type === "pcb_smtpad")

test("utdfn4 implements the SGM2036 UTDFN-1x1-4L land pattern", () => {
const pads = getPads()
expect(pads.map((pad) => pad.port_hints)).toEqual([
["1"],
["2"],
["3"],
["4"],
["thermalpad"],
])
expect(pads.every((pad) => pad.shape === "polygon")).toBe(true)
const bounds = pads.slice(0, 4).map((pad) => {
if (pad.shape !== "polygon") throw new Error("Expected polygon")
return [
Math.min(...pad.points.map((p) => p.x)),
Math.max(...pad.points.map((p) => p.x)),
Math.min(...pad.points.map((p) => p.y)),
Math.max(...pad.points.map((p) => p.y)),
]
})
const expected = [
[-0.45, -0.2, -0.65, -0.18],
[0.2, 0.45, -0.65, -0.25],
[0.2, 0.45, 0.25, 0.65],
[-0.45, -0.2, 0.25, 0.65],
]
for (let i = 0; i < 4; i++)
for (let j = 0; j < 4; j++) {
expect(bounds[i]![j]).toBeCloseTo(expected[i]![j]!, 10)
}
expect(
convertCircuitJsonToPcbSvg(fp.string("utdfn4").circuitJson()),
).toMatchSvgSnapshot(import.meta.path, "utdfn4_sgm2036")
})

test("utdfn4 exposed pad is a 0.48mm square rotated 45 degrees", () => {
const ep = getPads().find((pad) => pad.port_hints?.includes("thermalpad"))!
if (ep.shape !== "polygon") throw new Error("Expected diamond")
expect(ep.points).toHaveLength(4)
for (let i = 0; i < 4; i++) {
const a = ep.points[i]!,
b = ep.points[(i + 1) % 4]!
expect(Math.hypot(a.x - b.x, a.y - b.y)).toBeCloseTo(0.48, 10)
expect(Math.abs(a.x) + Math.abs(a.y)).toBeCloseTo(0.48 / Math.sqrt(2), 10)
expect(a.x === 0 || a.y === 0).toBe(true)
}
})

test("utdfn4 corner cuts preserve at least 0.2mm clearance from the exposed pad", () => {
for (const pad of getPads().slice(0, 4)) {
if (pad.shape !== "polygon") throw new Error("Expected polygon")
// Each convex signal pad stays in one quadrant. Its nearest parallel
// supporting line is separated from the diamond by this distance.
const minDiagonal = Math.min(
...pad.points.map((p) => Math.abs(p.x) + Math.abs(p.y)),
)
expect(
(minDiagonal - 0.48 / Math.sqrt(2)) / Math.sqrt(2),
).toBeGreaterThanOrEqual(0.2)
expect(
pad.points.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y)),
).toBe(true)
}
})

test("utdfn4 preserves the distinctive extended pin 1 instead of four identical pads", () => {
const pads = getPads()
expect(
pads.slice(0, 4).map((p) => (p.shape === "polygon" ? p.points.length : 0)),
).toEqual([4, 5, 5, 5])
})

test("utdfn accepts catalog spelling and optional pin count", () => {
const expected = getPads()
for (const definition of ["utdfn", "UTDFN4", "UTDFN-4", "UTDFN-4-EP(1x1)"]) {
expect(getPads(definition)).toEqual(expected)
}
expect(fp().utdfn().circuitJson()).toEqual(fp.string("utdfn4").circuitJson())
})

test("utdfn rejects other pin counts", () => {
for (const definition of ["utdfn3", "utdfn6"]) {
expect(() => fp.string(definition).circuitJson()).toThrow()
}
})

test("utdfn applies shared silkscreen and refdes modifiers", () => {
const result = fp
.string("UTDFN-4-EP(1x1)_nosilkscreen_norefdes")
.circuitJson()
expect(
result.filter((item) => item.type.startsWith("pcb_silkscreen")),
).toHaveLength(0)
expect(result.filter((item) => item.type === "pcb_smtpad")).toHaveLength(5)
})

test("utdfn exposed pad can be omitted explicitly", () => {
const result = fp().utdfn(4).ep(false).circuitJson()
expect(result.filter((item) => item.type === "pcb_smtpad")).toHaveLength(4)
expect(convertCircuitJsonToPcbSvg(result)).toMatchSvgSnapshot(
import.meta.path,
"utdfn4_no_ep",
)
})
Loading