Lab 6.4 — Noise Floor / PSD

Course 2 syllabus · Module 6 · Prev: « Lab 6.3 · Next: Lab 6.5 »

Goal

Measure how quiet your acquisition chain actually is. You’ll characterize the STM32 ADC’s noise floor, estimate the power spectral density (PSD) of the noise with Welch’s method (averaged, windowed periodograms) to beat down the variance of a single FFT, and from the measured noise convert to real figures of merit: SNR and ENOB (effective number of bits). This is the measurement that separates a spec sheet from reality — a 12-bit ADC almost never gives 12 real bits — and it’s the skill that lets you say, with numbers, “this is the smallest signal my system can see.” Every converter bring-up, sensor front-end, and RF/audio design ends here.

Equipment & parts

  • STM32 Nucleo-64 (NUCLEO-L476RG) with the Module 5 timer-triggered ADC + DMA project.
  • A grounded ADC input (a jumper from the analog pin to a mid-rail bias, or to ground through a known source impedance) for the noise-floor run; and the MCP4725 DAC to inject a known small tone for the SNR run.
  • Siglent SDS1104X-E scope to sanity-check the input and to compare its own FFT noise floor.
  • Host with numpy/scipy (scipy.signal.welch) and pyserial to collect long records and estimate the PSD off-board, plus an optional on-chip Welch implementation.

Safety & don’t-break-it

  • The input still must sit inside 0–3.3 V. For a true “grounded input” noise measurement, don’t literally tie the ADC pin to 0 V if your front-end op-amp is single-supply — bias it to mid-rail (~1.65 V) through a low impedance so the noise you measure is the converter’s, not a rail-clip artifact.
  • Source impedance matters. A high-impedance input pin picks up mains hum and thermal noise — for the converter-noise measurement, drive the pin from a low impedance (a buffer output or a small resistor to a stiff bias). Otherwise you’ll measure your breadboard’s antenna behavior, not the ADC.
  • Grounding and shielding. Keep leads short, share a single ground, and expect a 50/60 Hz line and its harmonics to show up — identify them so you don’t mistake pickup for converter noise.
  • No component-damage hazard here; the risk is measuring the wrong noise source and drawing a wrong conclusion.

Background

The power spectral density \(S_{xx}(f)\) describes how a random signal’s power is distributed over frequency; for a wide-sense-stationary process it is the Fourier transform of the autocorrelation (Wiener–Khinchin),

\[ S_{xx}(f) = \sum_{m=-\infty}^{\infty} r_{xx}[m]\, e^{-j 2\pi f m / f_s}, \qquad r_{xx}[m] = \mathbb{E}\{x[n]\,x[n+m]\}. \]

Total power is the integral of the PSD, \(\mathbb{E}\{x^2\} = \int S_{xx}(f)\,df\).

The periodogram and its variance problem. The natural estimate from one length-\(N\) record is the periodogram \(\hat{S}(f_k) = \frac{1}{N}|X[k]|^2\). It is asymptotically unbiased but its variance does not decrease with \(N\) — a single periodogram is a noisy estimate no matter how long the record; it just gets more (equally noisy) frequency points. That’s why a raw FFT of noise looks like grass.

Welch’s method. Split the record into \(K\) segments (optionally overlapping ~50%), apply a window \(w[n]\) to each, take the periodogram of each, and average them:

\[ \hat{S}_\text{Welch}(f_k) = \frac{1}{K}\sum_{i=1}^{K} \frac{1}{U N_\text{seg}}\,\big|X_i[k]\big|^2, \qquad U = \frac{1}{N_\text{seg}}\sum_{n} w[n]^2, \]

where \(U\) is the window’s power normalization. Averaging \(K\) approximately independent periodograms reduces the estimator variance by roughly a factor \(K\) (Course 1 Weeks 5–6 — the variance of a mean of \(K\) i.i.d. estimates is \(1/K\) of the single-sample variance):

\[ \operatorname{Var}\big[\hat{S}_\text{Welch}\big] \approx \frac{1}{K}\operatorname{Var}\big[\hat{S}_\text{periodogram}\big]. \]

So the noise floor estimate gets smoother (its standard deviation drops as \(1/\sqrt{K}\)), at the cost of coarser frequency resolution (shorter segments) — the fundamental resolution/variance trade of spectrum estimation. Overlap recovers some of the lost effective averaging.

From noise to SNR and ENOB. With a full-scale sinusoid of amplitude \(A\) (peak) into a converter, its power is \(A^2/2\). Integrate the measured PSD over all bins except the signal bin (and its harmonics, if measuring SINAD) to get the noise+distortion power \(P_n\). Then

\[ \text{SNR} = 10\log_{10}\frac{P_\text{signal}}{P_\text{noise}}\ \text{[dB]}, \]

and the effective number of bits follows from the ideal-quantizer SNR formula \(\text{SNR}_\text{ideal} = 6.02\,B + 1.76\) dB, inverted for the measured SNR:

\[ \boxed{\ \text{ENOB} = \frac{\text{SNR}_\text{measured} - 1.76}{6.02}\ } \]

A 12-bit ADC has an ideal SNR of \(6.02\cdot12 + 1.76 = 74\) dB, i.e. 12.0 ENOB — but thermal noise, reference noise, clock jitter, and layout typically knock the real number down to ~10–11 ENOB. That gap is exactly what this lab quantifies.

Quantization-noise density. For a full-scale range \(V_\text{FS}\) and \(B\) bits, one LSB is \(q = V_\text{FS}/2^B\) and the ideal quantization noise power is \(q^2/12\), spread over DC–\(f_s/2\). So the theoretical noise floor per bin drops as you increase \(N\) (finer bins = less noise per bin) — “FFT processing gain,” \(10\log_{10}(N/2)\) dB — which is why a bin’s noise can sit far below the total-noise LSB level. Don’t confuse per-bin floor with total noise; both are meaningful, and Welch reports the density.

Procedure

Part A — Grab a grounded-input noise record.

  1. Bias the ADC input to mid-rail from a low impedance and take no deliberate signal. Sample at \(f_s = 16\) kHz and stream a long record (e.g. \(2^{18}\) samples) to the host over the VCP.
  2. On the host, first look at the raw histogram of ADC codes: its spread (in LSBs) is the time-domain noise. A stuck single-code output means too little noise to even dither (rare); a spread of a few codes is normal.

Part B — Estimate the PSD with Welch.

  1. Run Welch on the record:

    import numpy as np, scipy.signal as sig
    f, Pxx = sig.welch(x_lsb, fs=16000, window="hann",
                       nperseg=1024, noverlap=512, scaling="density")
    # 10*log10(Pxx) vs f  -> the smoothed noise-floor PSD
  2. Sweep the number of segments \(K\) (by changing record length or nperseg) and watch the noise floor estimate smooth out — plot the estimator’s ripple vs. \(K\) and confirm the \(1/\sqrt{K}\) shrinkage of its standard deviation.

  3. Identify and label the mains 50/60 Hz line and its harmonics, plus any switching-supply spurs — these are pickup, not broadband converter noise.

Part C — SNR and ENOB with a known tone.

  1. Play a near-full-scale, bin-centered tone from the DAC into the ADC (e.g. 1 kHz, amplitude ~90% of range to avoid clipping). Take a windowed FFT.
  2. Compute signal power from the signal bin (+ its window main-lobe bins), and noise power by summing all other bins (exclude DC and, for SINAD, the harmonic bins). Form SNR, then ENOB via the boxed formula.
  3. Compare grounded-input (noise-only) vs. tone-present spectra on the same axes — the floor should be unchanged; only the signal bin rises.

Part D — On-chip Welch (optional real-time version).

  1. Implement the averaging on the STM32: accumulate \(|X[k]|^2\) across \(K\) successive windowed FFT blocks (reuse the Lab 6.3 FFT path), divide by \(K\), and stream the averaged PSD. This is the real-time embedded spectrum-monitor form.

Deliverable & expected results

Capture: the grounded-input Welch PSD (dB vs. Hz) with mains lines labeled, the same estimate at \(K=1\) vs. \(K=64\) showing the variance reduction, and the tone-present spectrum used for SNR/ENOB. Log the code histogram width, the measured SNR, and the derived ENOB.

Quantity Predicted Measured
Ideal 12-bit SNR \(6.02\cdot12+1.76 = 74.0\) dB
Ideal ENOB 12.0 bits
Realistic measured ENOB (STM32 12-bit ADC) ~10–11 bits
Welch variance reduction, \(K=64\) vs \(K=1\) ×64 (std ↓ ×8)
FFT processing gain, \(N=1024\) \(10\log_{10}(512)=27\) dB
Grounded-input noise, time domain a few LSB RMS
Mains line at 50/60 Hz present (pickup)

Analysis & reconciliation

First reconcile the time-domain and frequency-domain noise: the RMS of the code histogram (in volts) should equal the square root of the integrated PSD over DC–Nyquist — if they disagree, the window power normalization \(U\) or the density scaling is wrong. Then confront the ENOB gap: your measured SNR will fall short of 74 dB, and the deficit is real converter non-idealities (thermal + reference noise, sampling jitter, INL/DNL, and any pickup you failed to shield). Attribute as much as you can — mains lines and switching spurs are not broadband noise and shouldn’t be counted against ENOB if you’re reporting SNR (but they do count in SINAD). Confirm the Welch estimate’s ripple shrinks as \(1/\sqrt{K}\) exactly as Course 1 Weeks 5–6 predict for the variance of an average — this is the law of large numbers you can see on a plot. Finally, note that the per-bin noise floor sits well below the total-noise LSB level by the processing gain \(10\log_{10}(N/2)\); this is why the Lab 6.1 FIR’s deep stopband was measurable in principle but limited in practice — the converter noise floor is the real bottom of every measurement in this module.

Going further

  • Compare the scope’s FFT noise floor to the STM32’s on the same grounded input — the 8-bit scope ADC is noisier; you’ll see it directly.
  • Add a low-noise buffer (the Lab 4.1 MCP6002 follower) in front of the ADC and re-measure ENOB — a good source impedance often buys a fraction of a bit.
  • Try Bartlett (non-overlapping, no window) vs. Welch (windowed, overlapped) and confirm Welch’s lower variance for the same record length.
  • Deliberately add dither (a small pseudo-random signal) and observe the linearization of the quantizer’s DNL in the averaged spectrum — a classic technique that trades a touch of noise for lower spurious tones.
  • Vary the sample rate and oversample, then decimate: confirm the in-band noise drops by \(10\log_{10}(\text{OSR})\) dB (oversampling gain), the principle behind delta-sigma converters like the ADS1115.
  • Carry the measured noise variance \(\sigma_v^2\) forward: it is exactly the measurement-noise parameter \(R\) for the real-time Kalman filter in Lab 6.6 — a measured, not guessed, filter parameter.