Integral and Differential Nonlinearity (INL/DNL)
Contents
- What are Integral and Differential Nonlinearity?
- How do we measure it?
- Test Methodology
- Results
- Why does the DNL Spike?
- Conclusion
- References
What are Integral and Differential Nonlinearity?
In an ideal ADC, a range of input voltages produces the same digital output value. In ideal ADCs, the size of the range of voltages that produces the same output value is precisely the same for every step. In real ADCs, this is not the case.
Differential nonlinearity is the deviation of each step from the ideal ADC step size. If the DNL for every step is less than one, then there are no missing outputs; if the width of a step is greater than one, that means that can mean that other outputs never appear.
DNL is calculated as:
\[\mathrm{DNL}(\mathrm{i})=\frac{V_{\text {out }}(i+1)-V_{\text {out }}(i)}{\text { ideal LSB step width }}-1\]Integral nonlinearity is the deviation of a particular output code from the ideal. For example, if your 12 Bit ADC has an INL of 10 LSBs, you know that the output it reports is always within $10/2^{12} = 0.25\%$ of the “true” output code.
How do we measure it?
INL and DNL can be measured by applying a full-scale sine wave to the input and recording a lot of points. We can calculate the number of points required with the formula below, where $N$ is the number of ADC bits, $Z$ is the two sided Z-score for the confidence level we want (2.576 for 99%), and $\beta$ is the DNL resolution.
\[M=\frac{\pi 2^{N-1} Z^{2}}{\beta^{2}}\]In the testing, 10 Million points were collected, giving 0.07 LSB accuracy at a 99% confidence level (0.05LSBs at 95%)
DNL Calculation
We can calculate the DNL by comparing the number of times we saw a particular output to the number of times we expected to see it; if we saw more hits than we expected, then that output code is wider than it should be. Or more input voltages correspond to that output than we expect. Vice-versa, if it is smaller than we expect, then it has fewer voltages than we expect to map to that voltage.
The probability of a particular code being hit for a sine wave is below. Where $N$ is the number of bits in the ADC, $n$ is the code, the ADC input is $\pm V_\mathrm{FS}$, and the sine wave has an amplitude of $A$:
\[p(n)=\frac{1}{\pi}\left[\sin ^{-1}\left[\frac{V_{\mathrm{FS}}\left(n-2^{N-1}\right)}{A \times 2^{N}}\right]-\sin ^{-1}\left[\frac{V_{\mathrm{FS}}\left(n-1-2^{N-1}\right)}{A \times 2^{N}}\right]\right]\]We can calculate the DNL as, with the below equation, where we caculate the expected number of hits for that code based on the total number of samples collected times the probability of that code:
\[\mathrm{DNL}(n)=\frac{\text{# of hits}_\text{Actual}}{\text{Total # of hits} \times p(n)}-1\]INL calculation
The INL is the integral of the DNL; it can be calculated as:
\[\mathrm{INL}(n)=\sum_{i=0}^{n} \mathrm{DNL}(i)\]The problem you can run into is the INL measurement error is much higher than the DNL measurement error. The errors in DNL measurement are (approximately) normally distributed, so as you integrate the DNL, the error will grow with the root sum of squares of the accuracy above. The accuracy at the highest code is:
\[\text{INL Accuracy} = \sqrt{2^N \times M^2}\]We can calculate the INL accuracy for this test as:
\[\text{INL Accuracy} = \sqrt{2^{12} \times {0.07 \text{ LSB}}^2} = 4.48 \text{ LSB 99% confidence}\] \[\text{INL Accuracy} = 1.09 \text{ LSB 50% confidence}\]Test Methodology
I generated a 1kHz sine wave with a Rigol DG4000 function generator. I used the following code to sample the ADC at its full rate (500ksps) then dump the samples out the USB serial interface:
// Code modified from https://github.com/raspberrypi/pico-examples/blob/master/adc/adc_console/adc_console.c
#include <stdio.h>
#include <inttypes.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"
#include "hardware/dma.h"
#define N_SAMPLES 100000
uint16_t sample_buf[N_SAMPLES];
int main() {
stdio_init_all();
// DMA setup:
uint dma_chan = dma_claim_unused_channel(true);
dma_channel_config cfg = dma_channel_get_default_config(dma_chan);
channel_config_set_transfer_data_size(&cfg, DMA_SIZE_16);
channel_config_set_read_increment(&cfg, false);
channel_config_set_write_increment(&cfg, true);
channel_config_set_dreq(&cfg, DREQ_ADC);
adc_gpio_init(26);
adc_init();
adc_select_input(0);
adc_set_temp_sensor_enabled(false);
adc_fifo_setup(true, true, 1, false, false);
// Set SMPS_MODE Pin
gpio_init(23);
gpio_set_dir(23, GPIO_OUT);
gpio_put(23, 0); // 0 for PSM; 1 for PWM
// Capture samples then ship them out over USB
while (1) {
adc_fifo_drain();
dma_channel_configure(dma_chan, &cfg, sample_buf, &adc_hw->fifo, N_SAMPLES,true);
adc_run(true);
dma_channel_wait_for_finish_blocking(dma_chan);
adc_run(false);
printf("Done\n");
for (int i = 0; i < N_SAMPLES; i = i + 1) {
printf("%03x\n", sample_buf[i]);
}
}
return 0;
}
Results
Analysis code
import matplotlib.pyplot as plt
from scipy.optimize import minimize
import numpy as np
def sin_pdf(bits, amp, shift):
vfs = 1
pdf = list()
for n in range(2**bits - 1):
n += shift
res = (1/np.pi)*(np.arcsin((vfs*(n-2**(bits-1)))/(amp*2**bits))-np.arcsin((vfs*(n-1-2**(bits-1)))/(amp*2**bits)))
pdf.append(res)
return pdf
if __name__ == "__main__":
import json
dat = np.load("your-datafile.npz") # data in a 1d array called 'data_v'
hist, bin_edges = np.histogram(dat['data_v'], bins = list(range(2**12)), density=True)
bin_edges = bin_edges[:-1]
# Caculate best fit params
minf = lambda inp: np.sum(np.nan_to_num((np.asarray(hist) - sin_pdf(12, inp[0], inp[1]))**2))
res = minimize(minf, (0.5, 0))
# Plot Histogram
fig = plt.figure()
bars = plt.bar(bin_edges, hist/np.sum(hist), width=1, linewidth=1, color='blue', alpha=0.5, label="ADC Data")
plt.title("Sine Histogram Actual vs. Fitted")
plt.xlabel("ADC code")
plt.ylabel("Code probability")
# Set bar edges so you can see them at low zoom
for bar in bars:
bar.set_edgecolor("blue")
bar.set_linewidth(1)
bars = plt.bar(range(2**12-1), sin_pdf(12, res.x[0], res.x[1]), width=1, color='red', alpha=0.5, label="Fitted Data")
for bar in bars:
bar.set_edgecolor("red")
bar.set_linewidth(1)
plt.tight_layout()
plt.show()
fig = plt.figure()
plt.plot(range(2**12-1), (np.asarray(hist) / sin_pdf(12, res.x[0], res.x[1]))-1, color='red')
plt.title("ADC DNL")
plt.ylabel("DNL (LSBs)")
plt.xlabel("ADC code")
plt.show()
fig = plt.figure()
plt.plot(range(2**12-1), np.cumsum(np.nan_to_num((np.asarray(hist) / sin_pdf(12, res.x[0], res.x[1]))-1, nan=0.0, posinf=0.0, neginf=0.0)), color='red')
plt.title("ADC INL")
plt.ylabel("INL (LSBs)")
plt.xlabel("ADC code")
plt.show()