board
OCuLink Riser for the Supermicro AIOM Slot
Public Unreviewedby John Lauer
Passive OCP NIC 3.0 SFF card that breaks a Supermicro AIOM slot's PCIe x16 into four OCuLink 4i ports (4 x4, Gen4): 6-layer, 85 ohm, length-matched, DRC-clean KiCad project with Gerbers, BOM and CPL.
main
John Lauer
README: component table per the wiki format, open-source section, no duplicate hero; generators at gen/
39a59ef
7d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
#!/usr/bin/env python3
"""Assemble every route: high-speed (hs.py), fixed extras, maze-routed low-speed nets, GND fan-out, stitching,
zones and keepouts -> design/routes.json for build_board.py final."""
import json, math, os, sys, time
import numpy as np
from shapely.geometry import Polygon, Point, LineString, box
from shapely.ops import unary_union
from shapely import contains_xy
sys.path.insert(0, os.path.dirname(__file__))
import hs, maze
from maze import G, NX, NY, LI
ROOT = os.path.join(os.path.dirname(__file__), "..")
pads = json.load(open(os.path.join(ROOT, "design", "pads.json")))
lay = json.load(open(os.path.join(ROOT, "design", "layout.json")))
nl = json.load(open(os.path.join(ROOT, "design", "netlist.json")))
OUTLINE = Polygon(lay["outline"])
LAYMAP = {"F.Cu": "L1", "In2.Cu": "L3", "In4.Cu": "L5", "B.Cu": "L6"}
T, V = [], [] # all tracks and vias for routes.json
B = maze.Board()
# ------------------------------------------------------------------ fixed high-speed copper
pairs, lines = hs.all_routes()
for p in pairs:
for t in p.tracks:
T.append(t); B.track(*t, hs=True)
for v in p.vias:
V.append(v); B.via(v[0], v[1], v[2], v[3], hs=True)
for l in lines:
for t in l.tracks:
T.append(t); B.track(*t)
for v in l.vias:
V.append(v); B.via(v[0], v[1], v[2], v[3])
# ------------------------------------------------------------------ pads
PAD = {}
for p in pads:
net = p["net"] or f"NC:{p['ref']}.{p['num']}"
PAD[(p["ref"], p["num"], round(p["x"], 3), round(p["y"], 3))] = p
geom = unary_union([Polygon(o) for o in p["poly"] if len(o) >= 3]) if p["poly"] else Point(p["x"], p["y"]).buffer(p["w"] / 2)
p["geom"] = geom
lis = [LI[LAYMAP[l]] for l in p["layers"] if l in LAYMAP]
if len(p["layers"]) >= 6 or p["drill"][0] > 0:
lis = [0, 1, 2, 3]
# a plated hole occupies every layer with at least its drill
dg = Point(p["x"], p["y"]).buffer(max(p["drill"]) / 2 + 0.05)
p["geom"] = geom.union(dg)
B.paint_geom(lis, p["geom"], B.nid(net))
p["lis"] = lis
# ------------------------------------------------------------------ fixed extras
def add_track(t):
T.append(t); B.track(*t)
def add_via(v, hsflag=False):
V.append(v); B.via(v[0], v[1], v[2], v[3], hs=hsflag)
pin_net = {}
for n, ps in nl["nets"].items():
for q in ps:
pin_net[q] = n
# GND gold fingers: short stub off the inner end so the outer-layer GND pour picks them up
for p in pads:
if p["ref"] == "J1" and p["net"] == "GND":
layer = "L1" if "F.Cu" in p["layers"] else "L6"
add_track((layer, "GND", p["x"], hs.PAD_TOP, p["x"], hs.PAD_TOP - 0.55, 0.3))
cols = {}
for p in pads:
if p["ref"] == "J1":
cols.setdefault(round(p["x"], 3), {})["F" if "F.Cu" in p["layers"] else "B"] = p["net"]
for x, sides in sorted(cols.items()):
if sides.get("F") == "GND" and sides.get("B") == "GND":
add_via((x, hs.PAD_TOP - 0.62, "GND", 0.4, 0.2))
# +12V fingers B1..B6: one bar
b1 = hs.finger("B1"); b6 = hs.finger("B6")
add_track(("L1", "+12V_EDGE", b1[0], b1[1], b1[0], 111.6, 0.3))
for n in range(2, 7):
x, y = hs.finger(f"B{n}")
add_track(("L1", "+12V_EDGE", x, y, x, 111.6, 0.3))
add_track(("L1", "+12V_EDGE", b1[0], 111.6, b6[0], 111.6, 0.3))
# OCuLink GND pads: via-in-pad (filled and capped)
for p in pads:
if p["ref"] in ("J2", "J3", "J4", "J5") and p["net"] == "GND" and p["num"] != "SH":
add_via((p["x"], p["y"], "GND", hs.VIP_D, hs.VIP_H), True)
# ------------------------------------------------------------------ finger escape reservations
# every used signal/power finger keeps a 2.4 mm column above it on its own layer, so no other net's via lands there
HS_NETS = set()
for _p in pairs:
HS_NETS |= {_p.nnet, _p.pnet}
for p in pads:
if p["ref"] != "J1" or not p["net"] or p["net"] in ("GND", "+12V_EDGE") or p["net"] in HS_NETS:
continue
li = LI["L1"] if "F.Cu" in p["layers"] else LI["L6"]
w = 0.2
B.paint_geom(li, box(p["x"] - w / 2, hs.PAD_TOP - 2.4, p["x"] + w / 2, hs.PAD_TOP), B.nid(p["net"]))
# ------------------------------------------------------------------ static keepouts
yy, xx = B.Y, B.X
inside = contains_xy(OUTLINE.buffer(-0.42), xx, yy)
for li in range(4):
B.block[li] |= ~inside
# long-edge strips: no copper on outer layers within 2.75 mm, inner layers within 1.0 mm (OCP Fig. 8/10)
strip_outer = (xx < 2.75) | (xx > 76 - 2.75)
strip_inner = (xx < 1.0) | (xx > 75.0)
B.block[LI["L1"]] |= strip_outer; B.block[LI["L6"]] |= strip_outer
B.block[LI["L3"]] |= strip_inner; B.block[LI["L5"]] |= strip_inner
# gold-finger field: nothing else on the outer layers past the finger tops; inner copper clear of the bevel
B.block[LI["L1"]] |= yy > 112.05
B.block[LI["L6"]] |= yy > 112.05
B.block[LI["L3"]] |= yy > 113.6
B.block[LI["L5"]] |= yy > 113.6
B.block_via = np.zeros_like(B.block)
for li in range(4):
B.block_via[li] = B.block[li] | (yy > 111.6)
def terminals(net):
out = []
for p in pads:
if p["net"] != net:
continue
cells = set()
x0, y0, x1, y1 = p["geom"].bounds
i0, i1, j0, j1 = int(x0 / G), int(x1 / G) + 1, int(y0 / G), int(y1 / G) + 1
sub = contains_xy(p["geom"].buffer(-0.02), xx[j0:j1 + 1, i0:i1 + 1], yy[j0:j1 + 1, i0:i1 + 1])
js, iss = np.nonzero(sub)
for li in p["lis"]:
for j, i in zip(js + j0, iss + i0):
cells.add((li, int(j), int(i)))
if not cells:
cells = {(li, int(round(p["y"] / G)), int(round(p["x"] / G))) for li in p["lis"]}
out.append(cells)
# existing copper of the net (fixed lines) is one more terminal
nid = B.nid(net)
ex = set()
for (lay_, n, x1, y1, x2, y2, w) in T:
if n == net:
L = max(1, int(math.hypot(x2 - x1, y2 - y1) / G))
for k in range(L + 1):
x = x1 + (x2 - x1) * k / L; y = y1 + (y2 - y1) * k / L
ex.add((LI[lay_], int(round(y / G)), int(round(x / G))))
if ex:
out.insert(0, ex)
return out
L6_REGION = (yy > 101.0) & (xx < 30.0)
ROUTE = [
# bottom-side (A) finger escapes first: each needs a via site above its own finger
("PERST1#", 0.15, ("L1", "L3", "L5", "L6")), ("PRSNTA0#", 0.15, ("L1", "L3", "L6")),
("PRSNTB2#", 0.15, ("L1", "L3", "L6")), ("SMCLK0", 0.15, ("L1", "L3", "L5", "L6")),
("SMDAT0", 0.15, ("L1", "L3", "L5", "L6")), ("PERST2#", 0.15, ("L1", "L3", "L5", "L6")),
("PERST3#", 0.15, ("L1", "L3", "L5", "L6")), ("WAKE0#", 0.15, ("L1", "L3", "L5", "L6")),
("SLOT_ID1", 0.15, ("L1", "L3", "L5", "L6")),
# top side and power
("+3V3_EDGE", 0.2, ("L1", "L3", "L5")), ("+12V_EDGE", 0.2, ("L1", "L3", "L5")),
("PRSNTB3#", 0.15, ("L1", "L3")), ("PERST0#", 0.15, ("L1", "L3", "L5")),
("NIC_PWR_GOOD0", 0.15, ("L1", "L3", "L5")), ("SCAN0_LD#", 0.15, ("L1", "L3", "L5")),
("SCAN0_DATA_IN", 0.15, ("L1", "L3", "L5")), ("SCAN0_DATA_OUT", 0.15, ("L1", "L3", "L5")),
("SCAN0_CLK", 0.15, ("L1", "L3", "L5")), ("SLOT_ID0", 0.15, ("L1", "L3", "L5")),
("AUX_PWR_EN0", 0.15, ("L1", "L3", "L5")),
("PERST0_BUF", 0.15, ("L1", "L3", "L5")), ("PERST1_BUF", 0.15, ("L1", "L3", "L5")),
("PERST2_BUF", 0.15, ("L1", "L3", "L5")), ("PERST3_BUF", 0.15, ("L1", "L3", "L5")),
("PORT0_PERST#", 0.15, ("L1", "L3", "L5")), ("PORT1_PERST#", 0.15, ("L1", "L3", "L5")),
("PORT2_PERST#", 0.15, ("L1", "L3", "L5")), ("PORT3_PERST#", 0.15, ("L1", "L3", "L5")),
("PORT0_CPRSNT#", 0.15, ("L1", "L3", "L5")), ("PORT1_CPRSNT#", 0.15, ("L1", "L3", "L5")),
("PORT2_CPRSNT#", 0.15, ("L1", "L3", "L5")), ("PORT3_CPRSNT#", 0.15, ("L1", "L3", "L5")),
("FRU_WP", 0.15, ("L1", "L3", "L5")), ("SR0_SER", 0.15, ("L1", "L3", "L5")),
("LED3V3_A", 0.15, ("L1",)), ("LED12V_A", 0.15, ("L1",)),
]
CLR, CLR_HS = 0.14, 0.28
def paint_net(net, tracks, vias):
for t in tracks:
B.track(*t)
for v in vias:
B.via(v[0], v[1], v[2], v[3])
def route_list(order, routed, attempt_log):
fails = []
for (net, w, allow) in order:
terms = terminals(net)
if len(terms) < 2:
continue
paths, msg = maze.route_net(B, net, terms, w=w, clr=CLR, clr_hs=CLR_HS, allow=allow, l6_region=L6_REGION)
if paths is None:
fails.append(net); attempt_log.append(f"FAIL {net}: {msg}"); print(f"FAIL {net}: {msg}", flush=True); continue
tr, vi = [], []
for pth in paths:
a_, b_ = maze.path_to_copper(pth, net, w)
tr += a_; vi += b_
routed[net] = (tr, vi)
paint_net(net, tr, vi)
print(f"{net}: {len(paths)} branch(es), {len(tr)} tracks, {len(vi)} vias", flush=True)
return fails
def rebuild(routed):
"""Fresh raster with fixed copper + pads + reservations + everything routed so far."""
global B
lab0, hs0, blk, blkv = BASE
B.lab = lab0.copy(); B.hs = hs0.copy()
for net, (tr, vi) in routed.items():
paint_net(net, tr, vi)
if __name__ == "__main__":
t0 = time.time()
FIXED_T, FIXED_V = list(T), list(V)
BASE = (B.lab.copy(), B.hs.copy(), B.block.copy(), B.block_via.copy())
W = {n: (w, a) for (n, w, a) in ROUTE}
routed = {}
log = []
fails = route_list(ROUTE, routed, log)
priority = [] # every net that ever failed, routed first from then on
for rnd in range(10):
if not fails:
break
for n in fails:
if n not in priority:
priority.append(n)
print(f"--- rip-up round {rnd+1}: {fails} priority {priority}", flush=True)
rip = set()
for net in priority:
for (li, j, i) in set().union(*terminals(net)):
r = int(1.2 / G)
for lj in range(4):
blk = B.lab[lj, max(0, j - r):j + r + 1, max(0, i - r):i + r + 1]
for nid in np.unique(blk):
if nid >= 0 and B.netname[nid] in routed:
rip.add(B.netname[nid])
rip |= set(priority)
for n in rip:
routed.pop(n, None)
rebuild(routed)
# the nets that just failed get first pick, then the older failures, then the ripped-up rest
first = list(fails) + [n for n in priority if n not in fails]
order = [(n, *W[n]) for n in first] + [(n, *W[n]) for n in [x[0] for x in ROUTE] if n in rip and n not in priority]
fails = route_list(order, routed, log)
# ESD grounds: each ESD's GND pins to the GND pads of its connector
for k in range(4):
ref, conn = f"U{5+k}", f"J{2+k}"
esd_cells, conn_cells = [], set()
for p in pads:
if p["net"] != "GND":
continue
if p["ref"] == ref or (p["ref"] == conn and p["num"] in ("B11", "B14", "B17", "B20")):
x0, y0, x1, y1 = p["geom"].bounds
i0, i1, j0, j1 = int(x0 / G), int(x1 / G) + 1, int(y0 / G), int(y1 / G) + 1
sub = contains_xy(p["geom"].buffer(-0.02), xx[j0:j1 + 1, i0:i1 + 1], yy[j0:j1 + 1, i0:i1 + 1])
js, iss = np.nonzero(sub)
cells = {(LI["L1"], int(j + j0), int(i + i0)) for j, i in zip(js, iss)}
if p["ref"] == ref:
esd_cells.append(cells)
else:
conn_cells |= cells
terms = [conn_cells] + esd_cells
paths, msg = maze.route_net(B, "GND", terms, w=0.2, clr=CLR, clr_hs=CLR_HS, allow=("L1",))
if paths is None:
print(f"FAIL ESD GND {ref}: {msg}"); fails.append(f"GND:{ref}"); continue
tr, vi = [], []
for pth in paths:
a_, b_ = maze.path_to_copper(pth, "GND", 0.2); tr += a_; vi += b_
routed[f"GND:{ref}"] = (tr, vi); paint_net("GND", tr, vi)
print(f"GND {ref}: {len(tr)} tracks")
for net, (tr, vi) in routed.items():
T.extend(tr); V.extend(vi)
json.dump({"tracks": T, "vias": V, "fails": fails, "fixed_tracks": FIXED_T, "fixed_vias": FIXED_V},
open(os.path.join(ROOT, "design", "routes_ls.json"), "w"))
print("fails:", fails, f"[{time.time()-t0:.0f}s]")