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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
"""Silkscreen layout for the riser (run with KiCad 10's python.exe).
Reads the saved board, builds per-face obstacles (pads with their mask openings, vias, holes, part courtyards, the
board edge, the OCP long-edge strips, the finger field) and places, greedily with a bounded search of positions,
rotations and sizes measured with KiCad's own stroke-font bounds:
- every reference (and every R/C value beside it), test-point net labels, port and LED legends on F.SilkS,
- finger pin numbers at the ends of each finger group on both faces,
- the board identity, link and the BIOS note on B.SilkS.
Writes design/silk_plan.json (field updates by native field UUID, standalone texts) and, with "apply", a copy of the
board with the plan applied for DRC (design/silk_check.kicad_pcb). Every number in the plan is from this board."""
import json, os, sys, math
import pcbnew
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
SRC = sys.argv[1]
MODE = sys.argv[2] if len(sys.argv) > 2 else "plan"
MM, TO = pcbnew.FromMM, pcbnew.ToMM
b = pcbnew.LoadBoard(SRC)
STROKE = 0.15 # JLC pack: min text thickness 0.15 mm
REF_SIZES = (1.0, 0.8) # JLC pack: min text height 0.8 mm
VAL_SIZES = (0.8,)
CLR = 0.15 # silk to exposed copper / other silk
KEYED = {"R11", "R12", "R13", "R14", "R15", "R16", "R17", "R18"} # PERST# series 33R and pull-down 100k block
TP_LABEL = {"TP1": "3V3", "TP2": "GND", "TP3": "SMCLK", "TP4": "SMDAT", "TP5": "12V", "TP6": "PWRGD",
"TP7": "PERST0", "TP8": "PERST1", "TP9": "PERST2", "TP10": "PERST3", "TP11": "SCLK", "TP12": "SDOUT"}
LED_LABEL = {"D1": "3V3", "D2": "12V"}
def box(x0, y0, x1, y1):
return (min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1))
def grow(r, d):
return (r[0] - d, r[1] - d, r[2] + d, r[3] + d)
def hit(r, s):
return not (r[2] <= s[0] or s[2] <= r[0] or r[3] <= s[1] or s[3] <= r[1])
def bb(item):
e = item.GetBoundingBox()
return box(TO(e.GetLeft()), TO(e.GetTop()), TO(e.GetRight()), TO(e.GetBottom()))
# ------------------------------------------------------------------ obstacles per face
obst = {"F": [], "B": []}
for fp in b.GetFootprints():
for side, lay in (("F", pcbnew.F_CrtYd), ("B", pcbnew.B_CrtYd)):
cy = fp.GetCourtyard(lay)
if cy.OutlineCount():
e = cy.BBox()
r = box(TO(e.GetLeft()), TO(e.GetTop()), TO(e.GetRight()), TO(e.GetBottom()))
if fp.GetReference() != "J1": # J1's courtyard is the finger field, handled below
obst[side].append(("crtyd", fp.GetReference(), r))
for p in fp.Pads():
r = grow(bb(p), CLR + 0.05) # mask expansion + clearance
if p.IsOnLayer(pcbnew.F_Cu) or p.GetAttribute() in (pcbnew.PAD_ATTRIB_PTH, pcbnew.PAD_ATTRIB_NPTH):
obst["F"].append(("pad", fp.GetReference(), r))
if p.IsOnLayer(pcbnew.B_Cu) or p.GetAttribute() in (pcbnew.PAD_ATTRIB_PTH, pcbnew.PAD_ATTRIB_NPTH):
obst["B"].append(("pad", fp.GetReference(), r))
# vias are tented on both faces (board setup), so silk may cross them; only exposed copper is an obstacle
W, H = 76.0, 115.0
for side in ("F", "B"):
obst[side] += [("edge", "", (-1, -1, 3.05, H + 1)), ("edge", "", (W - 3.05, -1, W + 1, H + 1)), # strips + margin
("edge", "", (-1, -1, W + 1, 0.4)), ("fingers", "", (-1, 111.7, W + 1, H + 1))]
placed = {"F": [], "B": []}
def free(side, r, ignore=()):
if r[0] < 0.4 or r[2] > W - 0.4 or r[1] < 0.4 or r[3] > H - 0.4:
return False
for kind, ref, s in obst[side]:
if ref in ignore and kind == "crtyd":
continue
if hit(r, s):
return False
return not any(hit(grow(r, CLR / 2), grow(s, CLR / 2)) for s in placed[side])
def text_box(text, x, y, size, rot, side, align="center"):
t = pcbnew.PCB_TEXT(b)
t.SetText(text); t.SetTextSize(pcbnew.VECTOR2I(MM(size), MM(size))); t.SetTextThickness(MM(STROKE))
t.SetPosition(pcbnew.VECTOR2I(MM(x), MM(y))); t.SetTextAngleDegrees(rot)
t.SetLayer(pcbnew.F_SilkS if side == "F" else pcbnew.B_SilkS)
t.SetHorizJustify(pcbnew.GR_TEXT_H_ALIGN_CENTER if align == "center" else pcbnew.GR_TEXT_H_ALIGN_LEFT)
if side == "B":
t.SetMirrored(True)
return bb(t)
def search(text, cx, cy, around, side, sizes, ignore=(), prefer=None):
"""Positions on a ring around `around` (a box), nearest first, at each size and rotation."""
best = None
for size in sizes:
for rot in (0, 90):
for gap in (0.2, 0.5, 0.9, 1.4, 2.0, 2.8):
cands = []
x0, y0, x1, y1 = around
mx, my = (x0 + x1) / 2, (y0 + y1) / 2
tb = text_box(text, 0, 0, size, rot, side)
hw, hh = (tb[2] - tb[0]) / 2, (tb[3] - tb[1]) / 2
pts = []
for k in range(-6, 7): # slide along each side of the part
o = 0.35 * k
pts += [(mx + o, y0 - gap - hh), (mx + o, y1 + gap + hh), (x0 - gap - hw, my + o), (x1 + gap + hw, my + o)]
for (px, py) in pts:
cands.append((math.hypot(px - cx, py - cy), px, py))
if prefer:
cands.sort(key=lambda c: (c[0] + (0 if prefer(c[1], c[2]) else 50)))
else:
cands.sort()
for _, px, py in cands:
r = text_box(text, px, py, size, rot, side)
# centre the box on (px, py): KiCad positions by the text anchor, so shift
dx, dy = px - (r[0] + r[2]) / 2, py - (r[1] + r[3]) / 2
r = (r[0] + dx, r[1] + dy, r[2] + dx, r[3] + dy)
if free(side, r, ignore):
return dict(x=round(px + dx, 3), y=round(py + dy, 3), size=size, rot=rot, box=r)
return best
plan = {"fields": [], "texts": [], "unresolved": []}
# labels that failed on an earlier pass go first (a bounded repack instead of a single greedy order)
_pri_path = os.path.join(ROOT, "design", "silk_priority.json")
PRI = json.load(open(_pri_path)) if os.path.exists(_pri_path) else []
fps = sorted(b.GetFootprints(), key=lambda f: (f.GetReference() not in PRI, PRI.index(f.GetReference()) if f.GetReference() in PRI else 0,
f.GetReference()[0], f.GetPosition().y, f.GetPosition().x))
# ------------------------------------------------------------------ fixed legends first (they anchor the reading)
def add_text(text, x, y, size, side, rot=0, align="center", why=""):
r = text_box(text, x, y, size, rot, side, align)
placed[side].append(r)
plan["texts"].append(dict(text=text, layer=f"{side}.SilkS", x=round(x, 3), y=round(y, 3), height=size,
stroke=STROKE, rotation=rot, align=align, why=why))
# finger group ends, both faces: B pins on top, A pins below (A under B)
J1 = b.FindFootprintByReference("J1")
jx, jy = TO(J1.GetPosition().x), TO(J1.GetPosition().y)
ends = [("OCP_B1", -30.400), ("OCP_B14", -30.400 + 13 * 0.6), ("B1", -18.205), ("B28", -2.005), ("B29", 2.005),
("B42", 9.805), ("B43", 14.715), ("B70", 30.915)]
for k, (name, dx) in enumerate(ends):
# first pin of a group: text runs inward (right) from the finger; last pin: inward (left); clear of the slots
for side, nm in (("F", name), ("B", name.replace("B", "A"))):
r = text_box(nm, 0, 0, 0.8, 0, side)
half = (r[2] - r[0]) / 2
x = jx + dx + (half - 0.19 if k % 2 == 0 else -(half - 0.19))
yy = 109.75 if name == "OCP_B14" else 110.9 # the short OCP group: its two labels on two rows
add_text(nm, x, yy, 0.8, side, why=f"finger pin number at the {'start' if k % 2 == 0 else 'end'} of its group ({'top = B' if side == 'F' else 'bottom = A'} side)")
# the ports on the bottom face too (a mounted card may show only its underside)
for _k, _r in enumerate(("J2", "J3", "J4", "J5")):
_fp = b.FindFootprintByReference(_r)
_x = TO(_fp.GetPosition().x)
for _y in (12.0, 12.8, 13.6, 14.4, 15.2):
if free("B", text_box(f"PORT {_k}", _x, _y, 1.0, 0, "B")):
add_text(f"PORT {_k}", _x, _y, 1.0, "B", why=f"{_r} port number, bottom face")
break
else:
plan["unresolved"].append(f"{_r}: bottom-face port legend found no space")
# ------------------------------------------------------------------ references, values, labels
for fp in fps:
ref = fp.GetReference()
if ref in ("J1", "H1", "H2"):
continue
side = "B" if fp.IsFlipped() else "F"
cy_ = fp.GetCourtyard(pcbnew.F_CrtYd if side == "F" else pcbnew.B_CrtYd)
e = cy_.BBox() if cy_.OutlineCount() else fp.GetBoundingBox()
around = box(TO(e.GetLeft()), TO(e.GetTop()), TO(e.GetRight()), TO(e.GetBottom()))
cx, cy = (around[0] + around[2]) / 2, (around[1] + around[3]) / 2
label = ref
if ref in TP_LABEL:
label = f"{ref} {TP_LABEL[ref]}"
elif ref in LED_LABEL:
label = f"{ref} {LED_LABEL[ref]}"
elif ref == "JP1":
label = "JP1 FRU WRITE"
elif ref[0] == "J" and ref in ("J2", "J3", "J4", "J5"):
label = f"{ref} PORT {int(ref[1]) - 2}"
# the connectors face the faceplate: their labels go on the board side of the ESD parts
prefer = (lambda x, y: y > 11.5) if ref in ("J2", "J3", "J4", "J5") else None
got = search(label, cx, cy, around, side, REF_SIZES, ignore=(ref,), prefer=prefer)
split = None
if not got and label != ref:
got = search(ref, cx, cy, around, side, REF_SIZES, ignore=(ref,))
if got:
split = label[len(ref) + 1:] # the function goes beside the reference, as its own text
label = ref
if not got:
plan["unresolved"].append(f"{ref}: no free spot for its reference at 0.8 mm")
continue
placed[side].append(got["box"])
if label == ref:
f = fp.Reference()
plan["fields"].append(dict(ref=ref, field="Reference", id=f.m_Uuid.AsString(), x=got["x"], y=got["y"],
rotation=got["rot"], layer=f"{side}.SilkS", height=got["size"], stroke=STROKE,
visible=True))
else:
add_text(label, got["x"], got["y"], got["size"], side, got["rot"], why=f"{ref} with its function")
placed[side].pop() # add_text appended the same box again
if split:
sg = search(split, got["x"], got["y"], got["box"], side, (0.8,), ignore=(ref,))
if sg:
add_text(split, sg["x"], sg["y"], sg["size"], side, sg["rot"], why=f"{ref} function, beside its reference")
else:
plan["unresolved"].append(f"{ref}: its function '{split}' has no spot beside the reference")
# the value beside the reference, for R and C (the dense PERST# resistor block gets one key instead)
if ref[0] in "RC" and ref not in KEYED:
val = fp.GetValue()
vg = search(val, got["x"], got["y"], got["box"], side, VAL_SIZES, ignore=(ref,))
if vg:
placed[side].append(vg["box"])
f = fp.Value()
plan["fields"].append(dict(ref=ref, field="Value", id=f.m_Uuid.AsString(), x=vg["x"], y=vg["y"],
rotation=vg["rot"], layer=f"{side}.SilkS", height=vg["size"], stroke=STROKE,
visible=True))
else:
plan["unresolved"].append(f"{ref}: value {val} has no free spot beside its reference (in the bottom key)")
# one value key for the PERST# resistor block, as near the block as it fits
_xs = [TO(b.FindFootprintByReference(r).GetPosition().x) for r in KEYED]; _ys = [TO(b.FindFootprintByReference(r).GetPosition().y) for r in KEYED]
_blk = (min(_xs) - 0.6, min(_ys) - 0.4, max(_xs) + 0.6, max(_ys) + 0.4)
for _txt in ("R11-R14 33 R15-R18 100k", "R11-14 33 / R15-18 100k"):
kg = search(_txt, (_blk[0] + _blk[2]) / 2, (_blk[1] + _blk[3]) / 2, _blk, "F", (0.8,))
if kg:
add_text(_txt, kg["x"], kg["y"], 0.8, "F", kg["rot"], why="values of the PERST# resistor block (33 ohm series, 100k pull-down)")
break
else:
# under the block on the bottom face (mirrored, reads from below), still beside the parts it names
for _txt in ("R11-R14 33 R15-R18 100k", "R11-14 33 / R15-18 100k"):
kg = search(_txt, (_blk[0] + _blk[2]) / 2, (_blk[1] + _blk[3]) / 2, grow(_blk, -1.0), "B", (0.8,))
if kg:
add_text(_txt, kg["x"], kg["y"], 0.8, "B", kg["rot"], why="values of the PERST# resistor block, under it on the bottom face")
plan["unresolved"].append("R11-R18: values are keyed on the bottom face under the block (no top-side room)")
break
else:
plan["unresolved"].append("R11-R18: the value key found no spot near the block")
# ------------------------------------------------------------------ bottom: identity, link, BIOS note, value key
ident = [("OCP NIC 3.0 SFF > 4x OCuLink 4i", 1.6), ("PCIe Gen4 x16 as 4 x4 rev A", 1.2),
("Set the slot to x4x4x4x4 in BIOS", 1.0), ("wiki.adom.inc/john/oculink-aiom-riser", 1.0)]
y = 40.0
for text, size in ident:
r = text_box(text, 38.0, y, size, 0, "B")
while not free("B", r) and y < 100:
y += 0.5; r = text_box(text, 38.0, y, size, 0, "B")
if free("B", r):
add_text(text, 38.0, y, size, "B", why="board identity / service note")
y += size * 1.9
else:
plan["unresolved"].append(f"bottom: '{text}' found no space")
missing_vals = [u.split(":")[0] for u in plan["unresolved"] if "value " in u and b.FindFootprintByReference(u.split(":")[0]) is not None]
if missing_vals:
for chunk_start in range(0, len(missing_vals), 6):
chunk = missing_vals[chunk_start:chunk_start + 6]
line = " ".join(f"{r}={b.FindFootprintByReference(r).GetValue()}" for r in chunk)
r = text_box(line, 38.0, y, 0.8, 0, "B")
while not free("B", r) and y < 105:
y += 0.5; r = text_box(line, 38.0, y, 0.8, 0, "B")
if free("B", r):
add_text(line, 38.0, y, 0.8, "B", why="value key for parts whose value did not fit beside them")
y += 1.6
json.dump(plan, open(os.path.join(ROOT, "design", "silk_plan.json"), "w"), indent=1)
newpri = PRI + [u.split(":")[0] for u in plan["unresolved"] if u.split(":")[0] not in PRI and not u.startswith("bottom")]
json.dump(newpri, open(_pri_path, "w"))
print(f"fields {len(plan['fields'])}, texts {len(plan['texts'])}, unresolved {len(plan['unresolved'])}")
for u in plan["unresolved"]:
print(" unresolved:", u)
if MODE == "apply":
for fl in plan["fields"]:
fp = b.FindFootprintByReference(fl["ref"])
f = fp.Reference() if fl["field"] == "Reference" else fp.Value()
f.SetVisible(True)
f.SetLayer(pcbnew.F_SilkS if fl["layer"] == "F.SilkS" else pcbnew.B_SilkS)
f.SetTextSize(pcbnew.VECTOR2I(MM(fl["height"]), MM(fl["height"]))); f.SetTextThickness(MM(fl["stroke"]))
f.SetTextAngleDegrees(fl["rotation"])
f.SetHorizJustify(pcbnew.GR_TEXT_H_ALIGN_CENTER)
f.SetPosition(pcbnew.VECTOR2I(MM(fl["x"]), MM(fl["y"])))
for tx in plan["texts"]:
t = pcbnew.PCB_TEXT(b)
t.SetText(tx["text"]); t.SetTextSize(pcbnew.VECTOR2I(MM(tx["height"]), MM(tx["height"])))
t.SetTextThickness(MM(tx["stroke"])); t.SetPosition(pcbnew.VECTOR2I(MM(tx["x"]), MM(tx["y"])))
t.SetTextAngleDegrees(tx["rotation"])
t.SetLayer(pcbnew.F_SilkS if tx["layer"] == "F.SilkS" else pcbnew.B_SilkS)
t.SetHorizJustify(pcbnew.GR_TEXT_H_ALIGN_CENTER if tx["align"] == "center" else pcbnew.GR_TEXT_H_ALIGN_LEFT)
if tx["layer"] == "B.SilkS":
t.SetMirrored(True)
b.Add(t)
out = os.path.join(ROOT, "design", "silk_check.kicad_pcb")
pcbnew.SaveBoard(out, b)
print("applied ->", out)