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.
Recommended reading
Lyons Ch. 2 — the sampling theorem and aliasing: the impulse-train sampler and spectral replication. Course 1 Lesson 49 proves it from the Dirac comb.
Lyons Ch. 2 — the practitioner’s picture of sampling and aliasing (the folding diagram; sampling of real signals).
Course 1 Lesson 49 — distributions and the Dirac comb: uniform sampling is multiplication by \(\operatorname{\text{Ш}}_{T_s}(t) = \sum_n \delta(t - nT_s)\), whose transform is itself a comb, so the spectrum is replicated at every multiple of \(f_s\). Aliasing is those replicas overlapping — the rigorous reason for the folding you’ll measure.
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)
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)
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)
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.
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).
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).
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.
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.
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.
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}\).
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.