Lab 5.4 — Aliasing Lab

Course 2 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 49) 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.

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.
  • 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.