# -*- coding: utf-8 -*- u"""受控阻抗 + IPC-2221 载流线宽计算器(纯数学,不连桥接)。 /c/Python314/python .claude/pcb_calc.py # 回验本项目的定稿几何 /c/Python314/python .claude/pcb_calc.py --zdiff 90 # 求 90Ω 差分的线宽 /c/Python314/python .claude/pcb_calc.py --current 4 # 求 4A 的最小线宽 /c/Python314/python .claude/pcb_calc.py --sweep # 叠层/铜厚变了扫一遍 移植自 hyl64/jlcmcp 的 `src/calculators.ts`(MIT),改成 **mm 为主单位** (原版是 mil),并补了本项目的默认叠层。公式与 `五路直供电源板设计.md` §15.3 写的是同两条,所以这支脚本可以直接回验文档里的 Z0 / Zdiff。 ⚠️ §15.3 已经写了:**打样前用板厂的阻抗计算器复核,别信这里的近似公式。** 这支脚本的用途是「叠层/铜厚/线宽一改,先看会不会跑出 ±10% 窗口」,不是定稿依据。 """ from __future__ import print_function import argparse import math MIL = 0.0254 # 1 mil = 0.0254 mm # ---- 本项目定稿(五路直供电源板设计.md §10.1 / §15.3)------------------------ ER_FR4 = 4.3 # 嘉立创四层板 FR4 H_L1L2_MM = 0.2104 # L1-L2 介质厚度,1.6mm 标准叠层 T_1OZ_MM = 1.37 * MIL # 1 oz = 1.37 mil(§10.1 用的就是这个数,不是 1.4) T_2OZ_MM = 2.74 * MIL T_HALFOZ_MM = 0.685 * MIL W_USB_MM = 0.28 # §15.3 定稿线宽 S_USB_MM = 0.18 # §15.3 定稿间距 ZDIFF_TARGET = 90.0 # USB 2.0 HS,±10% DT_DEFAULT = 20.0 # °C 允许温升 —— 设计文档 §10.1 表头与 §4694 全篇用 20,不是 10 # --------------------------------------------------------------------------- # 阻抗 # --------------------------------------------------------------------------- def z0_microstrip(w_mm, h_mm, t_mm, er=ER_FR4): u"""表层微带线特征阻抗。Z0 = 87/√(εr+1.41) · ln(5.98h / (0.8W + t))""" return (87.0 / math.sqrt(er + 1.41)) * math.log(5.98 * h_mm / (0.8 * w_mm + t_mm)) def z0_stripline(w_mm, h_mm, t_mm, er=ER_FR4): u"""内层带状线。Z0 = 60/√εr · ln(4h / (π(W + t)))""" return (60.0 / math.sqrt(er)) * math.log((4.0 * h_mm) / (math.pi * (w_mm + t_mm))) def zdiff_microstrip(w_mm, s_mm, h_mm, t_mm, er=ER_FR4): u"""差分微带。Zdiff = 2·Z0 · (1 − 0.48·e^(−0.96·S/h))""" z0 = z0_microstrip(w_mm, h_mm, t_mm, er) return 2.0 * z0 * (1.0 - 0.48 * math.exp(-0.96 * s_mm / h_mm)) def zdiff_stripline(w_mm, s_mm, h_mm, t_mm, er=ER_FR4): u"""差分带状线。Zdiff = 120/√εr · ln(2h / (π(W + t + S)))""" return (120.0 / math.sqrt(er)) * math.log((2.0 * h_mm) / (math.pi * (w_mm + t_mm + s_mm))) def width_for_z(target, fn, h_mm, t_mm, er=ER_FR4, s_mm=None, lo=0.02, hi=5.0, tol=1e-4): u"""二分反解线宽。阻抗对线宽单调递减,所以 z > target 时要加宽。 lo/hi 单位 mm。返回 (w_mm, z_actual)。 """ def z_of(w): return fn(w, s_mm, h_mm, t_mm, er) if s_mm is not None else fn(w, h_mm, t_mm, er) if z_of(lo) < target or z_of(hi) > target: return (float('nan'), float('nan')) # 目标落在可解区间外 for _ in range(200): mid = (lo + hi) / 2.0 z = z_of(mid) if abs(z - target) < tol: return (mid, z) if z > target: lo = mid else: hi = mid mid = (lo + hi) / 2.0 return (mid, z_of(mid)) # --------------------------------------------------------------------------- # IPC-2221 载流 # --------------------------------------------------------------------------- def ipc_width_mm(current_a, t_mm=T_1OZ_MM, dt_c=DT_DEFAULT, external=True): u"""IPC-2221:I = k·ΔT^0.44·A^0.725,反解线宽。 k = 0.048(外层)/ 0.024(内层)。A 的单位是 mil²,所以内部换算。 返回 (线宽 mm, 截面积 mil²)。 """ k = 0.048 if external else 0.024 a_mil2 = (current_a / (k * dt_c ** 0.44)) ** (1.0 / 0.725) t_mil = t_mm / MIL return (a_mil2 / t_mil * MIL, a_mil2) # --------------------------------------------------------------------------- def verify(): print(u'=' * 74) print(u'回验 §15.3 定稿几何(W=%.2f / S=%.2f mm, h=%.4f mm, εr=%.1f, 1oz)' % (W_USB_MM, S_USB_MM, H_L1L2_MM, ER_FR4)) print(u'=' * 74) z0 = z0_microstrip(W_USB_MM, H_L1L2_MM, T_1OZ_MM) zd = zdiff_microstrip(W_USB_MM, S_USB_MM, H_L1L2_MM, T_1OZ_MM) print(u' Z0 = %.1f Ω (文档写 ≈57.5) %s' % (z0, u'✓ 对上' if abs(z0 - 57.5) < 1.0 else u'✗ 对不上')) print(u' Zdiff = %.1f Ω (文档写 ≈90.5) %s' % (zd, u'✓ 对上' if abs(zd - 90.5) < 1.0 else u'✗ 对不上')) lo, hi = ZDIFF_TARGET * 0.9, ZDIFF_TARGET * 1.1 print(u' 90 Ω ±10%% 窗口 = %.1f ~ %.1f Ω → %s' % (lo, hi, u'在窗口内' if lo <= zd <= hi else u'**出窗口**')) print(u'\n 文档那句「回到 2 oz 同样几何只剩 83 Ω,W 要收到约 0.24 mm」:') zd2 = zdiff_microstrip(W_USB_MM, S_USB_MM, H_L1L2_MM, T_2OZ_MM) w2, z2 = width_for_z(ZDIFF_TARGET, zdiff_microstrip, H_L1L2_MM, T_2OZ_MM, s_mm=S_USB_MM) print(u' 2oz 同几何 Zdiff = %.1f Ω (文档写 83) %s' % (zd2, u'✓ 对上' if abs(zd2 - 83) < 1.5 else u'✗ 对不上')) print(u' 2oz 下解 90 Ω 的线宽 = %.3f mm (文档写 ≈0.24) %s' % (w2, u'✓ 对上' if abs(w2 - 0.24) < 0.015 else u'✗ 对不上')) print(u'\n 反解自检(1oz,固定 S=%.2f):' % S_USB_MM) w1, z1 = width_for_z(ZDIFF_TARGET, zdiff_microstrip, H_L1L2_MM, T_1OZ_MM, s_mm=S_USB_MM) print(u' 要正好 90.0 Ω 需 W = %.3f mm(现用 %.2f,差 %+.0f µm)' % (w1, W_USB_MM, (W_USB_MM - w1) * 1000)) print(u'\n' + u'=' * 74) print(u'IPC-2221 载流(回验 §10.1 的「4A 外层 1oz 要 1.34 mm」)') print(u'=' * 74) print(u' %-8s %-10s %-8s %-12s %s' % (u'电流', u'层', u'ΔT', u'截面积', u'最小线宽')) for i, ext, dt in ((4.0, True, 20), (4.0, True, 10), (4.0, False, 20), (8.0, False, 20), (1.0, True, 20)): t = T_1OZ_MM if ext else T_HALFOZ_MM w, a = ipc_width_mm(i, t, dt, ext) print(u' %-8s %-10s %-8s %-12s %.2f mm' % (u'%.0f A' % i, u'外层 1oz' if ext else u'内层 0.5oz', u'%d °C' % dt, u'%.0f mil²' % a, w)) w4, _ = ipc_width_mm(4.0, T_1OZ_MM, 20, True) print(u'\n 4A / 外层 1oz / ΔT=20°C → %.2f mm (文档写 1.34) %s' % (w4, u'✓ 对上' if abs(w4 - 1.34) < 0.03 else u'✗ 对不上')) def sweep(): print(u'\n' + u'=' * 74) print(u'叠层敏感度:Zdiff 随介质厚度 h 与铜厚变化(W=%.2f / S=%.2f 固定)' % (W_USB_MM, S_USB_MM)) print(u'=' * 74) print(u' %-12s %-12s %-12s %s' % (u'h (mm)', u'1oz', u'2oz', u'判定(1oz)')) for h in (0.15, 0.18, 0.2104, 0.25, 0.30): a = zdiff_microstrip(W_USB_MM, S_USB_MM, h, T_1OZ_MM) b = zdiff_microstrip(W_USB_MM, S_USB_MM, h, T_2OZ_MM) ok = u'✓' if 81 <= a <= 99 else u'✗ 出窗口' mark = u' ← 定稿' if abs(h - H_L1L2_MM) < 1e-6 else u'' print(u' %-12s %-12s %-12s %s%s' % (u'%.4f' % h, u'%.1f Ω' % a, u'%.1f Ω' % b, ok, mark)) print(u'\n ⇒ h 从 0.2104 掉到 0.15(板厂换 prepreg)就掉出 ±10% 窗口。') print(u' §15.3 那句「叠层一变就得重算」是有量的:h 每变 0.01 mm,Zdiff 约变 %.1f Ω。' % abs(zdiff_microstrip(W_USB_MM, S_USB_MM, H_L1L2_MM + 0.01, T_1OZ_MM) - zdiff_microstrip(W_USB_MM, S_USB_MM, H_L1L2_MM, T_1OZ_MM))) def main(): ap = argparse.ArgumentParser() ap.add_argument('--zdiff', type=float, help=u'目标差分阻抗 Ω,反解线宽') ap.add_argument('--z0', type=float, help=u'目标单端阻抗 Ω,反解线宽') ap.add_argument('--spacing', type=float, default=S_USB_MM, help=u'差分间距 mm') ap.add_argument('--height', type=float, default=H_L1L2_MM, help=u'介质厚度 mm') ap.add_argument('--er', type=float, default=ER_FR4) ap.add_argument('--copper', choices=('0.5oz', '1oz', '2oz'), default='1oz') ap.add_argument('--stripline', action='store_true', help=u'内层带状线(默认表层微带)') ap.add_argument('--current', type=float, help=u'载流 A,求最小线宽') ap.add_argument('--dt', type=float, default=DT_DEFAULT, help=u'允许温升 °C(默认 20,跟设计文档 §10.1 一致)') ap.add_argument('--internal', action='store_true', help=u'载流按内层算') ap.add_argument('--sweep', action='store_true') a = ap.parse_args() t = {'0.5oz': T_HALFOZ_MM, '1oz': T_1OZ_MM, '2oz': T_2OZ_MM}[a.copper] if a.zdiff: fn = zdiff_stripline if a.stripline else zdiff_microstrip w, z = width_for_z(a.zdiff, fn, a.height, t, a.er, s_mm=a.spacing) print(u'目标 %.1f Ω 差分%s,S=%.2f h=%.4f εr=%.1f %s' % (a.zdiff, u'带状线' if a.stripline else u'微带', a.spacing, a.height, a.er, a.copper)) print(u' → W = %.3f mm (实得 %.2f Ω)' % (w, z)) return if a.z0: fn = z0_stripline if a.stripline else z0_microstrip w, z = width_for_z(a.z0, fn, a.height, t, a.er) print(u'目标 %.1f Ω 单端 → W = %.3f mm (实得 %.2f Ω)' % (a.z0, w, z)) return if a.current: w, area = ipc_width_mm(a.current, t, a.dt, not a.internal) print(u'%.2f A / %s / %s / ΔT=%.0f°C → 最小线宽 %.2f mm(截面 %.0f mil²)' % (a.current, u'内层' if a.internal else u'外层', a.copper, a.dt, w, area)) return verify() if a.sweep: sweep() if __name__ == '__main__': main()