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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
"""Run with KiCad 10's python.exe. Stage 'base': footprints, nets, outline -> board + pads.json.
Stage 'final': base + tracks/vias/zones from routes.json, zones filled, saved."""
import json, os, sys, math
import pcbnew
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
STD_FP = r"C:\Users\john\AppData\Local\Programs\KiCad\10.0\share\kicad\footprints"
PRJ_FP = os.path.join(ROOT, "kicad", "lib")
OUT = os.path.join(ROOT, "kicad", "ocp-oculink-riser.kicad_pcb")
STAGE = sys.argv[1] if len(sys.argv) > 1 else "base"
nl = json.load(open(os.path.join(ROOT, "design", "netlist.json")))
lay = json.load(open(os.path.join(ROOT, "design", "layout.json")))
MM = pcbnew.FromMM
def V(x, y):
return pcbnew.VECTOR2I(MM(x), MM(y))
LAYERS = {"L1": pcbnew.F_Cu, "L3": pcbnew.In2_Cu, "L5": pcbnew.In4_Cu, "L6": pcbnew.B_Cu,
"L2": pcbnew.In1_Cu, "L4": pcbnew.In3_Cu}
def new_board():
b = pcbnew.BOARD()
b.SetCopperLayerCount(6)
ds = b.GetDesignSettings()
ds.SetBoardThickness(MM(1.6))
# JLCPCB 6-layer standard process minimums (adom/pcb-design-rules jlcpcb-6L)
ds.m_MinClearance = MM(0.09); ds.m_TrackMinWidth = MM(0.09)
ds.m_ViasMinSize = MM(0.25); ds.m_ViasMinAnnularWidth = MM(0.05)
ds.m_MinThroughDrill = MM(0.15); ds.m_HoleToHoleMin = MM(0.2)
ds.m_HoleClearance = MM(0.12); ds.m_CopperEdgeClearance = MM(0.2)
try:
nc = ds.m_NetSettings.GetDefaultNetclass()
except Exception:
nc = None
if nc is not None:
nc.SetClearance(MM(0.12)); nc.SetTrackWidth(MM(0.15))
nc.SetViaDiameter(MM(0.4)); nc.SetViaDrill(MM(0.2))
try:
nc.SetDiffPairWidth(MM(0.163)); nc.SetDiffPairGap(MM(0.15))
except Exception:
pass
b.SetLayerName(pcbnew.In1_Cu, "In1.Cu")
# nets
nets = {}
ncp = os.path.join(ROOT, "design", "nc_nets.json")
nc = json.load(open(ncp)) if os.path.exists(ncp) else {}
for name in sorted(set(nl["nets"]) | set(nc.values())):
ni = pcbnew.NETINFO_ITEM(b, name)
b.Add(ni)
nets[name] = ni
pin_net = {}
for name, pins in nl["nets"].items():
for p in pins:
pin_net[p] = name
pin_net.update(nc)
# footprints
for c in nl["components"]:
lib, fpname = c["fp"].split(":")
libdir = os.path.join(PRJ_FP, lib + ".pretty") if lib == "Riser" else os.path.join(STD_FP, lib + ".pretty")
fp = pcbnew.FootprintLoad(libdir, fpname)
if fp is None:
raise SystemExit("cannot load " + c["fp"])
fp.SetReference(c["ref"])
fp.SetValue(c["value"])
fp.SetFPIDAsString(c["fp"])
for k, v in (("MPN", c.get("mpn", "")), ("Manufacturer", c.get("mfr", "")), ("Source", c.get("src", "")),
("Description", c.get("desc", ""))):
fp.SetField(k, v or "")
f_ = fp.GetField(k)
if f_ is not None:
f_.SetVisible(False)
# silk: the silkscreen step places every reference; until then they are hidden. Chip R/C keep no body silk
# use the project's no-silk copies (Riser:*_NoSilk); ICs, LEDs and connectors keep their pin-1 / polarity marks.
fp.Reference().SetVisible(False)
x, y, rot, side = lay["placement"][c["ref"]]
b.Add(fp)
fp.SetPosition(V(x, y))
fp.SetOrientationDegrees(rot)
if side == "B":
fp.Flip(V(x, y), pcbnew.FLIP_DIRECTION_TOP_BOTTOM)
for pad in fp.Pads():
key = f'{c["ref"]}.{pad.GetNumber()}'
if key in pin_net:
pad.SetNet(nets[pin_net[key]])
# 3D model binding
if c.get("model"):
fp.Models().clear()
m = pcbnew.FP_3DMODEL()
m.m_Filename = c["model"]
fp.Models().push_back(m)
# outline: lines and arcs (every corner filleted)
for e in lay["edges"]:
s = pcbnew.PCB_SHAPE(b)
if e[0] == "line":
s.SetShape(pcbnew.SHAPE_T_SEGMENT)
s.SetStart(V(e[1], e[2])); s.SetEnd(V(e[3], e[4]))
else:
s.SetShape(pcbnew.SHAPE_T_ARC)
s.SetArcGeometry(V(e[1], e[2]), V(e[3], e[4]), V(e[5], e[6]))
s.SetLayer(pcbnew.Edge_Cuts); s.SetWidth(MM(0.05))
b.Add(s)
return b, nets
def export_pads(b):
out = []
for fp in b.GetFootprints():
for pad in fp.Pads():
sz = pad.GetSize(pcbnew.F_Cu)
lset = [pcbnew.BOARD.GetStandardLayerName(l) for l in pad.GetLayerSet().CuStack()]
poly = pad.GetEffectivePolygon(pcbnew.F_Cu if pad.IsOnLayer(pcbnew.F_Cu) else pcbnew.B_Cu, pcbnew.ERROR_INSIDE)
outline = []
for oi in range(poly.OutlineCount()):
ol = poly.Outline(oi)
outline.append([(pcbnew.ToMM(ol.CPoint(i).x), pcbnew.ToMM(ol.CPoint(i).y)) for i in range(ol.PointCount())])
dr = pad.GetDrillSize()
out.append(dict(ref=fp.GetReference(), num=pad.GetNumber(), net=pad.GetNetname(),
x=pcbnew.ToMM(pad.GetPosition().x), y=pcbnew.ToMM(pad.GetPosition().y),
w=pcbnew.ToMM(sz.x), h=pcbnew.ToMM(sz.y), layers=lset, poly=outline,
drill=[pcbnew.ToMM(dr.x), pcbnew.ToMM(dr.y)], attr=str(pad.GetAttribute())))
return out
def add_routes(b, nets, which="routes.json"):
r = json.load(open(os.path.join(ROOT, "design", which)))
for (lay_, net, x1, y1, x2, y2, w) in r["tracks"]:
t = pcbnew.PCB_TRACK(b)
t.SetStart(V(x1, y1)); t.SetEnd(V(x2, y2)); t.SetWidth(MM(w))
t.SetLayer(LAYERS[lay_]); t.SetNet(nets[net])
b.Add(t)
for (x, y, net, d, h) in r["vias"]:
v = pcbnew.PCB_VIA(b)
v.SetPosition(V(x, y)); v.SetWidth(MM(d)); v.SetDrill(MM(h))
v.SetViaType(pcbnew.VIATYPE_THROUGH)
v.SetLayerPair(pcbnew.F_Cu, pcbnew.B_Cu)
v.SetNet(nets[net])
b.Add(v)
for z in r.get("zones", []):
zone = pcbnew.ZONE(b)
zone.SetLayer(LAYERS[z["layer"]] if z["layer"] in LAYERS else getattr(pcbnew, z["layer"]))
if z.get("keepout"):
zone.SetIsRuleArea(True)
zone.SetDoNotAllowTracks(z.get("no_tracks", True)); zone.SetDoNotAllowVias(z.get("no_vias", True))
zone.SetDoNotAllowZoneFills(z.get("no_pour", True)); zone.SetDoNotAllowPads(False)
zone.SetDoNotAllowFootprints(False)
if z.get("layers"):
ls = pcbnew.LSET()
for ln in z["layers"]:
ls.AddLayer(LAYERS[ln])
zone.SetLayerSet(ls)
else:
zone.SetNet(nets[z["net"]])
zone.SetMinThickness(MM(0.15))
zone.SetLocalClearance(MM(z.get("clearance", 0.2)))
zone.SetAssignedPriority(z.get("priority", 0))
zone.SetPadConnection(pcbnew.ZONE_CONNECTION_THERMAL if z.get("thermal", True) else pcbnew.ZONE_CONNECTION_FULL)
zone.SetThermalReliefGap(MM(0.25)); zone.SetThermalReliefSpokeWidth(MM(0.3))
zone.SetIslandRemovalMode(pcbnew.ISLAND_REMOVAL_MODE_ALWAYS)
ol = zone.Outline()
ol.NewOutline()
for (x, y) in z["poly"]:
ol.Append(MM(x), MM(y))
b.Add(zone)
for t in r.get("texts", []):
tx = pcbnew.PCB_TEXT(b)
tx.SetText(t["text"]); tx.SetPosition(V(t["x"], t["y"]))
tx.SetLayer(getattr(pcbnew, t["layer"].replace(".", "_")))
tx.SetTextSize(V(t["size"], t["size"])); tx.SetTextThickness(MM(t["size"] * 0.15))
if t.get("angle"):
tx.SetTextAngleDegrees(t["angle"])
if t["layer"].startswith("B."):
tx.SetMirrored(True)
b.Add(tx)
if __name__ == "__main__":
b, nets = new_board()
if STAGE == "base":
json.dump(export_pads(b), open(os.path.join(ROOT, "design", "pads.json"), "w"))
pcbnew.SaveBoard(OUT.replace(".kicad_pcb", "-base.kicad_pcb"), b)
print("base ok", len(b.GetFootprints()), "footprints")
elif STAGE == "start":
# the run's start board: interface parts in place, the rest staged beside the outline, live copper held back
st = json.load(open(os.path.join(ROOT, "design", "routes_start.json")))
for fp in b.GetFootprints():
ref = fp.GetReference()
if ref in st["stage"]:
x, y, rot, side = st["stage"][ref]
fp.SetPosition(V(x, y)); fp.SetOrientationDegrees(rot)
add_routes(b, nets, "routes_start.json")
filler = pcbnew.ZONE_FILLER(b)
filler.Fill(b.Zones())
out = os.path.join(ROOT, "aiflow", "start", "ocp-oculink-riser.kicad_pcb")
os.makedirs(os.path.dirname(out), exist_ok=True)
pcbnew.SaveBoard(out, b)
print("start ok", len(b.GetTracks()), "items")
else:
add_routes(b, nets)
filler = pcbnew.ZONE_FILLER(b)
filler.Fill(b.Zones())
pcbnew.SaveBoard(OUT, b)
print("final ok", len(b.GetTracks()), "track/via items,", b.GetAreaCount(), "zones")