Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56d576e613 |
@@ -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] = []
|
||||
|
||||
@@ -164,3 +164,50 @@ def test_cli_names_the_connections_it_could_not_route(tmp_path, capsys):
|
||||
assert main([str(dsn), "--engine", "exact", "-do", str(tmp_path / "out.ses")]) == 0
|
||||
err = capsys.readouterr().err
|
||||
assert "1 connection(s) not routed" in err and "N: A-1 to B-1" in err
|
||||
|
||||
|
||||
# --- inner plane layers (a 4-layer board with a GND plane on In1.Cu) --------
|
||||
|
||||
def _four_layer_dsn():
|
||||
return """(pcb four (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 30000 0 30000 -30000 0 -30000 0 0))
|
||||
(plane GND (polygon In1.Cu 0 500 -500 29500 -500 29500 -29500 500 -29500 500 -500))
|
||||
(via "Via[0-3]_600:300_um") (rule (width 250) (clearance 150)))
|
||||
(placement
|
||||
(component S (place G1 5000 -5000 front 0) (place G2 25000 -5000 front 0)
|
||||
(place G3 15000 -25000 front 0) (place A 5000 -15000 front 0) (place B 25000 -15000 front 0))
|
||||
(component T (place GT 15000 -15000 front 0)))
|
||||
(library
|
||||
(image S (pin smd 1 0 0))
|
||||
(image T (pin tht 1 0 0))
|
||||
(padstack smd (shape (rect F.Cu -300 -300 300 300)) (attach off))
|
||||
(padstack tht (shape (circle F.Cu 1000)) (shape (circle B.Cu 1000)) (attach off))
|
||||
(padstack "Via[0-3]_600:300_um" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off)))
|
||||
(network (net GND (pins G1-1 G2-1 G3-1 GT-1)) (net SIG (pins A-1 B-1))))"""
|
||||
|
||||
|
||||
def test_smd_pads_reach_an_inner_plane_by_their_own_via():
|
||||
dsn = parse_dsn(_four_layer_dsn())
|
||||
result, scale, names = route_dsn_board_exact(dsn)
|
||||
gnd = next(i + 1 for i, n in enumerate(dsn.nets) if n.name == "GND")
|
||||
sig = next(i + 1 for i, n in enumerate(dsn.nets) if n.name == "SIG")
|
||||
assert len(result.result.vias.get(gnd, [])) == 3 # one per SMD pad, none for the THT pad
|
||||
for _layer, pts in result.result.wires.get(gnd, []): # stubs, not tracks across the board
|
||||
length = sum(abs(q.x - p.x) + abs(q.y - p.y) for p, q in zip(pts, pts[1:], strict=False))
|
||||
assert length < 3000 * scale # under 3 mm: a stub to its via
|
||||
assert sig in result.result.routed_net_numbers # signals still route on the outer layers
|
||||
assert all(names[layer] in ("F.Cu", "B.Cu") for segs in result.result.wires.values() for layer, _ in segs)
|
||||
|
||||
|
||||
def test_a_through_hole_pad_inside_a_plane_needs_no_route():
|
||||
from freeroute.route.grid_router import _connected_groups
|
||||
|
||||
board = build_board(parse_dsn(_four_layer_dsn()))
|
||||
plane = [z for z in board.get_conduction_areas() if z.name == "GND"]
|
||||
gnd_pins = [p for p in board.get_pins() if p.component_ref in ("G1", "G2", "G3", "GT")]
|
||||
groups = _connected_groups(gnd_pins, plane, frozenset(id(z) for z in plane))
|
||||
hub = next(g for g in groups if g[1])
|
||||
assert [p.component_ref for p in hub[0]] == ["GT"]
|
||||
|
||||
Reference in New Issue
Block a user