Route to the real board outline, per connection, and out of fine-pitch pins
- The exact engine verified copper per net and dropped a whole net on any conflict: a 25-pin +3V3 net lost every connection to one bad segment. Verify and accept per ratsnest connection instead. - Pad and keepout cells were rounded outward, blocking cells up to half a step clear of the copper; 62 of the 64 pins of a 0.5 mm pitch LQFP could not start a route. Block only cells whose centre is inside the clearance zone, and let a net walk along inside its own pad, which adds no copper. - Routing was bounded by the outline's bounding box, so tracks cut across rounded corners. Keep the DSN boundary as a polygon; the grid blocks cells too close to it (scanline plus per-edge, precomputed for vias) and the exact and room engines reject copper that is. - --edge-clearance MM sets copper-to-edge clearance, e.g. KiCad's min_copper_edge_clearance, which the DSN does not carry. Default 0. On the TerraHawk rc4 power stage the exact engine now matches the FreeRouting JAR: same open count, no DRC errors, with a 0.5 mm edge rule.
This commit is contained in:
@@ -31,7 +31,7 @@ import math
|
||||
|
||||
from freeroute.board.board import BasicBoard
|
||||
from freeroute.board.items import ObstacleArea, Pin
|
||||
from freeroute.geometry import IntPoint
|
||||
from freeroute.geometry import IntBox, IntPoint
|
||||
|
||||
__all__ = ["GridRouter", "RouteResult", "route_board"]
|
||||
|
||||
@@ -59,7 +59,7 @@ class RouteResult:
|
||||
``routed_net_numbers`` are the nets that got any wire or via.
|
||||
"""
|
||||
|
||||
__slots__ = ("wires", "vias", "half_width", "unrouted")
|
||||
__slots__ = ("wires", "vias", "half_width", "unrouted", "connections")
|
||||
|
||||
def __init__(self, half_width: int) -> None:
|
||||
self.wires: dict[int, list[tuple[int, list[IntPoint]]]] = {}
|
||||
@@ -67,6 +67,8 @@ class RouteResult:
|
||||
self.half_width = half_width
|
||||
#: number of ratsnest connections left unrouted
|
||||
self.unrouted = 0
|
||||
#: the same copper per routed connection: (net_no, wires, vias)
|
||||
self.connections: list[tuple[int, list[tuple[int, list[IntPoint]]], list[IntPoint]]] = []
|
||||
|
||||
def add_wire(self, net_no: int, layer: int, path: list[IntPoint]) -> None:
|
||||
self.wires.setdefault(net_no, []).append((layer, path))
|
||||
@@ -132,6 +134,7 @@ class GridRouter:
|
||||
rip_penalty: float = 30.0,
|
||||
orthogonal: bool = False,
|
||||
via_half: int | None = None,
|
||||
edge_clearance: int | None = None,
|
||||
) -> None:
|
||||
self.board = board
|
||||
self.trace_width = max(trace_width, 1)
|
||||
@@ -151,6 +154,8 @@ class GridRouter:
|
||||
# centre via_half + clearance + half_width from the via's centre, so a
|
||||
# via also claims every cell within via_reach of its own.
|
||||
self.via_half = via_half or self.half_width
|
||||
# copper to board edge; KiCad's rule is usually wider than clearance
|
||||
self.edge_clearance = 0 if edge_clearance is None else edge_clearance
|
||||
need = self.via_half + self.clearance + self.half_width
|
||||
self.via_reach = max(0, math.ceil(need / self.step) - 1)
|
||||
|
||||
@@ -166,6 +171,7 @@ class GridRouter:
|
||||
|
||||
# static obstacles
|
||||
self._pad_block: dict[Node, set[int]] = {}
|
||||
self._pad_own: dict[Node, set[int]] = {}
|
||||
self._keepout: set[Node] = set()
|
||||
# per-connection occupancy (owner = connection index)
|
||||
self._cell_owner: dict[Node, int] = {}
|
||||
@@ -185,16 +191,57 @@ class GridRouter:
|
||||
return IntPoint(self.origin_x + cell[0] * self.step, self.origin_y + cell[1] * self.step)
|
||||
|
||||
def _cells_in_box(self, box, margin: int):
|
||||
lo_x = round((box.ll.x - margin - self.origin_x) / self.step)
|
||||
hi_x = round((box.ur.x + margin - self.origin_x) / self.step)
|
||||
lo_y = round((box.ll.y - margin - self.origin_y) / self.step)
|
||||
hi_y = round((box.ur.y + margin - self.origin_y) / self.step)
|
||||
"""Cells whose centre lies strictly inside ``box`` grown by ``margin``.
|
||||
|
||||
A trace centred there would come closer than ``margin`` to the box.
|
||||
Rounding the bounds outward instead blocked cells up to half a step
|
||||
clear of it, which walled in every pin of a 0.5 mm pitch package.
|
||||
"""
|
||||
lo_x = math.floor((box.ll.x - margin - self.origin_x) / self.step) + 1
|
||||
hi_x = math.ceil((box.ur.x + margin - self.origin_x) / self.step) - 1
|
||||
lo_y = math.floor((box.ll.y - margin - self.origin_y) / self.step) + 1
|
||||
hi_y = math.ceil((box.ur.y + margin - self.origin_y) / self.step) - 1
|
||||
for gx in range(max(lo_x, 0), min(hi_x, self.cols - 1) + 1):
|
||||
for gy in range(max(lo_y, 0), min(hi_y, self.rows - 1) + 1):
|
||||
yield gx, gy
|
||||
|
||||
def _outline_blocked(self, reach: int) -> set[Cell]:
|
||||
"""Cells whose centre is outside the outline or within ``reach`` of its edge."""
|
||||
outline = self.board.outline
|
||||
blocked: set[Cell] = set()
|
||||
for gy in range(self.rows): # outside: one scanline per row
|
||||
y = self.origin_y + gy * self.step
|
||||
spans = outline.row_inside_spans(y)
|
||||
for gx in range(self.cols):
|
||||
x = self.origin_x + gx * self.step
|
||||
if not any(lo < x < hi for lo, hi in spans):
|
||||
blocked.add((gx, gy))
|
||||
for a, b in outline.edges(): # too close: only cells near each edge
|
||||
edge_box = IntBox(min(a.x, b.x), min(a.y, b.y), max(a.x, b.x), max(a.y, b.y))
|
||||
for cell in self._cells_in_box(edge_box, reach):
|
||||
if cell not in blocked and outline.edge_distance(self._cell_center(cell)) < reach:
|
||||
blocked.add(cell)
|
||||
return blocked
|
||||
|
||||
def _inside_own_pad(self, cell: Cell, shape) -> bool:
|
||||
"""A trace centred on ``cell`` lies entirely on the pad's own copper."""
|
||||
c = self._cell_center(cell)
|
||||
h = self.half_width
|
||||
return all(
|
||||
shape.contains(IntPoint(c.x + dx, c.y + dy)) for dx in (-h, h) for dy in (-h, h)
|
||||
)
|
||||
|
||||
def _rasterize_obstacles(self) -> None:
|
||||
margin = self.clearance + self.half_width
|
||||
outline = self.board.outline
|
||||
self._via_edge_block: set[Cell] = set()
|
||||
if outline is not None:
|
||||
# cells too close to the real outline (rounded corners included):
|
||||
# for a trace on every layer, and separately for a via's wider copper
|
||||
for cell in self._outline_blocked(self.half_width + self.edge_clearance):
|
||||
for layer in self.layers:
|
||||
self._keepout.add((*cell, layer))
|
||||
self._via_edge_block = self._outline_blocked(self.via_half + self.edge_clearance)
|
||||
for item in self.board.get_items():
|
||||
if isinstance(item, Pin):
|
||||
box = item.shape.bounding_box() if item.shape is not None else None
|
||||
@@ -205,6 +252,13 @@ class GridRouter:
|
||||
for cell in self._cells_in_box(box, margin):
|
||||
for layer in pin_layers:
|
||||
self._pad_block.setdefault((*cell, layer), set()).add(net)
|
||||
# Copper of the pin's own net: a trace inside it adds nothing,
|
||||
# so it is open to that net however close the neighbours are.
|
||||
# That is how a route leaves a fine-pitch pin.
|
||||
for cell in self._cells_in_box(box, 0):
|
||||
if self._inside_own_pad(cell, item.shape):
|
||||
for layer in pin_layers:
|
||||
self._pad_own.setdefault((*cell, layer), set()).add(net)
|
||||
elif isinstance(item, ObstacleArea):
|
||||
if item.layer not in self.layers:
|
||||
continue
|
||||
@@ -214,7 +268,8 @@ class GridRouter:
|
||||
box = tile.bounding_box()
|
||||
if box.is_empty():
|
||||
continue
|
||||
for cell in self._cells_in_box(box, self.clearance):
|
||||
# the same distance the exact engine verifies against
|
||||
for cell in self._cells_in_box(box, margin):
|
||||
self._keepout.add((*cell, item.layer))
|
||||
|
||||
# --- occupancy cost -----------------------------------------------------
|
||||
@@ -233,7 +288,7 @@ class GridRouter:
|
||||
if node in self._keepout:
|
||||
return None
|
||||
pads = self._pad_block.get(node)
|
||||
if pads and any(n != net_no for n in pads):
|
||||
if pads and any(n != net_no for n in pads) and net_no not in self._pad_own.get(node, ()):
|
||||
return None
|
||||
blocking = None
|
||||
owner = self._cell_owner.get(node)
|
||||
@@ -264,6 +319,8 @@ class GridRouter:
|
||||
|
||||
def _via_placeable(self, cell: Cell, net_no: int, conns: list[_Connection]) -> bool:
|
||||
"""A through via needs its whole footprint free of other nets on every layer."""
|
||||
if cell in self._via_edge_block:
|
||||
return False
|
||||
return all(
|
||||
self._enter_cost(c, layer, net_no, False, conns) == 0.0
|
||||
for c in self._via_cells(cell)
|
||||
@@ -464,6 +521,7 @@ class GridRouter:
|
||||
result.add_wire(net_no, layer, points)
|
||||
for location in vias:
|
||||
result.add_via(net_no, location)
|
||||
result.connections.append((net_no, wires, vias))
|
||||
return result
|
||||
|
||||
|
||||
@@ -520,6 +578,7 @@ def route_board(
|
||||
max_passes: int = 10,
|
||||
orthogonal: bool = False,
|
||||
via_half: int | None = None,
|
||||
edge_clearance: int | None = None,
|
||||
) -> RouteResult:
|
||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||
router = GridRouter(
|
||||
@@ -532,5 +591,6 @@ def route_board(
|
||||
max_passes=max_passes,
|
||||
orthogonal=orthogonal,
|
||||
via_half=via_half,
|
||||
edge_clearance=edge_clearance,
|
||||
)
|
||||
return router.route()
|
||||
|
||||
Reference in New Issue
Block a user