Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83ad0f8ef7 | ||
|
|
6e852a929b | ||
|
|
f56bb10996 | ||
|
|
8f656006a6 | ||
|
|
6cc2406583 | ||
|
|
30297e94dc |
@@ -23,6 +23,7 @@ the containment invariant, and pads are centre-symmetric in practice).
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
|
||||
from freeroute.dsn.model import DsnBoard
|
||||
from freeroute.dsn.shapes import Circle, Path, Polygon, Rectangle, Shape
|
||||
@@ -38,6 +39,9 @@ from .transform import CoordinateTransform
|
||||
|
||||
__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:
|
||||
"""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)
|
||||
pad = _pad_tile(max(padstack.shapes, key=_shape_extent), transform)
|
||||
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(
|
||||
Via(
|
||||
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,
|
||||
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
|
||||
layers: list[int] = field(default_factory=list)
|
||||
shape: object = None
|
||||
#: drill diameter in board units, when the padstack name gives it
|
||||
drill: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -215,7 +215,8 @@ def route_board_exact(
|
||||
# connections of one net that end on the same plane cell share its
|
||||
# via: write it once, and never on top of a via already on the board
|
||||
acc.vias.extend(
|
||||
v for v in dict.fromkeys(vias) if v not in acc.vias and (net_no, v) not in fixed_vias
|
||||
v for v in dict.fromkeys(vias)
|
||||
if v not in acc.vias and not _on_fixed_via(net_no, v, fixed_vias, via_half)
|
||||
)
|
||||
for box, box_layers in boxes:
|
||||
oid = next(owner)
|
||||
@@ -645,6 +646,15 @@ def _end_in_rule_area(tree, wires, half_width) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _on_fixed_via(net_no, point, fixed_vias, via_half) -> bool:
|
||||
"""A routed via that reuses a via already on the board: the grid put it
|
||||
on the fixed via's cell, a little off its centre, and it is not written."""
|
||||
return any(
|
||||
net == net_no and abs(loc.x - point.x) <= via_half and abs(loc.y - point.y) <= via_half
|
||||
for net, loc in fixed_vias
|
||||
)
|
||||
|
||||
|
||||
def _copper_boxes(wires, vias, half_width, via_layers, via_half=None):
|
||||
via_half = via_half or half_width
|
||||
boxes: list[tuple[IntBox, frozenset[int]]] = []
|
||||
|
||||
@@ -257,6 +257,7 @@ class GridRouter:
|
||||
# an own-net via at the very same cell, which it simply reuses.
|
||||
drill = via_drill if via_drill else 2 * self.via_half
|
||||
need = drill + max(hole_to_hole, 0)
|
||||
self._via_drill, self._hole_gap, self._hole_need = drill, max(hole_to_hole, 0), need
|
||||
reach = math.ceil(need / self.step)
|
||||
self._hole_offsets = [
|
||||
(dx, dy)
|
||||
@@ -265,7 +266,10 @@ class GridRouter:
|
||||
if (dx or dy) and math.hypot(dx, dy) * self.step < need
|
||||
]
|
||||
self._via_nets: dict[Cell, dict[int, int]] = {} # routed via centres
|
||||
self._via_centres: list[IntPoint] | None = None # cache for _hole_clear_at
|
||||
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()
|
||||
|
||||
if layers is None:
|
||||
@@ -299,6 +303,24 @@ class GridRouter:
|
||||
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_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
|
||||
# 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.
|
||||
self._tht_holes: list[tuple[IntPoint, int]] = []
|
||||
self._tht_block: set[Cell] = set()
|
||||
for pin in board.get_pins():
|
||||
if pin.shape is None or sum(1 for layer in pin.layers if layer in self.layers) < 2:
|
||||
continue
|
||||
box = pin.shape.bounding_box()
|
||||
radius = min(box.ur.x - box.ll.x, box.ur.y - box.ll.y) // 2
|
||||
reach = radius + self._via_drill // 2 + self._hole_gap
|
||||
self._tht_holes.append((pin.location, reach))
|
||||
ring = IntBox(pin.location.x, pin.location.y, pin.location.x, pin.location.y)
|
||||
for cell in self._cells_in_box(ring, reach):
|
||||
self._tht_block.add(cell)
|
||||
|
||||
# --- grid helpers -------------------------------------------------------
|
||||
|
||||
@@ -516,6 +538,26 @@ class GridRouter:
|
||||
return False
|
||||
return self._holes_clear(cell, net_no)
|
||||
|
||||
def _near_path_via(self, node: Node, cell: Cell, came_from: dict) -> bool:
|
||||
"""A layer change earlier on the route that reaches ``node`` sits
|
||||
within hole spacing of ``cell`` (and is not the same through via).
|
||||
Committed vias are checked elsewhere; a route's own are not yet
|
||||
committed while it is searched. Looks back far enough to cover the
|
||||
hole spacing twice over, not along the whole route."""
|
||||
limit = 4 * (math.ceil(self._hole_need / self.step) + 1)
|
||||
need = self._hole_need / self.step
|
||||
steps = 0
|
||||
while node in came_from and steps < limit:
|
||||
prev = came_from[node]
|
||||
changes_layer = prev[:2] == node[:2] and prev[2] != node[2]
|
||||
if changes_layer and node[:2] != cell and (
|
||||
math.hypot(node[0] - cell[0], node[1] - cell[1]) < need
|
||||
):
|
||||
return True
|
||||
node = prev
|
||||
steps += 1
|
||||
return False
|
||||
|
||||
def _via_nets_at(self, cell: Cell) -> set[int]:
|
||||
nets = {net for net, n in self._via_nets.get(cell, {}).items() if n > 0}
|
||||
fixed = self._fixed_vias.get(cell)
|
||||
@@ -524,7 +566,16 @@ class GridRouter:
|
||||
return nets
|
||||
|
||||
def _holes_clear(self, cell: Cell, net_no: int) -> bool:
|
||||
"""No via centre within hole-to-hole reach, but an own-net one here."""
|
||||
"""No via centre within hole-to-hole reach, but an own-net one here,
|
||||
and no through-hole pad's hole within reach at all."""
|
||||
if cell in self._tht_block:
|
||||
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}:
|
||||
return False
|
||||
cx, cy = cell
|
||||
@@ -633,6 +684,8 @@ class GridRouter:
|
||||
pin = ends.get(node)
|
||||
if ok and isinstance(pin, Pin) and self._walled(node):
|
||||
ok = self._via_fits_at(pin.location, net_no)
|
||||
if ok and self._near_path_via(source, cell, came_from):
|
||||
ok = False # too close to a via earlier on this same route
|
||||
if ok:
|
||||
self._relax(source, target, g_via, goal_cell, g_score, came_from, open_heap)
|
||||
continue
|
||||
@@ -644,6 +697,8 @@ class GridRouter:
|
||||
ok = via_ok.get(cell)
|
||||
if ok is None:
|
||||
ok = via_ok[cell] = self._via_placeable(cell, net_no, conns)
|
||||
if ok and self._near_path_via(current, cell, came_from):
|
||||
ok = False
|
||||
else:
|
||||
ok = True
|
||||
if ok:
|
||||
@@ -717,7 +772,23 @@ class GridRouter:
|
||||
]
|
||||
copper += [(box, net) for net, _, box in self.board.get_fixed_copper()]
|
||||
self._static_copper = copper
|
||||
return not any(net != net_no and box.overlaps(grown) for box, net in self._static_copper)
|
||||
if any(net != net_no and box.overlaps(grown) for box, net in self._static_copper):
|
||||
return False
|
||||
return self._hole_clear_at(point)
|
||||
|
||||
def _hole_clear_at(self, point: IntPoint) -> bool:
|
||||
"""Hole spacing measured exactly from ``point`` to every via and
|
||||
through-hole pad (a via moved into a pad is not on its cell's centre)."""
|
||||
need = self._hole_need
|
||||
if self._via_centres is None: # rebuilt only after vias change
|
||||
self._via_centres = [loc for _net, loc in self._fixed_via_points] + [
|
||||
self._cell_center(c) for c, count in self._via_nets.items() if any(count.values())
|
||||
]
|
||||
if any(math.hypot(c.x - point.x, c.y - point.y) < need for c in self._via_centres):
|
||||
return False
|
||||
return not any(
|
||||
math.hypot(loc.x - point.x, loc.y - point.y) < reach for loc, reach in self._tht_holes
|
||||
)
|
||||
|
||||
def _stub_cells(self, pin_location: IntPoint, cell: Cell, half: int | None = None) -> list[Cell]:
|
||||
"""Cells where another net's trace would come too close to the stub."""
|
||||
@@ -993,6 +1064,8 @@ class GridRouter:
|
||||
for cell in conn.via_cells:
|
||||
count = self._via_nets.setdefault(cell, {})
|
||||
count[conn.net_no] = count.get(conn.net_no, 0) + 1
|
||||
if conn.via_cells:
|
||||
self._via_centres = None
|
||||
for cell, layer in conn.trace_cells:
|
||||
self._cell_owner[(*cell, layer)] = conn.index
|
||||
conn.half = half = self._conn_half(conn)
|
||||
@@ -1071,6 +1144,8 @@ class GridRouter:
|
||||
count = self._via_nets.get(cell)
|
||||
if count and count.get(conn.net_no):
|
||||
count[conn.net_no] -= 1
|
||||
if conn.via_cells:
|
||||
self._via_centres = None
|
||||
conn.via_cells = []
|
||||
conn.wires = []
|
||||
conn.vias = []
|
||||
|
||||
@@ -91,3 +91,18 @@ def test_a_fanned_out_signal_is_routed_from_its_via():
|
||||
assert sig in exact.result.routed_net_numbers
|
||||
assert not exact.result.vias.get(sig), "the fan-out via is already there"
|
||||
assert {names[layer] for layer, _ in exact.result.wires[sig]} == {"B.Cu"}
|
||||
|
||||
|
||||
def test_the_session_never_repeats_copper_already_on_the_board():
|
||||
# the JAR writes the DSN's wiring back into its session, and an applier
|
||||
# then adds it a second time on top of itself; freeroute writes only
|
||||
# what it routed
|
||||
from freeroute.route.pipeline import build_exact_routing_result
|
||||
|
||||
fixed_vias = {(5000.0, -11500.0), (10000.0, -11500.0)}
|
||||
fixed_paths = {(5000.0, -10000.0, 5000.0, -11500.0), (10000.0, -10000.0, 10000.0, -11500.0),
|
||||
(2000.0, -2000.0, 8000.0, -2000.0)}
|
||||
result = build_exact_routing_result(parse_dsn(_FANOUT), resolution=2)
|
||||
for net in result.nets:
|
||||
assert not {(v.x, v.y) for v in net.vias} & fixed_vias
|
||||
assert not {tuple(w.coords) for w in net.wires} & fixed_paths
|
||||
|
||||
@@ -54,3 +54,83 @@ def test_connections_sharing_a_plane_via_write_it_once():
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user