Route to inner plane layers with a via, not pad to pad

On a four-layer board with a GND plane on an inner power layer, freeroute
ignored the plane (it only used zones on routing layers) and chained every
GND pad to the next with tracks across the board. Now a through-hole pad
inside a plane is joined to it already, and for a net with a plane every
other group connects straight to the plane: a route may end on any cell
inside it where a via fits (checked lazily, like other vias), and ending
there places that via. Signal nets are unaffected.

Tested on a four-layer DSN: three SMD GND pads each get a short stub and
their own via, the THT GND pad needs nothing, and no track crosses the
board. Ahead of the TerraHawk ESC, a four-layer board.
This commit is contained in:
2026-09-27 14:19:13 -06:00
parent 6f3db3e0e3
commit 56d576e613
2 changed files with 112 additions and 17 deletions
+65 -17
View File
@@ -212,6 +212,10 @@ class GridRouter:
self._pad_own: dict[Node, set[int]] = {}
self._keepout: set[Node] = set()
self._via_keepout: set[Node] = set()
# inner power (plane) layers: not routed on, reached by vias
self._power_layers = {
i for i, layer in enumerate(board.layer_structure.arr) if not layer.is_signal
}
self._zone_cells: dict[int, list[Node]] = {}
# per-connection occupancy (owner = connection index)
self._cell_owner: dict[Node, int] = {}
@@ -393,6 +397,9 @@ class GridRouter:
self, conn: _Connection, allow_ripup: bool, conns: list[_Connection]
) -> list[Node] | None:
goal_cell = self._group_target(conn.b_group)
if not conn.b_group[0] and any(z.layer in self._power_layers for z in conn.b_group[1]):
# the plane is everywhere: aim at where the route starts instead
goal_cell = self._group_target(conn.a_group)
starts = self._group_ends(conn.a_group, conn.net_no, conns)
goals = self._group_ends(conn.b_group, conn.net_no, conns)
if not starts or not goals:
@@ -434,9 +441,18 @@ class GridRouter:
continue
current = entry[1]
if current in goals:
path = _reconstruct(came_from, current)
conn.a_end, conn.b_end = starts.get(path[0]), goals[current]
return path
end = goals[current]
cell = (current[0], current[1])
if isinstance(end, ConductionArea): # a plane end needs its via to fit
ok = via_ok.get(cell)
if ok is None:
ok = via_ok[cell] = self._via_placeable(cell, net_no, conns)
else:
ok = True
if ok:
path = _reconstruct(came_from, current)
conn.a_end, conn.b_end = starts.get(path[0]), end
return path
cx, cy, cl = current
base = g_score[current]
for dx, dy in self._neighbours:
@@ -499,10 +515,12 @@ class GridRouter:
return True
def _zone_nodes(self, zone) -> list[Node]:
"""Cells of a zone where a via of this net fits inside its copper."""
"""Cells of a zone where a via of this net fits inside its copper; for a
plane on a power layer, on every routing layer (a via reaches it)."""
key = id(zone)
if key not in self._zone_cells:
h = self.via_half
layers = [zone.layer] if zone.layer in self.layers else list(self.layers)
nodes = []
for tile in zone.tiles:
for cell in self._cells_in_box(tile.bounding_box(), 0):
@@ -511,7 +529,7 @@ class GridRouter:
zone.polygon.contains(IntPoint(c.x + dx, c.y + dy))
for dx in (-h, h) for dy in (-h, h)
):
nodes.append((*cell, zone.layer))
nodes.extend((*cell, layer) for layer in layers)
self._zone_cells[key] = sorted(set(nodes))
return self._zone_cells[key]
@@ -549,8 +567,10 @@ class GridRouter:
for node in sorted(self._end_nodes(pin, net_no, conns)):
ends.setdefault(node, pin)
for zone in group[1]:
plane = zone.layer in self._power_layers
for node in self._zone_nodes(zone):
ends.setdefault(node, None)
# a plane end is marked with the plane: the route ends in a via
ends.setdefault(node, zone if plane else None)
return ends
def _group_target(self, group) -> Cell:
@@ -704,6 +724,11 @@ class GridRouter:
def _commit(self, conn: _Connection, path: list[Node]) -> None:
wire_segs, via_cells = _split_path(path)
via_cells = list(via_cells)
if isinstance(conn.a_end, ConductionArea):
via_cells.append((path[0][0], path[0][1]))
if isinstance(conn.b_end, ConductionArea):
via_cells.append((path[-1][0], path[-1][1]))
conn.trace_cells = [(cell, layer) for layer, cells in wire_segs for cell in cells]
conn.via_cells = list(via_cells)
for cell, layer in conn.trace_cells:
@@ -724,8 +749,8 @@ class GridRouter:
conn.via_halo.append(c)
ends = ((conn.a_end, wire_segs[0], 0), (conn.b_end, wire_segs[-1], -1)) if wire_segs else ()
for pin, (layer, cells), at in ends:
if pin is None:
continue # ended on a zone: no stub
if not isinstance(pin, Pin):
continue # ended on a zone or a plane: no stub
cell = cells[at]
for c in self._stub_cells(pin.location, cell):
node = (*c, layer)
@@ -735,9 +760,9 @@ class GridRouter:
conn.wires = []
for idx, (layer, cells) in enumerate(wire_segs):
points = [self._cell_center(c) for c in cells]
if idx == 0 and conn.a_end is not None:
if idx == 0 and isinstance(conn.a_end, Pin):
points.insert(0, conn.a_end.location)
if idx == len(wire_segs) - 1 and conn.b_end is not None:
if idx == len(wire_segs) - 1 and isinstance(conn.b_end, Pin):
points.append(conn.b_end.location)
points = _simplify(points)
if len(points) >= 2:
@@ -835,12 +860,26 @@ class GridRouter:
for zone in self.board.get_conduction_areas():
if zone.layer in self.layers and zone.net_nos and self._zone_is_solid(zone):
zones_by_net.setdefault(zone.net_nos[0], []).append(zone)
# planes: zones of a net on an inner power layer, solid by construction
planes_by_net: dict[int, list] = {}
for zone in self.board.get_conduction_areas():
if zone.net_nos and zone.layer in self._power_layers:
planes_by_net.setdefault(zone.net_nos[0], []).append(zone)
plane_ids = frozenset(id(z) for zs in planes_by_net.values() for z in zs)
conns: list[_Connection] = []
nets = set(pins_by_net) | set(zones_by_net)
nets = set(pins_by_net) | set(zones_by_net) | set(planes_by_net)
# deterministic: fewer-pin nets first, then by net number
for net_no in sorted(nets, key=lambda n: (len(pins_by_net.get(n, [])), n)):
groups = _connected_groups(pins_by_net.get(net_no, []), zones_by_net.get(net_no, []))
for ga, gb in zip(groups, groups[1:], strict=False):
zones = zones_by_net.get(net_no, []) + planes_by_net.get(net_no, [])
groups = _connected_groups(pins_by_net.get(net_no, []), zones, plane_ids)
hubs = [g for g in groups if any(id(z) in plane_ids for z in g[1])]
if hubs:
# a plane net: every other group goes straight to the plane
# (usually a stub and a via), not pad to pad across the board
pairs = [(g, hubs[0]) for g in groups if g is not hubs[0]]
else:
pairs = list(zip(groups, groups[1:], strict=False))
for ga, gb in pairs:
conn = _Connection(len(conns), net_no, _first(ga[0]), _first(gb[0]))
conn.a_group, conn.b_group = ga, gb
conns.append(conn)
@@ -900,8 +939,15 @@ def _ends_of(conn) -> tuple[str, str]:
return _group_label(conn.a_group), _group_label(conn.b_group)
def _pin_touches_zone(pin: Pin, zone) -> bool:
"""A pad on the zone's layer whose centre or a corner lies in the zone."""
def _pin_touches_zone(pin: Pin, zone, plane: bool = False) -> bool:
"""A pad on the zone's layer whose centre or a corner lies in the zone.
For a plane on an inner power layer: a through-hole pad (on more than one
copper layer) inside it passes through the plane and is joined to it; an
SMD pad is not, and needs a via.
"""
if plane:
return len(pin.layers) > 1 and zone.polygon is not None and zone.polygon.contains(pin.location)
if zone.layer not in pin.layers or zone.polygon is None:
return False
if zone.polygon.contains(pin.location):
@@ -913,7 +959,9 @@ def _pin_touches_zone(pin: Pin, zone) -> bool:
return any(zone.polygon.contains(c) for c in corners)
def _connected_groups(pins: list[Pin], zones: list) -> list[tuple[list[Pin], list]]:
def _connected_groups(
pins: list[Pin], zones: list, planes: frozenset = frozenset()
) -> list[tuple[list[Pin], list]]:
"""Pins and zones of one net that copper already joins: a pad in a zone
is connected to it, and two zones sharing a pad are one. Only the groups
need routing between them. Pin groups come first, in pin order."""
@@ -927,7 +975,7 @@ def _connected_groups(pins: list[Pin], zones: list) -> list[tuple[list[Pin], lis
for j, zone in enumerate(zones):
for i, pin in enumerate(pins):
if _pin_touches_zone(pin, zone):
if _pin_touches_zone(pin, zone, id(zone) in planes):
parent[find(i)] = find(len(pins) + j)
groups: dict[int, tuple[list[Pin], list]] = {}
order: list[int] = []