got flexpcb and pinout spec'd, fpc draft
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Convert flux-ai/soic-16-n-to-8-pin-fpc-flex-adapter.ipc2581c to KiCad .kicad_pcb format.
|
||||
|
||||
Key design facts from IPC-2581C analysis:
|
||||
- TSSOP_L1-L8: castellated through-holes at y=-15.874, x=10.555..19.445 (1.27mm pitch)
|
||||
- TSSOP_R1-R8: castellated through-holes at y=-11.474, x=10.555..19.445 (1.27mm pitch)
|
||||
- FPC_1-10: SMD finger pads at x=30.5, y=-15.924..-11.424 (0.5mm pitch), 3mm x 0.35mm
|
||||
- Board outline: roughly 10..32 x -16.424..-10.924 (22mm x 5.5mm)
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
import math
|
||||
|
||||
IPC_FILE = 'soic-16-n-to-8-pin-fpc-flex-adapter.ipc2581c'
|
||||
OUT_FILE = '../soic-16-n-to-8-pin-fpc-flex-adapter.kicad_pcb'
|
||||
|
||||
NS = 'http://webstds.ipc.org/2581'
|
||||
def Q(name): return f'{{{NS}}}{name}'
|
||||
def untag(el): return el.tag.replace(f'{{{NS}}}', '')
|
||||
|
||||
|
||||
def parse_ipc(path):
|
||||
tree = ET.parse(path)
|
||||
root = tree.getroot()
|
||||
|
||||
# --- Line width dictionary ---
|
||||
line_widths = {}
|
||||
for dl in root.iter(Q('DictionaryLineDesc')):
|
||||
for entry in dl:
|
||||
lid = entry.get('id')
|
||||
ld = entry.find(Q('LineDesc'))
|
||||
if ld is not None:
|
||||
line_widths[lid] = float(ld.get('lineWidth', '0'))
|
||||
|
||||
# --- Standard primitives (CUSTOM_1 = castellation copper, CUSTOM_2 = tiny) ---
|
||||
std_prims = {}
|
||||
for ds in root.iter(Q('DictionaryStandard')):
|
||||
for entry in ds:
|
||||
sid = entry.get('id')
|
||||
pts = []
|
||||
for pb in entry.iter(Q('PolyBegin')):
|
||||
pts.append((float(pb.get('x')), float(pb.get('y'))))
|
||||
for ps in entry.iter(Q('PolyStepSegment')):
|
||||
pts.append((float(ps.get('x')), float(ps.get('y'))))
|
||||
std_prims[sid] = pts
|
||||
|
||||
# --- User primitives (UPOLY_N = copper fills, UCIRCLE_1 = castellation ring) ---
|
||||
upoly_defs = {} # id -> [(x,y), ...]
|
||||
ucircle_defs = {} # id -> diameter
|
||||
for du in root.iter(Q('DictionaryUser')):
|
||||
for entry in du:
|
||||
uid = entry.get('id')
|
||||
pts = []
|
||||
for pb in entry.iter(Q('PolyBegin')):
|
||||
pts.append((float(pb.get('x')), float(pb.get('y'))))
|
||||
for ps in entry.iter(Q('PolyStepSegment')):
|
||||
pts.append((float(ps.get('x')), float(ps.get('y'))))
|
||||
if pts:
|
||||
upoly_defs[uid] = pts
|
||||
for c in entry.iter(Q('Circle')):
|
||||
ucircle_defs[uid] = float(c.get('diameter'))
|
||||
|
||||
# --- Layer features ---
|
||||
# pads: {pin_name: {x, y, prim_id, layer}}
|
||||
pads = {}
|
||||
# upoly placements per layer: [(uid, offset_x, offset_y)]
|
||||
layer_upolys = {}
|
||||
# lines per layer: [(x1,y1,x2,y2,width)]
|
||||
layer_lines = {}
|
||||
# through-holes: [(x, y, diameter)]
|
||||
through_holes = []
|
||||
|
||||
for lf in root.iter(Q('LayerFeature')):
|
||||
layer = lf.get('layerRef')
|
||||
layer_upolys.setdefault(layer, [])
|
||||
layer_lines.setdefault(layer, [])
|
||||
|
||||
sets = list(lf)
|
||||
for s in sets:
|
||||
if untag(s) != 'Set':
|
||||
continue
|
||||
|
||||
# Pad elements (appear in Set[0] of each layer, only F.Cu has pin refs)
|
||||
for pad_el in s.findall(Q('Pad')):
|
||||
loc = pad_el.find(Q('Location'))
|
||||
if loc is None:
|
||||
continue
|
||||
x = float(loc.get('x'))
|
||||
y = float(loc.get('y'))
|
||||
pin_ref = pad_el.find(Q('PinRef'))
|
||||
pin = pin_ref.get('pin') if pin_ref is not None else ''
|
||||
sp_ref = pad_el.find(Q('StandardPrimitiveRef'))
|
||||
up_ref = pad_el.find(Q('UserPrimitiveRef'))
|
||||
prim_id = (sp_ref.get('id') if sp_ref is not None
|
||||
else (up_ref.get('id') if up_ref is not None else None))
|
||||
if pin and pin not in pads:
|
||||
pads[pin] = {'x': x, 'y': y, 'prim_id': prim_id, 'layer': layer}
|
||||
|
||||
# Features blocks (UPOLY refs and lines)
|
||||
for feat in s.findall(Q('Features')):
|
||||
loc = feat.find(Q('Location'))
|
||||
ox = float(loc.get('x', '0')) if loc is not None else 0
|
||||
oy = float(loc.get('y', '0')) if loc is not None else 0
|
||||
|
||||
up_ref = feat.find(Q('UserPrimitiveRef'))
|
||||
if up_ref is not None:
|
||||
layer_upolys[layer].append((up_ref.get('id'), ox, oy))
|
||||
|
||||
for line in feat.iter(Q('Line')):
|
||||
if line.get('startX') is None:
|
||||
continue
|
||||
lw_ref = line.find(Q('LineDescRef'))
|
||||
lw = line_widths.get(lw_ref.get('id'), 0.1) if lw_ref is not None else 0.1
|
||||
layer_lines[layer].append((
|
||||
float(line.get('startX')), float(line.get('startY')),
|
||||
float(line.get('endX')), float(line.get('endY')), lw
|
||||
))
|
||||
|
||||
for us in feat.findall(Q('UserSpecial')):
|
||||
for line in us.findall(Q('Line')):
|
||||
ld = line.find(Q('LineDesc'))
|
||||
lw = float(ld.get('lineWidth', '0.05')) if ld is not None else 0.05
|
||||
layer_lines[layer].append((
|
||||
float(line.get('startX')), float(line.get('startY')),
|
||||
float(line.get('endX')), float(line.get('endY')), lw
|
||||
))
|
||||
for poly in us.findall(Q('Polyline')):
|
||||
ld = poly.find(Q('LineDesc'))
|
||||
lw = float(ld.get('lineWidth', '0.05')) if ld is not None else 0.05
|
||||
pts = []
|
||||
pb = poly.find(Q('PolyBegin'))
|
||||
if pb is not None:
|
||||
pts.append((float(pb.get('x')), float(pb.get('y'))))
|
||||
for ps in poly.findall(Q('PolyStepSegment')):
|
||||
pts.append((float(ps.get('x')), float(ps.get('y'))))
|
||||
for i in range(len(pts) - 1):
|
||||
layer_lines[layer].append((*pts[i], *pts[i+1], lw))
|
||||
|
||||
# Through-holes (F.Cu_B.Cu layer)
|
||||
if layer == 'F.Cu_B.Cu':
|
||||
for s in sets:
|
||||
if untag(s) != 'Set':
|
||||
continue
|
||||
for hole in s.iter(Q('Hole')):
|
||||
through_holes.append((
|
||||
float(hole.get('x')), float(hole.get('y')),
|
||||
float(hole.get('diameter'))
|
||||
))
|
||||
|
||||
return {
|
||||
'pads': pads,
|
||||
'upoly_defs': upoly_defs,
|
||||
'ucircle_defs': ucircle_defs,
|
||||
'std_prims': std_prims,
|
||||
'layer_upolys': layer_upolys,
|
||||
'layer_lines': layer_lines,
|
||||
'through_holes': through_holes,
|
||||
'line_widths': line_widths,
|
||||
}
|
||||
|
||||
|
||||
def bbox(pts):
|
||||
xs = [p[0] for p in pts]
|
||||
ys = [p[1] for p in pts]
|
||||
return min(xs), max(xs), min(ys), max(ys)
|
||||
|
||||
|
||||
def upoly_is_fpc(pts):
|
||||
"""FPC pads span ~3mm in x (from ~29 to ~32)."""
|
||||
xmin, xmax, _, _ = bbox(pts)
|
||||
return (xmax - xmin) > 2.5
|
||||
|
||||
|
||||
def upoly_center_size(pts):
|
||||
xmin, xmax, ymin, ymax = bbox(pts)
|
||||
cx = (xmin + xmax) / 2
|
||||
cy = (ymin + ymax) / 2
|
||||
w = xmax - xmin
|
||||
h = ymax - ymin
|
||||
return cx, cy, w, h
|
||||
|
||||
|
||||
def generate_kicad(data, out_path):
|
||||
pads = data['pads']
|
||||
upoly_defs = data['upoly_defs']
|
||||
layer_upolys = data['layer_upolys']
|
||||
layer_lines = data['layer_lines']
|
||||
through_holes = data['through_holes']
|
||||
|
||||
# KiCad standard layer mapping (for 2-layer board)
|
||||
kicad_layers = [
|
||||
(0, 'F.Cu', 'signal'),
|
||||
(31, 'B.Cu', 'signal'),
|
||||
(35, 'F.Paste', 'user'),
|
||||
(34, 'B.Paste', 'user'),
|
||||
(37, 'F.SilkS', 'user', 'F.Silkscreen'),
|
||||
(36, 'B.SilkS', 'user', 'B.Silkscreen'),
|
||||
(39, 'F.Mask', 'user'),
|
||||
(38, 'B.Mask', 'user'),
|
||||
(44, 'Edge.Cuts', 'user'),
|
||||
(45, 'Margin', 'user'),
|
||||
(47, 'F.CrtYd', 'user', 'F.Courtyard'),
|
||||
(46, 'B.CrtYd', 'user', 'B.Courtyard'),
|
||||
(49, 'F.Fab', 'user', 'F.Fabrication'),
|
||||
(48, 'B.Fab', 'user', 'B.Fabrication'),
|
||||
]
|
||||
|
||||
lines = []
|
||||
lines.append('(kicad_pcb')
|
||||
lines.append('\t(version 20241229)')
|
||||
lines.append('\t(generator "ipc2581_to_kicad")')
|
||||
lines.append('\t(generator_version "2.0")')
|
||||
lines.append('\t(general')
|
||||
lines.append('\t\t(thickness 1.6)')
|
||||
lines.append('\t\t(legacy_teardrops no)')
|
||||
lines.append('\t)')
|
||||
lines.append('\t(paper "A4")')
|
||||
lines.append('\t(title_block')
|
||||
lines.append('\t\t(title "SOIC-16-N to 8-pin FPC Flex Adapter")')
|
||||
lines.append('\t\t(date "2026-05-28")')
|
||||
lines.append('\t\t(rev "1.0")')
|
||||
lines.append('\t)')
|
||||
|
||||
# Layers
|
||||
lines.append('\t(layers')
|
||||
for layer_def in kicad_layers:
|
||||
if len(layer_def) == 4:
|
||||
lines.append(f'\t\t({layer_def[0]} "{layer_def[1]}" {layer_def[2]} "{layer_def[3]}")')
|
||||
else:
|
||||
lines.append(f'\t\t({layer_def[0]} "{layer_def[1]}" {layer_def[2]})')
|
||||
lines.append('\t)')
|
||||
|
||||
lines.append('\t(net 0 "")')
|
||||
lines.append('\t(net 1 "Net-(Pad1)")')
|
||||
lines.append('')
|
||||
|
||||
# --- Board outline (Edge.Cuts) ---
|
||||
# Board outline
|
||||
edge_seen = set()
|
||||
for (x1, y1, x2, y2, lw) in layer_lines.get('Edge.Cuts', []):
|
||||
key = (round(x1, 4), round(y1, 4), round(x2, 4), round(y2, 4))
|
||||
key2 = (round(x2, 4), round(y2, 4), round(x1, 4), round(y1, 4))
|
||||
if key in edge_seen or key2 in edge_seen:
|
||||
continue
|
||||
edge_seen.add(key)
|
||||
lines.append(f'\t(gr_line')
|
||||
lines.append(f'\t\t(start {x1:.6f} {y1:.6f})')
|
||||
lines.append(f'\t\t(end {x2:.6f} {y2:.6f})')
|
||||
lines.append(f'\t\t(stroke (width 0.0500) (type solid))')
|
||||
lines.append(f'\t\t(layer "Edge.Cuts")')
|
||||
lines.append(f'\t)')
|
||||
|
||||
lines.append('')
|
||||
|
||||
# --- Silkscreen lines ---
|
||||
# Silkscreen
|
||||
silk_lines = layer_lines.get('F.SilkS', [])
|
||||
# Deduplicate (some lines appear twice due to duplicate sets)
|
||||
seen = set()
|
||||
for (x1, y1, x2, y2, lw) in silk_lines:
|
||||
key = (round(x1, 4), round(y1, 4), round(x2, 4), round(y2, 4))
|
||||
key2 = (round(x2, 4), round(y2, 4), round(x1, 4), round(y1, 4))
|
||||
if key in seen or key2 in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
lw = max(lw, 0.01)
|
||||
lines.append(f'\t(gr_line')
|
||||
lines.append(f'\t\t(start {x1:.6f} {y1:.6f})')
|
||||
lines.append(f'\t\t(end {x2:.6f} {y2:.6f})')
|
||||
lines.append(f'\t\t(stroke (width {lw:.4f}) (type solid))')
|
||||
lines.append(f'\t\t(layer "F.SilkS")')
|
||||
lines.append(f'\t)')
|
||||
|
||||
lines.append('')
|
||||
|
||||
# --- Footprint ---
|
||||
lines.append('\t(footprint "soic16-fpc-adapter"')
|
||||
lines.append('\t\t(layer "F.Cu")')
|
||||
lines.append('\t\t(at 0 0)')
|
||||
lines.append('\t\t(fp_text reference "U?" (at 21 -13.67) (layer "F.SilkS")')
|
||||
lines.append('\t\t\t(effects (font (size 1 1) (thickness 0.15)))')
|
||||
lines.append('\t\t)')
|
||||
lines.append('\t\t(fp_text value "SOIC16-FPC-Adapter" (at 21 -19) (layer "F.Fab")')
|
||||
lines.append('\t\t\t(effects (font (size 1 1) (thickness 0.15)))')
|
||||
lines.append('\t\t)')
|
||||
|
||||
# -- Through-hole castellation pads (TSSOP pads) --
|
||||
# Collect unique hole positions and match to pad names
|
||||
lines.append('')
|
||||
# Copper pad diameter = 0.636396mm (from UCIRCLE_1), drill = 0.45mm
|
||||
copper_d = 0.636396
|
||||
drill_d = 0.45
|
||||
|
||||
# Build a map from (x,y) to pin name
|
||||
pad_pos_to_pin = {}
|
||||
for pin, p in pads.items():
|
||||
key = (round(p['x'], 3), round(p['y'], 3))
|
||||
pad_pos_to_pin[key] = pin
|
||||
|
||||
hole_set = set()
|
||||
for (hx, hy, hd) in through_holes:
|
||||
key = (round(hx, 3), round(hy, 3))
|
||||
if key in hole_set:
|
||||
continue
|
||||
hole_set.add(key)
|
||||
pin = pad_pos_to_pin.get(key, '?')
|
||||
lines.append(f'\t\t(pad "{pin}" thru_hole circle')
|
||||
lines.append(f'\t\t\t(at {hx:.6f} {hy:.6f})')
|
||||
lines.append(f'\t\t\t(size {copper_d:.6f} {copper_d:.6f})')
|
||||
lines.append(f'\t\t\t(drill {drill_d:.3f})')
|
||||
lines.append(f'\t\t\t(layers "*.Cu" "*.Mask")')
|
||||
lines.append(f'\t\t)')
|
||||
|
||||
# -- SMD FPC finger pads --
|
||||
lines.append('')
|
||||
# Collect FPC pads from F.Cu UPOLY placements
|
||||
fpc_pads_added = set()
|
||||
for (uid, ox, oy) in layer_upolys.get('F.Cu', []):
|
||||
if uid not in upoly_defs:
|
||||
continue
|
||||
pts = upoly_defs[uid]
|
||||
if not upoly_is_fpc(pts):
|
||||
continue
|
||||
cx, cy, w, h = upoly_center_size(pts)
|
||||
cx += ox
|
||||
cy += oy
|
||||
# Find matching pin name
|
||||
key = (round(cx, 3), round(cy, 3))
|
||||
# FPC pad center y is between ymin and ymax of the upoly
|
||||
# Match to pad by proximity
|
||||
pin = '?'
|
||||
for pname, p in pads.items():
|
||||
if pname.startswith('FPC') and abs(p['y'] - cy) < 0.01 and abs(p['x'] - cx) < 0.1:
|
||||
pin = pname
|
||||
break
|
||||
if pin in fpc_pads_added:
|
||||
continue
|
||||
fpc_pads_added.add(pin)
|
||||
lines.append(f'\t\t(pad "{pin}" smd rect')
|
||||
lines.append(f'\t\t\t(at {cx:.6f} {cy:.6f})')
|
||||
lines.append(f'\t\t\t(size {w:.6f} {h:.6f})')
|
||||
lines.append(f'\t\t\t(layers "F.Cu" "F.Paste" "F.Mask")')
|
||||
lines.append(f'\t\t)')
|
||||
|
||||
lines.append('\t)') # end footprint
|
||||
lines.append('')
|
||||
|
||||
# --- Copper fills as gr_poly on F.Cu and B.Cu ---
|
||||
# Only output non-FPC (castellation) copper fills to avoid clutter
|
||||
layer_map = {'F.Cu': 'F.Cu', 'B.Cu': 'B.Cu'}
|
||||
for ipc_layer, kicad_layer in layer_map.items():
|
||||
upoly_list = layer_upolys.get(ipc_layer, [])
|
||||
written = set()
|
||||
for (uid, ox, oy) in upoly_list:
|
||||
if uid not in upoly_defs:
|
||||
continue
|
||||
pts = upoly_defs[uid]
|
||||
if upoly_is_fpc(pts):
|
||||
continue # FPC pads are already in footprint
|
||||
if uid in written:
|
||||
continue
|
||||
written.add(uid)
|
||||
cx, cy, w, h = upoly_center_size(pts)
|
||||
# Output as filled polygon
|
||||
lines.append(f'\t(gr_poly')
|
||||
lines.append(f'\t\t(pts')
|
||||
# Use simplified rect for octagonal pads to keep file clean
|
||||
hw = w / 2
|
||||
hh = h / 2
|
||||
px = cx + ox
|
||||
py = cy + oy
|
||||
lines.append(f'\t\t\t(xy {px-hw:.4f} {py-hh:.4f})')
|
||||
lines.append(f'\t\t\t(xy {px+hw:.4f} {py-hh:.4f})')
|
||||
lines.append(f'\t\t\t(xy {px+hw:.4f} {py+hh:.4f})')
|
||||
lines.append(f'\t\t\t(xy {px-hw:.4f} {py+hh:.4f})')
|
||||
lines.append(f'\t\t)')
|
||||
lines.append(f'\t\t(stroke (width 0) (type solid))')
|
||||
lines.append(f'\t\t(fill solid)')
|
||||
lines.append(f'\t\t(layer "{kicad_layer}")')
|
||||
lines.append(f'\t)')
|
||||
|
||||
lines.append(')') # end kicad_pcb
|
||||
|
||||
with open(out_path, 'w') as f:
|
||||
f.write('\n'.join(lines) + '\n')
|
||||
print(f"Written: {out_path}")
|
||||
print(f" Edge.Cuts lines: {len(edge_seen)}")
|
||||
print(f" Silkscreen lines: {len(seen)}")
|
||||
print(f" Through-holes: {len(hole_set)}")
|
||||
print(f" FPC SMD pads: {len(fpc_pads_added)}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import os
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
ipc_path = os.path.join(script_dir, IPC_FILE)
|
||||
out_path = os.path.join(script_dir, OUT_FILE)
|
||||
data = parse_ipc(ipc_path)
|
||||
print(f"Parsed: {len(data['pads'])} pads, "
|
||||
f"{sum(len(v) for v in data['layer_upolys'].values())} upoly placements, "
|
||||
f"{len(data['through_holes'])} holes")
|
||||
generate_kicad(data, out_path)
|
||||
Reference in New Issue
Block a user