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
The finished riser: KiCad project, Gerbers/BOM/CPL, design record, renders and analysis
09d929e
7d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
#!/usr/bin/env python3
"""2D finite-difference field solver for edge-coupled differential pairs on the
JLC06161H-3313 stackup (JLCPCB published values, read 2026-10-12 from jlcpcb.com/impedance).
Solves Laplace with layered dielectrics for the odd and even modes, with and without
dielectrics, and reports Zodd, Zeven, Zdiff and Zse. Rectangular conductors (no etch
trapezoid), lossless, quasi-static. Writes design/zsolve.json.
Layer use: L1 sig (ref L2), L2 GND, L3 sig (ref L4), L4 GND, L5 sig (ref L6), L6 GND.
"""
import json, sys
import numpy as np
import scipy.sparse as sp
import scipy.sparse.linalg as spla
C0 = 299792458.0
EPS0 = 8.8541878128e-12
D = 0.0025 # grid step, mm
def solve(regions, conductors, grounds, W, H, excite):
"""regions: list of (x0,x1,y0,y1,er) applied in order (later wins), y up, mm.
conductors: list of rectangles (x0,x1,y0,y1) for signal 1 and 2.
grounds: list of rectangles held at 0 V. The outer box is 0 V on top/bottom, Neumann on the sides."""
nx, ny = int(round(W / D)) + 1, int(round(H / D)) + 1
# cell permittivity (cells between nodes)
eps = np.ones((ny - 1, nx - 1))
xc = (np.arange(nx - 1) + 0.5) * D
yc = (np.arange(ny - 1) + 0.5) * D
for (x0, x1, y0, y1, er) in regions:
mx = (xc >= x0) & (xc <= x1)
my = (yc >= y0) & (yc <= y1)
eps[np.ix_(my, mx)] = er
xn = np.arange(nx) * D
yn = np.arange(ny) * D
fixed = np.zeros((ny, nx), bool)
val = np.zeros((ny, nx))
def mark(rect, v):
x0, x1, y0, y1 = rect
mx = (xn >= x0 - 1e-9) & (xn <= x1 + 1e-9)
my = (yn >= y0 - 1e-9) & (yn <= y1 + 1e-9)
fixed[np.ix_(my, mx)] = True
val[np.ix_(my, mx)] = v
for g in grounds:
mark(g, 0.0)
fixed[0, :] = True; val[0, :] = 0
fixed[-1, :] = True; val[-1, :] = 0
for c, v in zip(conductors, excite):
mark(c, v)
idx = -np.ones((ny, nx), int)
free = ~fixed
idx[free] = np.arange(free.sum())
n = int(free.sum())
# padded cell permittivity so out-of-range cells are 0 (Neumann at the sides)
ep = np.zeros((ny + 1, nx + 1))
ep[1:-1, 1:-1] = eps
# edge weights: horizontal edge (j,i)-(j,i+1) uses cells (j-1,i),(j,i) -> ep[j, i+1], ep[j+1, i+1]
wx = 0.5 * (ep[0:ny, 1:nx] + ep[1:ny + 1, 1:nx]) # shape (ny, nx-1)
wy = 0.5 * (ep[1:ny, 0:nx] + ep[1:ny, 1:nx + 1])
diag = np.zeros((ny, nx))
diag[:, :-1] += wx; diag[:, 1:] += wx
diag[:-1, :] += wy; diag[1:, :] += wy
rows, cols, data = [], [], []
b = np.zeros(n)
# horizontal couplings
for (sa, sb, w) in (((slice(None), slice(0, nx - 1)), (slice(None), slice(1, nx)), wx),
((slice(None), slice(1, nx)), (slice(None), slice(0, nx - 1)), wx),
((slice(0, ny - 1), slice(None)), (slice(1, ny), slice(None)), wy),
((slice(1, ny), slice(None)), (slice(0, ny - 1), slice(None)), wy)):
fa = free[sa]; fb = free[sb]
ia = idx[sa]; ib = idx[sb]; vb = val[sb]
m = fa & fb & (w > 0)
rows.append(ia[m]); cols.append(ib[m]); data.append(-w[m])
m2 = fa & (~fb) & (w > 0)
np.add.at(b, ia[m2], w[m2] * vb[m2])
rows.append(idx[free]); cols.append(idx[free]); data.append(diag[free])
A = sp.csr_matrix((np.concatenate(data), (np.concatenate(rows), np.concatenate(cols))), shape=(n, n))
x = spla.spsolve(A, b)
phi = val.copy()
phi[free] = x
# energy per unit length: 0.5 * eps0 * sum eps |E|^2 over cells (cell-centred gradient)
ex = (phi[:-1, 1:] - phi[:-1, :-1] + phi[1:, 1:] - phi[1:, :-1]) / (2 * D)
ey = (phi[1:, :-1] - phi[:-1, :-1] + phi[1:, 1:] - phi[:-1, 1:]) / (2 * D)
Wd = 0.5 * EPS0 * np.sum(eps * (ex ** 2 + ey ** 2)) * (D * D) # D cancels with 1/D^2: J/m per mm^2/mm^2
return Wd # J per metre of line (units consistent: E in V/mm, area in mm^2 -> V^2)
def pair(layer, w, s, er_override=None, with_mask=True):
"""Build geometry for a pair on layer L1, L3 or L5 of JLC06161H-3313."""
W = 2 * w + s + 1.6
x1 = W / 2 - s / 2 - w
c1 = (x1, x1 + w)
c2 = (W / 2 + s / 2, W / 2 + s / 2 + w)
if layer == "L1":
h = 0.0994; t = 0.035; er_pp = 4.1
top_air = 0.6
Hh = 0.03 + h + t + 0.03 + top_air
y_gnd = 0.03 # GND plane L2 slab from 0 to 0.03 (fixed 0)
y_sig = y_gnd + h
regions = [(0, W, y_gnd, y_sig, er_pp)]
if with_mask:
# conformal solder mask: 1.2 mil over substrate, 0.6 mil over trace (JLC: C1 1.2, C2 0.6, C3 1.2, er 3.8)
regions.append((0, W, y_sig, y_sig + 0.03048, 3.8))
for (a, bb) in (c1, c2):
regions.append((a - 0.01524, bb + 0.01524, y_sig, y_sig + t + 0.01524, 3.8))
conds = [(c1[0], c1[1], y_sig, y_sig + t), (c2[0], c2[1], y_sig, y_sig + t)]
grounds = [(0, W, 0, y_gnd)]
return regions, conds, grounds, W, Hh
if layer in ("L3", "L5"):
t = 0.0152
if layer == "L3":
h_ref = 0.1088; er_pp = 4.16 # L3 -> L4 through 2116 prepreg
else:
h_ref = 0.0994; er_pp = 4.1 # L5 -> L6 through 3313 prepreg
h_core = 0.55; er_core = 4.6 # the core to the far plane (L2 or L4)
g = 0.02
y0 = g
y_sig = y0 + h_ref - t # trace bottom sits so its top touches core? copper is on the core side
# inner copper is etched on the core: trace top face on the core surface, prepreg fills below it
y_core = y0 + h_ref # core bottom surface (trace top)
Hh = y_core + h_core + g
regions = [(0, W, y0, y_core, er_pp), (0, W, y_core, y_core + h_core, er_core)]
conds = [(c1[0], c1[1], y_core - t, y_core), (c2[0], c2[1], y_core - t, y_core)]
grounds = [(0, W, 0, y0), (0, W, Hh - g, Hh)]
return regions, conds, grounds, W, Hh
raise ValueError(layer)
def zdiff(layer, w, s):
reg, conds, gnds, W, H = pair(layer, w, s)
air = [(r[0], r[1], r[2], r[3], 1.0) for r in reg]
out = {}
for mode, ex in (("odd", (1.0, -1.0)), ("even", (1.0, 1.0))):
Wd = solve(reg, conds, gnds, W, H, ex)
Wa = solve(air, conds, gnds, W, H, ex)
# for (+1,-1) or (+1,+1): W = C_mode (per line) * 1V^2 (two lines, 0.5*C*V^2 each)
Cm = Wd # J/m: eps0 [F/m] * E^2 [V^2/mm^2] * area [mm^2]
Ca = Wa
Z = 1.0 / (C0 * np.sqrt(Cm * Ca))
out[mode] = {"Z": Z, "eeff": Cm / Ca}
zd = 2 * out["odd"]["Z"]
return {"layer": layer, "w": w, "s": s, "Zodd": out["odd"]["Z"], "Zeven": out["even"]["Z"],
"Zdiff": zd, "eeff_odd": out["odd"]["eeff"], "eeff_even": out["even"]["eeff"]}
if __name__ == "__main__":
res = []
cases = [(l, w, s) for l in ("L1", "L3", "L5") for (w, s) in
[(0.10, 0.15), (0.11, 0.15), (0.12, 0.15), (0.125, 0.15), (0.13, 0.15), (0.14, 0.15),
(0.10, 0.20), (0.11, 0.20), (0.12, 0.20)]]
if len(sys.argv) > 1:
cases = [(sys.argv[1], float(sys.argv[2]), float(sys.argv[3]))]
for c in cases:
r = zdiff(*c)
res.append(r)
print(f"{r['layer']} w={r['w']:.3f} s={r['s']:.3f} Zdiff={r['Zdiff']:.1f} Zodd={r['Zodd']:.1f} Zeven={r['Zeven']:.1f} eeff_odd={r['eeff_odd']:.2f}", flush=True)
if len(sys.argv) == 1:
json.dump(res, open("design/zsolve.json", "w"), indent=1)