- KiCad project generated by pcb/tools (design, route, schematic, fab outputs). - v0.4 fixes the v0.3 review findings: low-dropout 3.3 V regulator, OR'd 5 V supply, polyfuse, USB ESD protection, input clamps, ceramic decoupling at the drivers, A4988/DRV8825 jumper, ground stitching. DRC/ERC clean. - v0.3 (as ordered) kept in pcb/archive/v0.3. - FluidNC config.yaml for the board; rho hard limit on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
290 lines
12 KiB
Python
290 lines
12 KiB
Python
"""A small two-layer grid router (A* with turn penalties, vias, per-net widths).
|
|
|
|
Obstacle model: for every distinct "inflation radius" R (track half-width +
|
|
clearance + discretisation margin) we keep, per layer, an owner map:
|
|
-1 = free, k >= 0 = only copper of net k is within R, -2 = blocked for everyone.
|
|
A cell is usable by net k iff its owner is -1 or k.
|
|
"""
|
|
import heapq
|
|
import math
|
|
from array import array
|
|
|
|
G = 0.1 # grid pitch, mm
|
|
CLEAR = 0.2 # copper-copper clearance used by the router (DRC default is 0.15)
|
|
EDGE = 0.35 # copper-to-board-edge clearance
|
|
MARGIN = 0.08 # discretisation safety margin
|
|
VIA_D = 0.6
|
|
COST_STEP = 10
|
|
COST_DIAG = 14
|
|
COST_VIA = 90
|
|
COST_TURN = 4
|
|
COST_BCU = 1 # tiny extra per step on the bottom layer (keeps B.Cu freer for GND)
|
|
|
|
DIRS = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)]
|
|
|
|
|
|
def seg_dist(px, py, x1, y1, x2, y2):
|
|
dx, dy = x2 - x1, y2 - y1
|
|
L2 = dx * dx + dy * dy
|
|
if L2 == 0:
|
|
return math.hypot(px - x1, py - y1)
|
|
t = max(0.0, min(1.0, ((px - x1) * dx + (py - y1) * dy) / L2))
|
|
return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy))
|
|
|
|
|
|
class Router:
|
|
def __init__(self, width, height, radii):
|
|
self.W, self.H = width, height
|
|
self.nx = int(round(width / G)) + 1
|
|
self.ny = int(round(height / G)) + 1
|
|
self.radii = sorted(set(round(r, 3) for r in radii))
|
|
n = self.nx * self.ny
|
|
self.maps = {r: [array('h', [-1]) * n, array('h', [-1]) * n] for r in self.radii}
|
|
self.net_index = {}
|
|
self.objects = [] # (kind, data, net) for re-rasterising
|
|
self.tracks = [] # (x1,y1,x2,y2,w,layer,net)
|
|
self.vias = [] # (x,y,net)
|
|
self.net_cells = {} # net -> set of (layer, ix, iy) owned copper cells (tree)
|
|
self.no_via = bytearray(n) # 1 = a via centred here would touch or crowd a pad (any net)
|
|
|
|
# ------------------------------------------------------------ helpers
|
|
def nid(self, net):
|
|
if net is None:
|
|
return -2
|
|
if net not in self.net_index:
|
|
self.net_index[net] = len(self.net_index)
|
|
return self.net_index[net]
|
|
|
|
def cell(self, x, y):
|
|
return int(round(x / G)), int(round(y / G))
|
|
|
|
def xy(self, ix, iy):
|
|
return ix * G, iy * G
|
|
|
|
def _mark(self, layer_ids, net, x0, y0, x1, y1, inside):
|
|
"""inside(px,py,R) -> True if cell centre is within R of the object."""
|
|
k = self.nid(net)
|
|
for r in self.radii:
|
|
ix0 = max(0, int((x0 - r) / G) - 1)
|
|
iy0 = max(0, int((y0 - r) / G) - 1)
|
|
ix1 = min(self.nx - 1, int((x1 + r) / G) + 1)
|
|
iy1 = min(self.ny - 1, int((y1 + r) / G) + 1)
|
|
for L in layer_ids:
|
|
m = self.maps[r][L]
|
|
for iy in range(iy0, iy1 + 1):
|
|
py = iy * G
|
|
base = iy * self.nx
|
|
for ix in range(ix0, ix1 + 1):
|
|
idx = base + ix
|
|
v = m[idx]
|
|
if v == -2 or v == k:
|
|
continue
|
|
if inside(ix * G, py, r):
|
|
m[idx] = k if v == -1 else -2
|
|
|
|
def add_pad(self, pad):
|
|
layers = [0 if l == 'F.Cu' else 1 for l in pad.layers]
|
|
if pad.kind == 'np_thru_hole':
|
|
layers = [0, 1]
|
|
hole = pad.drill[0] / 2 if pad.drill else pad.hx
|
|
cx, cy = pad.x, pad.y
|
|
self._mark(layers, None, cx - hole, cy - hole, cx + hole, cy + hole,
|
|
lambda px, py, r: math.hypot(px - cx, py - cy) < hole + r)
|
|
return
|
|
net = pad.net
|
|
self._mark(layers, net, pad.x - pad.hx, pad.y - pad.hy, pad.x + pad.hx, pad.y + pad.hy,
|
|
lambda px, py, r: pad.dist(px, py) < r)
|
|
# drilled THT pads also block the other layer for different nets via the copper ring (both layers)
|
|
|
|
def keep_vias_off_pad(self, pad, gap):
|
|
"""Forbid via centres within (via radius + gap) of this pad's copper, whatever its net.
|
|
A via in or touching a pad lets solder drain into the hole during assembly."""
|
|
if pad.kind == 'np_thru_hole' or not pad.layers:
|
|
return
|
|
r = VIA_D / 2 + gap
|
|
ix0, iy0 = max(0, int((pad.x - pad.hx - r) / G) - 1), max(0, int((pad.y - pad.hy - r) / G) - 1)
|
|
ix1, iy1 = min(self.nx - 1, int((pad.x + pad.hx + r) / G) + 1), min(self.ny - 1, int((pad.y + pad.hy + r) / G) + 1)
|
|
for iy in range(iy0, iy1 + 1):
|
|
for ix in range(ix0, ix1 + 1):
|
|
if pad.dist(ix * G, iy * G) < r:
|
|
self.no_via[iy * self.nx + ix] = 1
|
|
|
|
def add_track(self, x1, y1, x2, y2, w, layer, net):
|
|
hw = w / 2
|
|
self._mark([layer], net, min(x1, x2) - hw, min(y1, y2) - hw, max(x1, x2) + hw, max(y1, y2) + hw,
|
|
lambda px, py, r: seg_dist(px, py, x1, y1, x2, y2) < hw + r)
|
|
self.tracks.append((x1, y1, x2, y2, w, 'F.Cu' if layer == 0 else 'B.Cu', net))
|
|
|
|
def add_via(self, x, y, net):
|
|
rv = VIA_D / 2
|
|
self._mark([0, 1], net, x - rv, y - rv, x + rv, y + rv,
|
|
lambda px, py, r: math.hypot(px - x, py - y) < rv + r)
|
|
self.vias.append((x, y, net))
|
|
|
|
def block_edges(self):
|
|
for r in self.radii:
|
|
lim = EDGE + r - CLEAR # r already includes clearance; edge rule replaces it
|
|
n = int(math.ceil(lim / G)) # cells with index < n (or > max-n) are too close
|
|
for L in (0, 1):
|
|
m = self.maps[r][L]
|
|
for iy in range(self.ny):
|
|
base = iy * self.nx
|
|
if iy < n or iy > self.ny - 1 - n:
|
|
for ix in range(self.nx):
|
|
m[base + ix] = -2
|
|
else:
|
|
for ix in list(range(n)) + list(range(self.nx - n, self.nx)):
|
|
m[base + ix] = -2
|
|
|
|
def block_rect(self, x0, y0, x1, y1):
|
|
"""Keep-out: nothing may enter (tracks are inflated by their radius)."""
|
|
for r in self.radii:
|
|
for L in (0, 1):
|
|
m = self.maps[r][L]
|
|
hw = r - CLEAR - MARGIN # track half width
|
|
for iy in range(max(0, int((y0 - hw) / G)), min(self.ny, int((y1 + hw) / G) + 2)):
|
|
for ix in range(max(0, int((x0 - hw) / G)), min(self.nx, int((x1 + hw) / G) + 2)):
|
|
px, py = ix * G, iy * G
|
|
if x0 - hw - CLEAR <= px <= x1 + hw + CLEAR and y0 - hw - CLEAR <= py <= y1 + hw + CLEAR:
|
|
m[iy * self.nx + ix] = -2
|
|
|
|
# ------------------------------------------------------------ search
|
|
def pad_cells(self, pad):
|
|
layers = [0 if l == 'F.Cu' else 1 for l in pad.layers]
|
|
cells = set()
|
|
ix0, iy0 = int((pad.x - pad.hx) / G), int((pad.y - pad.hy) / G)
|
|
ix1, iy1 = int((pad.x + pad.hx) / G) + 1, int((pad.y + pad.hy) / G) + 1
|
|
for iy in range(iy0, iy1 + 1):
|
|
for ix in range(ix0, ix1 + 1):
|
|
if pad.dist(ix * G, iy * G) <= 1e-9:
|
|
for L in layers:
|
|
cells.add((L, ix, iy))
|
|
if not cells:
|
|
ix, iy = self.cell(pad.x, pad.y)
|
|
for L in layers:
|
|
cells.add((L, ix, iy))
|
|
return cells
|
|
|
|
def search(self, net, r_track, r_via, sources, targets, tx, ty, max_expand=1500000):
|
|
k = self.nid(net)
|
|
nx = self.nx
|
|
mt = self.maps[r_track]
|
|
mv = self.maps[r_via]
|
|
own = self.net_cells.setdefault(net, set())
|
|
tset = targets
|
|
txi, tyi = tx / G, ty / G
|
|
|
|
def free(L, ix, iy):
|
|
if ix < 0 or iy < 0 or ix >= nx or iy >= self.ny:
|
|
return False
|
|
if (L, ix, iy) in tset:
|
|
return True
|
|
v = mt[L][iy * nx + ix]
|
|
return v == -1 or v == k
|
|
|
|
def via_ok(ix, iy):
|
|
if self.no_via[iy * nx + ix]:
|
|
return False
|
|
for L in (0, 1):
|
|
v = mv[L][iy * nx + ix]
|
|
if not (v == -1 or v == k):
|
|
return False
|
|
return True
|
|
|
|
def h(ix, iy):
|
|
dx, dy = abs(ix - txi), abs(iy - tyi)
|
|
return int(COST_STEP * (max(dx, dy) - min(dx, dy)) + COST_DIAG * min(dx, dy))
|
|
|
|
openh = []
|
|
g = {}
|
|
came = {}
|
|
for (L, ix, iy) in sources:
|
|
s = (L, ix, iy, 8)
|
|
g[s] = 0
|
|
heapq.heappush(openh, (h(ix, iy), 0, s))
|
|
expanded = 0
|
|
while openh:
|
|
f, gc, s = heapq.heappop(openh)
|
|
if g.get(s, 1 << 60) < gc:
|
|
continue
|
|
L, ix, iy, d = s
|
|
if (L, ix, iy) in tset:
|
|
path = [(L, ix, iy)]
|
|
while s in came:
|
|
s = came[s]
|
|
path.append(s[:3])
|
|
path.reverse()
|
|
return path
|
|
expanded += 1
|
|
if expanded > max_expand:
|
|
return None
|
|
for nd, (dx, dy) in enumerate(DIRS):
|
|
jx, jy = ix + dx, iy + dy
|
|
if not free(L, jx, jy):
|
|
continue
|
|
if dx and dy:
|
|
# don't cut corners between two blocked orthogonal neighbours
|
|
if not (free(L, ix + dx, iy) or free(L, ix, iy + dy)):
|
|
continue
|
|
step = COST_DIAG
|
|
else:
|
|
step = COST_STEP
|
|
if d != 8 and nd != d:
|
|
step += COST_TURN * (1 if (nd - d) % 8 in (1, 7) else 3)
|
|
if L == 1:
|
|
step += COST_BCU
|
|
ns = (L, jx, jy, nd)
|
|
ng = gc + step
|
|
if ng < g.get(ns, 1 << 60):
|
|
g[ns] = ng
|
|
came[ns] = s
|
|
heapq.heappush(openh, (ng + h(jx, jy), ng, ns))
|
|
# via
|
|
if via_ok(ix, iy):
|
|
ns = (1 - L, ix, iy, 8)
|
|
ng = gc + COST_VIA
|
|
if ng < g.get(ns, 1 << 60) and free(1 - L, ix, iy):
|
|
g[ns] = ng
|
|
came[ns] = s
|
|
heapq.heappush(openh, (ng + h(ix, iy), ng, ns))
|
|
return None
|
|
|
|
def commit_path(self, net, path, w, start_xy=None, end_xy=None):
|
|
"""Turn a cell path into merged segments + vias, add them as obstacles."""
|
|
own = self.net_cells.setdefault(net, set())
|
|
pts = [] # (layer, x, y)
|
|
for (L, ix, iy) in path:
|
|
pts.append((L, ix * G, iy * G))
|
|
own.add((L, ix, iy))
|
|
segs = []
|
|
vias = []
|
|
run = [pts[0]]
|
|
for p in pts[1:]:
|
|
if p[0] != run[-1][0]:
|
|
vias.append((p[1], p[2]))
|
|
if len(run) > 1:
|
|
segs.append(run)
|
|
run = [p]
|
|
else:
|
|
run.append(p)
|
|
if len(run) > 1:
|
|
segs.append(run)
|
|
out = []
|
|
for run in segs:
|
|
# merge collinear points
|
|
simp = [run[0]]
|
|
for i in range(1, len(run) - 1):
|
|
a, b, c = simp[-1], run[i], run[i + 1]
|
|
d1 = (round((b[1] - a[1]) / G), round((b[2] - a[2]) / G))
|
|
d2 = (round((c[1] - b[1]) / G), round((c[2] - b[2]) / G))
|
|
if d1[0] * d2[1] - d1[1] * d2[0] != 0 or d1[0] * d2[0] + d1[1] * d2[1] < 0:
|
|
simp.append(b)
|
|
simp.append(run[-1])
|
|
for a, b in zip(simp, simp[1:]):
|
|
out.append((a[1], a[2], b[1], b[2], w, a[0], net))
|
|
for (x, y) in vias:
|
|
self.add_via(x, y, net)
|
|
for (x1, y1, x2, y2, ww, L, n) in out:
|
|
self.add_track(x1, y1, x2, y2, ww, L, n)
|
|
return len(out), len(vias)
|