diff --git a/src/freeroute/cli.py b/src/freeroute/cli.py index f32d386..bbce7a7 100644 --- a/src/freeroute/cli.py +++ b/src/freeroute/cli.py @@ -189,10 +189,19 @@ def build_parser() -> argparse.ArgumentParser: ) parser.add_argument( "--fanout", - action="store_true", + nargs="?", + const="all", + choices=["all", "supply"], help="grid and exact engines: before routing, give every fine-pitch pin a stub " "outward past its pad row (and a via to its plane, for a plane net), committed " - "as copper so later routes cannot wall it in", + "as copper so later routes cannot wall it in; --fanout supply: plane pins only", + ) + parser.add_argument( + "--fanout-min-pins", + type=int, + default=32, + metavar="N", + help="with --fanout: fan out only packages with at least N fine-pitch pins (default 32)", ) parser.add_argument( "--jobs", @@ -224,7 +233,7 @@ def main(argv: list[str] | None = None) -> int: ("promote", args.promote_failures is not None), ("access", args.access_cost is not None), ("rollback", not args.rollback), - ("fanout", args.fanout), + ("fanout", bool(args.fanout)), ) if asked ] @@ -260,7 +269,8 @@ def main(argv: list[str] | None = None) -> int: "heuristic_weight": args.heuristic_weight, "access_cost": args.access_cost or 0.0, "hole_to_hole_mm": args.hole_to_hole, - "fanout": args.fanout, + "fanout": args.fanout or False, + "fanout_min_pins": args.fanout_min_pins, "rollback": args.rollback, } elif args.engine == "grid": @@ -272,7 +282,8 @@ def main(argv: list[str] | None = None) -> int: "resolution": 1 if args.grid_resolution is None else args.grid_resolution, "access_cost": args.access_cost or 0.0, "hole_to_hole_mm": args.hole_to_hole, - "fanout": args.fanout, + "fanout": args.fanout or False, + "fanout_min_pins": args.fanout_min_pins, "rollback": args.rollback, } if args.engine == "room": diff --git a/src/freeroute/route/fanout.py b/src/freeroute/route/fanout.py index aaaa41f..4821241 100644 --- a/src/freeroute/route/fanout.py +++ b/src/freeroute/route/fanout.py @@ -43,6 +43,10 @@ class FanoutRules: 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: @@ -54,7 +58,12 @@ def plan_fanout(board: BasicBoard, rules: FanoutRules) -> list: if z.net_nos and not board.layer_structure.arr[z.layer].is_signal: planes.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) @@ -68,22 +77,27 @@ def plan_fanout(board: BasicBoard, rules: FanoutRules) -> list: reach = _pad_reach(pin, direction) + rules.clearance + half + rules.trace_width end = _step(pin.location, direction, reach) plane = planes.get(net) - if plane is not None: - # stagger the via of every other pin along the side outward - pitch = 2 * rules.via_half + rules.hole_to_hole - 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 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 + 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 - if placed.stub_fits(pin.location, end, layer, net, half): - items.append(placed.add_stub(pin.location, end, layer, net, half)) + # 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 @@ -100,7 +114,7 @@ def _packages(board: BasicBoard, rules: FanoutRules) -> dict[str, list[Pin]]: fine = _FINE_PITCH_FACTOR * (rules.trace_width + 2 * rules.clearance) out = {} for ref, pins in by_ref.items(): - if len(pins) < _MIN_PINS: + if len(pins) < max(rules.min_pins, _MIN_PINS): continue close = sum( 1 for p in pins diff --git a/src/freeroute/route/pipeline.py b/src/freeroute/route/pipeline.py index 4ff9c6e..2c45950 100644 --- a/src/freeroute/route/pipeline.py +++ b/src/freeroute/route/pipeline.py @@ -88,7 +88,9 @@ def _dsn_units_per_um(dsn: DsnBoard) -> float: return {"um": 1.0, "mm": 0.001, "mil": 1 / 25.4, "inch": 1 / 25400}.get(dsn.unit, 1.0) -def _place_fanout(board, dsn, scale, layers, width, clearance, edge, hole_to_hole_mm) -> None: +def _place_fanout( + board, dsn, scale, layers, width, clearance, edge, hole_to_hole_mm, mode=True, min_pins=32 +) -> None: """Put the fan-out's stubs and vias on the board, before routing.""" from .fanout import FanoutRules, plan_fanout @@ -102,6 +104,8 @@ def _place_fanout(board, dsn, scale, layers, width, clearance, edge, hole_to_hol edge_clearance=edge or 0, signal_layers=layers or [i for i, layer in enumerate(dsn.layers) if layer.is_signal], via_padstack=_via_padstack(dsn), + min_pins=min_pins, + supply_only=mode == "supply", ) for item in plan_fanout(board, rules): board.insert(item) @@ -151,7 +155,8 @@ def route_dsn_board( route_first: frozenset | None = None, access_cost: float = 0.0, hole_to_hole_mm: float = 0.25, - fanout: bool = False, + fanout: bool | str = False, + fanout_min_pins: int = 32, rollback: bool = True, ) -> tuple[RouteResult, int, list[str]]: """Route a parsed :class:`DsnBoard`; return the board-unit result, the scale, @@ -165,7 +170,7 @@ def route_dsn_board( clearance_board = round(_rule_clearance_dsn(dsn) * scale) if fanout: _place_fanout(board, dsn, scale, layers, width_board, clearance_board, - edge_clearance_board, hole_to_hole_mm) + edge_clearance_board, hole_to_hole_mm, fanout, fanout_min_pins) result = route_board( board, resolution=resolution, @@ -234,7 +239,8 @@ def build_routing_result( route_first: frozenset | None = None, access_cost: float = 0.0, hole_to_hole_mm: float = 0.25, - fanout: bool = False, + fanout: bool | str = False, + fanout_min_pins: int = 32, rollback: bool = True, ) -> RoutingResult: """Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult.""" @@ -245,6 +251,7 @@ def build_routing_result( access_cost=access_cost, hole_to_hole_mm=hole_to_hole_mm, fanout=fanout, + fanout_min_pins=fanout_min_pins, rollback=rollback, ) return _to_routing_result(route, dsn, scale, layer_names) @@ -265,7 +272,8 @@ def route_dsn_board_exact( route_first: frozenset | None = None, access_cost: float = 0.0, hole_to_hole_mm: float = 0.25, - fanout: bool = False, + fanout: bool | str = False, + fanout_min_pins: int = 32, rollback: bool = True, ) -> tuple[ExactRouteResult, int, list[str]]: """Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal, @@ -280,7 +288,7 @@ def route_dsn_board_exact( clearance_board = round(_rule_clearance_dsn(dsn) * scale) if fanout: _place_fanout(board, dsn, scale, layers, width_board, clearance_board, - edge_clearance_board, hole_to_hole_mm) + edge_clearance_board, hole_to_hole_mm, fanout, fanout_min_pins) exact = route_board_exact( board, resolution=resolution, @@ -321,7 +329,8 @@ def build_exact_routing_result( route_first: frozenset | None = None, access_cost: float = 0.0, hole_to_hole_mm: float = 0.25, - fanout: bool = False, + fanout: bool | str = False, + fanout_min_pins: int = 32, rollback: bool = True, ) -> RoutingResult: """Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult.""" @@ -340,6 +349,7 @@ def build_exact_routing_result( access_cost=access_cost, hole_to_hole_mm=hole_to_hole_mm, fanout=fanout, + fanout_min_pins=fanout_min_pins, rollback=rollback, ) return _to_routing_result(exact.result, dsn, scale, layer_names) diff --git a/tests/route/test_fanout.py b/tests/route/test_fanout.py index f6c791e..0b5013a 100644 --- a/tests/route/test_fanout.py +++ b/tests/route/test_fanout.py @@ -48,7 +48,7 @@ def _rules(dsn): s = dsn.resolution.value return FanoutRules(trace_width=150 * s, clearance=150 * s, via_half=225 * s, via_drill=200 * s, hole_to_hole=250 * s, edge_clearance=0, signal_layers=[0, 3], - via_padstack="Via[0-3]_450:200_um") + via_padstack="Via[0-3]_450:200_um", min_pins=8) def test_every_pin_gets_a_stub_outward_past_its_pad(): @@ -71,7 +71,7 @@ def test_a_plane_pin_gets_a_via_and_needs_no_route(): board = build_board(dsn) items = plan_fanout(board, _rules(dsn)) assert len([i for i in items if isinstance(i, Via)]) == 1 - exact, _scale, _ = route_dsn_board_exact(dsn, resolution=2, fanout=True) + exact, _scale, _ = route_dsn_board_exact(dsn, resolution=2, fanout=True, fanout_min_pins=8) gnd = next(i + 1 for i, n in enumerate(dsn.nets) if n.name == "GND") assert not any(f[0] == gnd for f in exact.result.failed) assert exact.drc_clean