Lab 5.4 — Aliasing Lab

Course 3 syllabus · Module 5 · Prev: « Lab 5.3 · Next: Lab 6.1 »

Goal

See aliasing happen — not as a theorem, but as a real tone that lands at the wrong frequency in your captured data. Using the timer-triggered, DMA-fed acquisition from Lab 5.3, you sample a DAC-generated sine at a fixed \(f_s\) while sweeping the sine’s frequency above and below the Nyquist limit \(f_s/2\). Below Nyquist the measured frequency tracks the source; above it, the tone folds back and masquerades as a lower frequency. Then you insert the Lab 4.4 active low-pass filter ahead of the ADC as an anti-alias filter and watch the folded tones disappear before they can be sampled. This is the single most important practical lesson in sampling: what you don’t filter out before the ADC, you can never separate afterward.

Equipment & parts

  • STM32 NUCLEO-L476RG running the Lab 5.3 acquisition firmware.
  • MCP4725 DAC (3.3 V powered) generating the test sine (Lab 3.3); for source tones above ~1 kHz the STM32 on-chip DAC (timer + DMA driven) gives a cleaner high-frequency sine — either is a legitimate “DAC-generated” source.
  • The Lab 4.4 active low-pass filter (MCP6002) as the anti-alias stage.
  • Siglent SDS1104X-E (optional cross-check of the true source frequency and its FFT).
  • Host Python (numpy/matplotlib) to FFT the captured buffer.

Wiring & bench setup

The signal chain: the DAC’s DC-biased sine feeds A0 either directly (Part B, the aliasing runs) or through the Lab 4.4 active LPF (Part C, the anti-alias runs); the Siglent taps whatever node feeds A0 throughout.

flowchart LR
  DAC["Sine source<br/>MCP4725 OUT or<br/>STM32 DAC A2 (PA4)<br/>biased at 1.65 V"]
  LPF["Lab 4.4 active LPF<br/>MCP6002, fc ≈ 1 kHz"]
  ADC["NUCLEO A0 (PA0)<br/>Lab 5.3 acquisition<br/>fs = 2 kHz"]
  SCOPE["Siglent CH1<br/>10x probe"]
  DAC -- "Part B: direct" --> ADC
  DAC -- "Part C" --> LPF
  LPF --> ADC
  ADC -. "probe the A0 node<br/>ground clip → − rail" .-> SCOPE

flowchart LR
  DAC["Sine source<br/>MCP4725 OUT or<br/>STM32 DAC A2 (PA4)<br/>biased at 1.65 V"]
  LPF["Lab 4.4 active LPF<br/>MCP6002, fc ≈ 1 kHz"]
  ADC["NUCLEO A0 (PA0)<br/>Lab 5.3 acquisition<br/>fs = 2 kHz"]
  SCOPE["Siglent CH1<br/>10x probe"]
  DAC -- "Part B: direct" --> ADC
  DAC -- "Part C" --> LPF
  LPF --> ADC
  ADC -. "probe the A0 node<br/>ground clip → − rail" .-> SCOPE

Pin map (every wire):

From To Pin/jack
MCP4725 VDD / GND / SCL / SDA (if it is the source) unchanged from Lab 5.1 3V3 / GND / D15 (PB8) / D14 (PB9)
Sine source output Part B: ADC input direct · Part C: filter input MCP4725 OUT, or A2 (PA4 = DAC1_OUT1) for the on-chip DAC
MCP6002 VDD (pin 8) breadboard + rail (3.3 V), 100 nF to − rail at the pin 3V3
MCP6002 VSS (pin 4) breadboard − rail GND
Filter output (MCP6002 OUTA, pin 1) ADC input (Part C only) A0 (PA0)
Siglent CH1 probe tip (probe and channel menu at 10×) the node feeding A0
Siglent probe ground clip breadboard − rail
 sine source              Lab 4.4 active LPF               NUCLEO
 (MCP4725 OUT             ┌───────────────┐
  or A2/PA4) ●──Part B────┼───────────────┼──────────►● A0/PA0 (ADC1_IN5)
             │            │   MCP6002     │           ▲
             └──Part C───►● in        out ●───────────┘
                          │ (fc ≈ 1 kHz)  │    Siglent CH1 (10×) on the
                          └───────────────┘    A0 node; clip → − rail

Setup gotchas: keep the MCP6002’s 100 nF decoupling at pin 8; verify the sine’s min/max on the scope before landing the A0 jumper (Safety below); when switching Part B ↔︎ Part C, move the one jumper at the A0 end so the source and scope tap stay put.

Safety & don’t-break-it

  • The sine must stay 0 – 3.3 V into the ADC. A sine has to be DC-biased to sit inside the ADC’s unipolar range — center it at ~1.65 V with a swing that never goes below 0 V or above 3.3 V (e.g. 1.65 V ± 1.0 V). Verify the min/max on the scope before wiring to PA0; a sine that dips negative or clips the 3.3 V rail damages the pin and distorts the spectrum with a different nonlinearity you’ll mistake for aliasing.
  • Keep the MCP4725 (and the MCP6002 filter) on the 3.3 V rail so nothing downstream can exceed VREF+.
  • Common ground across DAC, filter, ADC, and scope.

Project & environment setup

Firmware — reuse firmware/m5-daq/ exactly as Lab 5.3 left it (TIM2-triggered ADC1 + circular DMA); only the timer retargets for this lab’s rate:

CubeMX page Setting
Timers → TIM2 Prescaler = 79, Counter Period (ARR) = 499\(f_s = \dfrac{80\times10^6}{80 \times 500} = 2\ \text{kHz}\), the \(f_N = 1\) kHz grid of this lab (Procedure step 1)
Analog → ADC1 + DMA unchanged from Lab 5.3 — TIM2 TRGO trigger, circular half-word DMA, DMA1 channel 1 NVIC
Analog → DAC1 only if the on-chip DAC is your sine source: OUT1 enabled (PA4 = A2), driven by a second timer’s TRGO + DMA from a sine table — set that timer from your chosen tone frequency
Connectivity → USART2 115200 8-N-1 — the buffer-dump path to the host (Procedure step 1)
Clock Configuration 80 MHz HCLK per the setup essentials

Host — the FFT half of the lab runs in the course venv (see Toolchain):

source venv/bin/activate        # numpy + matplotlib + pyserial
mkdir -p labs/lab-5-4/host labs/lab-5-4/captures

Two small scripts, both yours to write: labs/lab-5-4/host/capture.py (pyserial — read one dumped buffer from /dev/tty.usbmodem* at 115200 and save it as labs/lab-5-4/captures/tone-<f>hz.csv; ~15 lines) and labs/lab-5-4/host/fft_plot.py (numpy for the FFT and peak-bin readout, matplotlib for the spectrum plot).

Keep this lab’s reconciliation in labs/lab-5-4/host/analysis.ipynb — the notebook convention — and export final figures next to it.

Where results go:

Artifact Path
Bench note (alias table, bin width, AAF attenuation) labs/lab-5-4/notes.md
Captured buffers, one per source tone labs/lab-5-4/captures/tone-<f>hz.csv (append -aaf for Part C runs)
FFT plots labs/lab-5-4/host/fft-<f>hz.png
(Optional) Siglent cross-check of the true source frequency labs/lab-5-4/captures/source-check.csv

Background

Uniform sampling at rate \(f_s\) replicates the signal’s spectrum at every integer multiple of \(f_s\). The Nyquist frequency is

\[f_N = \frac{f_s}{2},\]

and a real sinusoid is represented unambiguously only if its frequency \(f < f_N\). A pure tone at \(f > f_N\) is indistinguishable, after sampling, from a tone at the aliased frequency

\[f_\text{alias} = \bigl|\,f - k f_s\,\bigr|,\qquad k = \operatorname{round}\!\left(\frac{f}{f_s}\right),\]

which always lands in the baseband \([0, f_N]\). Geometrically the frequency axis folds at multiples of \(f_N\): as \(f\) climbs past \(f_N\), the apparent frequency walks back down toward 0, then back up, and so on.

The Dirac-comb view (Course 1 Lesson 35) makes the mechanism exact. Sampling multiplies \(x(t)\) by \(\operatorname{\text{Ш}}_{T_s}(t)=\sum_n \delta(t-nT_s)\); in frequency this convolves \(X(f)\) with a comb of spacing \(f_s\):

\[X_s(f) = f_s \sum_{k=-\infty}^{\infty} X(f - k f_s).\]

If \(X\) has energy above \(f_N\), the shifted copies overlap and add — the high tone’s replica falls into the baseband as \(f_\text{alias}\), and no post-processing can pull it back out. The only cure is to remove that energy before sampling: an anti-alias low-pass filter with its passband below \(f_N\). The Lab 4.4 active LPF is exactly that filter.

Theory — Multirate Systems: Decimation, Interpolation, and Polyphase

Real systems rarely agree on one sample rate: CD audio lives at \(44.1\) kHz, professional audio and USB interfaces at \(48\) kHz, and every conversion between them must happen digitally, without returning to analog. The ratio is the awkward rational number \(48/44.1 = 160/147\), so “resampling” cannot mean “keep every few samples.” This section builds the machinery: changing sample rate by integers (\(M\) down, \(L\) up), by rationals \(L/M\), doing it cheaply (polyphase) — and then, in Lab 3.4’s theory section, running the same ideas in reverse order inside modern AD/DA converters (oversampling and sigma–delta), where they buy resolution instead of rate.

Decimation: filter, then downsample

Downsampling by integer \(M\) keeps every \(M\)th sample, \(x_{\text{new}}[m] = x_{\text{old}}[Mm]\), so \(f_{s,\text{new}} = f_{s,\text{old}}/M\). By the sampling picture above, the spectrum’s periodic replications are now only \(f_{s,\text{new}}\) apart: unless the signal band \(B\) satisfies \(f_{s,\text{new}} > 2B\), the replications overlap and alias. Decimation is therefore the two-step recipe lowpass filter first, then downsample. To preserve a band of width \(B'\), the filter (a lowpass of the kind designed in Lab 6.1) must have its stopband start no later than

\[ f_{\text{stop}} = f_{s,\text{new}} - B', \]

so that spectral folding lands nothing inside the kept band. Three facts to internalize: downsampling is not time-invariant (shift the input by one sample and a different subsequence emerges); it changes no time-domain amplitudes; but DFT magnitudes drop by \(M\) (fewer samples in the transform). For large \(M\), two-stage decimation \(M = M_1 M_2\) with the biggest factor first slashes filter cost — Lyons’s \(M = 100\) example drops from about \(2727\) taps in one stage to about \(250\) in two (\(M_1 = 25\), \(M_2 = 4\)); a passband-ripple budget \(R\) is split \(R/2\) per cascaded filter.

Interpolation: upsample, then filter

Interpolation by integer \(L\) is the dual recipe: upsample by inserting \(L-1\) zeros between samples (“zero stuffing”), then lowpass filter. Upsampling raises the rate to \(L f_{s,\text{old}}\) and exposes the old spectral replications as images centered at multiples of \(f_{s,\text{old}}\); the interpolation filter’s whole job is to remove them, so the interpolation’s fidelity is the filter’s stopband attenuation. Two standing facts: zero stuffing loses a factor \(L\) of amplitude, so the filter needs DC gain \(L\) (equivalently \(\sum_k h[k] = L\)); and repeating samples instead of stuffing zeros rolls the passband off by \(\sin(x)/x\) — avoid it.

Rational conversion by \(L/M\)

Cascade interpolate-by-\(L\) then decimate-by-\(M\), sharing one combined lowpass that serves both as anti-image and anti-alias filter; its passband must end at \(\min\bigl(f_{s,\text{old}}/2,\ (f_{s,\text{old}}/2)(L/M)\bigr)\) (in radian terms, cutoff \(\min(\pi/L, \pi/M)\) at the high internal rate). The motivating example: CD \(44.1\) kHz to DAT \(48\) kHz is \(L/M = 160/147\); to \(96\) kHz it is \(320/147\).

The polyphase idea

The naive cascades are wasteful twice over: zero stuffing makes most filter products zero, and downsampling discards most filter outputs. Polyphase decomposition eliminates both wastes. Split an \(N\)-tap prototype \(h[k]\) by index residue mod \(Q\):

\[ H(z) = H_0(z^Q) + z^{-1} H_1(z^Q) + \cdots + z^{-(Q-1)} H_{Q-1}(z^Q), \]

giving \(Q\) subfilters of \(N/Q\) taps each. The noble identities — downsampling commutes with \(H(z^Q) \to H(z)\), and dually for upsampling — move the rate change to the cheap side of the subfilters, and a commutating switch replaces the delay-and-resample scaffolding. The payoff: for decimation by \(M\), the switch deals \(M\) consecutive inputs to the \(M\) subfilters, whose summed outputs give one result — nothing computed is discarded and all arithmetic runs at the low rate, \(1/M\) the multiplies per unit time; for interpolation by \(L\), each input fires the \(L\) subfilters in rotation with no multiply-by-zero. Rational \(L/M\) resampling reduces to a modulo-\(L\) counter selecting which subfilter (coefficient set) produces each output. The structure is stated here without the block-diagram derivation.

NoteConnection

DSP and this course. DSP’s multirate weeks are this section; the labs put real silicon behind it. The converters on this course’s bench — the MCP4725 DAC and ADS1115 ADC of Labs 3.2–3.5 and the STM32’s on-chip ADC — are oversampling converters: the ADS1115 is a delta-sigma part whose data-rate register trades \(L\) for effective bits exactly per the \(3/9/15\) dB ladder of Lab 3.4’s theory section, and the STM32’s hardware-oversampling registers literally implement \(\downarrow L\) accumulation. The anti-alias and reconstruction filters flanking them are Lab 4.4’s Sallen–Key stages — gentle analog filters that oversampling makes sufficient — and this lab’s aliasing validation is what happens when the decimation recipe’s “filter first” step is skipped.

Worked by hand

P1 (decimation design). A \(48\) kHz stream must drop to \(8\) kHz keeping the band \(B' = 3.2\) kHz. Then \(M = 6\); the filter passes \(0\)\(3.2\) kHz and its stopband must start by \(f_{\text{stop}} = 8 - 3.2 = 4.8\) kHz; after decimation the spectral replications sit \(8\) kHz apart.

P2 (polyphase bookkeeping). Decompose a 9-tap \(h[k]\) for decimation by \(3\): \(H_0 = \{h_0, h_3, h_6\}\), \(H_1 = \{h_1, h_4, h_7\}\), \(H_2 = \{h_2, h_5, h_8\}\). Naive filter-then-downsample runs \(9\) multiplies per input sample (\(27\) per surviving output); polyphase runs \(3\) subfilters of \(3\) taps once per output: \(9\) multiplies, all at the low rate — the promised factor-\(M\) saving.

Theory exercises

Theory exercise 1 [Hand] — (Lyons 10.1, 10.8.) (a) To decimate \(x[n]\) by four, should the lowpass filter act before or after discarding samples? Give the ideal filter’s cutoff in Hz (in terms of the input rate \(f_s\)) and in radians/sample, and its required DC gain. (b) Answer the same three questions for interpolation by three: does the zero-stuffing happen before or after the filter, where is the cutoff, and what DC gain avoids amplitude loss?

Theory exercise 2 [Hand] — (Lyons 10.14.) A CD signal at \(f_{s,\text{CD}} = 44.1\) kHz must become a DAT signal at \(48\) kHz. If we interpolate by \(L = 160\), by what factor \(M\) must we decimate to land exactly on \(48\) kHz?

Procedure

Part A — Set up the sampler and a sweepable source.

  1. Run the Lab 5.3 firmware with a fixed, known \(f_s\) — pick \(f_s = \mathbf{2\ kHz}\) (TIM2 PSC = 79 / ARR = 499 per Project & environment setup) so \(f_N = 1\ \text{kHz}\) sits in a range the DAC can straddle. Have process_block() copy a full buffer to the host over the VCP (or capture into RAM and dump once).
  2. Configure the DAC to output a DC-biased sine at 1.65 V center. Confirm on the scope that it never leaves 0 – 3.3 V.

Part B — Sweep through Nyquist (no anti-alias filter).

  1. For each source frequency \(f\) in a sweep that crosses \(f_N\) — e.g. 300, 800, 1200, 1700, 2100 Hz — capture one buffer and FFT it on the host. Record the peak bin = the measured frequency.
  2. Below \(f_N\) (300, 800 Hz) the peak sits at the true \(f\). At and above \(f_N\) (1200, 1700, 2100 Hz) the peak appears at \(f_\text{alias}=|f-kf_s|\) — a lower frequency than the source. Cross-check the true source frequency independently on the Siglent so you can prove the ADC is lying, not the DAC.
  3. Note the tell-tale ambiguity: the 1200 Hz source and an 800 Hz source produce the same peak bin — they are aliases of each other.

Part C — Add the anti-alias filter.

  1. Insert the Lab 4.4 active LPF between the DAC output and the ADC input (per Wiring & bench setup), with its cutoff \(f_c\) set at (or just below) \(f_N = 1\ \text{kHz}\).
  2. Repeat the sweep. Now the sub-Nyquist tones pass through and read correctly, but as \(f\) climbs above \(f_c\) the source amplitude is rolled off before the ADC — the aliased peak’s amplitude collapses (by the filter’s stopband attenuation) instead of appearing full-strength at the wrong frequency. Record the aliased-peak amplitude with and without the filter.

Deliverable & expected results

Host FFT plots for two or three source frequencies, and the alias table below. Predicted \(f_\text{alias}\) uses \(f_s = 2\ \text{kHz}\), \(f_N = 1\ \text{kHz}\).

Source \(f\) \(k=\operatorname{round}(f/f_s)\) Predicted \(f_\text{alias}=|f-kf_s|\) Measured peak (no AAF) Alias amplitude (with AAF)
300 Hz 0 300 Hz (no alias)
800 Hz 0 800 Hz (no alias)
1200 Hz 1 800 Hz … (attenuated)
1700 Hz 1 300 Hz … (attenuated)
2100 Hz 1 100 Hz … (attenuated)

Analysis & reconciliation

For each source tone compute \(k=\operatorname{round}(f/f_s)\) and \(f_\text{alias}=|f-kf_s|\) by hand and confirm the FFT peak lands there (to within the FFT bin width \(\Delta f = f_s/N\) — report that resolution). The clean result is that 1200 Hz and 800 Hz are indistinguishable in the un-filtered captures: same bin, and if you overlay them, essentially the same samples. With the anti-alias filter in place, the sub-Nyquist tones are unchanged but the super-Nyquist tones’ peaks drop by the filter’s attenuation at that frequency — quantify it against the Lab 4.4 Bode rolloff (a first-order stage gives ~ −20 dB/decade past \(f_c\), so the gap between filtered and unfiltered alias amplitude should match the filter’s magnitude at \(f\)). If a residual aliased tone survives, it’s because a single-pole filter isn’t steep enough between \(f_c\) and \(f_s - f_c\) — the real-world reason anti-alias filters are higher-order, and the motivation for both sharper analog filters and oversampling.

NoteJetson/Pi note

Aliasing is a property of sampling, not of the STM32 — but demonstrating it needs a converter whose \(f_s\) you control around audio rates, which the SBCs lack on-chip (the ADS1115 tops out at 860 SPS, and a USB audio codec hides its anti-alias filter inside the silicon — you can’t remove it to watch folding happen). So the demonstration stays here; on the Jetson the same folding math reappears computationally in the Module 9 labs when you decimate or resample streams (e.g. Lab 9.3), where skipping the pre-decimation low-pass produces exactly the fold-down you measured on this bench.

Going further

  • Fold the whole axis. Sweep finely from 0 to \(2f_s\) and plot measured-vs-source frequency; you’ll trace the classic triangular folding diagram (Lyons Ch. 2) with vertices at multiples of \(f_N\).
  • Oversample instead of filter. Raise \(f_s\) to 8 kHz and repeat — the same source tones now sit below the new \(f_N\) and stop aliasing, showing the oversampling-vs-filtering trade.
  • DC-offset and even harmonics. Because the sine is biased, any DAC nonlinearity adds harmonics that also alias; identify which extra peaks are aliased harmonics vs. the fundamental’s alias.
  • Sample below Nyquist on purpose. If the signal is sparse in some basis (a few tones, a few taps), it can be recovered from far fewer samples than Nyquist demands — this is compressed sensing, the constructive counterpart of everything this lab warned about. Such, Embedded AI Ch. 6, project #8 (“Using Compressed Sensing for Efficient Sampling”) is a worked version to read for the method (on the book’s own hardware, not this bench’s); the theory is Course 1’s sparse-recovery material (Lesson 40).
  • This anti-alias front end is now mandatory for every real acquisition in Module 6 — the FIR/IIR/FFT labs assume the band above \(f_N\) has already been removed.