The victim is no longer an open probe. Adding R2 and C2 turns the secondary into its own series L₂–R2–C2 loop, driven by the coupled EMF M dI₁/dt. What we now measure is the voltage across C2 — the induced EMF after an R2C2 low-pass filter. Two consequences: (1) the victim waveform stops looking like dI₁/dt and instead tracks the integral of the secondary current, i.e. it follows I₁(t); (2) the peak is heavily attenuated — here a 500 mV EMF spike becomes a 100 mV bump (≈5× smaller) — a real, measurable victim. Everything below is first-principles: coupled KVL, a closed-form estimate, and a full 4-state simulation that confirms it.
Because L₁=0.88 nH is tiny, all the action is in the first ~100 ps — so the x-axis is zoomed to ps. On I₁(t) the red line is the initial slope S=Vagg/L₁=1.13 mA/ps, the purple marker is 2L₁/R₁=35 ps, and the blue dot is the turning point (slope=0) at t≈101 ps where I₁≈Vagg/R₁=19.7 mA.
Before any coupling check we sort the package/IC pins into functional groups. Each group has a typical signal-integrity role — noisy switching nets are natural aggressors; low-tolerance sense/reference nets are natural victims. (Representative multiphase-buck example — edit the two tables below to match your design.)
| Function group | Typical SI role | Why | Example pins |
|---|---|---|---|
| PWM | Aggressor | large dV, fast edges, big dI/dt | HDRV, LDRV, SW_NODE, PWM1…n, BOOT |
| digital high-speed | Aggressor | fast edges, periodic clock harmonics | CLK, SCLK, SYNC, DATA, PGOOD |
| voltage / gnd | Aggressor & return | carries load-step current; shared-return coupling | VIN, VDD, PGND, AGND, VDD_GND |
| analog V/I driving | Aggressor | drives current into a load / gate | IDRV, DAC_OUT, COMP, ITH |
| current sensing | Victim | μV–mV budget; tiny Verr | ISEN+, ISEN−, CS, IMON |
| analog V sensing | Victim | reference / feedback accuracy | FB, VSEN, VREF, VOSNS, TSEN |
Each row is a driver pad that injects noise. dV is the output swing; dV/dt and f describe the edge/rate; Rout is the driver output resistance (it sets the peak loop current I1,pk≈dV/Rout); W·T·L set the trace self-inductance. Even an infinite dV/dt is checkable — the loop inductance limits dI1/dt, so the checker uses a worst-case voltage step (the ∞-slew limit); a finite dV/dt only relaxes the result.
| Driver IC | Driver pad | Function | dV (V) | dV min | dV max | dV/dt (V/ns) | f (MHz) | Rout (Ω) | Rout min | Rout max | W (µm) | T (µm) | L (µm) |
|---|
Each row is a receiver pad we must protect. Verr is its noise tolerance (the pass/fail limit); Rin, Cin form the receiver low-pass that integrates & attenuates the coupled EMF; d is the center-to-center spacing to the aggressor (same layer, or the layer-to-layer distance); W·T·L set the victim trace self-inductance and the parallel-run coupling length.
| Receiver IC | Rx pad | Function | Filter | fc (MHz) | Verr (mV) | Rin (Ω) | Rin min | Rin max | Cin (pF) | Cin min | Cin max | parallel d to aggr (µm) | W (µm) | T (µm) | L (µm) |
|---|
The checker auto-parses the aggressor & victim tables above and evaluates every aggressor→victim pair with the simple closed-form limit Vvictim,pk = M·Vagg / (R1·R2·C2) (R1=driver Rout, R2=receiver Rin, C2=receiver Cin). This first release scores the inductive-crosstalk mechanism; the other SI mechanisms are scaffolded and will light up in later revisions.
net, coupled_net pair for import into layout tools / spreadsheets.
Per-victim verdict — worst aggressor for each victim (inductive only):
| Victim (IC ยท pad) | Verr | Worst aggressor | Vvictim,pk (induced) | Filter (ยง6) | Vvictim,pk (post-filter) | Margin | Inductive |
|---|
All aggressor→victim pairs (worst first):
| Aggressor | Victim | d (µm) | overlap L (µm) | M (pH) | Vvictim,pk closed-form |
Vvictim,pk (sim) 4-state → §10 ↵ |
Vvictim,pk (post-filter) |
Verr | Margin | Inductive |
|---|
Coupled KVL with the mutual inductance matrix (states \(I_1,V_{C_1},I_2,V_{C_2}\)):
$$\begin{bmatrix}L_1&M\\M&L_2\end{bmatrix}\!\begin{bmatrix}\dot I_1\\\dot I_2\end{bmatrix} =\begin{bmatrix}V_{agg}-R_1 I_1-V_{C_1}\\-R_2I_2-V_{C_2}\end{bmatrix},\qquad \dot V_{C_1}=\frac{I_1}{C_1},\quad \dot V_{C_2}=\frac{I_2}{C_2}$$Secondary is R-dominated for this fast pulse (\(\omega L_2\ll R_2\)), so the induced EMF drops mostly across \(R_2\): \(\;I_2\approx -\dfrac{M}{R_2}\dot I_1\). Feeding that into the cap and integrating:
$$V_{victim}=\frac{1}{C_2}\!\int I_2\,dt\approx-\frac{M}{R_2C_2}\!\int \dot I_1\,dt =-\frac{M}{R_2C_2}\,I_1(t)$$ $$\boxed{\,V_{victim,pk}\approx\frac{M}{R_2C_2}\,I_{1,pk} =\frac{M\,V_{agg}}{R_1\,R_2\,C_2}\quad(\text{since }I_{1,pk}\approx V_{agg}/R_1)\,}$$That boxed formula is the slow-filter limit (τ2 >> pulse). Two caveats: (1) the primary R1 must appear — it sets I1,pk=Vagg/R1; your sketch's box "2M Vagg/R2C2" dropped it (and used a rectangular area, hence the factor 2). (2) With the light filter chosen here (τ2=50 ps ≈ the ~100 ps pulse) we are not in the slow-filter limit, so that formula over-reads — only the full 4-state solve is exact.
| Quantity (Case B) | Formula | Value |
|---|---|---|
| Induced EMF peak (pre-filter) | \((M/L_1)V_{agg}\) | 500 mV (M=442 pH, k=0.50) |
| Primary peak current | \(I_{1,pk}\approx V_{agg}/R_1\) | 19.7 mA |
| Slow-filter formula \((M V_{agg}/R_1R_2C_2)\) | over-reads (τ2≈pulse) | 177 mV |
| Filtered victim peak (full 4-state sim) | exact | 100 mV |
Primary: Vagg=1 V, R1=50 Ω, L₁=0.88 nH (1 mm wire), C1=100 pF; nominal coupling = Case B M=441.7 pH (k=0.50); victim R2=0 kΩ, C2=1 pF.
| Rank | Parameter | Symbol | Requirement | Simulation | Status | |
|---|---|---|---|---|---|---|
| #1 | ๐ฏ | Victim peak (filtered, Case B) | Vvictim across C2 | ≤ 150 mV | โ pass | |
| #2 | โก | Induced EMF peak (pre-filter) | (M/L₁)Vagg | info | โน๏ธ info | |
| #3 | ๐ | Coupling coefficient (Case B) | k=M/√(L₁L₂) | ≤ 0.6 | โ pass | |
| #4 | ๐ | Damping (no ring) | ζ | ≥ 1.0 | โ pass | |
| #5 | ๐ | Peak loop current | I₁,pk | ≤ 25 mA | โ pass | |
| #6 | ๐ | Primary decay | τ=R1C1 | ≤ 6 ns | โ pass | |
| #7 | ๐งฏ | Victim filter (light) | τ2=R2C2 | info | โน๏ธ info | |
| #8 | ๐ก | Natural frequency | f₀ | info | โน๏ธ info |
Self-contained — copy, paste, run with nothing but the Python standard library (no numpy / scipy / matplotlib). It builds the mutual inductance from the geometry, RK4-solves the 4-state ODE, and prints an ASCII waveform with a Verr spec line — the trace beyond spec is red, the part within spec green, with the peak marked (*) and labeled — reproducing the default-populated Spec Checker case (§5–7): aggressor PWM_HDRV into ISEN_CS1 (Rin=200 Ω → 3.18 mV, FAIL) and ISEN_CS2 (Rin=1000 Ω → 0.85 mV, PASS vs the 1 mV budget). Same file in the repo: inductive_crosstalk_model.py.
# Coupled 4-state inductive-crosstalk model โ copy, paste, run (pure standard library).
# python3 this_file.py # no numpy / scipy / matplotlib needed; prints an ASCII waveform
#
# Reproduces the DEFAULT case populated in the Spec Checker (section 7):
# aggressor PWM_HDRV : dV=5 V, Rout=100 ohm, 30x10 um trace, 4000 um long
# victims ISEN_CS1 (Rin=200 ohm) and ISEN_CS2 (Rin=1000 ohm), Cin=1 pF, d=100 um, 150 um long
# The waveform is drawn with a Verr spec line; the part of the trace BEYOND spec is red, the
# part within spec is green (ANSI colors โ most terminals show them).
import math
mu0 = 4e-7*math.pi
RED, GREEN, RESET = "\033[91m", "\033[92m", "\033[0m"
def Lself(L,w,t): return mu0*L/(2*math.pi)*(math.log(2*L/(w+t))+0.5+0.2235*(w+t)/L) # trace self-L
def Mmut(d,L): return mu0*L/(2*math.pi)*(math.asinh(L/d)-math.sqrt(1+(d/L)**2)+d/L) # parallel mutual
# --- default-populated geometry / drive ---
Vagg, R1, C1 = 5.0, 100.0, 100e-12
La = Lself(4000e-6, 30e-6, 10e-6) # aggressor self-inductance (4 mm trace)
Lv = Lself( 150e-6, 30e-6, 10e-6) # victim self-inductance (150 um trace)
M = Mmut(100e-6, min(4000e-6, 150e-6))# mutual over the 150 um overlap at d = 100 um
def simulate(R2, C2):
"""RK4 solve of x=[I1,Vc1,I2,Vc2]; returns (times, victim voltage across C2)."""
det = La*Lv - M*M
def f(x):
I1,Vc1,I2,Vc2 = x
b1 = Vagg - R1*I1 - Vc1 # primary branch
b2 = - R2*I2 - Vc2 # secondary branch (no source)
return (( Lv*b1 - M*b2)/det, I1/C1, (-M*b1 + La*b2)/det, I2/C2)
dt = min(La/R1, Lv/R2, R2*C2, math.sqrt(Lv*C2))/25
T = max(8*R2*C2, 40*La/R1, 3e-9); N = min(60000, math.ceil(T/dt)); dt = T/N
x=[0.0]*4; ts=[0.0]; vs=[0.0]
for i in range(N):
k1=f(x); x2=[x[j]+.5*dt*k1[j] for j in range(4)]
k2=f(x2); x3=[x[j]+.5*dt*k2[j] for j in range(4)]
k3=f(x3); x4=[x[j]+dt*k3[j] for j in range(4)]; k4=f(x4)
for j in range(4): x[j]+=dt/6*(k1[j]+2*k2[j]+2*k3[j]+k4[j])
ts.append((i+1)*dt); vs.append(x[3])
return ts, vs
def _paint(chars, cols):
"""chars: list of characters; cols: list of None/'r'/'g' -> ANSI string (run-length grouped)."""
out=""; i=0; n=len(chars)
while i<n:
c=cols[i]
if c is None: out+=chars[i]; i+=1
else:
j=i
while j<n and cols[j]==c: j+=1
out+=(RED if c=='r' else GREEN)+"".join(chars[i:j])+RESET; i=j
return out
def ascii_plot(ts, vs, title, Verr, W=60, H=17):
"""ASCII waveform with a Verr spec line; trace beyond +/-Verr is red, within is green."""
ip = max(range(len(vs)), key=lambda i: abs(vs[i])); vpk, tpk = vs[ip], ts[ip]
Tplot = min(max(9*tpk, 300e-12), ts[-1])
lo = min(0.0, min(vs), -Verr*1.15); hi = max(0.0, max(vs), Verr*1.15) # keep +/-Verr in view
row = lambda v: min(H-1, max(0, int(round((hi-v)/(hi-lo)*(H-1)))))
col = lambda t: min(W-1, max(0, int(round(t/Tplot*(W-1)))))
grid=[[' ']*W for _ in range(H)]; cols=[[None]*W for _ in range(H)]
rSp, rSm, r0 = row(Verr), row(-Verr), row(0.0)
for c in range(W): # dashed spec lines at +/-Verr
if c%3==0:
if grid[rSp][c]==' ': grid[rSp][c]='.'
if grid[rSm][c]==' ': grid[rSm][c]='.'
for c in range(W): grid[r0][c]='-' # zero axis
pkc=col(tpk); j=0
for c in range(W): # curve (on top), colored by spec
tt=c/(W-1)*Tplot
while j+1<len(ts) and ts[j+1]<tt: j+=1
r=row(vs[j]); beyond = abs(vs[j])>Verr
grid[r][c] = '*' if c==pkc else 'o'
cols[r][c] = 'r' if beyond else 'g'
ann=' <- peak = %.3f mV'%(vpk*1e3); rp=row(vpk); s=max(0,min(pkc+2,W-len(ann)-1))
for i,ch in enumerate(ann): # peak label (color = peak's spec state)
if s+i<W and grid[rp][s+i]==' ':
grid[rp][s+i]=ch; cols[rp][s+i] = 'r' if abs(vpk)>Verr else 'g'
slab=' spec (Verr)' # spec-line tag on the -Verr row
s2=W-len(slab)
for i,ch in enumerate(slab):
if 0<=s2+i<W and grid[rSm][s2+i] in ' .': grid[rSm][s2+i]=ch
ylab={0:'%7.3f'%(hi*1e3), r0:'%7.3f'%0.0, rSm:'%7.3f'%(-Verr*1e3), H-1:'%7.3f'%(lo*1e3)}
out=[title]
for ri in range(H):
out.append(ylab.get(ri,' ')+' |'+_paint(grid[ri], cols[ri]))
out.append(' +'+'-'*W)
out.append(' 0'+' '*(W//2-4)+'time [ps]'+' '*(W//2-13)+'%d'%round(Tplot*1e12))
return '\n'.join(out), vpk
print("La=%.3f nH Lv=%.4f nH M=%.2f pH (Vagg=%.0f V, R1=%.0f ohm, Cin=1 pF)\n"
% (La*1e9, Lv*1e9, M*1e12, Vagg, R1))
Verr = 1e-3
for name, Rin in [("ISEN_CS1", 200.0), ("ISEN_CS2", 1000.0)]:
ts, vs = simulate(Rin, 1e-12)
plot, vpk = ascii_plot(ts, vs, "PWM_HDRV -> %s (Rin=%.0f ohm) [victim V across C2, mV]"%(name,Rin), Verr)
ok = abs(vpk) <= Verr
print(plot)
print(" Verr = %.3f mV -> %s%s%s\n" % (Verr*1e3, GREEN if ok else RED, "PASS" if ok else "FAIL", RESET))
La=4.640 nH Lv=0.0772 nH M=19.79 pH (Vagg=5 V, R1=100 ohm, Cin=1 pF)
PWM_HDRV -> ISEN_CS1 (Rin=200 ohm) [victim V across C2, mV]
1.150 |
|. . . . . . . . . . . . . . . . . . . .
|
|
0.000 |o-------------------------------------------------------oooo
| ooooooooooooooo
| ooooooo
| ooooo
-1.000 |. . . . . . . . .oooo . . . . . . spec (Verr)
| o ooo
| oo
| oo
| o oo
| oo
| o oo
| o ooo
-3.176 | oo*o <- peak = -3.176 mV
+------------------------------------------------------------
0 time [ps] 792
Verr = 1.000 mV -> FAIL
PWM_HDRV -> ISEN_CS2 (Rin=1000 ohm) [victim V across C2, mV]
1.150 |
|. . . . . . . . . . . . . . . . . . . .
|
|
|
|
|
|
0.000 |o-----------------------------------------------------------
| o
| ooooooooooooooooo
| o oooooooooooo
| o oooooooooo
| o oooooooo
| ooo*oooo peak = -0.846 mV
-1.000 |. . . . . . . . . . . . . . . . spec (Verr)
-1.150 |
+------------------------------------------------------------
0 time [ps] 1310
Verr = 1.000 mV -> PASS