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)
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user