How a voltage becomes numbers.
On this page
An analog-to-digital converter (ADC) measures the voltage held at its input once per sample, at a fixed rate, and outputs each measurement as a whole number, a code. Three choices shape it: the voltage range its codes cover, how many codes it has, and how fast it samples. Range and code count set the smallest change it can see and the noise its rounding adds; how fast to sample is Aliasing and Anti-Aliasing Filters.
Every ADC has a reference voltage, Vref, that sets the top and bottom of what it can measure. The ADC on this page has Vref = 1.2 V and is differential: it measures one input minus the other, which can come out negative. So it reads from −1.2 V to +1.2 V, a span of 2.4 V. Anything beyond saturates, and the output flatlines at its top or bottom code.
Quantization rounds the voltage to the nearest code: a ramp goes in, a staircase comes out, and each stair step is one LSB (Least Significant Bit), the voltage width of one step. The resolution, B bits, sets how many steps fit in the range; together with Vref it sets the smallest detectable change.
Bit depth decides how fine the stairs are, and that sets your quantization noise floor (the level of rounding noise sitting under the signal), measured as the signal-to-noise ratio (SNR).
The sampling rate has its own failure, aliasing, and its own fix, the anti-alias filter in front of the ADC. Both live on Aliasing and Anti-Aliasing Filters.
Figure 2. A 1 V sine and its ADC output. 1The ±1.2 V rails, where it saturates.
In volts, one LSB is the full span divided by the number of codes. At ±1.2 V and 10 bits that is 2.34 mV per code. A wider Vref stretches each step; more bits shrink every step.
Figure 3. 1One LSB, the same width at every bit depth. 2A 100 µV spike to scale.
At 10 bits a 100 µV neural spike fills under a twentieth of one step, so the ADC cannot see it.
One conversion shifts, scales, rounds and clips a voltage to give its code.
Figure 4. A toy 3-bit ADC over ±1.2 V reading 0.42 V. 1The sample.
Two voltages converted to codes in NumPy; 1.5 V is past the rail and clips to the top code.
import numpy as np
V_REF, B = 1.2, 10 # V_ref and B from the cards
lsb = 2 * V_REF / 2**B
v_in = np.array([0.42, 1.5]) # 1.5 V is past the rail
code = np.round((v_in + V_REF) / lsb)
code = np.clip(code, 0, 2**B - 1)
v_level = -V_REF + code * lsb
error = v_in - v_level # e in the card
print(code, error)
Rounding to the nearest code leaves a quantization error of at most half an LSB either way, even on a perfect noiseless signal. On a slow ramp the error is a sawtooth from −½ LSB to +½ LSB that resets at every step crossing.
On a real signal those sawtooth cycles fall out of phase and blur into noise with zero average, every value in the range about equally likely. That is a uniform random variable, so its variance (how spread out the errors are) is the LSB squared over twelve.
Over this page’s ±1.2 V range, the 3-bit converter of Figure 4 steps 0.3 V and a 16-bit one 36.6 µV; the card gives their typical rounding errors, about 0.087 V and 10.6 µV.
A check of the LSB²/12 rule on 100,000 random voltages at 8 bits, drawn below the top level so none saturates. The errors stay within about ±0.5 LSB, and the variance ratio printed last comes out near 1.0.
import numpy as np
V_REF, B, N_SAMPLES = 1.2, 8, 100_000
lsb = 2 * V_REF / 2**B
rng = np.random.default_rng(0)
v_in = rng.uniform(-V_REF, V_REF - lsb, N_SAMPLES)
code = np.round((v_in + V_REF) / lsb)
error = v_in - (-V_REF + code * lsb)
print(error.min() / lsb, error.max() / lsb)
print(error.var() / (lsb**2 / 12))
Figure 5. 1Steps on the true ramp. 2Rounding error. 3How often each occurs.
The signal-to-noise ratio (SNR) compares signal power to noise power, in decibels.
| Noise source | Noise power (V2) | Ratio | SNR |
|---|---|---|---|
| Quiet bedroom | 10−5 | 1,000 | 30 dB |
| Fan nearby | 10−3 | 10 | 10 dB |
| 10-bit rounding alone | 4.6 × 10−7 | 21,800 | 43.4 dB |
In both rooms the room’s noise, not the converter’s rounding, sets the SNR.
For a full-scale sine wave (one that swings across the whole range the ADC can measure), that ratio grows by about 6 dB for every bit.
A sine half a step inside full scale, quantized at 10 bits. The SNR is the mean signal power over the mean error power, and it prints about 62 dB, as 6.02 B + 1.76 predicts.
import numpy as np
V_REF, B, N_SAMPLES = 1.2, 10, 100_000
FREQ = 0.01234 # cycles per sample
lsb = 2 * V_REF / 2**B
amp = V_REF - lsb / 2
sig = amp * np.sin(2 * np.pi * FREQ * np.arange(N_SAMPLES))
code = np.clip(np.round((sig + V_REF) / lsb), 0, 2**B - 1)
quant = -V_REF + code * lsb
p_signal = np.mean(sig**2)
p_noise = np.mean((sig - quant)**2)
print(10 * np.log10(p_signal / p_noise))
Figure 7. 1Each bit adds a 6.02 dB block. 2A full-scale sine adds 1.76 dB.
Neural signals are tens to hundreds of times smaller than the ADC's 2.34 mV step, so wired straight in they read as zero. The amplifier multiplies the electrode voltage by a constant gain. At a gain of 250 one code covers 9.375 µV at the electrode, but the input range that fits between the ±1.2 V rails shrinks by the same factor, so the signal clips sooner.
The gain card in numbers. Dividing by GAIN turns the 2.34 mV ADC step into 9.375 µV at the electrode and the ±1.2 V rails into a ±4.8 mV input range.
V_REF, B, GAIN = 1.2, 10, 250
lsb_adc = 2 * V_REF / 2**B
lsb_electrode = lsb_adc / GAIN
in_range = V_REF / GAIN # electrode side, volts
print(lsb_adc, lsb_electrode, in_range)
Figure 8. A 1 mV spike at gains 1 to 2,000. 1The input range. 2The amplified spike.
At the ADC input the range stays ±1.2 V whatever the gain. Adding bits splits it into finer steps; only a smaller reference voltage shrinks the range itself.
Flash compares the voltage against every level at once, with one comparator (a circuit that reports which of two voltages is larger) per level. It is the fastest, but it needs 2B − 1 comparators, so each extra bit doubles them; it stays at low bit depths and uses the most power.
SAR (successive-approximation register) finds one bit per comparison with the middle of the remaining range, so three comparisons cover eight levels. It sits in the middle, with medium speed and resolution and low power.
Sigma-delta samples far faster than it needs to and averages the noise away: slow, but extremely fine. A feedback loop keeps a running sum (sigma) of the difference (delta) between its coarse output and the input, which turns the rounding noise into fast wiggles from one sample to the next; averaging many samples smooths those wiggles away. Sampling far faster than needed is called oversampling; Aliasing and Anti-Aliasing Filters puts it to a second use.
The SAR search as a function, run on the 3-bit sample of Section 2.3. Each pass of the loop asks one higher-or-lower question and appends one bit, so sar(0.42) returns 5, binary 101.
def sar(v_in, v_ref=1.2, B=3):
lsb = 2 * v_ref / 2**B
lo, hi = -v_ref - lsb / 2, v_ref - lsb / 2 # half-step shift
code = 0
for _ in range(B):
mid = (lo + hi) / 2
bit = int(v_in >= mid)
code = 2 * code + bit
lo, hi = (mid, hi) if bit else (lo, mid)
return code
print(sar(0.42))
Audio takes the same path as the electrode in Figure 1, with a microphone in its place. Software never sees volts, only the integer codes the converter wrote, usually inside a WAV file.
| Step | How often, on what data | State it keeps | Cost |
|---|---|---|---|
| Gain stage | set once at design time, on the electrode voltage | none | one amplifier |
| Flash ADC | once per sample, on the held voltage | none | one comparator per level, all in one step |
| SAR ADC | once per sample, one clock cycle per bit, on the held voltage | the bits found so far | one comparison per bit |
| Sigma-delta ADC | many 1-bit samples per output, on the input voltage | the running sum, and the averaging filter that keeps one output in every R samples | one output for every R one-bit samples, where R, the oversampling ratio, is how many times faster it samples than it outputs |
Rates, state and cost come from the sources in References.
A one-second tone stored as 16-bit codes and read back with SciPy. wavfile.read returns the rate and the int16 codes, and the last line turns them into floats between −1 and 1.
import numpy as np
from scipy.io import wavfile
FS, TONE_HZ, FULL_SCALE = 16000, 440, 32768
t = np.arange(FS) / FS
tone = 0.5 * np.sin(2 * np.pi * TONE_HZ * t)
codes = np.round(tone * (FULL_SCALE - 1)).astype(np.int16)
wavfile.write("tone.wav", FS, codes)
rate, read_back = wavfile.read("tone.wav")
print(rate, read_back.dtype) # 16000 int16
signal_float = read_back.astype(np.float64) / FULL_SCALE
| Setting | Default | Source |
|---|---|---|
| 16-bit PCM WAV | int16 codes, −32768 to +32767 | scipy.io.wavfile.read |
| 24-bit PCM WAV | returned as int32, shifted to the top bits | scipy.io.wavfile.read |
| Best SNR at 16 bits | about 98 dB | the SNR card in Section 2.5 |
| Knob, where it enters | Raise it | Lower it | Default |
|---|---|---|---|
| Sample rate fs, the time step 1/fs between samples | Higher frequencies fit below fs/2; more data per second | Less data; anything above fs/2 folds down, so below twice the highest frequency present the signal aliases | 16 kHz for speech (torchaudio kaldi.mfcc) |
| Bits B, in codes = 2B | Each extra bit halves the step and adds about 6 dB of SNR; 16 bits give about 98 dB | Coarser steps, louder quantization noise; B = 1 keeps only the sign | 16-bit PCM (scipy.io.wavfile.read) |
| Gain, multiplies the electrode voltage before the ADC | Finer steps at the electrode, LSB/gain; the input range ±Vref/gain shrinks until normal signals saturate | Wider input range, coarser steps; at a gain of 1 one step is 2.34 mV and a 100 µV spike reads as zero | None; pick it so the largest expected input stays inside ±Vref/gain (this page uses 250) |
SciPy documents the reader at scipy.io.wavfile.read.
| What you see | Likely cause | What to do |
|---|---|---|
| Flat tops on the peaks | The input went past ±Vref and saturated | Lower the gain or widen Vref |
| The signal looks blocky, in visible steps | The LSB is too big for the signal | More gain or more bits |
| A hiss floor you cannot lower | Quantization noise, LSB2/12 | More bits, 6.02 dB each, or more gain |
| Tones at frequencies you never had | Aliasing | See Aliasing and Anti-Aliasing Filters |
What goes wrong when sampling is too slow? That is the next page, Aliasing and Anti-Aliasing Filters.