This is the culmination of Modules 0–7: a complete, robust, real-time DSP system on the STM32, built from every skill in the course. A timer-triggered ADC + DMA front end (Module 5) feeds an FIR/FFT processing stage (Module 6), whose result drives a DAC / UART output — all running as tasks under the FreeRTOS pipeline (Lab 7.2), guarded by the watchdog and fault handling (Lab 7.1). You will fully characterize it — end-to-end latency, throughput, and CPU headroom — and then do the reconciliation that defines this whole course: an A/B comparison of the digital filter against the Module 4 analog active filter, the “analog twin,” measuring both with the same instruments and explaining every difference. The deliverable is a staff-level write-up that could stand in a design review: what was predicted, what was measured, where they diverged, and why.
Recommended reading
Kuo (all) — the whole book comes together here: real-time architecture (Ch. 1), fixed-point vs. float (Ch. 3), FIR/IIR real-time implementation (Ch. 4–5), FFT (Ch. 6), and DMA/interrupt-driven I/O. Use it as the implementation reference for the pipeline.
Lyons (all) — the practitioner’s view: FIR design and linear phase, the DFT/FFT, windowing and spectral leakage, and the number-format/quantization chapters. Your filter-design and spectrum-interpretation decisions come from here.
Course 1 — the rigorous backing. Where you justify a design choice formally, the justification comes from the theory course, not from a third DSP book: Lesson 48 (complex analysis — poles, the ROC, and the stability criterion behind the filter), Lesson 49 (distributions and the Dirac comb — the sampling theorem behind the ADC front end), Lessons 36–37 (the DFT/FFT in practice, filter design, fixed point and overflow), and Lesson 6 (least squares behind FIR design).
Equipment & parts
STM32 Nucleo-64 (NUCLEO-L476RG) + USB to ST-LINK.
Signal source: the MCP4725 DAC (Module 3) or the STM32’s own DAC/PWM generating the test stimulus — the Siglent base unit has no built-in generator, so you synthesize the input. A second Nucleo DAC channel or an external source also works.
Module 4 analog active filter (Lab 4.4) on the breadboard — the “analog twin” for the A/B comparison, built around the MCP6002.
STM32CubeMX + CLion (per the project workflow); the FreeRTOS project from Lab 7.2 + ADC/DMA from Module 5.
Wiring & bench setup
The whole course on one bench: a synthesized source drives the analog anti-alias/bias stage into the ADC, the FreeRTOS pipeline filters it, and the reconstructed DAC output comes back out — while the same source feeds the Lab 4.4 analog twin so the scope can A/B both, the Saleae times the stages, and UART metrics stream to the Mac.
flowchart LR SRC["Source: MCP4725 DAC<br/>(I2C1: D15/D14)"] AA["Anti-alias + bias<br/>MCP6002 half A (Lab 4.4)"] ADC["ADC in A0 (PA0)<br/>TIM→ADC→DMA (Lab 5.3)"] RTOS["FreeRTOS pipeline<br/>Acquire→Process→Output<br/>FIR/FFT + IWDG (Labs 7.1/7.2)"] OUT["DAC out A2 (PA4)<br/>MCP6002 half B follower (Lab 4.1)"] TWIN["Analog twin<br/>Lab 4.4 active LPF"] SCOPE["Siglent SDS1104X-E<br/>CH1 source · CH2 twin · CH3 digital"] SAL["Saleae CH0–CH2<br/>stage pins D7/D8/D6"] HOST["Mac<br/>USART2 VCP metrics log"] SRC --> AA --> ADC --> RTOS --> OUT --> SCOPE SRC --> TWIN --> SCOPE RTOS -.-> SAL RTOS --> HOST
flowchart LR
SRC["Source: MCP4725 DAC<br/>(I2C1: D15/D14)"]
AA["Anti-alias + bias<br/>MCP6002 half A (Lab 4.4)"]
ADC["ADC in A0 (PA0)<br/>TIM→ADC→DMA (Lab 5.3)"]
RTOS["FreeRTOS pipeline<br/>Acquire→Process→Output<br/>FIR/FFT + IWDG (Labs 7.1/7.2)"]
OUT["DAC out A2 (PA4)<br/>MCP6002 half B follower (Lab 4.1)"]
TWIN["Analog twin<br/>Lab 4.4 active LPF"]
SCOPE["Siglent SDS1104X-E<br/>CH1 source · CH2 twin · CH3 digital"]
SAL["Saleae CH0–CH2<br/>stage pins D7/D8/D6"]
HOST["Mac<br/>USART2 VCP metrics log"]
SRC --> AA --> ADC --> RTOS --> OUT --> SCOPE
SRC --> TWIN --> SCOPE
RTOS -.-> SAL
RTOS --> HOST
Pin map (every wire; one common ground ties the whole chain together):
From
To
Pin/jack
MCP4725 VDD / GND
breadboard + / − rails ← Nucleo
3V3 / GND
MCP4725 SCL / SDA
I2C1
D15 (PB8) / D14 (PB9)
MCP4725 OUT (source)
anti-alias input, analog-twin input, and Siglent CH1 (10× probe)
—
Anti-alias/bias output (MCP6002 half A)
ADC input
A0 (PA0)
Analog twin output (Lab 4.4 filter)
Siglent CH2 (10× probe)
—
STM32 DAC out
MCP6002 half B follower input
A2 (PA4)
Follower output
Siglent CH3 (10× probe)
—
MCP6002 VDD / VSS (each chip)
WANPTEK rail per Lab 4.4 (+ 100 nF at pin 8) / − rail
Setup gotchas: the MCP6002 is a dual op-amp — one chip hosts the ADC-side anti-alias/bias (half A) and the DAC-output follower (half B), while the analog twin is its own Lab 4.4 build (second MCP6002). Most MCP4725 breakouts carry their own I²C pull-ups (Lab 3.1 bus). Set the bias so the signal sits centered near 1.65 V inside 0–3.3 V at the ADC pin (see Safety). If you use the STM32’s own DAC as the source instead of the MCP4725, note that the second DAC channel is PA5 = D13, shared with LD2 — prefer the MCP4725 as source so PA4 stays the pipeline output. Set each 10× probe’s switch and channel menu to 10×, and set the WANPTEK rail per Lab 4.4 before powering the op-amps.
Safety & don’t-break-it
Respect the 3.3 V analog world. The STM32 ADC input must stay 0–3.3 V (V_DDA) — it is not 5 V tolerant on analog pins. Bias and scale your test signal so it sits inside 0–3.3 V before it reaches the ADC pin; a swing that clips the rail corrupts the measurement and can stress the pin. Use the Module 4 buffer/divider to set the DC bias.
Common grounds across the whole chain. Source, analog filter, Nucleo, scope, and Saleae must share one ground, or your A/B comparison measures ground loops, not filters. Clip scope grounds only to circuit ground.
Discharge and de-power for analog changes. When rewiring the MCP6002 filter, power down the WANPTEK rail; observe MCP6002 supply limits (1.8–6 V) and handle the DIP by the body (ESD).
DAC output drive is weak. The STM32/MCP4725 DAC outputs can’t drive heavy loads — buffer with the MCP6002 follower (Lab 4.1) before driving anything but a scope’s high-impedance input.
Watchdog under the debugger. As in Lab 7.1, enable “Debug IWDG stopped” so the running watchdog doesn’t reset the board while you’re halted characterizing timing. Keep a known-good image to reflash if a short timeout loops.
Nothing here is high-voltage or high-power — the hazards are all signal-integrity and part-protection.
Project & environment setup
Firmware — reuse the Module 7 project (firmware/m7-rtos/: IWDG + fault handler from Lab 7.1, FreeRTOS from Lab 7.2). This lab folds in the Module 5/6 peripherals; confirm/add in the .ioc:
CubeMX page
Setting
Analog → ADC1
IN5 (PA0 = A0), 12-bit; external trigger = the trigger timer’s TRGO (Lab 5.2/5.3 config); DMA circular with half- and full-transfer interrupts enabled in NVIC
Timers → trigger timer
TRGO on update; prescaler/ARR set from your chosen \(f_s\) (period = 80 MHz / \(f_s\) counts, e.g. 16 kHz)
Analog → DAC1
OUT1 (PA4 = A2), output buffer on — the reconstructed output
Connectivity → I2C1
100 kHz on PB8/PB9 (D15/D14) — only if the MCP4725 is your source
Middleware → FREERTOS / SYS
as set in Lab 7.2 (CMSIS_V2; HAL timebase = TIM6) — confirm
Timers → IWDG
as set in Lab 7.1 (Prescaler 32, Reload 4095; “Debug IWDG stopped”) — confirm
Two owner-written scripts under labs/lab-7-3/host/: log_metrics.py (pyserial — timestamp each UART block-metrics line into labs/lab-7-3/captures/metrics.log; ~20 lines) and response_overlay.py (numpy/matplotlib for the two measured sweeps on one plot; scipy.signal — firwin/freqz — to overlay the designed FIR response as the reference curve; ~30 lines).
Keep this lab’s reconciliation in labs/lab-7-3/host/analysis.ipynb — the notebook convention — and export final figures next to it.
Where results go:
Artifact
Path
The capstone write-up (Part E)
labs/lab-7-3/reconciliation.md
Frequency-sweep tables (analog twin / digital path)
The timer sets the sample rate \(f_s\); the ADC samples on each trigger; DMA moves samples into a circular buffer with half- and full-transfer interrupts so Process always has a complete block while Acquire fills the other half (ping-pong). The hard constraint is that a block of \(N\) samples must be fully processed before the next block completes:
\[
C_\text{process}(N) \;\le\; N \cdot T_s = \frac{N}{f_s}.
\]
CPU headroom is the fraction of the budget left over:
\[
U = \frac{C_\text{process}}{N\,T_s}, \qquad \text{headroom} = 1 - U .
\]
You want \(U\) comfortably below 1 (say \(\le 0.7\)) so jitter and interrupts never push a block past its deadline. Throughput is \(f_s\) if the per-sample tasks keep up, or \(f_s \cdot (\text{block rate})\) for block processing; latency is the acquire-edge-to-output-edge time from Lab 7.2.
The FIR filter and its analog twin
The digital stage is a linear-phase FIR:
\[
y[n] = \sum_{k=0}^{M-1} h[k]\,x[n-k],
\]
with a passband/stopband you choose to match the Module 4 analog active filter’s corner. The analog twin (Lab 4.4) is a Sallen–Key (or MFB) active low-pass with cutoff \(f_c\) and a Butterworth/Bessel response. The A/B comparison holds the specification constant (same \(f_c\), same input) and contrasts the two implementations:
Property
Analog active filter
Digital FIR
Phase
nonlinear (analog)
exactly linear (symmetric taps)
Cutoff accuracy
R/C tolerance (±5–10%)
exact (defined by coefficients)
Noise/quantization
thermal + op-amp noise
quantization + rounding noise
Aliasing
none (continuous)
needs anti-alias filter before ADC
Reconfigurability
rewire parts
change coefficients
Latency
\(\sim\) group delay of the analog network
\(\frac{M-1}{2}T_s\) (FIR group delay) + pipeline
The anti-alias point is the deep one: the digital filter cannot exist without an analog low-pass in front of the ADC (Module 4’s other job), so the honest comparison is “analog filter alone” vs. “analog anti-alias + digital filter.” Course 1 Lesson 33 is the reason the anti-alias filter is mandatory.
Procedure
Part A — Assemble the pipeline
Start from the Lab 7.2 FreeRTOS project. Replace the placeholder acquire with the Module 5 timer-triggered ADC + DMA circular buffer (Lab 5.3): TIM triggers the ADC at \(f_s\), DMA into a double buffer, half/full-transfer callbacks hand a ready block to Acquire, which posts it to \(Q_1\).
In Process, run the FIR (Lab 6.1) — use the CMSIS-DSP arm_fir_f32 (or a fixed-point variant per Kuo Ch. 3). Add an FFT path (Lab 6.3, arm_rfft_fast_f32) so the same pipeline can output a live spectrum. Keep the per-stage GPIO toggles from Lab 7.2.
In Output, write filtered samples to the DAC (reconstruct the analog output) and/or stream block metrics over UART (VCP). Buffer the DAC with the MCP6002 follower if driving anything real.
Add the Lab 7.1 watchdog: refresh it from the lowest-priority task, and keep the fault handler in place. Now a hang or fault in any stage is caught.
Part B — Characterize timing and headroom
With the Saleae on the three stage pins (Lab 7.2 method), capture per-stage execution time and end-to-end latency at a chosen \(f_s\) and block size \(N\) (e.g. \(f_s = 16\) kHz, \(N = 256\)).
Read \(C_\text{process}\) (the FIR/FFT stage width) and compute utilization\(U = C_\text{process}/(N T_s)\) and headroom\(1-U\). Cross-check with vTaskGetRunTimeStats CPU percentages (independent method).
Find the throughput ceiling: raise \(f_s\) (or \(M\), or \(N\)) until Process misses its block deadline (queue backs up / a deadline pin overruns). Record the max sustainable \(f_s\) and filter length.
Part C — Measure the digital filter’s response
Sweep the input frequency using the MCP4725/STM32 DAC as a source (step through frequencies, since there’s no analog sweep generator). At each frequency, measure the digital output amplitude on the scope (or from UART-reported RMS) and build the measured magnitude response\(|H_d(f)|\). Confirm the cutoff and stopband against the designed FIR.
Capture the live FFT output for a known tone and compare the bin, leakage, and noise floor to Lab 6.3 expectations (windowing matters — note which window).
Part D — A/B against the analog twin
Build/attach the Module 4 active low-pass (Lab 4.4) with the same cutoff as the FIR (per Wiring & bench setup). Drive both the analog filter and the ADC front end from the same source at each swept frequency.
On the scope, compare analog-filter output vs. DAC-reconstructed digital-filter output: magnitude at each frequency, phase (linear-phase FIR vs. the analog network’s nonlinear phase — use scope cursors / X-Y), transient/step response, and noise floor (scope FFT or Fluke AC).
Tabulate both magnitude responses on one plot (host Python from the CSV exports), mark both cutoffs, and annotate where and why they diverge.
Part E — The staff-level write-up
Write labs/lab-7-3/reconciliation.md: the block diagram, the predicted numbers, the measured numbers, the timing/headroom budget, both filter responses overlaid, and a decision-review-quality discussion — which implementation you’d ship for which requirement, and the role of the anti-alias filter. This is the capstone artifact.
Deliverable & expected results
The full firmware/m7-rtos/ capstone project, host analysis scripts, Saleae + Siglent captures, and the reconciliation write-up. Record:
Quantity
Predicted
Measured
Sample rate \(f_s\) / block \(N\)
16 kHz / 256 → block period 16 ms
…
Process time \(C_\text{process}\) (FIR \(M\) taps, CMSIS-DSP @ 80 MHz)
\(\approx M\cdot N\) MACs / throughput
…
Utilization \(U = C_\text{process}/(N T_s)\)
target \(\le 0.7\)
…
CPU headroom \(1-U\)
\(\ge 0.3\)
…
End-to-end latency \(L\)
FIR group delay \(\frac{M-1}{2}T_s\) + pipeline
…
Throughput ceiling (max sustainable \(f_s\))
where \(U \to 1\)
…
Digital cutoff \(f_c\)
FIR design value (exact)
…
Analog twin cutoff \(f_c\)
R/C design value (±tol)
…
Passband phase (digital vs. analog)
linear vs. nonlinear
…
Watchdog recovery on injected fault
resets & resumes within \(t_\text{IWDG}\)
…
Analysis & reconciliation
Timing. Predict \(C_\text{process}\) from the MAC count (\(M\) taps \(\times\)\(N\) samples) and the Cortex-M4F’s single-cycle MAC / CMSIS-DSP throughput, then reconcile against the Saleae pulse width. Expect measured time to exceed the ideal MAC count because of flash wait states, FFT twiddle/bit-reversal overhead, and RTOS context switches — attribute the gap and confirm \(U\) stays below your safety threshold. If headroom is thin, that’s a real finding: state what you’d cut (shorter FIR, fixed-point, lower \(f_s\)) to buy margin.
Digital vs. analog. The magnitude responses should broadly agree in the passband; the informative differences are the reasons they diverge. The FIR cutoff is exact and its phase is linear (so the waveform shape is preserved — visible as no phase distortion on a multi-tone input), while the analog filter’s cutoff carries R/C tolerance (±5–10%, measure the actual parts from Lab 0.2/0.3 to explain the shift) and its phase is nonlinear (visible as waveform-shape change near cutoff). Compare noise floors: the analog path shows op-amp/thermal noise; the digital path shows quantization noise set by the 12-bit ADC and the coefficient word length (Kuo Ch. 3 / Lyons number-format chapters). Explicitly note that the digital result depends on the analog anti-alias filter in front of the ADC — the fair A/B is “analog filter” vs. “analog anti-alias + FIR,” and Course 1 Lesson 33 is why. Close with the staff-level judgment: for a fixed, well-defined corner with tight phase requirements, the FIR wins on accuracy and reconfigurability at the cost of latency and an anti-alias filter; for the lowest latency and no sampling constraints, the analog filter wins.
Robustness. Inject a fault mid-run (a deliberate bad access, per Lab 7.1) and confirm the watchdog + handler recover the system without corrupting the DMA buffers or leaving the DAC latched — a real product must survive this, and demonstrating it is the difference between a demo and a system.
Going further
Fixed-point port: re-implement the FIR in Q15 (Kuo Ch. 3), measure the headroom gain and the added quantization noise, and reconcile the SNR change.
Adaptive filter: swap the fixed FIR for an LMS adaptive filter (Hayes) that cancels a known interferer — the pipeline is already the right architecture for it.
On-chip vs. edge: hand the same captured block to the Bonus Module 8 Pi 5 / Jetson pipeline and compare a learned filter against this classical one — the bridge from this course’s classical DSP to the edge-ML differentiator (Lab 8.1). A full Jetson twin of this capstone already exists in pieces: the Lab 7.2 Jetson pipeline is its skeleton, the Lab 6.1 harness its DSP stage, and the Lab 7.1 Jetson procedure its watchdog — assembling them against this lab’s capture is the natural bonus build.
Formal schedulability: attach the rate-monotonic utilization bound (Lab 7.2) and the worst-case latency analysis as an appendix, turning the measured characterization into a proof.
Field-log the reconciliation: stream the block metrics over UART to the host and log them for a long run, so you can show timing/headroom stability over minutes — the kind of evidence a design review actually wants.