A fixed via sits anywhere, not on a cell centre, and may have a bigger drill than the router's via; checking hole spacing between cell centres with one drill let the flight controller's routed 0.2 mm via come 0.19 mm from a hand-placed 0.25 mm one. The board now carries each fixed via's drill (from KiCad's padstack name), and cells are checked against fixed vias exactly.
127 lines
5.9 KiB
Python
127 lines
5.9 KiB
Python
"""Vias keep hole-to-hole spacing, even within one net."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import itertools
|
|
import math
|
|
|
|
from freeroute.dsn import parse_dsn
|
|
from freeroute.route.pipeline import route_dsn_board_exact
|
|
|
|
# four GND pads 0.5 mm apart over a GND plane: each drops a via to the plane
|
|
_DSN = """(pcb holes (parser) (resolution um 10) (unit um)
|
|
(structure
|
|
(layer F.Cu (type signal)) (layer In1.Cu (type power)) (layer In2.Cu (type power))
|
|
(layer B.Cu (type signal))
|
|
(boundary (path pcb 0 0 0 20000 0 20000 -20000 0 -20000 0 0))
|
|
(plane GND (polygon In1.Cu 0 500 -500 19500 -500 19500 -19500 500 -19500 500 -500))
|
|
(via "Via[0-3]_500:300_um") (rule (width 150) (clearance 150)))
|
|
(placement (component S (place G1 9000 -10000 front 0) (place G2 9500 -10000 front 0)
|
|
(place G3 10000 -10000 front 0) (place G4 10500 -10000 front 0)))
|
|
(library
|
|
(image S (pin smd 1 0 0))
|
|
(padstack smd (shape (rect F.Cu -150 -300 150 300)) (attach off))
|
|
(padstack "Via[0-3]_500:300_um" (shape (circle F.Cu 500)) (shape (circle B.Cu 500)) (attach off)))
|
|
(network (net GND (pins G1-1 G2-1 G3-1 G4-1))))"""
|
|
|
|
|
|
def test_a_nets_own_vias_keep_hole_to_hole_spacing():
|
|
dsn = parse_dsn(_DSN)
|
|
exact, scale, _ = route_dsn_board_exact(dsn, resolution=2)
|
|
assert exact.result.failed == []
|
|
vias = exact.result.vias[1]
|
|
assert len(set(vias)) == len(vias), "a via written twice"
|
|
need = (300 + 250) * scale # drill + 0.25 mm, centre to centre
|
|
for a, b in itertools.combinations(vias, 2):
|
|
assert math.hypot(a.x - b.x, a.y - b.y) >= need, f"vias at {a} and {b} too close"
|
|
|
|
|
|
def test_connections_sharing_a_plane_via_write_it_once():
|
|
# eight GND pads 0.4 mm apart: the hole spacing makes several of them
|
|
# end on the same plane via, which must come out once, not once each
|
|
pads = " ".join(f"(place G{i} {9000 + 400 * i} -10000 front 0)" for i in range(8))
|
|
pins = " ".join(f"G{i}-1" for i in range(8))
|
|
dsn = parse_dsn(
|
|
_DSN.replace(
|
|
"(place G1 9000 -10000 front 0) (place G2 9500 -10000 front 0)\n"
|
|
" (place G3 10000 -10000 front 0) (place G4 10500 -10000 front 0)",
|
|
pads,
|
|
)
|
|
.replace("(pins G1-1 G2-1 G3-1 G4-1)", f"(pins {pins})")
|
|
.replace("(rect F.Cu -150 -300 150 300)", "(rect F.Cu -125 -300 125 300)")
|
|
)
|
|
exact, _scale, _ = route_dsn_board_exact(dsn, resolution=2)
|
|
vias = exact.result.vias[1]
|
|
assert len(vias) < 8, "no via is shared, so this board no longer tests sharing"
|
|
assert len(set(vias)) == len(vias), "a shared via written more than once"
|
|
|
|
|
|
def _router(dsn_text, via_drill_um=300):
|
|
from freeroute.board import build_board
|
|
from freeroute.route.grid_router import GridRouter
|
|
from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn
|
|
|
|
dsn = parse_dsn(dsn_text)
|
|
s = dsn.resolution.value
|
|
return GridRouter(
|
|
build_board(dsn), trace_width=round(_rule_width_dsn(dsn) * s),
|
|
clearance=round(_rule_clearance_dsn(dsn) * s), resolution=2, orthogonal=True,
|
|
via_drill=via_drill_um * s, hole_to_hole=250 * s,
|
|
), s
|
|
|
|
|
|
_THT = """(pcb tht (parser) (resolution um 10) (unit um)
|
|
(structure (layer F.Cu (type signal)) (layer B.Cu (type signal))
|
|
(boundary (path pcb 0 0 0 20000 0 20000 -20000 0 -20000 0 0))
|
|
(via "Via[0-1]_500:300_um") (rule (width 150) (clearance 150)))
|
|
(placement (component T (place J1 10000 -10000 front 0)))
|
|
(library (image T (pin tht 1 0 0))
|
|
(padstack tht (shape (circle F.Cu 1700)) (shape (circle B.Cu 1700)) (attach off))
|
|
(padstack "Via[0-1]_500:300_um" (shape (circle F.Cu 500)) (shape (circle B.Cu 500)) (attach off)))
|
|
(network (net SIG (pins J1-1))))"""
|
|
|
|
|
|
def test_no_via_goes_near_a_through_hole_pads_hole_even_of_its_own_net():
|
|
router, s = _router(_THT)
|
|
pin = router._cell_of(router.board.get_pins()[0].location)
|
|
assert not router._holes_clear(pin, 1) # own net, right on the pad
|
|
far = (pin[0] + 40, pin[1])
|
|
assert router._holes_clear(far, 1)
|
|
|
|
|
|
def test_a_via_in_a_pad_keeps_hole_spacing_from_a_fixed_via():
|
|
from freeroute.geometry import IntPoint
|
|
|
|
text = _THT.replace("(network (net SIG (pins J1-1))))",
|
|
"(network (net SIG (pins J1-1)))\n"
|
|
" (wiring (via \"Via[0-1]_500:300_um\" 3000 -3000 (net SIG) (type route))))")
|
|
router, s = _router(text)
|
|
assert not router._hole_clear_at(IntPoint(3400 * s, -3000 * s)) # 0.4 mm < 0.55 mm
|
|
assert router._hole_clear_at(IntPoint(3600 * s, -3000 * s))
|
|
|
|
|
|
def test_hole_spacing_from_a_fixed_via_is_measured_from_where_it_really_is():
|
|
# a fixed via off the grid with its own 0.25 mm drill, and the router's
|
|
# via a smaller 0.2 mm one (the flight controller's case): a new via needs
|
|
# its centre 0.1 + 0.125 + 0.25 = 0.475 mm away, measured exactly, where
|
|
# one rule for all vias gave 0.2 + 0.25 = 0.45 from the cell centres
|
|
text = _THT.replace("(network (net SIG (pins J1-1))))",
|
|
"(network (net SIG (pins J1-1)) (net GND))\n"
|
|
" (wiring (via \"Via[0-1]_500:250_um\" 3030 -3030 (net GND) (type route))))")
|
|
text = text.replace(
|
|
'(attach off)))\n (network',
|
|
'(attach off))\n (padstack "Via[0-1]_500:250_um" (shape (circle F.Cu 500)) '
|
|
'(shape (circle B.Cu 500)) (attach off)))\n (network',
|
|
)
|
|
router, s = _router(text, via_drill_um=200)
|
|
via = next(v for v in router.board.get_items() if getattr(v, "drill", 0))
|
|
assert via.drill == 250 * s
|
|
loc = via.location
|
|
for cell in [(x, y) for x in range(router.cols) for y in range(router.rows)]:
|
|
c = router._cell_center(cell)
|
|
d = math.hypot(c.x - loc.x, c.y - loc.y)
|
|
if router.step * 1.5 < d < 470 * s:
|
|
assert not router._holes_clear(cell, 1), f"{d / s:.0f} um from the fixed via"
|
|
elif 560 * s < d < 700 * s and cell not in router._tht_block:
|
|
assert router._holes_clear(cell, 1)
|