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; +}