A 12-bit converter with capacitor mismatch, an analog trim array on the top capacitors, and a calibration routine that finds its own trim codes. Then the digital block that corrects offset and gain, checked against the model that specifies it at every input code.
Ask for the files, read them, run them, try to break them.
It needs Python and an open-source Verilog simulator, Icarus Verilog or Verilator. No licensed tool is required to run anything on this page.
Every figure on this page is reproduced by running the example's code, and the command that reproduces it is given beside it.
At 12 bits with a 3 % unit-capacitor sigma, the top capacitor's error is about 1.4 LSB rms. A part like that misses a half-LSB linearity spec badly, and no amount of digital cleverness recovers the missing charge. The array itself has to be trimmed.
The calibration needs no external reference and no tester. For a binary-weighted array the ideal condition at every bit is that the capacitor equals the sum of all the lower capacitors plus the terminator, and the amount by which that fails is the linearity step at that transition. So the converter measures its own error with its own comparator, and trims until the residue is nulled.
Result on the default part, with six capacitors trimmed: peak DNL 1.435 to 0.112 LSB. FAIL to PASS.
MEASURED python sar_adc_trim_demo.py --trim-caps 6. The script's own default trims three capacitors, which nulls the three trimmed transitions and leaves the peak at an untrimmed one; six is where the whole part passes.
The question is how deep the trim has to go, and what that costs in area.
That is a yield curve, and it takes one sweep.
| capacitors trimmed | mean peak DNL | worst | yield at 0.5 LSB |
|---|---|---|---|
| 0 | 1.97 | 5.79 | 2 % |
| 3 | 0.71 | 2.08 | 30 % |
| 5 | 0.37 | 1.08 | 78 % |
| 6 | 0.25 | 0.65 | 97 % |
| 7 | 0.17 | 0.49 | 100 % |
MEASURED 150 parts per point, 1,350 simulated parts, nine depths, a few seconds. python sar_adc_trim_demo.py --sweep 150
What matters about the table is that asking the question cost nothing. When a sweep is free, you run the sweep instead of picking a number and defending it later.
Adding trim capacitance moves a threshold. Which direction depends on wiring and on the sign convention in the trim control logic. If those two disagree, calibration drives every code the wrong way, and it completes normally, reports plausible trim codes, and hands you a worse part than you started with.
So the routine does not assume the polarity. It applies a known trim step, measures which direction reduces the residue, and takes the answer from the measurement.
yield at 0.5 LSB DNL with the polarity check in place
mean peak DNL 0.24 LSB
yield with the check bypassed
mean peak DNL 4.79 LSB
MEASURED 200 Monte Carlo parts each, six capacitors trimmed. Both runs complete without an error. python sar_adc_trim_demo.py --trim-caps 6 --mc 200, then the same with --break-sign.
Run it yourself with --break-sign and watch calibration succeed at making the part worse.
The offset and gain correction is fixed-point arithmetic in SystemVerilog. It is specified by a model, so the model is the oracle: every input code goes through both, and they must agree exactly.
Then the negative control. Switch the RTL to truncate where the model rounds:
Half the transfer function is wrong. The maximum error is a single count. Nothing about it would look wrong on any plot you would normally draw. This is the failure the practice exists to catch, and an exhaustive comparison that takes a second to run catches it.
MEASURED check_rtl.py against adc_offset_gain_corr.sv, exit code 1 on any mismatch.
The golden model ships as both a Python function and a MATLAB function, verified arithmetically identical over the full input space.
If your team has MATLAB, MATLAB is the target: your scripts, your conventions, your plots. If you would rather the flow needed no seat, Python is the target. Mixed teams are normal and fine.
One honest note for teams weighing Octave as a free substitute: base scripting and plotting port with modest effort, but Simulink and Fixed-Point Designer have no Octave equivalent, so anything built on fi() objects or a .slx model does not travel. Better to know that now than in week two.
| Runs on your laptop | Runs on your tools |
|---|---|
| Converter model, mismatch, trim, yield sweeps (Python) | Verilog-A and Verilog-AMS views |
| Digital correction RTL (SystemVerilog) | OCEAN campaign scripts |
| Exhaustive RTL simulation (Icarus Verilog, Verilator) | SKILL generation |
| Real-number behavioural view | Transistor-level benches on your PDK |
| Plots and reports |
The right-hand column is the half that is tool-bound by nature, and it is what the on-premises deployment exists to produce. For that half the deliverable is a generator plus a checker that validates the output without running the tool: every function verified against an API whitelist built from your installed release, argument counts checked against real usage. A script that fails the check never reaches your simulator.
Code that passes the check calls no hallucinated functions, which is the failure that bites in practice. It can still be wrong in other ways.
One archive, sent by email on request.
sar_adc_trim_demo.py, adc_offset_gain_corr.sv, tb_adc_corr.sv, check_rtl.py, adc_corr_golden.m, selftest_matlab_twin.py, run_all.sh
Every artefact here is paired with something mechanical, an exhaustive comparison, a known-answer probe, an API whitelist, because generated code is assumed wrong until a check says otherwise.
Book a scoping conversation