component
TPS16850VMAR - 9 V to 80 V, 20 A eFuse
Public Made by Adomby adom
Texas Instruments TPS16850VMAR: 9 V to 80 V, 20 A stackable integrated hot-swap eFuse with a 3.5 mOhm FET, fast current monitor and auto-retry fault response, LQFN-CLIP-23 (VMA, 6.0 x 5.0 mm). KiCad symbol and footprint, Fusion and Altium libraries, datasheet-built 3D model and datasheet.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
#!/usr/bin/env python3
"""Check the page footprint pad by pad.
1. Against TI's printed land pattern (SLVSHA1D p.55): the footprint's copper (red fill) and solder-mask openings
(magenta) drawn at TI's printed 15X scale (15 x 300 / 25.4 = 177.17 px/mm) on the 300 dpi render of p.55. The
offset is found by maximising the overlap of our copper outlines with the drawing's blue copper strokes (scale
also searched +-0.5 %), so nothing about the pads is assumed. The comb mask openings are scored on TI's green.
2. Against Ultra Librarian's TI VMA0023A footprint preview (static.ultralibrarian.com part-preview SVG, read as
data): per pad number, copper / mask / paste area and symmetric-difference area, rasterised at 200 px/mm.
KiCad's standard library has no VMA0023A footprint (Package_DFN_QFN.pretty listed 2026-10-21), so no KiCad
comparison is possible.
usage: fp_check.py page.kicad_mod ul.svg p55-300dpi.png out.json out.png
"""
import json, math, re, sys
import numpy as np
from PIL import Image, ImageDraw, ImageFont
fp_path, ul_path, p55, out_json, out_png = sys.argv[1:6]
def sexp(text):
toks = re.findall(r'\(|\)|"(?:\\.|[^"\\])*"|[^\s()]+', text)
st = [[]]
for t in toks:
if t == '(': st.append([])
elif t == ')': x = st.pop(); st[-1].append(x)
else: st[-1].append(t[1:-1] if t.startswith('"') else t)
return st[0][0]
def ch(n, k): return next((c for c in n if isinstance(c, list) and c and c[0] == k), None)
def chs(n, k): return [c for c in n if isinstance(c, list) and c and c[0] == k]
def rrect_poly(cx, cy, w, h, r, n=6):
if r <= 0: return [(cx - w/2, cy - h/2), (cx + w/2, cy - h/2), (cx + w/2, cy + h/2), (cx - w/2, cy + h/2)]
pts = []
for qx, qy, a0 in ((1, 1, 0), (-1, 1, 90), (-1, -1, 180), (1, -1, 270)):
ox, oy = cx + qx * (w/2 - r), cy + qy * (h/2 - r)
for i in range(n + 1):
a = math.radians(a0 + 90 * i / n); pts.append((ox + r * math.cos(a), oy + r * math.sin(a)))
return pts
def circle_poly(cx, cy, r, n=24):
return [(cx + r * math.cos(2*math.pi*i/n), cy + r * math.sin(2*math.pi*i/n)) for i in range(n)]
fp = sexp(open(fp_path).read())
ours = {}
for p in chs(fp, 'pad'):
num, kind, shape = p[1], p[2], p[3]
if kind == 'thru_hole': continue
at = ch(p, 'at'); x, y = float(at[1]), float(at[2])
sz = ch(p, 'size'); w, h = float(sz[1]), float(sz[2])
layers = ch(p, 'layers')[1:]
if shape == 'custom':
polys = [[(x + float(q[1]), y + float(q[2])) for q in ch(gp, 'pts')[1:]] for gp in chs(ch(p, 'primitives'), 'gr_poly')]
else:
r = float(ch(p, 'roundrect_rratio')[1]) * min(w, h) if shape == 'roundrect' else 0
polys = [rrect_poly(x, y, w, h, r)]
d = ours.setdefault(num, {"cu": [], "mask": [], "paste": []})
if 'F.Cu' in layers: d["cu"] += polys
if 'F.Mask' in layers: d["mask"] += polys
if 'F.Paste' in layers: d["paste"] += polys
vias_ours = sorted((float(ch(p, 'at')[1]), float(ch(p, 'at')[2]), float(ch(p, 'drill')[1]), float(ch(p, 'size')[1]))
for p in chs(fp, 'pad') if p[2] == 'thru_hole')
ep_paste = ours.pop('', None)
if ep_paste: ours['24']['paste'] += ep_paste['paste']
MIL = 1 / 39.37008
svg = open(ul_path).read()
ul, anchors = {}, {}
for g in re.finditer(r'<g id="([^"]+)"[^>]*>(.*?)</g>', svg, re.S):
gid, body = g.group(1), g.group(2)
key = {"TOP": "cu", "TOP_SOLDER_MASK": "mask", "TOP_SOLDER_PASTE": "paste"}.get(gid)
if not key: continue
unnamed = []
for t, a in re.findall(r'<(rect|polygon|circle)\b([^>]*)>', body):
d = dict(re.findall(r'([\w:-]+)\s*=\s*"([^"]*)"', a))
num = d.get('data-pin_number')
if t == 'rect':
x, y, w, h = [float(d[k]) * MIL for k in ('x', 'y', 'width', 'height')]
if w < 0.05: continue
poly = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
elif t == 'polygon':
poly = [tuple(float(v) * MIL for v in q.split(',')) for q in d['points'].split()]
else:
cx, cy, r = float(d['cx']) * MIL, float(d['cy']) * MIL, float(d['r']) * MIL
if int(num) >= 25:
if key == "cu" and r > 0.15: ul.setdefault("vias", []).append((round(cx, 3), round(cy, 3), round(2 * r, 3)))
continue
anchors[num] = (cx, cy); continue
if num is None: unnamed.append(poly)
else: ul.setdefault(num, {"cu": [], "mask": [], "paste": []})[key].append(poly)
for poly in unnamed:
xs = [p[0] for p in poly]; ys = [p[1] for p in poly]
cx, cy = (min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2
if max(xs) - min(xs) < 1.2 and max(ys) - min(ys) < 1.2 and abs(cx) < 1.5 and abs(cy) < 1.2:
ul.setdefault('24', {"cu": [], "mask": [], "paste": []})[key].append(poly); continue
best = min(anchors, key=lambda n: (anchors[n][0] - cx) ** 2 + (anchors[n][1] - cy) ** 2)
ul.setdefault(best, {"cu": [], "mask": [], "paste": []})[key].append(poly)
S = 200; W, H = int(8 * S), int(7 * S)
def mask_of(polys):
im = Image.new('1', (W, H), 0); d = ImageDraw.Draw(im)
for p in polys: d.polygon([(W/2 + x * S, H/2 + y * S) for x, y in p], fill=1)
return np.array(im, bool)
def bbox(m):
ys, xs = np.nonzero(m)
if not len(xs): return None
return [round((xs.min() - W/2) / S, 3), round((ys.min() - H/2) / S, 3), round((xs.max() + 1 - W/2) / S, 3), round((ys.max() + 1 - H/2) / S, 3)]
rows = []
for num in sorted(ours, key=int):
row = {"pad": num}
for k in ("cu", "mask", "paste"):
a, b = mask_of(ours[num][k]), mask_of(ul.get(num, {}).get(k, []))
row[k] = {"page_mm2": round(a.sum() / S**2, 4), "ul_mm2": round(b.sum() / S**2, 4),
"xor_mm2": round((a ^ b).sum() / S**2, 4), "page_bbox_mm": bbox(a), "ul_bbox_mm": bbox(b)}
rows.append(row)
img = Image.open(p55).convert('RGB')
arr = np.asarray(img).astype(int)
blue = (arr[:, :, 2] > 180) & (arr[:, :, 0] < 120) & (arr[:, :, 1] > 60) & (arr[:, :, 1] < 200)
green = (arr[:, :, 1] > 160) & (arr[:, :, 0] < 160) & (arr[:, :, 2] < 120)
PX = 15 * 300 / 25.4
def outline_pts(polys, s, ox, oy):
pts = []
for p in polys:
for (x1, y1), (x2, y2) in zip(p, p[1:] + p[:1]):
n = max(2, int(math.hypot(x2 - x1, y2 - y1) * s / 2))
for i in range(n):
t = i / n; pts.append((ox + (x1 + (x2 - x1) * t) * s, oy + (y1 + (y2 - y1) * t) * s))
return np.array(pts)
def dilate(m, r):
o = m.copy()
for dx in range(-r, r + 1):
for dy in range(-r, r + 1): o |= np.roll(np.roll(m, dx, 1), dy, 0)
return o
cu_all = [p for n in ours for p in ours[n]["cu"]]
ys, xs = np.nonzero(blue); cx0, cy0 = xs.mean(), ys.mean()
dil = dilate(blue, 1); best = None
for s in (PX * f for f in (0.995, 1.0, 1.005)):
P = outline_pts(cu_all, s, 0, 0)
for ox in range(int(cx0 - 80), int(cx0 + 80), 2):
for oy in range(int(cy0 - 80), int(cy0 + 80), 2):
q = (P + (ox, oy)).astype(int)
ok = (q[:, 0] >= 0) & (q[:, 0] < blue.shape[1]) & (q[:, 1] >= 0) & (q[:, 1] < blue.shape[0])
sc = dil[q[ok, 1], q[ok, 0]].mean()
if best is None or sc > best[0]: best = (sc, s, ox, oy)
sc, s, ox0, oy0 = best
for ox in range(ox0 - 2, ox0 + 3):
for oy in range(oy0 - 2, oy0 + 3):
q = outline_pts(cu_all, s, ox, oy).astype(int); v = dil[q[:, 1], q[:, 0]].mean()
if v > sc: sc, ox0, oy0 = v, ox, oy
gdil = dilate(green, 2)
comb_mask = [p for n in ('7', '10', '22') for p in ours[n]["mask"]]
qc = outline_pts(comb_mask, s, ox0, oy0).astype(int)
mask_score = float(gdil[qc[:, 1], qc[:, 0]].mean())
ov = img.copy().convert('RGBA'); lay = Image.new('RGBA', ov.size, (0, 0, 0, 0)); d = ImageDraw.Draw(lay)
for n in ours:
for p in ours[n]["cu"]:
d.polygon([(ox0 + x * s, oy0 + y * s) for x, y in p], fill=(255, 60, 40, 70), outline=(220, 30, 20, 255))
for p in ours[n]["mask"]:
d.line([(ox0 + x * s, oy0 + y * s) for x, y in p + p[:1]], fill=(255, 0, 200, 255), width=2)
for x, y, dr, sz in vias_ours:
d.ellipse([ox0 + (x - sz/2) * s, oy0 + (y - sz/2) * s, ox0 + (x + sz/2) * s, oy0 + (y + sz/2) * s], outline=(220, 30, 20, 255), width=2)
ov = Image.alpha_composite(ov, lay).convert('RGB')
crop = ov.crop((max(0, int(ox0 - 4.0 * s)), max(0, int(oy0 - 3.4 * s)), int(ox0 + 4.9 * s), int(oy0 + 3.6 * s)))
canvas = Image.new('RGB', (crop.width, crop.height + 100), 'white'); canvas.paste(crop, (0, 0))
dd = ImageDraw.Draw(canvas); font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 22)
dd.text((20, crop.height + 10), "TPS16850VMAR footprint (red fill = copper, magenta = solder mask opening) drawn at TI's 15X scale", fill='black', font=font)
dd.text((20, crop.height + 50), f"on TI SLVSHA1D p.55 land pattern. Copper outline on TI's blue strokes: {sc*100:.1f} %; comb mask openings on TI's green: {mask_score*100:.1f} %", fill='black', font=font)
canvas.save(out_png)
res = {"schema": "adom/footprint-check@1", "footprint": fp_path.rsplit('/', 1)[-1],
"datasheet": "TI SLVSHA1D (Rev. D, July 2026) p.55 LAND PATTERN EXAMPLE and p.56 SOLDER PASTE EXAMPLE, VMA0023A 4230225/A 11/2023",
"overlay": {"image": "docs/proof/tps16850vmar-footprint-check.png", "scale_px_per_mm": round(s, 3),
"printed_scale": "15X at 300 dpi = 177.165 px/mm",
"copper_outline_on_ti_strokes": round(sc, 4), "comb_mask_on_ti_green": round(mask_score, 4),
"note": "TI draws the metal under the solder mask of pads 7, 10 and 22 dashed, so part of those copper outlines falls in the gaps between dashes; every solid TI stroke is covered",
"method": "outline points of every copper polygon sampled every 2 px, scored against TI's blue strokes dilated by 1 px; offset searched +-80 px, scale +-0.5 %"},
"ultra_librarian": {"source": "https://static.ultralibrarian.com/part-previews/footprints/6142033/detailed-VMA0023A-MFG.svg",
"note": "TI's VMA0023A footprint as published through Ultra Librarian (TI's CAD partner); preview SVG read as data; layers TOP / TOP_SOLDER_MASK / TOP_SOLDER_PASTE; differences come from corner treatment (UL chamfers 0.05 mm, this footprint rounds plain pads R0.05 as TI prints and keeps the custom shapes square)"},
"rows": rows, "vias": {"page": vias_ours, "ul": sorted(ul.get("vias", []))},
"kicad": "KiCad's standard library has no VMA0023A / LQFN-CLIP-23 footprint (gitlab.com/kicad/libraries/kicad-footprints Package_DFN_QFN.pretty, 780 files, listed 2026-10-21); no KiCad comparison is possible."}
json.dump(res, open(out_json, 'w'), indent=1)
print(f"overlay {sc:.3f} mask {mask_score:.3f} scale {s:.2f}; max xor cu {max(r['cu']['xor_mm2'] for r in rows):.3f} mm2; vias {len(vias_ours)}/{len(ul.get('vias', []))}")