13 KiB
TIMDAC Theory of Operation
TIMDAC is not a PWM DAC, where a waveform is generated whose average value equals the desired output, and then filtered all the way down to DC. This method is often proposed, but is not really feasible at high precision — to achieve 1 LSB of filtered ripple, at 15 bits with a 36 MHz counting frequency, a first-order filter will settle in about a minute. (A basic second-order filter will settle in about a second, which is much much better, but still very limiting.) Furthermore, some of the same measures TIMDAC uses to mitigate other problems at high precision, for example the constant-impedance reference chopper, are still necessary to get these specs.
Instead of PWM, TIMDAC uses an integrating sample-and-hold topology. A single pulse is integrated, producing a ramp from 0 up to the product of the pulse width and the reference slope. At the end of the pulse, the final value is sampled and held in an output capacitor. TIMDAC can update in as little as 1.3ms, with an "ideally zero" ripple limited only by analog switch performance and layout quality.
Hardware
Reference chopper
Analog switch U1B
implements a constant-impedance reference chopper. By
sending the voltage reference either into the 0V virtual ground of the
integrator or directly into ground, it ensures that the load on the voltage
reference is stable — the AC peak-to-peak component is equal to the integrator
offset voltage divided by the slope resistor, or about 12 nA on 30 µA. This
stability ensures that the voltage reference output impedance and load
regulation error do not come into play, which provides a few LSBs improvement
to INL with typical voltage references.
Integrator and ramp inverter
Op amp U2A
is connected as an inverting integrator, so when a positive-going
pulse is applied to it, a negative-going ramp is produced. Because the reference
chopper fully disconnects the reference during the space period rather than
applying 0V, U2A
's input offset voltage is not integrated into an error term;
only the leakage current is integrated. This will eventually add up to an
error, so the discharge switch is held engaged whenever possible, and because
U2
is a low-leakage JFET op amp, the resulting error is negligible.
Analog switch U1C
discharges the integrating capacitor through R4
when
switched on; R4
is also shared with the sample-and-hold circuit as the
charging resistor for negative ramps.
Because there are few options for noninverting integrators that provide the
speed and precision needed, a second op amp U2B
inverts the inverted ramp,
producing a positive-going ramp. The resistor ratio on this inverter will
induce a gain error; the tuning comparator senses the positive ramp in order
to prioritize this most likely polarity (TODO, #3).
Tuning comparator
Comparator U5
detects whether the positive-going ramp has crossed the
reference voltage. R15
adds hysteresis, and D1
blocks the hysteresis on
the leading edge so the switching threshold is as accurate as possible
(TODO rev 1.1 uses an RC circuit instead).
The pulldown resistor R17 reduces the output voltage from 5V to 3.3V. As per the note in the schematic, it can be removed if R16 pulls up to the system IO voltage.
Polarity switch and output multiplexer
Analog switch U1A
selects between the positive and negative ramps; it may
be omitted for non-bipolar designs. The selected ramp is steered to the
output capacitors through multiplexer U3
, which is inhibited until the ramp
has stabilized.
Sample-and-hold amplifiers
The output capacitor bank holds the output voltages in between pulse transfers.
To stabilize the output voltage, they are buffered by high-impedance op amps
U4A
etc. Charging current is limited by resistor R4
(for negative ramps)
or R5
(for positive ramps); these are placed on the pre-multiplexer side to
reduce part count.
Firmware
Layer 1: low-level DAC driver
The low-level driver steps the external hardware through its various states. It is responsible for discharging the integrator, generating a pulse to be integrated into a ramp, waiting for the integrator to stabilize, and finally, either transferring the final ramp value to the output or using it for tuning.
┌────┐
│IDLE│ ◄───────────────────────────────┐
└┬───┘ │
│ │
│emit or tune │
▼ │
┌─────────┐ │
│DISCHARGE│ │
└┬────────┘ │
│ │
│ │
┌┴────┐ │
│PULSE│ │
└┬────┘ │
│ │
▼ │
┌─────────┐tune ┌────────────────┐ │
│STABILIZE├─────►│Evaluate tuning ├────┘
└┬────────┘ └────────────────┘ ▲
│emit │
▼ │
┌────────┐ │
│TRANSFER├─────────────────────────────┘
└────────┘
Layer 2: tuning driver
The tuning driver, which is also part of timdac_ll.c
, exists as an
intermediate layer between the low-level and high-level driver. It is triggered
by the high-level driver, and itself triggers the emission of one pulse from
the low-level driver. The pulse it emits, and the comparator output at the
end of that pulse, is used as the comparison in a binary-search successive
approximation. Each time it runs, the range is narrowed by half, until it has
been narrowed down to a single value. Once this happens, the resulting
measurement is pushed into a low-pass filter, which smooths the tuning over
time to reduce noise, and the output of this filter provides the tuning
scale factor.
┌────┐
│Idle│ ◄──────────────────────────────────────────┐
└┬───┘ │
│timdac_ll_tune() │
▼ │
┌──────────────┐ no ┌──────────────────┐ │
│top/bot valid?├────────┤Reinit to 65535, 0├──────┘
└┬─────────────┘ └──────────────────┘ ▲
│yes │
▼ │
┌────────────────┐ no ┌──────────────────────┐ │
│top/bot unequal?├─────►│Tuning is complete ├──┘
└┬───────────────┘ │Push top/bot into the │ ▲
│yes │tuning low-pass filter│ │
│ └──────────────────────┘ │
▼ │
┌──────────────────────────────────────┐ │
│Start a pulse with width = (top+bot)/2│ │
└┬─────────────────────────────────────┘ │
│ │
▼ │
┌─────────────────────────────────┐ │
│Read the tuning comparator output│ │
└┬───────────────┬────────────────┘ │
│ramp<vref │ramp>vref │
│ │ │
▼ ▼ │
┌─────────┐ ┌─────────┐ │
│bot = mid│ │top = mid│ │
└┬────────┘ └┬────────┘ │
│ │ │
└─────────────► └────────────────────────────────┘
Layer 3: scan (high-level) driver
The high-level driver sequences all of the above continuously through its various states. It is triggered by the timer stop interrupt, and always starts the timer, so it will continue forever (TODO, #1).
If the first tuning cycle has not yet completed, it will always initiate the
next phase, so that outputs are never scanned prior to a tuning. It will also
initiate a scan periodically, as defined by TIMDAC_TUNE_INTERVAL
. If there
is no tuning needed, it will next check to see if a channel is in the
queue-jump pending slot, and if there is, this channel is emitted next. When
this happens, the queue-jump slot is "blanked" for one cycle so that rarely
updated channels always get scanned. If none of these less common scenarios
occur, the next channel in a simple round-robin scan sequence is emitted.
┌─────────────────────┐ ┌──────────────────────┐
│Step low-level driver│◄───────────────────┤Wait for timer to stop│
└┬────────────────────┘ └──────────────────────┘
│ ▲
▼ │
┌──────────────────────┐ no ┌─────────────┐ │
│First tuning complete?├─────►│Initiate tune├───────────────────┘
└┬─────────────────────┘ └─────────────┘ ▲
│yes ▲ │
▼ │ │
┌────────────────────────┐ yes │ │
│Reached tuning interval?├───────────┘ │
└┬───────────────────────┘ │
│no │
▼ │
yes ┌────────────────────────┐ │
┌───┤Queue-jump slot blanked?│ │
│ └┬───────────────────────┘ │
│ │no │
│ ▼ │
│ ┌───────────────────────────┐ yes ┌─────────────────┐ │
│ │Channel in queue-jump slot?├─────►│Emit queue jumper│ │
│ └┬──────────────────────────┘ └┬────────────────┘ │
│ │no │ │
│ │ ▼ │
│ │ ┌─────────────────────┐ │
│ │ │Blank queue-jump slot├──────┘
│ ▼ └─────────────────────┘ ▲
│ ┌─────────────────┐ │
└──►│Emit next channel├─────────────────────────────────────────────┘
└─────────────────┘