Hole spacing to a fixed via measured from where it is, with its own drill
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.
This commit is contained in:
@@ -23,6 +23,7 @@ the containment invariant, and pads are centre-symmetric in practice).
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
import re
|
||||||
|
|
||||||
from freeroute.dsn.model import DsnBoard
|
from freeroute.dsn.model import DsnBoard
|
||||||
from freeroute.dsn.shapes import Circle, Path, Polygon, Rectangle, Shape
|
from freeroute.dsn.shapes import Circle, Path, Polygon, Rectangle, Shape
|
||||||
@@ -38,6 +39,9 @@ from .transform import CoordinateTransform
|
|||||||
|
|
||||||
__all__ = ["build_board"]
|
__all__ = ["build_board"]
|
||||||
|
|
||||||
|
# DSN units per micrometre, for drills KiCad gives in um in via padstack names
|
||||||
|
_DSN_PER_UM = {"um": 1.0, "mm": 0.001, "mil": 1 / 25.4, "inch": 1 / 25400}
|
||||||
|
|
||||||
|
|
||||||
def build_board(dsn: DsnBoard) -> BasicBoard:
|
def build_board(dsn: DsnBoard) -> BasicBoard:
|
||||||
"""Construct a :class:`BasicBoard` from a parsed :class:`DsnBoard`."""
|
"""Construct a :class:`BasicBoard` from a parsed :class:`DsnBoard`."""
|
||||||
@@ -503,10 +507,15 @@ def _insert_wiring(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransf
|
|||||||
location = transform.dsn_to_board_point(via.x, via.y)
|
location = transform.dsn_to_board_point(via.x, via.y)
|
||||||
pad = _pad_tile(max(padstack.shapes, key=_shape_extent), transform)
|
pad = _pad_tile(max(padstack.shapes, key=_shape_extent), transform)
|
||||||
box = pad.bounding_box()
|
box = pad.bounding_box()
|
||||||
|
drill = re.search(r"_([\d.]+):([\d.]+)_um", via.padstack)
|
||||||
|
drill_board = (
|
||||||
|
round(transform.dsn_to_board(float(drill.group(2)) * _DSN_PER_UM.get(dsn.unit, 1.0)))
|
||||||
|
if drill else 0
|
||||||
|
)
|
||||||
board.insert(
|
board.insert(
|
||||||
Via(
|
Via(
|
||||||
net_nos=[numbers[via.net]], fixed=True, padstack_name=via.padstack,
|
net_nos=[numbers[via.net]], fixed=True, padstack_name=via.padstack,
|
||||||
location=location, layers=sorted(layers or signal_layers),
|
drill=drill_board, location=location, layers=sorted(layers or signal_layers),
|
||||||
shape=IntBox(box.ll.x + location.x, box.ll.y + location.y,
|
shape=IntBox(box.ll.x + location.x, box.ll.y + location.y,
|
||||||
box.ur.x + location.x, box.ur.y + location.y),
|
box.ur.x + location.x, box.ur.y + location.y),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ class Via(Item):
|
|||||||
#: layer indices the via's pads occupy, and the largest pad as a box
|
#: layer indices the via's pads occupy, and the largest pad as a box
|
||||||
layers: list[int] = field(default_factory=list)
|
layers: list[int] = field(default_factory=list)
|
||||||
shape: object = None
|
shape: object = None
|
||||||
|
#: drill diameter in board units, when the padstack name gives it
|
||||||
|
drill: int = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -268,6 +268,8 @@ class GridRouter:
|
|||||||
self._via_nets: dict[Cell, dict[int, int]] = {} # routed via centres
|
self._via_nets: dict[Cell, dict[int, int]] = {} # routed via centres
|
||||||
self._via_centres: list[IntPoint] | None = None # cache for _hole_clear_at
|
self._via_centres: list[IntPoint] | None = None # cache for _hole_clear_at
|
||||||
self._fixed_vias: dict[Cell, int] = {}
|
self._fixed_vias: dict[Cell, int] = {}
|
||||||
|
# fixed vias sit anywhere, not on cell centres: exact hole checks
|
||||||
|
self._fixed_holes: list[tuple[IntPoint, int]] = []
|
||||||
self._fixed_via_points: set[tuple[int, IntPoint]] = set()
|
self._fixed_via_points: set[tuple[int, IntPoint]] = set()
|
||||||
|
|
||||||
if layers is None:
|
if layers is None:
|
||||||
@@ -301,6 +303,9 @@ class GridRouter:
|
|||||||
if isinstance(item, Via) and item.fixed and item.net_nos:
|
if isinstance(item, Via) and item.fixed and item.net_nos:
|
||||||
self._fixed_vias[self._cell_of(item.location)] = item.net_nos[0]
|
self._fixed_vias[self._cell_of(item.location)] = item.net_nos[0]
|
||||||
self._fixed_via_points.add((item.net_nos[0], item.location))
|
self._fixed_via_points.add((item.net_nos[0], item.location))
|
||||||
|
# its own drill, from KiCad's padstack name (Via[0-3]_500:250_um)
|
||||||
|
reach = self._via_drill // 2 + (item.drill or self._via_drill) // 2 + self._hole_gap
|
||||||
|
self._fixed_holes.append((item.location, reach))
|
||||||
# Through-hole pads have holes too, and a route changes layer at one
|
# Through-hole pads have holes too, and a route changes layer at one
|
||||||
# for free, so no via (of any net) goes within hole spacing of one.
|
# for free, so no via (of any net) goes within hole spacing of one.
|
||||||
# The DSN has no drill for a pad; its smaller half size stands in.
|
# The DSN has no drill for a pad; its smaller half size stands in.
|
||||||
@@ -545,6 +550,12 @@ class GridRouter:
|
|||||||
and no through-hole pad's hole within reach at all."""
|
and no through-hole pad's hole within reach at all."""
|
||||||
if cell in self._tht_block:
|
if cell in self._tht_block:
|
||||||
return False
|
return False
|
||||||
|
if self._fixed_holes:
|
||||||
|
here = self._cell_center(cell)
|
||||||
|
for loc, reach in self._fixed_holes:
|
||||||
|
d = math.hypot(loc.x - here.x, loc.y - here.y)
|
||||||
|
if d < reach and d > self.step: # beyond its own cell: too close
|
||||||
|
return False
|
||||||
if self._via_nets_at(cell) - {net_no}:
|
if self._via_nets_at(cell) - {net_no}:
|
||||||
return False
|
return False
|
||||||
cx, cy = cell
|
cx, cy = cell
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ def test_connections_sharing_a_plane_via_write_it_once():
|
|||||||
assert len(set(vias)) == len(vias), "a shared via written more than once"
|
assert len(set(vias)) == len(vias), "a shared via written more than once"
|
||||||
|
|
||||||
|
|
||||||
def _router(dsn_text):
|
def _router(dsn_text, via_drill_um=300):
|
||||||
from freeroute.board import build_board
|
from freeroute.board import build_board
|
||||||
from freeroute.route.grid_router import GridRouter
|
from freeroute.route.grid_router import GridRouter
|
||||||
from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn
|
from freeroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn
|
||||||
@@ -66,7 +66,7 @@ def _router(dsn_text):
|
|||||||
return GridRouter(
|
return GridRouter(
|
||||||
build_board(dsn), trace_width=round(_rule_width_dsn(dsn) * s),
|
build_board(dsn), trace_width=round(_rule_width_dsn(dsn) * s),
|
||||||
clearance=round(_rule_clearance_dsn(dsn) * s), resolution=2, orthogonal=True,
|
clearance=round(_rule_clearance_dsn(dsn) * s), resolution=2, orthogonal=True,
|
||||||
via_drill=300 * s, hole_to_hole=250 * s,
|
via_drill=via_drill_um * s, hole_to_hole=250 * s,
|
||||||
), s
|
), s
|
||||||
|
|
||||||
|
|
||||||
@@ -98,3 +98,29 @@ def test_a_via_in_a_pad_keeps_hole_spacing_from_a_fixed_via():
|
|||||||
router, s = _router(text)
|
router, s = _router(text)
|
||||||
assert not router._hole_clear_at(IntPoint(3400 * s, -3000 * s)) # 0.4 mm < 0.55 mm
|
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))
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user