From 2a9c91b025ba0df04adb15950a9c086df14b20e7 Mon Sep 17 00:00:00 2001 From: jaseg Date: Thu, 12 Oct 2023 20:44:52 +0200 Subject: Add coil test board gen --- coil_test_board.py | 377 +++++++++++++++++++++++++++++++++++++++++++ gerbonara/cad/kicad/pcb.py | 1 + twisted_coil_gen_twolayer.py | 17 +- 3 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 coil_test_board.py 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') diff --git a/gerbonara/cad/kicad/pcb.py b/gerbonara/cad/kicad/pcb.py index 9249a9d..f0e6f87 100644 --- a/gerbonara/cad/kicad/pcb.py +++ b/gerbonara/cad/kicad/pcb.py @@ -585,6 +585,7 @@ class Board: self.groups.append(obj) case Footprint(): self.footprints.append(obj) + obj.board = self case _: for elem in self.map_gn_cad(obj): self.add(elem) diff --git a/twisted_coil_gen_twolayer.py b/twisted_coil_gen_twolayer.py index b46fc23..db5e26f 100644 --- a/twisted_coil_gen_twolayer.py +++ b/twisted_coil_gen_twolayer.py @@ -289,7 +289,8 @@ def traces_to_gmsh_mag(traces, mesh_out, bbox, model_name='gerbonara_board', log airbox_physical = gmsh.model.add_physical_group(3, [airbox], name='airbox') trace_physical = gmsh.model.add_physical_group(3, [toplevel_tag], name='trace') - gmsh.model.mesh.setSize([(0, tag) for dim, tag in gmsh.model.getBoundary([(3, toplevel_tag)], recursive=True) if dim == 0], 0.300) + gmsh.model.mesh.setSize([(0, tag) for dim, tag in gmsh.model.getBoundary([(3, toplevel_tag)], recursive=True) if dim == 0], 0.100) + gmsh.model.mesh.setSize([(0, tag) for dim, tag in gmsh.model.getBoundary([(3, substrate)], recursive=True) if dim == 0], 0.200) #interface_tags_top = gmsh.model.getBoundary([(3, contact_tag_top)], oriented=False) #interface_tags_bottom = gmsh.model.getBoundary([(3, contact_tag_bottom)], oriented=False) @@ -308,11 +309,13 @@ def traces_to_gmsh_mag(traces, mesh_out, bbox, model_name='gerbonara_board', log gmsh.option.setNumber('Mesh.MeshSizeFromCurvature', 32) gmsh.option.setNumber('Mesh.Smoothing', 10) - gmsh.option.setNumber('Mesh.Algorithm3D', 10) + gmsh.option.setNumber('Mesh.Algorithm3D', 10) # HXT gmsh.option.setNumber('Mesh.MeshSizeMax', 10) gmsh.option.setNumber('Mesh.MeshSizeMin', 0.08) gmsh.option.setNumber('General.NumThreads', multiprocessing.cpu_count()) + print('Writing geo file') + gmsh.write('/tmp/test.geo_unrolled') print('Meshing') gmsh.model.mesh.generate(dim=3) print('Writing to', str(mesh_out)) @@ -583,25 +586,25 @@ def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_d print(f'Warning: Defaulting to {trace_width:.2f} mm trace width.', file=sys.stderr) if trace_width is None: - if clearance > projected_spiral_pitch: + if round(clearance, 3) > round(projected_spiral_pitch, 3): raise click.ClickException(f'Error: Given clearance of {clearance:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.') trace_width = projected_spiral_pitch - clearance print(f'Calculated trace width for {clearance:.2f} mm clearance is {trace_width:.2f} mm.', file=sys.stderr) elif clearance is None: - if trace_width > projected_spiral_pitch: + if round(trace_width, 2) > round(projected_spiral_pitch, 2): raise click.ClickException(f'Error: Given trace width of {trace_width:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.') clearance = projected_spiral_pitch - trace_width print(f'Calculated clearance for {trace_width:.2f} mm trace width is {clearance:.2f} mm.', file=sys.stderr) else: - if trace_width > projected_spiral_pitch: + if round(trace_width, 2) > round(projected_spiral_pitch, 2): raise click.ClickException(f'Error: Given trace width of {trace_width:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.') clearance_actual = projected_spiral_pitch - trace_width - if clearance_actual < clearance: + if round(clearance_actual, 3) < round(clearance, 3): raise click.ClickException(f'Error: Actual clearance for {trace_width:.2f} mm trace is {clearance_actual:.2f} mm, which is lower than the given clearance of {clearance:.2f} mm.') - if via_diameter < trace_width: + if round(via_diameter, 2) < round(trace_width, 2): print(f'Clipping via diameter from {via_diameter:.2f} mm to trace width of {trace_width:.2f} mm.', file=sys.stderr) via_diameter = trace_width -- cgit