From 2db0202fa2d7d900eee6ff215401a22a3fd69a27 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 11:48:47 +0700 Subject: [PATCH] analog M4e: the whole SAR converter -- our own ngspice + PSP103 codes of the golden deck pinned, a behavioural converter compared conversion by conversion (Closes #10818) specs/analog/sar_adc.t27 (AnalogSarAdc), first slice of M4e of #10524 (epic #10486). Reference: the unmodified golden deck of uberWAVE (wp2-parser/golden/top.spice, GPL-2.0, fetched at run time, never committed) run in ngspice-47 with the IHP PSP103 models (IHP-Open-PDK 21133f21, mos_tt, cap_typ, TEMP 50, tran 0.1n 16u), 3894.9 s wall. The output code of each of the 31 conversions (V(out) = 1.5 V code / 2048, read after the clks rise that latches it) and the held top plates VDP, VDN of each are pinned with the deck sha256, the uberWAVE commit and the command. 30 of 31 codes equal uberWAVE's own published run; the 31st is the .ic start-up conversion. Converter: MSB first, Bit_10 after the first decision, a sign comparator with a stated offset, the M4a split capacitive DAC (exact charge redistribution) and M1's step, on the held samples. - the merged blocks as they are match 0 of 31 (-21 .. +12 codes): a straight line, gain 1.02 and offset +4.6 codes, the first wrong decision 0.11 .. 21.4 mV from its threshold - plus one top-plate parasitic (0.63 units, 77 fF) and one comparator offset (6.7 mV), fitted on a stated grid: 30 of 31 match, the miss is one code off at 0.133 mV from a threshold; the fit made on the even conversions scores the odd ones 14 of 15, on the odd the even ones 15 of 16 - an ideal-sampling input instead of the held plates: 4 of 31 (the PSP103 switch's lag is LSBs) - negative controls: 2-unit bridge 0 of 31, Bit_8 with 3 units 0, no parasitic 2, no offset 0, LSB-first plate order fails 8 of 10 tests, CP_FIT 0.70 fails 4 of 10 Levels: reference PSP103 (whole deck); sampling PSP103 (from the run); DAC, comparator and SAR sequence behavioural (M4b square law and M4c are not called; M4d replaces the sequence). Also: sar_cdac.t27 gains derive_k_top (a parasitic from the top plate to ground; derive_k delegates, no caller changes) and one test; its seal and sar_switch's closure entry are renewed. 10 tests, 0 vacuous, sealed (all hashes MATCH). Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/analog_AnalogSarAdc.json | 37 ++ .trinity/seals/analog_AnalogSarCdac.json | 18 +- .trinity/seals/analog_AnalogSarSwitch.json | 4 +- specs/analog/sar_adc.t27 | 712 +++++++++++++++++++++ specs/analog/sar_cdac.t27 | 50 +- 5 files changed, 806 insertions(+), 15 deletions(-) create mode 100644 .trinity/seals/analog_AnalogSarAdc.json create mode 100644 specs/analog/sar_adc.t27 diff --git a/.trinity/seals/analog_AnalogSarAdc.json b/.trinity/seals/analog_AnalogSarAdc.json new file mode 100644 index 0000000000..59ca136c6f --- /dev/null +++ b/.trinity/seals/analog_AnalogSarAdc.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+ea35d9b72", + "closure": [ + { + "spec": "specs/analog/linear.t27", + "spec_hash": "sha256:3ddef7be183aba583ed285334ee389b48a65b41335e98914514235db7c4e5d62" + }, + { + "spec": "specs/analog/sar_cdac.t27", + "spec_hash": "sha256:a7c45039214a8b2e92333a88befc95af33ff482ce08af3be97e2c5b8be3eacd3" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:190cf9c7a9bbc52e2ff243a068d5a52cef270186bb973987eccac07f356766a3", + "gen_hash_rust": "sha256:c022f3879d7688e5ab333d9a2aa777d397f1abc693347dd7d8a53aa44d77d051", + "gen_hash_verilog": "sha256:099481e4176136e97c8f372988dbd2fb9cb69cdfaaf4a3fd6b33a96bc3457a65", + "gen_hash_zig": "sha256:2e98efa78c53a211ce004f0b1480314821d694a39abc8b3091232e4394c1e616", + "module": "AnalogSarAdc", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-19T04:48:15Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:cedecad9b8f1a481da765debb6c64cc4c9c2a4567953dba05367fe6d7ff76094", + "spec_path": "specs/analog/sar_adc.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 10, + "total": 10, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/analog_AnalogSarCdac.json b/.trinity/seals/analog_AnalogSarCdac.json index 4150b7e5c3..84ac9c313e 100644 --- a/.trinity/seals/analog_AnalogSarCdac.json +++ b/.trinity/seals/analog_AnalogSarCdac.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+9b556b7c3", + "built_by": "t27c-bootstrap@0.5.2+ea35d9b72", "closure": [ { "spec": "specs/analog/linear.t27", @@ -7,23 +7,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:a4fb3d480089e4e90b488d2470ac9e1c592434c4264457ba3be4655c1237ec9c", - "gen_hash_rust": "sha256:76e21ceaecfeac4d922f02d53f9a9a877b2404bacc43797bc1c3ccb674d919ea", - "gen_hash_verilog": "sha256:780bcc67575db05dade43bfbe1b3d37431adb427076b86b0958269568a62fcbd", - "gen_hash_zig": "sha256:511cd3ff1f9ed0df9ffc60e32b4a45a4bac154a42e6a3299b38d1dfd42bdad10", + "gen_hash_c": "sha256:5320565fd4a79c6d03f9d5afa624b976d319c83c6c299b2f08626a571ab38cda", + "gen_hash_rust": "sha256:eea88aa66e2f6ad15b3e19e5635e91e3783e08ce0e499a30864ce7417cb64888", + "gen_hash_verilog": "sha256:7ded65d7a4dfb765f420eb3964b40f0e1af7c0080db2406940a3f855e8f55389", + "gen_hash_zig": "sha256:3e2b5e8f2ccf86b10e43fd4de1fb6c0155f3b9674c3f2ac26693de79b2b667e9", "module": "AnalogSarCdac", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T17:35:26Z", + "sealed_at": "2026-10-19T03:59:24Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:0fa41f4de9f797a091d30fdf51896c81971b6d0504587431267172179a711309", + "spec_hash": "sha256:a7c45039214a8b2e92333a88befc95af33ff482ce08af3be97e2c5b8be3eacd3", "spec_path": "specs/analog/sar_cdac.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 12, - "total": 12, + "passed": 13, + "total": 13, "vacuous": 0 }, "toolchain": { diff --git a/.trinity/seals/analog_AnalogSarSwitch.json b/.trinity/seals/analog_AnalogSarSwitch.json index d3b0d0bbcb..3f8d0787ed 100644 --- a/.trinity/seals/analog_AnalogSarSwitch.json +++ b/.trinity/seals/analog_AnalogSarSwitch.json @@ -11,7 +11,7 @@ }, { "spec": "specs/analog/sar_cdac.t27", - "spec_hash": "sha256:0fa41f4de9f797a091d30fdf51896c81971b6d0504587431267172179a711309" + "spec_hash": "sha256:a7c45039214a8b2e92333a88befc95af33ff482ce08af3be97e2c5b8be3eacd3" }, { "spec": "specs/analog/sar_comparator.t27", @@ -26,7 +26,7 @@ "module": "AnalogSarSwitch", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T02:37:55Z", + "sealed_at": "2026-10-19T03:59:52Z", "sealed_by": "t27c-bootstrap@0.5.2", "spec_hash": "sha256:122726502a51cb4c8980674b819ee3251bf8ee69a5586d6cd44a5eafcf284cb4", "spec_path": "specs/analog/sar_switch.t27", diff --git a/specs/analog/sar_adc.t27 b/specs/analog/sar_adc.t27 new file mode 100644 index 0000000000..3cb3cd5001 --- /dev/null +++ b/specs/analog/sar_adc.t27 @@ -0,0 +1,712 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/analog/sar_adc.t27 -- the whole SAR ADC of the golden deck, conversion by conversion: our own ngspice + PSP103 codes, and a behavioural converter run on the same samples +; M4e of the SAR ADC milestone (#10524, epic #10486, issue #10818), first slice. The converter is the golden deck of uberWAVE (wp2-parser/golden/top.spice, GPL-2.0: read at +; run time, never vendored; its RAW file is never committed either): two tracking switches, the differential split capacitive DAC, the dynamic comparator, the SAR logic of +; eleven D flip-flops, 1265 transistor instances. This file (1) pins the output code of every conversion of OUR OWN ngspice + PSP103 run of that deck, with the held input of each; +; (2) runs a behavioural converter, built from the merged blocks, on the same samples; (3) compares the two conversion by conversion and says what the difference is. +; Everything hand-written is this file and one function added to M4a (derive_k_top in specs/analog/sar_cdac.t27): the DAC arithmetic and the switch event are M4a's and M1's +; (specs/analog/linear.t27), called by name. No deck is committed: the reference is a run of the golden deck, not a netlist of ours. +; - +; THE REFERENCE RUN (FR-002: the unmodified deck, paths substituted only; anyone can repeat it). +; deck https://gitlab.symbioticeda.com/Juan_25/uberwave/-/raw/main/wp2-parser/golden/top.spice, sha256 c5ad548a3e1a181f1b8e81e92ead2f630e14861a36b796169f4fec6b95746e23; +; uberWAVE HEAD c75a89c8fae0eecdf18a3b31c4f2b55af56b1ae2 (read 2026-10-19); the deck last changed in 8e015311a43eee436b25734865de68b7e0617ee0 (2026-04-01). +; changes the two `.lib` paths /usr/local/share/OpenPDKs/IHP-Open-PDK -> a checkout of IHP-Open-PDK at commit 21133f219b1a6da10d803e345761e77415ceff40; and six `pre_osdi` +; lines (psp103, psp103_nqs, r3_cmc, mosvar, cap_cmomi, cap_cmomf .osdi, compiled from the PDK at that commit, M3b #10516) at the top of the deck's own .control +; block, where uberWAVE's Dockerfile puts them in spinit. Nothing else: `tran 0.1n 16u`, `.options TEMP = 50`, the `.ic` and `.nodeset` lines, `.save` and `write` +; (an ASCII RAW of v(out), v(VDP), v(VDN), v(Vinp), v(Vinn)) are the deck's own; its `plot` is ignored in batch mode. +; command ngspice -b run_full.spice ngspice-47 (Homebrew, SPARSE 1.3), models mos_tt and cap_typ (PSP103 through OSDI), TEMP 50 C (TNOM 27 C). +; cost 2026-10-19 03:38:22Z to 04:43:17Z: wall 3894.9 s (65 min), user 3285.7 s, sys 42.5 s, on a Mac shared with other jobs (load average 5 to 40); 161633 time +; points; a 31 MB ASCII RAW (not committed, not needed again: the numbers below are all that is read from it). +; cross-check uberWAVE publishes the RAW of this deck (ngspice-46, 2026-03-30; its Dockerfile also sets ngbehavior=hsa). Against it: 30 of the 31 codes are identical +; (conversion 0, which starts from the .ic state, is 1006 here and 1007 there); at the pinned instants from conversion 1 on VDP and VDN agree to 0.007 and 0.012 mV +; and V(out) to under 0.001 mV; the whole traces, compared every 0.5 ns, differ by at most 1.1 mV (VDP) and 1.3 mV (VDN) from 600 ns on (edge timing). And +; `set ngbehavior=hsa` changes nothing here: a 0.5 us run with and without it differs by 0 V in all five saved vectors. A 1.1 us run of the same deck with every +; node saved (CK1..CK11, Valid, clkc, vocp, vocn, the bits) is identical to this run to 0 V over its length; it is where the instants below were read. +; - +; THE CODE. The deck's own output is V(out): E1..E11 chain the eleven latched bits out_11 .. out_1 with weights 0.5 .. 2^-11, so V(out) = 1.5 V code / 2048 with the bits at +; 0 / 1.5 V. out_k is the D flip-flop of DAC_control stage k, latched on the rising edge of clks, so conversion k (clks high 0 .. 84 ns of each 500 ns period: the track +; phase; the SAR runs in the low phase) shows its code from 500 (k + 1) ns. NG_CODE is round(V(out) 2048 / 1.5 V) read at 500 (k + 1) + 10 ns; NG_OUT keeps the voltage, and a +; test checks that every one is within 0.03 LSB of its integer. 31 conversions: the run ends at 16 us, where the 32nd conversion would latch. One LSB of the 11-bit code is +; 3 V / 2048 = 1.466 mV of VDP - VDN. +; THE SAMPLES. S_VP and S_VN are V(VDP) and V(VDN) at 500 k + 98 ns: the track phase has ended (clks fell at 83 ns, 50 %), the comparator clock clkc has not yet started to rise +; (it passes half of Vdd 20.1 ns after clks falls: 103.12 and 603.04 ns in the 1.1 us run), and VDP and VDN move by less than 0.25 mV between 90 and 98 ns. They are the top +; plates the first comparison sees, with the switches' lag and injection in them. S_IP and S_IN are the deck's input, V(Vinp) = 0.75 V - 0.6 V sin(2 pi 62.5 kHz t) and V(Vinn) +; = 0.75 V + 0.6 V sin(...), at 500 k + 84 ns, the instant the switch is opening: the input an IDEAL switch would have sampled. The sine's 16 us period is 32 conversions, so +; conversion k + 16 samples the negative of conversion k (a test). Conversion 0 starts from the deck's .ic state (everything at 0 V) and its track phase has not settled: its +; held common mode is 13.8 mV off and VDN is 10.6 mV from the input; from conversion 1 on the held plates are within 3.1 mV of the common mode and 3.6 mV of the 84 ns input. +; - +; THE BEHAVIOURAL CONVERTER, block by block (FR-001: the level of every block is also LEVEL_* below). +; sampling none: the held top plates S_VP, S_VN of the run are the input (the PSP103 switch, as simulated). An ideal-sampling variant (S_IP, S_IN) is scored too. +; DAC M4a: ideal capacitors, exact charge redistribution, the 1-unit bridge, ratios K = C^-1 C_b as Q.24 words, a plate move is M1's step with D = 0. One +; addition, derive_k_top: a parasitic Cp from the top plate to ground joins the Vout corner of C, so every Vout ratio is 2^j / (1023 + 32 Cp) (a test in +; sar_cdac.t27). Both halves are modelled (the P half on VDP with the bits, the N half on VDN with the complements), each from its own sample; net1 starts at +; the sample and the comparator never sees it (a move of Vout does not depend on the state). +; comparator the sign of VDP - VDN + off, on words: true means "VDP above VDN", which is the latch's dp winning (M4b's convention) and out_k = 1 (D0 = vocn at CK_k, whose +; inverse drives the plate). M4b's latch is NOT run: 31 x 11 regenerations at 0.1 ps would be a different PR, and the decision here is a stated number. +; SAR logic decision i = 0 .. 10, MSB first; a set decision lowers the P plate and raises the N plate by VCM, a clear one the reverse; the plate is Bit_10 after the first +; decision, Bit_9 after the second, ... Bit_1 after the tenth; the eleventh decision moves no plate (Bit_0 has no capacitor); the code is the 11 decisions. Read +; from DAC_control_top, DAC_control, neg_bottom_plate_logic and SAR_logic of the deck. M4d (#10754) replaces this sequence with the gates. +; With K from the merged M4a, a zero offset and no parasitic, the converter is exactly the ideal quantizer floor(1023 dv / 1.5 V + 1024) (a test over all 2048 bin centres). +; - +; THE RESULT, 31 conversions, exact code match (model = ngspice), three rows. +; 1 the merged blocks as they are (Cp = 0, off = 0): 0 of 31 match. model - ngspice is -21 .. +12 codes, and the first wrong decision of each lies 0.11 .. 21.4 mV from its +; threshold: 24 of the 31 are more than 2 mV away, so these are not roundings. The error is a straight line: ngspice's code about the middle (1023.5) is 1.0199 times the +; model's plus 4.60 codes, and no point is 0.61 code (0.9 mV) off that line (least squares, a test). A gain of 1.02 is the DAC steps being 1.95 % smaller than ideal +; capacitors give (a parasitic on the top plate takes 1023 / (1023 + 32 Cp) of every step: 0.9816 at the fit), and an offset of 4.6 codes (6.7 mV) is the comparator +; deciding "above" while VDP is still that far below VDN. A bridge or a unit count that is wrong bends the line (M4a) and does not give this; the controls below show it. +; 2 + Cp = 0.63 units and off = +6.7 mV: 30 of 31 match. model - ngspice is -1 .. 0 codes; the one miss, conversion 25 (1841 against 1842), has its first wrong decision at the +; tenth, 0.133 mV from the threshold: 0.09 LSB. Moving the offset by 0.2 mV to 6.9 mV matches 30 again and the miss becomes conversion 16 (1053 against 1052, 0.196 mV): the +; last code is below the resolution of a two-number model. +; 3 the same on an ideal-sampling input (S_IP, S_IN): 4 of 31 with the fit, 1 of 31 without it. The PSP103 switch's lag and injection (up to 3.6 mV from conversion 1 on) are +; one to three LSB of the 11-bit code and cannot be left out of a converter model. +; THE FIT. Cp = 0.63 units of the 9 um x 9 um cap_cmim (122.94 fF, M4c), 77 fF; off = +6.7 mV (the comparator decides "above" when VDP - VDN + 6.7 mV > 0). It is the optimum +; of a stated grid (a test: Cp = 0.55 + 0.004 a units, a = 0 .. 40; off = 5.0 mV + 0.1 mV b, b = 0 .. 40): most matches first, then the least total distance-from-threshold of +; the misses. It was made on ALL 31 conversions, so row 2 describes this run, it does not predict another. The held-out check is separate and is a test: the same search on +; the 16 even conversions alone (all 16 match, Cp 0.626, 6.7 mV) scores the 15 odd ones it never saw 14 of 15; on the 15 odd alone (all 15, Cp 0.630, 6.9 mV) the 16 even 15 of +; 16. The optimum is narrow in the offset and broad in Cp: Cp -0.02 / +0.02 units matches 23 / 20, the offset -1 / +1 mV matches 11 / 6. +; WHAT THE TWO NUMBERS ARE. Cp is everything on the top plate that M4a does not have: the explicit 4 fF capacitors (C3, C5 on VDP, C2 in the switch: 12 fF) and the device +; capacitance of the comparator's 36-unit input device and of the switch, which this file does not decompose. off is not the comparator's offset alone. Reading the 22 decisions +; of the two conversions in the 1.1 us all-nodes run directly (the real VDP - VDN a fixed time before clkc passes half of Vdd, against the decided bit) puts the latch's decision +; point at +2.5 .. +3.1 mV for 1 ns before, +2.0 .. +2.6 mV for 0.5 ns, +3.3 .. +3.9 mV for 1.5 ns and +3.3 .. +4.0 mV for 2 ns: the kickback onto VDP and VDN at the clock +; edge (they fall by 12 and 25 mV in the first 2 ns, 45 and 81 mV by 104 ns) moves the comparator's view under its own decision. The fitted 6.7 mV may also absorb what else the +; model lacks (the plate drivers' settling, the injection of each plate switch; not separated here). Neither number is claimed beyond: with these two, a behavioural converter reproduces the codes +; of this run. The comparator and the top-plate load are what M4b and M3b at PSP103 level must supply for the offset and Cp to go away. +; - +; THE LEVELS (FR-001). Reference: the whole deck at PSP103 (OSDI) level. Sampling: PSP103, taken from the reference run. DAC: behavioural (ideal capacitors, exact charge +; redistribution) plus one fitted parasitic. Comparator: behavioural (a sign and a fitted offset); the square-law M4b is not called. SAR logic: behavioural sequence. The tracking +; switch of M4c is square law and is not called either: its lag at the sine's zero crossing is 1.27 mV, and this deck's held plates sit 2.5 mV from the 84 ns input there +; (conversion 16, slope 0.236 mV/ns, injection included): not the same quantity, but the level of the switch matters at the size of a few LSB. +; - +; THE NEGATIVE CONTROLS (tests that pass by seeing the failure), each on the fitted model's 30 of 31: the bridge of 2 units instead of 1 matches 0 of 31 (it also fails the +; ideal sweep); Bit_8 with 3 units instead of 4 matches 0; no parasitic matches 2; no offset 0; the offset of the wrong sign 0; Cp or the offset moved by 0.02 units or 1 mV +; loses 7 to 24 matches. +; - +; WHAT A PASS SAYS. The output code of each of 31 conversions of the golden deck at PSP103 (ngspice-47, IHP-Open-PDK 21133f21, TEMP 50 C), with the held plates of each, are +; pinned with their provenance and agree with uberWAVE's own published run in 30 of 31 codes. The merged blocks alone (ideal DAC, a sign) match none of them: they are 21 codes +; low to 12 codes high, on a straight line with gain 1.02 and offset +4.6 codes. With one top-plate parasitic and one comparator offset they match 30 of 31, the other is one +; code off at 0.133 mV from a threshold, and the same search on half the conversions predicts the other half 14 of 15 and 15 of 16. The sequence is MSB first with Bit_10 after +; the first decision; the ideal converter is the exact quantizer; a changed bridge, unit count, parasitic or offset is caught. +; WHAT IT DOES NOT SAY. Nothing about speed: no timing is modelled, the reference took 65 minutes and the behavioural converter does 31 conversions in a fraction of a second, +; which is not a comparison of the same thing. Nothing about power, noise, mismatch, supply or temperature, or about another corner: one run, TEMP 50 C, mos_tt, cap_typ. +; Nothing about the comparator as a circuit: the sign and offset are a stated behaviour fitted to the codes, not M4b's latch, and the offset's cause is not found. Nothing about +; the switch: the samples are the run's, not M4c's. Nothing about the SAR logic as gates (M4d). Nothing about conversion 0 beyond its code (it is the .ic transient). The fit +; uses the 31 codes it is then scored on: row 2 is a description of this run, only the held-out rows are predictions, and 15 and 16 conversions of one sine are few. Nothing +; about a board: no bitstream, no clock, no LUT, DSP or BRAM. +; ASCII only, English only. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module AnalogSarAdc; + +use analog::linear::{NEG, MAG, q_sm, sm_f64, pack, is_neg, add_sm, fabs64}; +use analog::sar_cdac::{UNITS, SIDE, PLATES, K_Q, derive_k_top, event_k, plate_step}; + +pub const KIND : str = "analog-cell"; +pub const ID : str = "analog/sar_adc"; +pub const REFERENCE : str = "uberWAVE wp2-parser/golden/top.spice (read at run time, GPL-2.0, not vendored); specs/analog/sar_cdac.t27"; + +; --- the levels of every block (FR-001) --------------------------------------------------------------------------------- +pub const LEVEL_SAMPLE : str = "ngspice 47 + IHP PSP103 (the whole deck): VDP and VDN read from the run; ideal input variant also reported"; +pub const LEVEL_DAC : str = "behavioural: ideal capacitors, exact charge redistribution (M4a sar_cdac), plus one parasitic on the top plate"; +pub const LEVEL_COMPARATOR : str = "behavioural: the sign of VDP - VDN + offset (M4b sar_comparator is not run)"; +pub const LEVEL_LOGIC : str = "behavioural: one decision per step, MSB first (M4d, #10754, replaces it with the gates)"; + +; --- the converter ------------------------------------------------------------------------------------------------------- +/// Decisions per conversion: the MSB against the sample alone, ten plate moves, the last against the 10-plate residual. +pub const DECISIONS : usize = 11; +pub const CODES11 : u32 = 2048; +pub const VDD_V : f64 = 1.5; +pub const VCM_V : f64 = 0.75; + +/// The negation of a sign-magnitude word (zero stays zero). +pub fn neg_word(w: u32) -> u32 { + return pack(w & MAG, !is_neg(w)); +} + +/// The comparator, behavioural: true iff VDP - VDN + off > 0, on words (M1's adder and a sign test). `off` is a word, volts * 2^24. +pub fn decide(p: u32, n: u32, off: u32) -> bool { + const d : u32 = add_sm(add_sm(p, neg_word(n)), off); + if ((d & MAG) == 0) { return false; } + return !is_neg(d); +} + +/// The SAR sequence of the deck, the one implementation. Decision i = 0 .. 10, MSB first: the model decides from the top-plate words +/// (xp, xn: net1 and Vout of each half), then the plate of the DECIDED bit moves: a set decision (VDP above VDN) lowers the P plate and +/// raises the N plate, a clear one the reverse; the plate is index PLATES - 1 - i (Bit_10 after the first decision, Bit_1 after the +/// tenth); the eleventh decision moves no plate. res[i] is VDP - VDN + off in volts before decision i. With `force` the plates follow +/// the bits of `ng` instead of the model's own (ngspice's path) and wrong[i] is 1 where the model's sign differs from ngspice's bit. +/// Returns the code the model's decisions give. +pub fn sar_core(k: []const u32, vp: u32, vn: u32, off: u32, force: bool, ng: u32, res: []f64, wrong: []u32) -> u32 { + var xp : [2]u32 = [vp, vp]; + var xn : [2]u32 = [vn, vn]; + var yp : [2]u32 = [0, 0]; + var yn : [2]u32 = [0, 0]; + var code : u32 = 0; + var i : usize = 0; + while (i < DECISIONS) { + res[i] = sm_f64(xp[1]) - sm_f64(xn[1]) + sm_f64(off); + const d : bool = decide(xp[1], xn[1], off); + const nb : bool = ((ng >> ((DECISIONS - 1 - i) as u32)) & 1) == 1; + wrong[i] = 0; + if (force && d != nb) { wrong[i] = 1; } + var b : bool = d; + if (force) { b = nb; } + code = code * 2; + if (d) { code = code + 1; } + if (i < PLATES) { + const j : usize = PLATES - 1 - i; + event_k(k, j, plate_step(!b), &xp, &yp); + event_k(k, j, plate_step(b), &xn, &yn); + xp[0] = yp[0]; + xp[1] = yp[1]; + xn[0] = yn[0]; + xn[1] = yn[1]; + } + i = i + 1; + } + return code; +} + +/// The 11-bit code of one conversion from the held top-plate words vp, vn. +pub fn convert_sar(k: []const u32, vp: u32, vn: u32, off: u32) -> u32 { + var res : [11]f64 = [0.0] ** 11; + var wrong : [11]u32 = [0] ** 11; + return sar_core(k, vp, vn, off, false, 0, &res, &wrong); +} + +/// What a conversion run down ngspice's own path shows: the first decision (1 .. 11, 0 = none) at which the model's sign differs from +/// ngspice's bit, the model's |VDP - VDN + off| there (its distance from the decision threshold), and how many decisions differ in all. +pub struct Miss { first: u32, margin: f64, nviol: u32 } + +/// The model's DAC with the plates following ngspice's bits `ng`, the model's sign compared with each bit. +pub fn follow(k: []const u32, vp: u32, vn: u32, off: u32, ng: u32) -> Miss { + var res : [11]f64 = [0.0] ** 11; + var wrong : [11]u32 = [0] ** 11; + const c : u32 = sar_core(k, vp, vn, off, true, ng, &res, &wrong); + var first : u32 = 0; + var margin : f64 = 0.0; + var nviol : u32 = 0; + var i : usize = 0; + while (i < DECISIONS) { + if (wrong[i] == 1) { + nviol = nviol + 1; + if (first == 0) { + first = (i as u32) + 1; + margin = fabs64(res[i]); + } + } + i = i + 1; + } + return Miss { first: first, margin: margin, nviol: nviol }; +} + +/// Words of f64 ratios: q_sm of each. +pub fn words_of(kf: []const f64) -> [20]u32 { + var w : [20]u32 = [0] ** 20; + var i : usize = 0; + while (i < 20) { + w[i] = q_sm(kf[i]); + i = i + 1; + } + return w; +} + +/// The model's ratio words for a unit list, a bridge of `bridge` units and a top-plate parasitic of `top` units (the deck: UNITS, 1, fit). +pub fn model_k(units: []const u32, bridge: f64, top: f64) -> [20]u32 { + const kf : [20]f64 = derive_k_top(units, &SIDE, bridge, 0.0, top); + return words_of(&kf); +} + +/// The ideal quantizer of the 11-bit converter: floor(x + 1024), x = (VDP - VDN) 1023 / VDD, clamped to 0 .. 2047. +pub fn ideal_code(dv: f64) -> u32 { + const x : f64 = dv * 1023.0 / VDD_V + 1024.0; + if (x < 0.0) { return 0; } + if (x >= 2047.0) { return 2047; } + return x as u32; +} + +pub const N_CONV : usize = 31; +; --- BEGIN PINNED DATA (the reference run: see the header) ----------------------------------------------------------------- +/// The 11-bit code of conversion k: round(V(out) / 1.5 V * 2048) read 10 ns after the next clks rise. +pub const NG_CODE : [31]u32 = [ + 1006, 841, 686, 544, + 421, 321, 248, 205, + 194, 215, 267, 348, + 456, 585, 732, 889, + 1052, 1215, 1370, 1512, + 1635, 1735, 1808, 1851, + 1862, 1842, 1789, 1708, + 1601, 1471, 1325 +]; +/// V(out) of the deck, volts, 10 ns after the clks rise that ends conversion k (the weighted sum of the eleven latched bits). +pub const NG_OUT : [31]f64 = [ + 0.7368048349, 0.6159615380, 0.5024358303, 0.3984347947, + 0.3083441989, 0.2351059647, 0.1816385989, 0.1501465324, + 0.1420899864, 0.1574705269, 0.1955537495, 0.2548808443, + 0.3339811239, 0.4284588474, 0.5361270039, 0.6511147285, + 0.7704907794, 0.8898825590, 1.0034052305, 1.1074094505, + 1.1974932298, 1.2707379805, 1.3242028305, 1.3556989171, + 1.3637552288, 1.3491070388, 1.3102877209, 1.2509640994, + 1.1725953709, 1.0773779913, 0.9704497699 +]; +/// V(VDP) of the deck at 500 k + 98 ns, volts: the held top plate of the P half, 5 ns before the comparator clock reaches half of Vdd. +pub const S_VP : [31]f64 = [ + 0.7269686961, 0.6147933219, 0.5032696326, 0.4011759707, + 0.3124819051, 0.2406802002, 0.1883776363, 0.1576606066, + 0.1496928808, 0.1647834218, 0.2023500934, 0.2609311830, + 0.3383049515, 0.4314737982, 0.5367822844, 0.6503503377, + 0.7677272898, 0.8835405443, 0.9950495472, 1.0971101614, + 1.1857916925, 1.2576286619, 1.3099852189, 1.3407667777, + 1.3487980360, 1.3337693499, 1.2962426444, 1.2376783030, + 1.1602905824, 1.0670825958, 0.9616920181 +]; +/// V(VDN) at 500 k + 98 ns, volts. +pub const S_VN : [31]f64 = [ + 0.7592181064, 0.8835812897, 0.9950151627, 1.0969568676, + 1.1855623259, 1.2575908974, 1.3099882461, 1.3408895055, + 1.3490788173, 1.3342519239, 1.2969787025, 1.2386549828, + 1.1615809854, 1.0686704755, 0.9633558564, 0.8499779529, + 0.7327315350, 0.6147452441, 0.5033252558, 0.4013487230, + 0.3127180743, 0.2407567296, 0.1884153908, 0.1575863310, + 0.1494670132, 0.1643685269, 0.2016967212, 0.2600430190, + 0.3371111770, 0.4299774858, 0.5351597503 +]; +/// V(Vinp) at 500 k + 84 ns, volts, the instant clks has fallen and the tracking switch is opening: the deck input 0.75 V - 0.6 V sin(2 pi 62.5 kHz t). +pub const S_IP : [31]f64 = [ + 0.7302115555, 0.6136012706, 0.5022327124, 0.4003857122, + 0.3119741930, 0.2403957598, 0.1884011319, 0.1579884335, + 0.1503264076, 0.1657095015, 0.2035465516, 0.2623835013, + 0.3399592796, 0.4332926928, 0.5387969902, 0.6524177009, + 0.7697884445, 0.8863987294, 0.9977672876, 1.0996142878, + 1.1880258070, 1.2596042402, 1.3115988681, 1.3420115665, + 1.3496735924, 1.3342904985, 1.2964534484, 1.2376164987, + 1.1600407204, 1.0667073072, 0.9612030098 +]; +/// V(Vinn) at 500 k + 84 ns, volts: 0.75 V + 0.6 V sin(2 pi 62.5 kHz t). +pub const S_IN : [31]f64 = [ + 0.7697884445, 0.8863987294, 0.9977672876, 1.0996142878, + 1.1880258070, 1.2596042402, 1.3115988681, 1.3420115665, + 1.3496735924, 1.3342904985, 1.2964534484, 1.2376164987, + 1.1600407204, 1.0667073072, 0.9612030098, 0.8475822991, + 0.7302115555, 0.6136012706, 0.5022327124, 0.4003857122, + 0.3119741930, 0.2403957598, 0.1884011319, 0.1579884335, + 0.1503264076, 0.1657095015, 0.2035465516, 0.2623835013, + 0.3399592796, 0.4332926928, 0.5387969902 +]; +/// The code of the merged blocks as they are (Cp = 0, offset 0) for each held sample: pinned so the comparison table is asserted element by element. +pub const BASE_CODE : [31]u32 = [ + 1002, 840, 688, 549, + 428, 330, 259, 217, + 206, 226, 277, 357, + 462, 589, 733, 887, + 1047, 1207, 1359, 1498, + 1619, 1717, 1788, 1830, + 1841, 1821, 1770, 1690, + 1585, 1458, 1314 +]; +/// The code of the fitted model (Cp = CP_FIT, offset OFF_FIT_V) for each held sample. +pub const FIT_CODE : [31]u32 = [ + 1006, 841, 686, 544, + 421, 321, 248, 205, + 194, 215, 267, 348, + 456, 585, 732, 889, + 1052, 1215, 1370, 1512, + 1635, 1735, 1808, 1851, + 1862, 1841, 1789, 1708, + 1601, 1471, 1325 +]; +; --- END PINNED DATA ------------------------------------------------------------------------------------------------------ + +; --- scoring: the conversions of one set (par 0 = even k, 1 = odd k, 2 = all) ----------------------------------------------- +/// Is conversion i in the set par? +pub fn in_set(i: usize, par: u32) -> bool { + if (par == 2) { return true; } + return ((i & 1) as u32) == par; +} + +/// How many conversions of the set give ngspice's code, from the held words of vp, vn (arrays of volts). +pub fn match_count_p(k: []const u32, off: u32, vp: []const f64, vn: []const f64, par: u32) -> u32 { + var n : u32 = 0; + var i : usize = 0; + while (i < N_CONV) { + if (in_set(i, par) && convert_sar(k, q_sm(vp[i]), q_sm(vn[i]), off) == NG_CODE[i]) { n = n + 1; } + i = i + 1; + } + return n; +} + +/// All 31. +pub fn match_count(k: []const u32, off: u32, vp: []const f64, vn: []const f64) -> u32 { + return match_count_p(k, off, vp, vn, 2); +} + +/// The sum and the largest of the first-violation margins over the set's conversions that do not match, volts. +pub struct Margins { sum: f64, worst: f64 } + +pub fn miss_margins(k: []const u32, off: u32, vp: []const f64, vn: []const f64, par: u32) -> Margins { + var sum : f64 = 0.0; + var worst : f64 = 0.0; + var i : usize = 0; + while (i < N_CONV) { + if (in_set(i, par) && convert_sar(k, q_sm(vp[i]), q_sm(vn[i]), off) != NG_CODE[i]) { + const m : Miss = follow(k, q_sm(vp[i]), q_sm(vn[i]), off, NG_CODE[i]); + sum = sum + m.margin; + if (m.margin > worst) { worst = m.margin; } + } + i = i + 1; + } + return Margins { sum: sum, worst: worst }; +} + +pub struct Range { lo: f64, hi: f64 } + +/// The smallest and the largest signed code error (model - ngspice) over the 31 conversions. +pub fn err_range(k: []const u32, off: u32, vp: []const f64, vn: []const f64) -> Range { + var lo : f64 = 0.0; + var hi : f64 = 0.0; + var i : usize = 0; + while (i < N_CONV) { + const c : u32 = convert_sar(k, q_sm(vp[i]), q_sm(vn[i]), off); + const e : f64 = (c as f64) - (NG_CODE[i] as f64); + if (e < lo) { lo = e; } + if (e > hi) { hi = e; } + i = i + 1; + } + return Range { lo: lo, hi: hi }; +} + +; --- the fit: two numbers the merged blocks do not have --------------------------------------------------------------------- +/// The top-plate parasitic, in units of the 9 um x 9 um cap_cmim (122.94 fF). +pub const CP_FIT : f64 = 0.63; +/// The comparator offset: it decides "VDP above VDN" when VDP - VDN + OFF_FIT_V > 0, volts. +pub const OFF_FIT_V : f64 = 0.0067; +/// The grid the fit searches: Cp = 0.55 + 0.004 a, a = 0 .. 40; offset = 5.0 mV + 0.1 mV b, b = 0 .. 40. +pub const FIT_CP0 : f64 = 0.55; +pub const FIT_DCP : f64 = 0.004; +pub const FIT_NA : usize = 41; +pub const FIT_OFF0 : f64 = 0.005; +pub const FIT_DOFF : f64 = 0.0001; +pub const FIT_NB : usize = 41; + +pub struct Fit { cp: f64, off: f64, hits: u32, viol: f64 } + +/// The grid optimum on the set par: the most exact matches, ties broken by the smallest sum of first-violation margins. +pub fn fit_grid(par: u32, vp: []const f64, vn: []const f64) -> Fit { + var best : Fit = Fit { cp: 0.0, off: 0.0, hits: 0, viol: 1000000.0 }; + var a : usize = 0; + while (a < FIT_NA) { + const cp : f64 = FIT_CP0 + FIT_DCP * (a as f64); + const k : [20]u32 = model_k(&UNITS, 1.0, cp); + var b : usize = 0; + while (b < FIT_NB) { + const off : f64 = FIT_OFF0 + FIT_DOFF * (b as f64); + const w : u32 = q_sm(off); + const hits : u32 = match_count_p(&k, w, vp, vn, par); + if (hits >= best.hits) { + const v : f64 = miss_margins(&k, w, vp, vn, par).sum; + if (hits > best.hits || v < best.viol) { + best = Fit { cp: cp, off: off, hits: hits, viol: v }; + } + } + b = b + 1; + } + a = a + 1; + } + return best; +} + +; --- the tests ------------------------------------------------------------------------------------------------------------ + +test the_ideal_sar_reads_every_bin_centre { + var c : u32 = 0; + while (c < CODES11) { + const dv : f64 = (((c as f64) - 1023.5) * VDD_V) / 1023.0; + const vp : u32 = q_sm(VCM_V + dv / 2.0); + const vn : u32 = q_sm(VCM_V - dv / 2.0); + assert_eq(convert_sar(&K_Q, vp, vn, 0), c); + assert_eq(ideal_code(dv), c); + c = c + 1; + } +} + +test the_sar_is_msb_first_each_plate_step_halves_and_the_msb_is_the_sign_of_the_sample { + var res : [11]f64 = [0.0] ** 11; + var wrong : [11]u32 = [0] ** 11; + // VDP 0.5 V above VDN: the sample alone decides the first bit + const c : u32 = sar_core(&K_Q, q_sm(1.0004), q_sm(0.5), 0, false, 0, &res, &wrong); + assert_eq(c, ideal_code(0.5004)); + assert fabs64(res[0] - 0.5004) < 0.0000001; + // between two decisions the residual moves by VDD 2^(9 - i) / 1023: Bit_10 (512 of 1023) first, Bit_1 (1 of 1023) last + var i : usize = 0; + var w : f64 = 512.0; + while (i < 10) { + assert fabs64(fabs64(res[i] - res[i + 1]) - VDD_V * w / 1023.0) < 0.0000005; + w = w / 2.0; + i = i + 1; + } + // a set decision lowers the residual, a clear one raises it: 0.5 > 0 sets the MSB, 0.5 - 0.7507 < 0 clears the next + assert(res[0] > 0.0); + assert(res[1] < 0.0); + assert(res[1] < res[0]); + assert(res[2] > res[1]); + assert_eq(c >> 10, 1); + assert_eq((c >> 9) & 1, 0); + // the MSB follows the sign of the sample for either polarity, and the eleven decisions fill 11 bits + assert_eq(convert_sar(&K_Q, q_sm(0.5), q_sm(1.0), 0) >> 10, 0); + assert(convert_sar(&K_Q, q_sm(1.5), q_sm(0.0), 0) < CODES11); + // run down its own bits the model has no wrong decision; one flipped bit is seen at that decision + const own : Miss = follow(&K_Q, q_sm(1.0004), q_sm(0.5), 0, c); + assert_eq(own.first, 0); + assert_eq(own.nviol, 0); + const flipped : Miss = follow(&K_Q, q_sm(1.0004), q_sm(0.5), 0, c ^ 4); + assert(flipped.first >= 1); + assert_eq(flipped.first, 9); +} + +test a_decision_is_the_sign_of_vdp_minus_vdn_plus_the_offset_on_words { + const a : u32 = q_sm(0.75); + assert(!decide(a, a, 0)); + assert(decide(q_sm(0.7500001), a, 0)); + assert(!decide(a, q_sm(0.7500001), 0)); + // negative words (sign-magnitude): -0.1 V is above -0.2 V + assert(decide(q_sm(0.0 - 0.1), q_sm(0.0 - 0.2), 0)); + assert(!decide(q_sm(0.0 - 0.2), q_sm(0.0 - 0.1), 0)); + // the offset moves the threshold: +5 mV decides "above" for VDP 4 mV under VDN and not for 6 mV under + assert(decide(q_sm(0.746), a, q_sm(0.005))); + assert(!decide(q_sm(0.744), a, q_sm(0.005))); + assert(!decide(q_sm(0.754), a, q_sm(0.0 - 0.005))); + assert(decide(q_sm(0.756), a, q_sm(0.0 - 0.005))); + assert_eq(neg_word(0), 0); + assert_eq(neg_word(5), NEG | 5); + assert_eq(neg_word(NEG | 5), 5); +} + +test the_pinned_reference_is_the_decks_own_sine_and_its_code_is_read_from_out { + var k : usize = 0; + var cm_worst : f64 = 0.0; + while (k < N_CONV) { + // the 11 latched bits weigh 0.5 ... 2^-11 of 1.5 V in the deck's chain of E sources: V(out) = 1.5 V code / 2048 + assert fabs64(NG_OUT[k] * 2048.0 / VDD_V - (NG_CODE[k] as f64)) < 0.03; + assert(NG_CODE[k] < CODES11); + // the inputs are the deck's: Vinp + Vinn = 2 VCM = 1.5 V (VDD3 and VDD8 are the same sine with opposite signs) + assert fabs64(S_IP[k] + S_IN[k] - VDD_V) < 0.0000000002; + // 16 conversions are 8 us, half the 16 us period of 62.5 kHz: the sine changes sign + if (k + 16 < N_CONV) { + assert fabs64(S_IP[k] + S_IP[k + 16] - VDD_V) < 0.0000000002; + assert fabs64(S_IN[k] + S_IN[k + 16] - VDD_V) < 0.0000000002; + } + // the held top plates keep the common mode of the sample, within a few mV; conversion 0 starts from the .ic state (all nodes at + // 0 V) and its 82 ns of tracking do not settle: 13.8 mV + const cm : f64 = fabs64(S_VP[k] + S_VN[k] - VDD_V); + if (k > 0 && cm > cm_worst) { cm_worst = cm; } + if (k == 0) { assert(cm > 0.01); } + // the amplitude is 0.6 V about 0.75 V + assert fabs64(S_IP[k] - VCM_V) < 0.6000000001; + k = k + 1; + } + assert cm_worst < 0.004; + // the extremes of the sine fall in the middle of the 31 samples: conversions 8 and 24 sit within 0.01 V of the 0.15 V and 1.35 V peaks + assert fabs64(S_IP[8] - 0.15) < 0.01; + assert fabs64(S_IP[24] - 1.35) < 0.01; + // the ngspice codes rise and fall with the held differential: monotone in VDP - VDN + k = 1; + while (k < N_CONV) { + const d0 : f64 = S_VP[k - 1] - S_VN[k - 1]; + const d1 : f64 = S_VP[k] - S_VN[k]; + if (d1 > d0) { assert(NG_CODE[k] > NG_CODE[k - 1]); } + if (d1 < d0) { assert(NG_CODE[k] < NG_CODE[k - 1]); } + k = k + 1; + } +} + +test the_merged_blocks_as_they_are_match_none_of_the_conversions_and_the_error_has_a_shape { + const k0 : [20]u32 = model_k(&UNITS, 1.0, 0.0); + // the ratio words are M4a's own + var i : usize = 0; + while (i < 20) { + assert_eq(k0[i], K_Q[i]); + i = i + 1; + } + assert_eq(match_count(&k0, 0, &S_VP, &S_VN), 0); + i = 0; + while (i < N_CONV) { + assert_eq(convert_sar(&k0, q_sm(S_VP[i]), q_sm(S_VN[i]), 0), BASE_CODE[i]); + assert(BASE_CODE[i] != NG_CODE[i]); + i = i + 1; + } + // model - ngspice: the model is 12 codes above ngspice at the low end of the sine and 21 codes below it at the high end + const r : Range = err_range(&k0, 0, &S_VP, &S_VN); + assert_eq(r.lo, 0.0 - 21.0); + assert_eq(r.hi, 12.0); + // the error is a straight line, a gain and an offset: ngspice's code about the middle (1023.5) is a times the model's plus b, by + // least squares over the 31 conversions, and no point is a full code (1.47 mV) off the line + var sx : f64 = 0.0; + var sy : f64 = 0.0; + i = 0; + while (i < N_CONV) { + sx = sx + ((BASE_CODE[i] as f64) - 1023.5); + sy = sy + ((NG_CODE[i] as f64) - 1023.5); + i = i + 1; + } + const mx : f64 = sx / (N_CONV as f64); + const my : f64 = sy / (N_CONV as f64); + var sxx : f64 = 0.0; + var sxy : f64 = 0.0; + i = 0; + while (i < N_CONV) { + const dx : f64 = (BASE_CODE[i] as f64) - 1023.5 - mx; + const dy : f64 = (NG_CODE[i] as f64) - 1023.5 - my; + sxx = sxx + dx * dx; + sxy = sxy + dx * dy; + i = i + 1; + } + const a : f64 = sxy / sxx; + const b : f64 = my - a * mx; + assert(a > 1.019); + assert(a < 1.021); + assert(b > 4.5); + assert(b < 4.7); + var line : f64 = 0.0; + i = 0; + while (i < N_CONV) { + const e : f64 = fabs64((NG_CODE[i] as f64) - 1023.5 - (a * ((BASE_CODE[i] as f64) - 1023.5) + b)); + if (e > line) { line = e; } + i = i + 1; + } + assert(line < 0.62); + assert(line > 0.59); + // a miss that is not a rounding: the first wrong decision of the model is 0.11 mV .. 21.4 mV from its threshold, and 24 of the 31 are + // more than 2 mV (1.4 LSB) away + var far : u32 = 0; + i = 0; + while (i < N_CONV) { + const f : Miss = follow(&k0, q_sm(S_VP[i]), q_sm(S_VN[i]), 0, NG_CODE[i]); + assert(f.first >= 3); + assert(f.first <= 11); + if (f.margin > 0.002) { far = far + 1; } + assert(f.margin < 0.0215); + i = i + 1; + } + assert_eq(far, 24); +} + +test with_the_top_plate_parasitic_and_the_comparator_offset_30_of_31_conversions_match_and_the_miss_is_a_fraction_of_a_millivolt { + const kf : [20]u32 = model_k(&UNITS, 1.0, CP_FIT); + const off : u32 = q_sm(OFF_FIT_V); + assert_eq(match_count(&kf, off, &S_VP, &S_VN), 30); + var i : usize = 0; + var misses : u32 = 0; + while (i < N_CONV) { + const c : u32 = convert_sar(&kf, q_sm(S_VP[i]), q_sm(S_VN[i]), off); + assert_eq(c, FIT_CODE[i]); + if (c != NG_CODE[i]) { + misses = misses + 1; + // off by one code, and the first wrong decision is the last or one of the last: a margin of a fraction of a millivolt + assert(c == NG_CODE[i] + 1 || c + 1 == NG_CODE[i]); + const f : Miss = follow(&kf, q_sm(S_VP[i]), q_sm(S_VN[i]), off, NG_CODE[i]); + assert(f.first >= 8); + assert(f.margin < 0.0003); + assert_eq(f.nviol, 1); + } + i = i + 1; + } + assert_eq(misses, 1); + const mm : Margins = miss_margins(&kf, off, &S_VP, &S_VN, 2); + assert(mm.worst < 0.0003); + assert(mm.worst > 0.0001); + const r : Range = err_range(&kf, off, &S_VP, &S_VN); + assert_eq(r.lo, 0.0 - 1.0); + assert_eq(r.hi, 0.0); + // one LSB of the 11-bit code is VDD / 1023 = 1.466 mV: the misses are within 0.15 of it + assert(mm.worst < 0.15 * VDD_V / 1023.0); +} + +test the_two_numbers_are_the_optimum_of_a_stated_grid_and_a_narrow_one { + const f : Fit = fit_grid(2, &S_VP, &S_VN); + assert fabs64(f.cp - CP_FIT) < 0.000000001; + assert fabs64(f.off - OFF_FIT_V) < 0.000000001; + assert_eq(f.hits, 30); + // the grid is wide enough: the optimum is inside it, not on an edge + assert(CP_FIT > FIT_CP0 + 3.0 * FIT_DCP); + assert(CP_FIT < FIT_CP0 + ((FIT_NA - 4) as f64) * FIT_DCP); + assert(OFF_FIT_V > FIT_OFF0 + 3.0 * FIT_DOFF); + assert(OFF_FIT_V < FIT_OFF0 + ((FIT_NB - 4) as f64) * FIT_DOFF); + // narrow: 0.02 units (2.5 fF) of Cp or 1 mV of offset costs matches, and 0.63 units is 77 fF + const kp : [20]u32 = model_k(&UNITS, 1.0, CP_FIT + 0.02); + const km : [20]u32 = model_k(&UNITS, 1.0, CP_FIT - 0.02); + const kf : [20]u32 = model_k(&UNITS, 1.0, CP_FIT); + assert_eq(match_count(&kp, q_sm(OFF_FIT_V), &S_VP, &S_VN), 20); + assert_eq(match_count(&km, q_sm(OFF_FIT_V), &S_VP, &S_VN), 23); + assert_eq(match_count(&kf, q_sm(OFF_FIT_V + 0.001), &S_VP, &S_VN), 6); + assert_eq(match_count(&kf, q_sm(OFF_FIT_V - 0.001), &S_VP, &S_VN), 11); +} + +test the_fit_made_on_one_half_of_the_conversions_predicts_the_other_half { + // fit on the even conversions (16), score the odd ones (15) that the fit never saw, and the other way round + const fe : Fit = fit_grid(0, &S_VP, &S_VN); + const ke : [20]u32 = model_k(&UNITS, 1.0, fe.cp); + assert_eq(fe.hits, 16); + assert_eq(match_count_p(&ke, q_sm(fe.off), &S_VP, &S_VN, 1), 14); + const fo : Fit = fit_grid(1, &S_VP, &S_VN); + const ko : [20]u32 = model_k(&UNITS, 1.0, fo.cp); + assert_eq(fo.hits, 15); + assert_eq(match_count_p(&ko, q_sm(fo.off), &S_VP, &S_VN, 0), 15); + // the two halves find the same two numbers to within 0.005 units (0.6 fF) and 0.3 mV + assert fabs64(fe.cp - fo.cp) < 0.005; + assert fabs64(fe.off - fo.off) < 0.0003; +} + +test an_ideal_sampling_instant_hides_the_switch_and_loses_the_match { + // the SAME converter on the deck's input at the end of the clks fall (84 ns) instead of the held top plates: the switch's + // tracking lag and injection are 1 .. 3 mV, one to two LSB of 1.47 mV + const kf : [20]u32 = model_k(&UNITS, 1.0, CP_FIT); + const k0 : [20]u32 = model_k(&UNITS, 1.0, 0.0); + assert_eq(match_count(&kf, q_sm(OFF_FIT_V), &S_IP, &S_IN), 4); + assert_eq(match_count(&k0, 0, &S_IP, &S_IN), 1); + var k : usize = 1; + var worst : f64 = 0.0; + while (k < N_CONV) { + const dp : f64 = fabs64(S_VP[k] - S_IP[k]); + const dn : f64 = fabs64(S_VN[k] - S_IN[k]); + if (dp > worst) { worst = dp; } + if (dn > worst) { worst = dn; } + k = k + 1; + } + // the held plates differ from the input at 84 ns by at most 3.7 mV from conversion 1 on, by more than an 11-bit LSB (1.47 mV); the first + // conversion, which starts from 0 V, by 12.5 mV + assert(worst > 0.003); + assert(worst < 0.004); + assert(fabs64(S_VN[0] - S_IN[0]) > 0.01); + assert(worst > VDD_V / 1023.0); +} + +test one_changed_block_breaks_the_match { + const off : u32 = q_sm(OFF_FIT_V); + // the bridge capacitor of 2 units instead of 1 + const kb : [20]u32 = model_k(&UNITS, 2.0, CP_FIT); + assert_eq(match_count(&kb, off, &S_VP, &S_VN), 0); + // Bit_8 with 3 units instead of 4 + var bad : [10]u32 = UNITS; + bad[7] = 3; + const ku : [20]u32 = model_k(&bad, 1.0, CP_FIT); + assert_eq(match_count(&ku, off, &S_VP, &S_VN), 0); + // no parasitic + const k0 : [20]u32 = model_k(&UNITS, 1.0, 0.0); + assert_eq(match_count(&k0, off, &S_VP, &S_VN), 2); + // no offset, and the offset of the wrong sign + const kf : [20]u32 = model_k(&UNITS, 1.0, CP_FIT); + assert_eq(match_count(&kf, 0, &S_VP, &S_VN), 0); + assert_eq(match_count(&kf, q_sm(0.0 - OFF_FIT_V), &S_VP, &S_VN), 0); + // every one of these is far below the fitted 30 + assert(match_count(&kf, off, &S_VP, &S_VN) > match_count(&kb, off, &S_VP, &S_VN) + 20); + // the bridge of 2 units also fails the ideal sweep that the 1-unit bridge passes: M4a's seal catches it by the stored words, + // here the sequence's own bin-centre test sees it + var wrong_bins : u32 = 0; + var c : u32 = 0; + while (c < CODES11) { + const dv : f64 = (((c as f64) - 1023.5) * VDD_V) / 1023.0; + const kb0 : [20]u32 = model_k(&UNITS, 2.0, 0.0); + if (convert_sar(&kb0, q_sm(VCM_V + dv / 2.0), q_sm(VCM_V - dv / 2.0), 0) != c) { wrong_bins = wrong_bins + 1; } + c = c + 128; + } + assert(wrong_bins >= 8); +} diff --git a/specs/analog/sar_cdac.t27 b/specs/analog/sar_cdac.t27 index 37b827f692..5b4bf4fc93 100644 --- a/specs/analog/sar_cdac.t27 +++ b/specs/analog/sar_cdac.t27 @@ -480,14 +480,16 @@ pub fn side_sum(units: []const u32, side: []const u8, which: u8) -> f64 { return s; } -/// The ratios by the matrix route: C = [[CL + Cb, -Cb], [-Cb, CM + Cb]] (dummy units to ground join CL), C_b the column of -/// the plate's units on its own node, K = C^-1 C_b by the adjugate. Unit capacitance cancels. Layout as K_Q. -pub fn derive_k(units: []const u32, side: []const u8, bridge: f64, dummy: f64) -> [20]f64 { +/// The ratios by the matrix route: C = [[CL + Cb, -Cb], [-Cb, CM + Cb + Ct]] (dummy units to ground join CL, Ct = `top`, a parasitic +/// from the top plate Vout to ground, joins the Vout corner), C_b the column of the plate's units on its own node, K = C^-1 C_b by +/// the adjugate. Unit capacitance cancels. Layout as K_Q. M4e (#10818) added `top`: the converter of the golden deck loads the top +/// plate with more than the DAC (the 4 fF capacitors, the comparator input, the switch), which scales every Vout ratio. +pub fn derive_k_top(units: []const u32, side: []const u8, bridge: f64, dummy: f64, top: f64) -> [20]f64 { var k : [20]f64 = [0.0] ** 20; const cl : f64 = side_sum(units, side, SIDE_L) + dummy; const cm : f64 = side_sum(units, side, SIDE_M); const c00 : f64 = cl + bridge; - const c11 : f64 = cm + bridge; + const c11 : f64 = cm + top + bridge; const c01 : f64 = 0.0 - bridge; const det : f64 = c00 * c11 - c01 * c01; var j : usize = 0; @@ -503,6 +505,11 @@ pub fn derive_k(units: []const u32, side: []const u8, bridge: f64, dummy: f64) - return k; } +/// The deck's DAC with no parasitic on the top plate: derive_k_top with top = 0. +pub fn derive_k(units: []const u32, side: []const u8, bridge: f64, dummy: f64) -> [20]f64 { + return derive_k_top(units, side, bridge, dummy, 0.0); +} + /// The same ratios by the closed form of the split array: with D = CL CM + Cb (CL + CM), a plate of u units on net1 moves /// net1 by u (CM + Cb) / D and Vout by u Cb / D; a plate on Vout moves net1 by u Cb / D and Vout by u (CL + Cb) / D. pub fn closed_k(units: []const u32, side: []const u8, bridge: f64, dummy: f64) -> [20]f64 { @@ -1024,3 +1031,38 @@ test the_textbook_bridge_is_ideal_only_with_a_dummy_unit_and_the_deck_has_none { assert worst_inl(&kk).err < 0.000000001; assert worst_gain(&kk).err > 0.49999; } + +test a_parasitic_on_the_top_plate_scales_every_vout_ratio_and_keeps_the_staircase_straight { + // Ct from Vout to ground joins the Vout corner of C: D' = CL (CM + Ct) + Cb (CL + CM + Ct) = 1023 + 32 Ct for the deck, so a + // Vout ratio is 2^j / (1023 + 32 Ct): still binary (no bend), the whole staircase scaled by 1023 / (1023 + 32 Ct) (M4e, #10818) + const k0 : [20]f64 = derive_k_top(&UNITS, &SIDE, 1.0, 0.0, 0.0); + const kd : [20]f64 = derive_k(&UNITS, &SIDE, 1.0, 0.0); + var i : usize = 0; + while (i < 20) { + assert fabs64(k0[i] - kd[i]) < 0.000000000000000001; + i = i + 1; + } + const ct : f64 = 0.63; + const kt : [20]f64 = derive_k_top(&UNITS, &SIDE, 1.0, 0.0, ct); + const den : f64 = 1023.0 + 32.0 * ct; + const one : u32 = 1; + var j : usize = 0; + while (j < PLATES) { + const pw : f64 = (one << (j as u32)) as f64; + assert fabs64(kt[2 * j + 1] - pw / den) < 0.000000000000001; + // net1: a plate of its own array moves it 2^j (32 + Ct) / D', a plate of the other array 2^(j - 5) / D' + if (j < 5) { + assert fabs64(kt[2 * j] - pw * (32.0 + ct) / den) < 0.000000000000001; + } else { + assert fabs64(kt[2 * j] - pw / 32.0 / den) < 0.000000000000001; + } + j = j + 1; + } + assert worst_inl(&kt).err < 0.000000001; + // the span from code 0 to code 1023 shrinks from VDD to VDD 1023 / (1023 + 32 Ct) + const span : f64 = node_volts(&kt, 1, 1023, 0.0) - node_volts(&kt, 1, 0, 0.0); + assert fabs64(span - VDD_V * 1023.0 / den) < 0.00000000000001; + // a parasitic on the top plate is not a dummy unit: the dummy on net1 detunes the bridge and bends the staircase, Ct does not + const kdm : [20]f64 = derive_k_top(&UNITS, &SIDE, 1.0, 0.63, 0.0); + assert worst_inl(&kdm).err > 0.1; +}