Let pins on turned fine-pitch pads leave along the pad's axis
A QFN placed at 45 degrees had no exit on the grid: every horizontal or vertical step from a pin passes its neighbours inside clearance. A pin whose pad is turned more than ten degrees off the grid axes now leaves along the pad's own axis, away from the part, until an L from there to a grid cell clears every other net's pad, then searches as usual. The slanted leg is checked and stored as an exact slab (segment_slab), not its bounding box, in the grid stub checks and in the exact engine. Where pins leave side by side, a cell names only its first owner, so stubs are also indexed per cell for every connection near them; the exact stub-vs-stub check, trace entry and rip-up all consult that index. A pin with an axis exit and no clear cell reports no way out instead of falling back to a cell on another net's route. diagnose.py replays with the exits.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
"""Pins of a fine-pitch part turned off the grid axes leave along their pads.
|
||||
|
||||
A QFN at 45 degrees has no straight exit: every horizontal or vertical step
|
||||
from a pin passes its neighbours closer than the clearance (0.35 mm away on
|
||||
each axis at 0.5 mm pitch). The grid leaves along the pad's own axis first,
|
||||
past the pin row, and the exact engine checks that slanted leg on its exact
|
||||
copper. Found on th-eda's lift ESC, where a QFN-24 at 45 degrees left 15
|
||||
connections unroutable on an empty board.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from freeroute.board import build_board
|
||||
from freeroute.dsn import parse_dsn
|
||||
from freeroute.geometry import IntPoint, overlaps_2d, segment_slab
|
||||
from freeroute.route.grid_router import GridRouter
|
||||
from freeroute.route.pipeline import (
|
||||
_rule_clearance_dsn,
|
||||
_rule_width_dsn,
|
||||
_via_half_board,
|
||||
build_exact_routing_result,
|
||||
)
|
||||
|
||||
|
||||
def _qfn_side_dsn(rotation: float) -> str:
|
||||
"""Two rows of five 0.25 x 0.8 mm pads at 0.5 mm pitch (one side of a
|
||||
QFN and its opposite side), turned by ``rotation``; each pin of the top
|
||||
row is its own net, routed to a pad 4.5 mm out, the pads fanned 1.5 mm
|
||||
apart."""
|
||||
cx, cy = 100000, -100000
|
||||
t = math.radians(rotation)
|
||||
out = (-math.sin(t), math.cos(t)) # the pads' outward axis after turning
|
||||
along = (math.cos(t), math.sin(t)) # the pin row
|
||||
pins, targets, nets = [], [], []
|
||||
for i in range(5):
|
||||
x = -1000 + 500 * i
|
||||
pins += [f"(pin P {i + 1} {x} 1000)", f"(pin P {i + 6} {x} -1000)"]
|
||||
px = cx + x * math.cos(t) - 1000 * math.sin(t)
|
||||
py = cy + x * math.sin(t) + 1000 * math.cos(t)
|
||||
spread = 1500 * (i - 2) - x # fan the far pads out to 1.5 mm apart
|
||||
tx = px + 4500 * out[0] + spread * along[0]
|
||||
ty = py + 4500 * out[1] + spread * along[1]
|
||||
targets.append(f"(place T{i + 1} {round(tx)} {round(ty)} front 0)")
|
||||
nets.append(f"(net N{i} (pins U1-{i + 1} T{i + 1}-1))")
|
||||
return f"""(pcb qfn (parser) (resolution um 10) (unit um)
|
||||
(structure
|
||||
(layer F.Cu (type signal)) (layer B.Cu (type signal))
|
||||
(boundary (path pcb 0 115000 -115000 85000 -115000 85000 -85000 115000 -85000
|
||||
115000 -115000))
|
||||
(via "Via[0-1]_450:200_um")
|
||||
(rule (width 150) (clearance 150)))
|
||||
(placement
|
||||
(component Q (place U1 {cx} {cy} front {rotation}))
|
||||
(component T {" ".join(targets)}))
|
||||
(library
|
||||
(image Q {" ".join(pins)})
|
||||
(image T (pin t 1 0 0))
|
||||
(padstack P (shape (rect F.Cu -125 -400 125 400)) (attach off))
|
||||
(padstack t (shape (rect F.Cu -300 -300 300 300)) (attach off))
|
||||
(padstack "Via[0-1]_450:200_um" (shape (circle F.Cu 450)) (shape (circle B.Cu 450))
|
||||
(attach off)))
|
||||
(network {" ".join(nets)}))"""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rotation", [45, 30, -45])
|
||||
def test_a_turned_fine_pitch_part_gets_its_pins_out(rotation):
|
||||
result = build_exact_routing_result(parse_dsn(_qfn_side_dsn(rotation)), resolution=2, shove=True)
|
||||
assert result.unrouted == []
|
||||
|
||||
|
||||
def test_the_exit_runs_along_the_pad_axis_and_clears_the_neighbours():
|
||||
dsn = parse_dsn(_qfn_side_dsn(45))
|
||||
s = dsn.resolution.value
|
||||
board = build_board(dsn)
|
||||
router = GridRouter(
|
||||
board,
|
||||
trace_width=round(_rule_width_dsn(dsn) * s),
|
||||
clearance=round(_rule_clearance_dsn(dsn) * s),
|
||||
via_half=_via_half_board(dsn, s),
|
||||
orthogonal=True,
|
||||
)
|
||||
pins = {p.name: p for p in board.get_pins() if p.component_ref == "U1"}
|
||||
pin = pins["3"]
|
||||
exit_point = router._axis_exit(pin)
|
||||
assert exit_point is not None
|
||||
dx, dy = exit_point.x - pin.location.x, exit_point.y - pin.location.y
|
||||
assert abs(abs(dx) - abs(dy)) <= 2 # along the 45-degree axis
|
||||
leg = segment_slab(pin.location, exit_point, router.half_width + router.clearance)
|
||||
for name in ("2", "4"):
|
||||
assert not overlaps_2d(leg, pins[name].shape)
|
||||
|
||||
|
||||
def test_a_pad_on_the_grid_axes_keeps_the_usual_exits():
|
||||
dsn = parse_dsn(_qfn_side_dsn(0))
|
||||
s = dsn.resolution.value
|
||||
board = build_board(dsn)
|
||||
router = GridRouter(board, trace_width=round(_rule_width_dsn(dsn) * s),
|
||||
clearance=round(_rule_clearance_dsn(dsn) * s), via_half=_via_half_board(dsn, s))
|
||||
pin = next(p for p in board.get_pins() if p.component_ref == "U1" and p.name == "3")
|
||||
assert router._axis_exit(pin) is None
|
||||
|
||||
|
||||
def test_a_slanted_slab_is_tight_where_a_bounding_box_is_not():
|
||||
slab = segment_slab(IntPoint(0, 0), IntPoint(10000, 10000), 750)
|
||||
# a point beside the diagonal but inside its bounding box
|
||||
from freeroute.geometry import IntBox
|
||||
|
||||
assert not overlaps_2d(slab, IntBox(8000, 0, 8100, 100))
|
||||
assert overlaps_2d(slab, IntBox(4900, 4900, 5100, 5100))
|
||||
Reference in New Issue
Block a user