230 lines
11 KiB
Python
230 lines
11 KiB
Python
"""Fan out fine-pitch pins before routing: commit each pin's way out as copper.
|
|
|
|
On a dense board the pins of a fine-pitch package get walled in: their own
|
|
neighbours' pads on either side, and other nets' copper that got there first
|
|
across the only way out. Reserving the way out did not help (the reserved
|
|
cells still had to be routed, and lost to congestion there); committing it
|
|
does, which is what a person does by hand.
|
|
|
|
Each fine-pitch pin of a perimeter package (QFP, QFN, LGA: pads on the edge,
|
|
none inside) gets a straight stub outward, perpendicular to its side of the
|
|
package, just past its pad row. A pin whose net has an inner plane also gets
|
|
a via at the stub's end, staggered between neighbouring pins so their holes
|
|
keep their spacing, and so reaches the plane with nothing left to route.
|
|
Every stub and via is checked exactly against pads, keepouts, the board edge,
|
|
copper already on the board and earlier fan-out, and left out where it would
|
|
not fit. The result goes on the board as fixed copper of its net (marked
|
|
``generated``), so the router starts from the stub's end or the via, and the
|
|
session writes it out with the routes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
import math
|
|
|
|
from freeroute.board.board import BasicBoard
|
|
from freeroute.board.items import Pin, Trace, Via
|
|
from freeroute.geometry import IntBox, IntPoint
|
|
from freeroute.geometry.polyline import Polyline, PolylineShape, corner_box
|
|
|
|
# a package is fanned out when it has this many pins and they are this close
|
|
_MIN_PINS = 8
|
|
_FINE_PITCH_FACTOR = 1.2 # neighbour centres within this many (trace + 2 clearance)
|
|
|
|
|
|
@dataclass
|
|
class FanoutRules:
|
|
trace_width: int
|
|
clearance: int
|
|
via_half: int
|
|
via_drill: int
|
|
hole_to_hole: int
|
|
edge_clearance: int
|
|
signal_layers: list[int]
|
|
via_padstack: str
|
|
#: smallest package fanned out (a big dense part is where pins get walled in)
|
|
min_pins: int = 32
|
|
#: fan out plane pins only (a stub and a via each), as a person would first
|
|
supply_only: bool = False
|
|
|
|
|
|
def plan_fanout(board: BasicBoard, rules: FanoutRules) -> list:
|
|
"""The stubs and vias to put on the board before routing."""
|
|
half = max(rules.trace_width // 2, 1)
|
|
# a net may have several plane zones (a main plane and an island)
|
|
planes: dict[int, list] = {}
|
|
for z in board.get_conduction_areas():
|
|
if z.net_nos and not board.layer_structure.arr[z.layer].is_signal:
|
|
planes.setdefault(z.net_nos[0], []).append(z)
|
|
# copper pours on the routing layers, by net: a pad inside one is joined
|
|
# by the fill, and a stub there would take the room its thermal spokes need
|
|
pours: dict[int, list] = {}
|
|
for z in board.get_conduction_areas():
|
|
if z.net_nos and board.layer_structure.arr[z.layer].is_signal:
|
|
pours.setdefault(z.net_nos[0], []).append(z)
|
|
placed = _Placed(board, rules)
|
|
by_net: dict[int, list[Pin]] = {}
|
|
for pin in board.get_pins():
|
|
for n in pin.net_nos:
|
|
by_net.setdefault(n, []).append(pin)
|
|
items: list = []
|
|
pitch = 2 * rules.via_half + rules.hole_to_hole
|
|
for pins in _packages(board, rules).values():
|
|
centre = IntPoint(
|
|
sum(p.location.x for p in pins) // len(pins), sum(p.location.y for p in pins) // len(pins)
|
|
)
|
|
for k, pin in enumerate(sorted(pins, key=lambda p: (p.location.x, p.location.y))):
|
|
net = pin.net_nos[0] if pin.net_nos else None
|
|
if net is None:
|
|
continue
|
|
layer = next(layer for layer in pin.layers if layer in rules.signal_layers)
|
|
if any(z.layer == layer and z.polygon is not None and z.polygon.contains(pin.location)
|
|
for z in pours.get(net, [])):
|
|
continue # the pour joins it
|
|
direction = _outward(pin.location, centre)
|
|
reach = _pad_reach(pin, direction) + rules.clearance + half + rules.trace_width
|
|
end = _step(pin.location, direction, reach)
|
|
plane = planes.get(net)
|
|
partners = [q for q in by_net.get(net, []) if q is not pin]
|
|
# a signal whose other pins are all on other layers has to change
|
|
# layer somewhere: do it at the stub's end, away from the pads
|
|
elsewhere = bool(partners) and all(layer not in q.layers for q in partners)
|
|
if plane is None and (rules.supply_only or not elsewhere):
|
|
if not rules.supply_only and placed.stub_fits(pin.location, end, layer, net, half):
|
|
items.append(placed.add_stub(pin.location, end, layer, net, half))
|
|
continue
|
|
# stagger the via of every other pin along the side outward
|
|
options = [reach + rules.via_half + rules.clearance + (k % 2) * pitch,
|
|
reach + rules.via_half + rules.clearance + ((k + 1) % 2) * pitch]
|
|
for d in options:
|
|
at = _step(pin.location, direction, d)
|
|
if plane is not None and not any(
|
|
z.polygon is not None and z.polygon.contains(at) for z in plane
|
|
):
|
|
continue
|
|
if placed.stub_fits(pin.location, at, layer, net, half) and placed.via_fits(at, net):
|
|
items.append(placed.add_stub(pin.location, at, layer, net, half))
|
|
items.append(placed.add_via(at, net))
|
|
break
|
|
return items
|
|
|
|
|
|
def _packages(board: BasicBoard, rules: FanoutRules) -> dict[str, list[Pin]]:
|
|
"""Fine-pitch perimeter packages: many single-layer pins, close together."""
|
|
by_ref: dict[str, list[Pin]] = {}
|
|
for pin in board.get_pins():
|
|
if pin.shape is None or not pin.component_ref:
|
|
continue
|
|
on = [layer for layer in pin.layers if layer in rules.signal_layers]
|
|
if len(on) != 1:
|
|
continue # through-hole pins change layer for free
|
|
by_ref.setdefault(pin.component_ref, []).append(pin)
|
|
fine = _FINE_PITCH_FACTOR * (rules.trace_width + 2 * rules.clearance)
|
|
out = {}
|
|
for ref, pins in by_ref.items():
|
|
if len(pins) < max(rules.min_pins, _MIN_PINS):
|
|
continue
|
|
close = sum(
|
|
1 for p in pins
|
|
if min(math.hypot(p.location.x - q.location.x, p.location.y - q.location.y)
|
|
for q in pins if q is not p) < fine * 2
|
|
)
|
|
if close >= len(pins) // 2:
|
|
out[ref] = pins
|
|
return out
|
|
|
|
|
|
def _outward(point: IntPoint, centre: IntPoint) -> tuple[int, int]:
|
|
"""Away from the package centre, along the nearer axis (its side's normal)."""
|
|
dx, dy = point.x - centre.x, point.y - centre.y
|
|
if abs(dx) >= abs(dy):
|
|
return (1 if dx >= 0 else -1, 0)
|
|
return (0, 1 if dy >= 0 else -1)
|
|
|
|
|
|
def _pad_reach(pin: Pin, direction: tuple[int, int]) -> int:
|
|
"""How far the pad reaches from its centre in ``direction``."""
|
|
box = pin.shape.bounding_box()
|
|
if direction[0] > 0:
|
|
return box.ur.x - pin.location.x
|
|
if direction[0] < 0:
|
|
return pin.location.x - box.ll.x
|
|
if direction[1] > 0:
|
|
return box.ur.y - pin.location.y
|
|
return pin.location.y - box.ll.y
|
|
|
|
|
|
def _step(point: IntPoint, direction: tuple[int, int], distance: float) -> IntPoint:
|
|
return IntPoint(point.x + round(direction[0] * distance), point.y + round(direction[1] * distance))
|
|
|
|
|
|
class _Placed:
|
|
"""Copper a fan-out stub or via has to clear, growing as fan-out is placed."""
|
|
|
|
def __init__(self, board: BasicBoard, rules: FanoutRules) -> None:
|
|
self.board, self.rules = board, rules
|
|
self.copper: list[tuple[IntBox, frozenset[int], int]] = [] # box, layers, net
|
|
for pin in board.get_pins():
|
|
if pin.shape is not None:
|
|
net = pin.net_nos[0] if pin.net_nos else -1
|
|
self.copper.append((pin.shape.bounding_box(), frozenset(pin.layers), net))
|
|
for net, layers, box in board.get_fixed_copper():
|
|
self.copper.append((box, layers, net))
|
|
self.keepouts = [
|
|
(tile.bounding_box(), o.layer, o.blocks_wires, o.blocks_vias, o.clearance)
|
|
for o in board.get_obstacle_areas() for tile in o.tiles
|
|
]
|
|
self.holes: list[tuple[IntPoint, int]] = [] # centre, radius
|
|
for item in board.get_items():
|
|
if isinstance(item, Via) and item.fixed:
|
|
self.holes.append((item.location, (item.drill or rules.via_drill) // 2))
|
|
for pin in board.get_pins():
|
|
if pin.shape is not None and sum(1 for layer in pin.layers if layer in rules.signal_layers) > 1:
|
|
b = pin.shape.bounding_box()
|
|
self.holes.append((pin.location, min(b.ur.x - b.ll.x, b.ur.y - b.ll.y) // 2))
|
|
|
|
def _clear(self, box: IntBox, layers: frozenset[int], net: int, kind: str) -> bool:
|
|
grown = box.offset(self.rules.clearance)
|
|
for other, other_layers, other_net in self.copper:
|
|
if other_net != net and layers & other_layers and other.overlaps(grown):
|
|
return False
|
|
for kbox, klayer, wires, vias, needs in self.keepouts:
|
|
if klayer not in layers or not (wires if kind == "wire" else vias):
|
|
continue
|
|
if kbox.overlaps(grown if needs else box):
|
|
return False
|
|
outline = self.board.outline
|
|
return outline is None or outline.box_clear(box, self.rules.edge_clearance)
|
|
|
|
def stub_fits(self, a: IntPoint, b: IntPoint, layer: int, net: int, half: int) -> bool:
|
|
boxes = list(PolylineShape(Polyline([a, b]), half).tiles()) + [corner_box(b, half)]
|
|
return all(self._clear(box, frozenset({layer}), net, "wire") for box in boxes)
|
|
|
|
def via_fits(self, at: IntPoint, net: int) -> bool:
|
|
h = self.rules.via_half
|
|
box = IntBox(at.x - h, at.y - h, at.x + h, at.y + h)
|
|
if not self._clear(box, frozenset(self.rules.signal_layers), net, "via"):
|
|
return False
|
|
mine = self.rules.via_drill // 2
|
|
return all(
|
|
math.hypot(c.x - at.x, c.y - at.y) >= mine + r + self.rules.hole_to_hole
|
|
for c, r in self.holes
|
|
)
|
|
|
|
def add_stub(self, a: IntPoint, b: IntPoint, layer: int, net: int, half: int) -> Trace:
|
|
for box in list(PolylineShape(Polyline([a, b]), half).tiles()) + [corner_box(b, half)]:
|
|
self.copper.append((box, frozenset({layer}), net))
|
|
return Trace(net_nos=[net], fixed=True, layer=layer, half_width=half, corners=[a, b],
|
|
generated=True)
|
|
|
|
def add_via(self, at: IntPoint, net: int) -> Via:
|
|
h = self.rules.via_half
|
|
box = IntBox(at.x - h, at.y - h, at.x + h, at.y + h)
|
|
layers = list(self.rules.signal_layers)
|
|
self.copper.append((box, frozenset(layers), net))
|
|
self.holes.append((at, self.rules.via_drill // 2))
|
|
return Via(net_nos=[net], fixed=True, padstack_name=self.rules.via_padstack,
|
|
location=at, layers=layers, shape=box, drill=self.rules.via_drill,
|
|
generated=True)
|