summaryrefslogtreecommitdiff
path: root/coil_test_board.py
diff options
context:
space:
mode:
authorjaseg <git@jaseg.de>2023-10-12 20:44:52 +0200
committerjaseg <git@jaseg.de>2023-10-12 20:44:52 +0200
commit2a9c91b025ba0df04adb15950a9c086df14b20e7 (patch)
tree5e3e747ece2a837ec30b1514aed2d24e9a0aa1d8 /coil_test_board.py
parent2d4c40c0f79319a96cc68a484bf82059313b827b (diff)
downloadgerbonara-2a9c91b025ba0df04adb15950a9c086df14b20e7.tar.gz
gerbonara-2a9c91b025ba0df04adb15950a9c086df14b20e7.tar.bz2
gerbonara-2a9c91b025ba0df04adb15950a9c086df14b20e7.zip
Add coil test board gen
Diffstat (limited to 'coil_test_board.py')
-rw-r--r--coil_test_board.py377
1 files changed, 377 insertions, 0 deletions
diff --git a/coil_test_board.py b/coil_test_board.py
new file mode 100644
index 0000000..43b7062
--- /dev/null
+++ b/coil_test_board.py
@@ -0,0 +1,377 @@
+#!/usr/bin/env python3
+
+import math
+import itertools
+import datetime
+import tempfile
+import subprocess
+
+import gerbonara.cad.kicad.pcb as pcb
+import gerbonara.cad.kicad.footprints as fp
+import gerbonara.cad.primitives as cad_pr
+import gerbonara.cad.kicad.graphical_primitives as kc_gr
+
+cols = 6
+rows = 4
+
+coil_specs = [
+ {'n': 1, 's': True, 't': 1, 'c': 0.20, 'w': 5.00, 'v': 0.40},
+ {'n': 2, 's': True, 't': 1, 'c': 0.20, 'w': 3.00, 'v': 0.40},
+ {'n': 3, 's': True, 't': 1, 'c': 0.20, 'w': 1.50, 'v': 0.40},
+ {'n': 5, 's': True, 't': 1, 'c': 0.20, 'w': 0.80, 'v': 0.40},
+ {'n': 10, 's': True, 't': 1, 'c': 0.20, 'w': 0.50, 'v': 0.40},
+ {'n': 25, 's': True, 't': 1, 'c': 0.15, 'w': 0.25, 'v': 0.40},
+
+ {'n': 1, 's': False, 't': 1, 'c': 0.20, 'w': 5.00, 'v': 0.40},
+ {'n': 2, 's': False, 't': 1, 'c': 0.20, 'w': 3.00, 'v': 0.40},
+ {'n': 3, 's': False, 't': 1, 'c': 0.20, 'w': 1.50, 'v': 0.40},
+ {'n': 5, 's': False, 't': 1, 'c': 0.20, 'w': 0.80, 'v': 0.40},
+ {'n': 10, 's': False, 't': 1, 'c': 0.20, 'w': 0.50, 'v': 0.40},
+ {'n': 25, 's': False, 't': 1, 'c': 0.15, 'w': 0.25, 'v': 0.40},
+
+ {'n': 1, 's': False, 't': 2, 'c': 0.20, 'w': 5.00, 'v': 0.40},
+ {'n': 2, 's': False, 't': 3, 'c': 0.20, 'w': 3.00, 'v': 0.40},
+ {'n': 3, 's': False, 't': 2, 'c': 0.20, 'w': 1.50, 'v': 0.40},
+ {'n': 5, 's': False, 't': 2, 'c': 0.20, 'w': 0.80, 'v': 0.40},
+ {'n': 10, 's': False, 't': 3, 'c': 0.20, 'w': 0.50, 'v': 0.40},
+ {'n': 25, 's': False, 't': 2, 'c': 0.15, 'w': 0.25, 'v': 0.40},
+
+ {'n': 1, 's': False, 't': 3, 'c': 0.20, 'w': 5.00, 'v': 0.40},
+ {'n': 2, 's': False, 't': 5, 'c': 0.20, 'w': 3.00, 'v': 0.40},
+ {'n': 3, 's': False, 't': 4, 'c': 0.20, 'w': 1.50, 'v': 0.40},
+ {'n': 5, 's': False, 't': 3, 'c': 0.20, 'w': 0.80, 'v': 0.40},
+ {'n': 10, 's': False, 't': 7, 'c': 0.20, 'w': 0.50, 'v': 0.40},
+ {'n': 25, 's': False, 't': 7, 'c': 0.15, 'w': 0.25, 'v': 0.40},
+]
+
+version_string = 'v1.0'
+coil_border = 7 # mm
+cut_gap = 3 # mm
+tooling_border = 10 # mm
+vscore_extra = 10 # mm
+mouse_bite_width = 8 # mm
+mouse_bite_hole_dia = 0.5
+mouse_bite_hole_spacing = 0.3
+hole_offset = 5
+hole_dia = 3.2
+coil_dia = 35 # mm
+pad_offset = 2 # mm
+pad_dia = 2.0 # mm
+pad_length = 3.5 # mm
+pad_drill = 1.1 # mm
+pad_pitch = 2.54 # mm
+v_cuts = False # FIXME DEBUG
+mouse_bites = False # FIXME DEBUG
+coil_pitch = coil_dia + coil_border*2 + cut_gap
+
+total_width = coil_pitch*cols + 2*tooling_border + cut_gap
+total_height = coil_pitch*rows + 2*tooling_border + cut_gap
+
+tile_width = tile_height = coil_dia + 2*coil_border
+drawing_text_size = 2.0
+
+print(f'Calculated board size: {total_width:.2f} * {total_height:.2f} mm')
+print(f'Tile size: {tile_height:.2f} * {tile_height:.2f} mm')
+
+x0, y0 = 100, 100
+
+xy = pcb.XYCoord
+b = pcb.Board.empty_board(page=pcb.PageSettings(page_format='A2'))
+
+b.add(kc_gr.Rectangle(xy(x0, y0), xy(x0+total_width, y0+total_height), layer='Edge.Cuts', stroke=pcb.Stroke(width=0.15)))
+
+def do_line(x0, y0, x1, y1, off_x=0, off_y=0):
+ b.add(kc_gr.Line(xy(x0+off_x, y0+off_y),
+ xy(x1+off_x, y1+off_y),
+ layer='Edge.Cuts', stroke=pcb.Stroke(width=0.15)))
+
+if v_cuts:
+ for y in range(rows):
+ for off_y in [0, tile_height]:
+ y_pos = y0 + tooling_border + cut_gap + off_y + y*coil_pitch
+ do_line(x0 - vscore_extra, y_pos, x0 + total_width + vscore_extra, y_pos)
+ b.add(kc_gr.Text(text='V-score',
+ at=pcb.AtPos(x0 + total_width + vscore_extra + drawing_text_size/2, y_pos, 0),
+ layer=kc_gr.TextLayer('Edge.Cuts'),
+ effects=pcb.TextEffect(
+ font=pcb.FontSpec(size=xy(drawing_text_size, drawing_text_size),
+ thickness=drawing_text_size/10),
+ justify=pcb.Justify(h=pcb.Atom.left))))
+
+
+ for x in range(cols):
+ for off_x in [0, tile_width]:
+ x_pos = x0 + tooling_border + cut_gap + off_x + x*coil_pitch
+ do_line(x_pos, y0 - vscore_extra, x_pos, y0 + total_height + vscore_extra)
+ b.add(kc_gr.Text(text='V-score',
+ at=pcb.AtPos(x_pos, y0 + total_height + vscore_extra + drawing_text_size/2, 90),
+ layer=kc_gr.TextLayer('Edge.Cuts'),
+ effects=pcb.TextEffect(
+ font=pcb.FontSpec(size=xy(drawing_text_size, drawing_text_size),
+ thickness=drawing_text_size/10),
+ justify=pcb.Justify(h=pcb.Atom.right))))
+
+def draw_corner(x0, y0, spokes):
+ right, top, left, bottom = [True if c.lower() in 'y1' else False for c in spokes]
+
+ l = (tile_width - mouse_bite_width)/2 - cut_gap/2
+
+ if right:
+ do_line(cut_gap/2, -cut_gap/2, cut_gap/2 + l, -cut_gap/2, x0, y0)
+ do_line(cut_gap/2, cut_gap/2, cut_gap/2 + l, cut_gap/2, x0, y0)
+ b.add(kc_gr.Arc(start=xy(x0+cut_gap/2+l, y0-cut_gap/2),
+ end=xy(x0+cut_gap/2+l, y0+cut_gap/2),
+ center=xy(x0+cut_gap/2+l, y0),
+ layer='Edge.Cuts',
+ stroke=pcb.Stroke(width=0.15)))
+
+ else:
+ do_line(cut_gap/2, -cut_gap/2, cut_gap/2, cut_gap/2, x0, y0)
+
+ if left:
+ do_line(-cut_gap/2, -cut_gap/2, -cut_gap/2 - l, -cut_gap/2, x0, y0)
+ do_line(-cut_gap/2, cut_gap/2, -cut_gap/2 - l, cut_gap/2, x0, y0)
+ b.add(kc_gr.Arc(end=xy(x0-cut_gap/2-l, y0-cut_gap/2),
+ start=xy(x0-cut_gap/2-l, y0+cut_gap/2),
+ center=xy(x0-cut_gap/2-l, y0),
+ layer='Edge.Cuts',
+ stroke=pcb.Stroke(width=0.15)))
+
+ else:
+ do_line(-cut_gap/2, -cut_gap/2, -cut_gap/2, cut_gap/2, x0, y0)
+
+ if bottom:
+ do_line(-cut_gap/2, cut_gap/2, -cut_gap/2, cut_gap/2 + l, x0, y0)
+ do_line(cut_gap/2, cut_gap/2, cut_gap/2, cut_gap/2 + l, x0, y0)
+ b.add(kc_gr.Arc(end=xy(x0-cut_gap/2, y0+cut_gap/2+l),
+ start=xy(x0+cut_gap/2, y0+cut_gap/2+l),
+ center=xy(x0, y0+cut_gap/2+l),
+ layer='Edge.Cuts',
+ stroke=pcb.Stroke(width=0.15)))
+
+ else:
+ do_line(-cut_gap/2, cut_gap/2, cut_gap/2, cut_gap/2, x0, y0)
+
+ if top:
+ do_line(-cut_gap/2, -cut_gap/2, -cut_gap/2, -cut_gap/2 - l, x0, y0)
+ do_line(cut_gap/2, -cut_gap/2, cut_gap/2, -cut_gap/2 - l, x0, y0)
+ b.add(kc_gr.Arc(start=xy(x0-cut_gap/2, y0-cut_gap/2-l),
+ end=xy(x0+cut_gap/2, y0-cut_gap/2-l),
+ center=xy(x0, y0-cut_gap/2-l),
+ layer='Edge.Cuts',
+ stroke=pcb.Stroke(width=0.15)))
+ else:
+
+ do_line(-cut_gap/2, -cut_gap/2, cut_gap/2, -cut_gap/2, x0, y0)
+
+
+def make_mouse_bite(x, y, rot=0, width=mouse_bite_width, hole_dia=mouse_bite_hole_dia, hole_spacing=mouse_bite_hole_spacing, **kwargs):
+
+ pitch = hole_dia + hole_spacing
+ num_holes = int(math.floor((width - hole_spacing) / pitch))
+
+ actual_spacing = (width - num_holes*hole_dia) / (num_holes + 1)
+
+ f = fp.Footprint(name='mouse_bite', _version=None, generator=None, at=fp.AtPos(x, y, rot), **kwargs)
+ for i in range(num_holes):
+ f.pads.append(fp.Pad(
+ number='1',
+ type=fp.Atom.np_thru_hole,
+ shape=fp.Atom.circle,
+ at=fp.AtPos(-width/2 + actual_spacing + i*pitch + hole_dia/2, 0, 0),
+ size=xy(hole_dia, hole_dia),
+ drill=fp.Drill(diameter=hole_dia),
+ footprint=f))
+ return f
+
+
+def make_hole(x, y, dia, **kwargs):
+ f = fp.Footprint(name='hole', _version=None, generator=None, at=fp.AtPos(x, y, 0), **kwargs)
+ f.pads.append(fp.Pad(
+ number='1',
+ type=fp.Atom.np_thru_hole,
+ shape=fp.Atom.circle,
+ at=fp.AtPos(0, 0, 0),
+ size=xy(dia, dia),
+ drill=fp.Drill(diameter=dia),
+ footprint=f))
+ return f
+
+
+def make_pads(x, y, rot, n, pad_dia, pad_length, drill, pitch, **kwargs):
+ f = fp.Footprint(name=f'conn_gen_01x{n}', _version=None, generator=None, at=fp.AtPos(x, y, rot), **kwargs)
+
+ for i in range(n):
+ f.pads.append(fp.Pad(
+ number=str(i+1),
+ type=fp.Atom.thru_hole,
+ shape=fp.Atom.oval,
+ at=fp.AtPos(-pitch*(n-1)/2 + i*pitch, 0, rot),
+ size=xy(pad_dia, pad_length),
+ drill=fp.Drill(diameter=drill),
+ footprint=f))
+
+ return f
+
+
+corner_x0 = x0 + tooling_border + cut_gap/2
+corner_y0 = y0 + tooling_border + cut_gap/2
+corner_x1 = x0 + total_width - tooling_border - cut_gap/2
+corner_y1 = y0 + total_height - tooling_border - cut_gap/2
+
+# Corners
+draw_corner(corner_x0, corner_y0, 'YNNY')
+draw_corner(corner_x0, corner_y1, 'YYNN')
+draw_corner(corner_x1, corner_y0, 'NNYY')
+draw_corner(corner_x1, corner_y1, 'NYYN')
+
+# T junctions
+for x in range(1, cols):
+ draw_corner(corner_x0 + x*coil_pitch, corner_y0, 'YNYY')
+ draw_corner(corner_x0 + x*coil_pitch, corner_y1, 'YYYN')
+
+for y in range(1, rows):
+ draw_corner(corner_x0, corner_y0 + y*coil_pitch, 'YYNY')
+ draw_corner(corner_x1, corner_y0 + y*coil_pitch, 'NYYY')
+
+# X Junctions
+for x in range(1, cols):
+ for y in range(1, rows):
+ draw_corner(corner_x0 + x*coil_pitch, corner_y0 + y*coil_pitch, 'YYYY')
+
+# Mouse bites
+if mouse_bites:
+ for x in range(0, cols):
+ for y in range(0, rows):
+ tile_x0 = x0 + tooling_border + cut_gap + x*coil_pitch
+ tile_y0 = y0 + tooling_border + cut_gap + y*coil_pitch
+
+ b.add(make_mouse_bite(tile_x0 + tile_width/2, tile_y0, 0))
+ b.add(make_mouse_bite(tile_x0 + tile_width/2, tile_y0 + tile_height, 0))
+ b.add(make_mouse_bite(tile_x0, tile_y0 + tile_height/2, 90))
+ b.add(make_mouse_bite(tile_x0 + tile_width, tile_y0 + tile_height/2, 90))
+
+# Mounting holes
+for x in range(0, cols):
+ for y in range(0, rows):
+ tile_x0 = x0 + tooling_border + cut_gap + x*coil_pitch + tile_width/2
+ tile_y0 = y0 + tooling_border + cut_gap + y*coil_pitch + tile_height/2
+
+ dx = tile_width/2 - hole_offset
+ dy = tile_height/2 - hole_offset
+ b.add(make_hole(tile_x0 - dx, tile_y0 - dy, hole_dia))
+ b.add(make_hole(tile_x0 - dx, tile_y0 + dy, hole_dia))
+ b.add(make_hole(tile_x0 + dx, tile_y0 - dy, hole_dia))
+ b.add(make_hole(tile_x0 + dx, tile_y0 + dy, hole_dia))
+
+# border graphics
+c = 3
+for layer in ['F.SilkS', 'B.SilkS']:
+ b.add(kc_gr.Rectangle(start=xy(x0, y0), end=xy(x0+c, y0+total_height), layer=layer, stroke=pcb.Stroke(width=0),
+ fill=kc_gr.FillMode(pcb.Atom.solid)))
+ b.add(kc_gr.Rectangle(start=xy(x0, y0), end=xy(x0+total_width, y0+c), layer=layer, stroke=pcb.Stroke(width=0),
+ fill=kc_gr.FillMode(pcb.Atom.solid)))
+ b.add(kc_gr.Rectangle(start=xy(x0+total_width-c, y0), end=xy(x0+total_width, y0+total_height), layer=layer, stroke=pcb.Stroke(width=0),
+ fill=kc_gr.FillMode(pcb.Atom.solid)))
+ b.add(kc_gr.Rectangle(start=xy(x0, y0+total_height-c), end=xy(x0+total_width, y0+total_height), layer=layer, stroke=pcb.Stroke(width=0),
+ fill=kc_gr.FillMode(pcb.Atom.solid)))
+
+a = 3
+timestamp = datetime.datetime.now().strftime('%Y-%m-%d')
+b.add(kc_gr.Text(text=f'Planar inductor test panel {version_string} {timestamp} © 2023 Jan Götte, FG KOM, TU Darmstadt',
+ at=pcb.AtPos(x0 + c + a/3, y0 + c + a/3),
+ layer=kc_gr.TextLayer('F.SilkS'),
+ effects=pcb.TextEffect(
+ font=pcb.FontSpec(size=xy(a, a),
+ thickness=a/5),
+ justify=pcb.Justify(h=pcb.Atom.left, v=pcb.Atom.top))))
+
+for index, ((y, x), spec) in enumerate(zip(itertools.product(range(rows), range(cols)), coil_specs), start=1):
+ pass
+ with tempfile.NamedTemporaryFile(suffix='.kicad_mod') as f:
+ tile_x0 = x0 + tooling_border + cut_gap + x*coil_pitch + tile_width/2
+ tile_y0 = y0 + tooling_border + cut_gap + y*coil_pitch + tile_height/2
+
+ for key, alias in {
+ 'inner_diameter': 'id',
+ 'outer_diameter': 'od',
+ 'trace_width': 'w',
+ 'turns': 'n',
+ 'twists': 't',
+ 'clearance': 'c',
+ 'single_layer': 's',
+ 'via_drill': 'v'}.items():
+ if alias in spec:
+ spec[key] = spec.pop(alias)
+
+ if 'via_diameter' not in spec:
+ spec['via_diameter'] = spec['trace_width']
+
+ if 'inner_diameter' not in spec:
+ spec['inner_diameter'] = 15
+
+ if 'outer_diameter' not in spec:
+ spec['outer_diameter'] = 35
+
+ args = ['python', '-m', 'twisted_coil_gen_twolayer']
+ for k, v in spec.items():
+ if not isinstance(v, bool) or v:
+ args.append('--' + k.replace('_', '-'))
+ if v is not True:
+ args.append(str(v))
+ args.append(f.name)
+ subprocess.run(args, check=True)
+
+ coil = fp.Footprint.open_mod(f.name)
+ coil.at = fp.AtPos(tile_x0, tile_y0, 0)
+ b.add(coil)
+
+ t = [f'n={spec["turns"]}',
+ f'{spec["twists"]} twists',
+ f'w={spec["trace_width"]:.2f}mm']
+ if spec.get('single_layer'):
+ t.append('single layer')
+
+ sz = 1.5
+ b.add(kc_gr.Text(text='\\n'.join(t),
+ at=pcb.AtPos(tile_x0, tile_y0),
+ layer=kc_gr.TextLayer('B.SilkS'),
+ effects=pcb.TextEffect(
+ font=pcb.FontSpec(size=xy(sz, sz),
+ thickness=sz/5),
+ justify=pcb.Justify(h=None, v=None, mirror=True))))
+
+ b.add(kc_gr.Text(text=f'{version_string} {timestamp}\\nTile {index}',
+ at=pcb.AtPos(tile_x0, tile_y0 - tile_height/2 + sz),
+ layer=kc_gr.TextLayer('B.SilkS'),
+ effects=pcb.TextEffect(
+ font=pcb.FontSpec(size=xy(sz, sz),
+ thickness=sz/5),
+ justify=pcb.Justify(h=None, v=pcb.Atom.top, mirror=True))))
+
+ b.add(kc_gr.Text(text=f'{index}',
+ at=pcb.AtPos(tile_x0, tile_y0 - tile_height/2 + sz),
+ layer=kc_gr.TextLayer('F.SilkS'),
+ effects=pcb.TextEffect(
+ font=pcb.FontSpec(size=xy(sz, sz),
+ thickness=sz/5),
+ justify=pcb.Justify(h=None, v=pcb.Atom.top, mirror=False))))
+
+ pads_x0 = tile_x0 + tile_width/2 - pad_offset
+ pads = make_pads(pads_x0, tile_y0, 270, 2, pad_dia, pad_length, pad_drill, pad_pitch)
+ b.add(pads)
+
+ w = min(spec.get('trace_width', pad_dia), pad_dia)
+ x, y, _r, _f = pads.pad(2).abs_pos
+ w2 = (x - pad_length/2, y)
+ x, y, _r, _f = pads.pad(1).abs_pos
+ w1 = (x - pad_length/2, y)
+ b.add(cad_pr.Trace(w, pads.pad(1), coil.pad(1), waypoints=[w1], orientation=['cw'], side='top'))
+ b.add(cad_pr.Trace(w, pads.pad(2), coil.pad(2), waypoints=[w2], orientation=['ccw'], side='bottom'))
+
+ k = 3
+ for layer in ['F.SilkS', 'B.SilkS']:
+ b.add(kc_gr.Rectangle(start=xy(x-k/2, y-pad_pitch-k/2), end=xy(x+k/2, y-pad_pitch), layer=layer, stroke=pcb.Stroke(width=0),
+ fill=kc_gr.FillMode(pcb.Atom.solid)))
+
+b.write('coil_test_board.kicad_pcb')