Committed vias were checked for hole spacing, but a route's own vias are not committed until it is done, so a route hopping layers (more of them on 6 layers) could put two of its vias 0.41 mm apart, as th-eda's 6-layer flight controller showed. A via is now also checked against layer changes earlier on its own route.
137 lines
6.4 KiB
Python
137 lines
6.4 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)
|
|
|
|
|
|
def test_a_route_keeps_its_own_vias_apart():
|
|
# a route that changed layer at (10, 10) may not change layer again one
|
|
# cell away; the same cell is the same through via, and far is fine
|
|
router, _s = _router(_THT)
|
|
came_from = {(10, 10, 1): (10, 10, 0), (11, 10, 1): (10, 10, 1), (12, 10, 1): (11, 10, 1)}
|
|
assert router._near_path_via((12, 10, 1), (11, 10), came_from)
|
|
assert not router._near_path_via((12, 10, 1), (10, 10), came_from)
|
|
assert not router._near_path_via((12, 10, 1), (40, 10), came_from)
|