Controller board: ESP32 + 2x StepStick carrier (v0.4), FluidNC config
- 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>
This commit is contained in:
1 parent
c498882bbb
commit
1400cc8afd
46 files changed
+226470
No files matched your search
@@ -0,0 +1,289 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user