timdac/doc/TOO.md

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

Small reference schematic

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├─────────────────────────────────────────────┘
    └─────────────────┘