From 1f8145768b77e2171e27fef33bc683e5a453beb6 Mon Sep 17 00:00:00 2001 From: Yuren Hao Date: Fri, 17 Jul 2026 15:37:05 -0500 Subject: =?UTF-8?q?hw=5Fsim=20v1:=20datasheet-anchored=20column=20?= =?UTF-8?q?=E2=80=94=20C=5Fbus=3D5.4nF=20kills=20naive=20design=20(16.6us)?= =?UTF-8?q?;=20best=20comp=20point=203.14us=20borderline=20vs=203us=20comm?= =?UTF-8?q?it=20gate;=20offset=20rejection=20x3088;=20three=20design=20opt?= =?UTF-8?q?ions=20logged?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014FAPDWQ49M5Ye3NpTndTpn --- hw_sim/NOTES.md | 23 ++++++++++ hw_sim/cell_v1.py | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 hw_sim/NOTES.md create mode 100644 hw_sim/cell_v1.py (limited to 'hw_sim') diff --git a/hw_sim/NOTES.md b/hw_sim/NOTES.md new file mode 100644 index 0000000..ace5528 --- /dev/null +++ b/hw_sim/NOTES.md @@ -0,0 +1,23 @@ +# hw_sim ledger + +## v0 (cell_v0.py, 2026-07-17): behavioral sanity +- 64-branch settle 1.14 us with ASSUMED 60 pF node — later shown ~100x optimistic. +- Two-phase offset rejection x376 (5 mV Vos). + +## v1 (cell_v1.py, 2026-07-17): datasheet-anchored — FIRST REAL DESIGN VERDICT +Anchors: AD7528 R=11k typ, R_eq(code) 0.8R-2R, C_OUT 50-120 pF/chip, settling 350/400 ns +(the chip is never the bottleneck); MCP6022 GBW 10 MHz, Vos max 500 uV. +KEY PHYSICS: 64 OUT pins share the column bus -> C_bus = 64 x ~85 pF = 5.4 nF. +E1 (settle, commit <=3 us / kill >7 us): + naive (R_f=10k, no comp) 16.6 us KILLED + R_f=10k + C_f 93 pF 8.2 us killed + R_f=3k + C_f 170 pF 5.3 us survives kill, misses commit + R_f=1k + C_f 294 pF 3.14 us 5% over commit — borderline +E2 at (1k, 294pF): Vos 500 uV -> single-read 2.71 mV, two-phase differential 0.88 uV + = x3088 rejection, 100x under the 0.1 mV contrast floor. Offset story SOLID. +DESIGN OPTIONS to clear the commit gate (for Dillavou session): + (a) faster TIA: GBW 50 MHz class (e.g. OPA356/OPA2356 ~$1-2) -> projected ~1.4 us; + (b) bus segmentation: 2x32 loads, two TIAs, digital sum -> C_bus/2 -> ~2.2 us at 10 MHz; + (c) accept 3.14 us: wall-clock 9-14 d -> ~5% slip, not a cliff. +NEXT (v1.1): resistor-mismatch Monte Carlo -> ENOB-after-cal proxy; op-amp noise integration +vs contrast floor; then stage C = SPICE-in-the-loop EP training (8x8 toy). diff --git a/hw_sim/cell_v1.py b/hw_sim/cell_v1.py new file mode 100644 index 0000000..ae2cf3b --- /dev/null +++ b/hw_sim/cell_v1.py @@ -0,0 +1,129 @@ +"""v1: datasheet-anchored column sim. +AD7528 channel = datasheet Thevenin equivalent: V_thev = VREF*N/256, R_eq(code) in [0.8R, 2R], +R = 11k typ; C_OUT 50-120 pF code-dependent -> 64 outputs share one summing bus: +C_bus = 64 x C_OUT ~ 3.2-7.7 nF (the v0 60 pF assumption was ~100x optimistic). +TIA = MCP6022-class linear model (GBW 10 MHz, A0 1e5, Vos up to 500 uV). +E1: settle vs feedback R_f and compensation C_f -> find the (R_f, C_f) that meets the + <=3 us commit gate, or report the gate FAILS at the naive design point. +E2: two-phase offset rejection at the chosen operating point with Vos = 500 uV (datasheet max). +Sources: AD7528 datasheet (R typ 11k, Req 0.8R-2R, COUT 50-120 pF, settling 350/400 ns max); +MCP6022 (GBW 10 MHz, Vos max +-500 uV). +""" +import os +os.environ.setdefault('NGSPICE_LIBRARY_PATH', '/home/yurenh2/miniconda3/lib/libngspice.so') +import numpy as np +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +from PySpice.Spice.Netlist import Circuit, SubCircuit + +N = 64 +R_LAD = 11e3 +R_EQ = 1.3 * R_LAD # mid-code equivalent (0.8R..2R) -> 14.3k +C_OUT = 85e-12 # mid-code per-chip output capacitance +C_BUS = N * C_OUT # 5.44 nF on the summing bus +A0 = 1e5 +GBW = 10e6 +F_P = GBW / A0 # 100 Hz dominant pole +VREF_STEP = 0.1 # per-branch Thevenin step (VREF*code/256 scale) + +class OpAmp(SubCircuit): + NODES = ('inp', 'inn', 'out') + def __init__(self, name, vos=0.0): + super().__init__(name, *self.NODES) + self.V('os', 'inp', 'inpo', vos) + self.B('gain', 'x', self.gnd, v=f'{A0}*(v(inpo)-v(inn))') + rp = 1e6 + cp = 1.0 / (2 * np.pi * F_P * rp) + self.R('p', 'x', 'p1', rp) + self.C('p', 'p1', self.gnd, cp) + self.B('buf', 'o', self.gnd, v='v(p1)') + self.R('out', 'o', 'out', 25) + +def build(rf, cf, vos=0.0, nudge=False): + c = Circuit('ep_col_v1') + c.subcircuit(OpAmp('opamp', vos=vos)) + for i in range(N): + c.PulseVoltageSource(f'in{i}', f'n{i}', c.gnd, initial_value=0, pulsed_value=VREF_STEP, + delay_time=1e-6, rise_time=50e-9, fall_time=50e-9, + pulse_width=1, period=2) + c.R(f'w{i}', f'n{i}', 'sum', R_EQ) + c.C('bus', 'sum', c.gnd, C_BUS) + c.R('f', 'out', 'sum', rf) + if cf > 0: + c.C('f', 'out', 'sum', cf) + c.X('amp', 'opamp', c.gnd, 'sum', 'out') + if nudge: + c.PulseVoltageSource('nud', 'nn', c.gnd, initial_value=0, pulsed_value=1.0, + delay_time=1e-6, rise_time=50e-9, fall_time=50e-9, + pulse_width=1, period=2) + c.R('nudge', 'nn', 'sum', 500e3) # 2 uA nudge + return c + +def run(c, end=40e-6, step=4e-9): + sim = c.simulator(temperature=27, nominal_temperature=27) + an = sim.transient(step_time=step, end_time=end) + return np.array(an.time), np.array(an['out']) + +def settle_time(t, v, tol=1e-3): + vf = v[-1] + band = tol * abs(vf) if abs(vf) > 1e-9 else tol + outside = np.where(np.abs(v - vf) > band)[0] + if len(outside) == 0: return 0.0 + idx = outside[-1] + 1 + return (t[idx] - 1e-6) if idx < len(t) else np.inf + +# ---------- E1: (R_f, C_f) design sweep ---------- +print('E1: settle vs (R_f, C_f) [commit <=3us | kill >7us] C_bus = %.2f nF' % (C_BUS * 1e9)) +results = [] +for rf in (10e3, 3e3, 1e3): + # optimal TIA comp: C_f ~ sqrt(C_bus / (2*pi*GBW*R_f)) + cf_opt = np.sqrt(C_BUS / (2 * np.pi * GBW * rf)) + for cf in (0.0, cf_opt, 2 * cf_opt): + t, v = run(build(rf, cf)) + ts = settle_time(t, v) + ideal = -N * VREF_STEP * rf / R_EQ + err = abs(v[-1] - ideal) / abs(ideal) + results.append((rf, cf, ts, v[-1], err)) + print(f' R_f={rf/1e3:4.0f}k C_f={cf*1e12:7.1f}pF t_settle={ts*1e6:7.2f}us ' + f'Vout={v[-1]*1e3:8.2f}mV static_err={err*100:.3f}%') + +ok = [r for r in results if r[2] <= 3e-6 and r[4] < 0.01] +best = min(ok, key=lambda r: r[2]) if ok else min(results, key=lambda r: r[2]) +verdict = 'COMMIT GATE MET' if ok else 'GATE FAILED at all tested points' +print(f'E1 verdict: {verdict} -> best (R_f={best[0]/1e3:.0f}k, C_f={best[1]*1e12:.0f}pF, t={best[2]*1e6:.2f}us)') + +# ---------- E2: offset rejection at the chosen point ---------- +rf_b, cf_b = best[0], best[1] +_, v_free = run(build(rf_b, cf_b, vos=500e-6)) +t2, v_nud = run(build(rf_b, cf_b, vos=500e-6, nudge=True)) +ideal = -N * VREF_STEP * rf_b / R_EQ +ideal_diff = -2e-6 * rf_b +single_err = abs(v_free[-1] - ideal) +diff_err = abs((v_nud[-1] - v_free[-1]) - ideal_diff) +print(f'E2: Vos=500uV | single-read err {single_err*1e3:.3f} mV | two-phase diff err ' + f'{diff_err*1e6:.2f} uV | rejection x{single_err/max(diff_err,1e-12):.0f} | ' + f'contrast floor 0.1-1 mV -> {"OK" if diff_err < 1e-4 else "MARGINAL/FAIL"}') + +# ---------- figure ---------- +fig, axes = plt.subplots(1, 2, figsize=(12, 4.4)) +for rf in (10e3, 3e3, 1e3): + cf_opt = np.sqrt(C_BUS / (2 * np.pi * GBW * rf)) + t, v = run(build(rf, cf_opt)) + axes[0].plot(t * 1e6, v * 1e3, label=f'R_f={rf/1e3:.0f}k, C_f={cf_opt*1e12:.0f}pF ' + f'({settle_time(t,v)*1e6:.2f}µs)') +axes[0].axvspan(1, 4, color='#2e7d32', alpha=0.07) +axes[0].axvline(8, color='#b03a2e', ls=':', lw=1) +axes[0].set_xlabel('t (µs)'); axes[0].set_ylabel('V_out (mV)') +axes[0].set_title(f'E1: 64-chip column, C_bus={C_BUS*1e9:.1f}nF (datasheet C_OUT) — comp-C TIA') +axes[0].legend(fontsize=8) +axes[1].bar(['single read\n(500 µV Vos, ×noise-gain)', 'two-phase\ndifferential'], + [single_err * 1e3, diff_err * 1e3], color=['#b03a2e', '#2e7d32']) +axes[1].set_yscale('log'); axes[1].set_ylabel('|error| (mV)') +axes[1].axhline(0.1, color='#888', ls='--', lw=1) +axes[1].text(0.5, 0.105, 'contrast signal floor 0.1 mV', fontsize=8, color='#666') +axes[1].set_title(f'E2 @ (R_f={rf_b/1e3:.0f}k): rejection ×{single_err/max(diff_err,1e-12):.0f}') +fig.suptitle('EP column v1 — AD7528 Thevenin + bus C + MCP6022-class TIA (datasheet-anchored)', fontsize=11) +fig.tight_layout() +fig.savefig('/home/yurenh2/ept/assets/figs/fig_spice_v1.png', dpi=150) +print('DONE_SPICE_V1') -- cgit v1.2.3