Course 3 — Embedded DSP: From the Bench to Real-Time Firmware
A fully hands-on lab course for DSP embedded software / firmware engineering
A lab-first course that takes someone who already has the mathematics (see the prerequisite below) and turns them into an engineer who can design, measure, and ship real-time digital signal processing on embedded hardware. Every idea is learned by building it on a breadboard, driving it from a microcontroller, and measuring it with real instruments, then reconciling the measurement against theory. The target is fluency for DSP embedded software / firmware engineering roles: sampling and quantization, mixed-signal I/O, FIR/IIR filters and the FFT running in real time on a Cortex-M, robust interrupt/DMA firmware, and, as a modern differentiator, on-device ML signal processing on edge accelerators.
Prerequisite: Course 1 — Math Foundations for Signal Processing, Machine Learning, and Artificial Intelligence is assumed as mastered. The linear algebra, real & complex analysis, probability, and distribution-theory/Fourier material there is applied here, never re-taught. Where a lab leans on a specific Course 1 result, it links back to the relevant lesson.
Note on AI use: The lab write-ups on this site are drafted with AI assistance so every lab has the same structure to work from. The substance is mine: every circuit is built by hand, every measurement is taken on my own bench, every line of firmware and analysis code is written and debugged by me, and every predicted-vs-measured reconciliation is my own. AI is used only to typeset the notes and keep the format consistent. You only learn this by building it.
How each lab is structured
Every lab file follows the same template so it can be worked as a self-contained bench session:
- Goal: what skill/concept the lab builds and why it matters for the role.
- Recommended reading: the specific chapters (from the books below and, where relevant, Course 1) to read before touching the bench.
- Equipment & parts: exactly which instruments and components from the inventory.
- Wiring & bench setup (labs with a physical hookup): the exact connections — a signal-chain diagram, a pin-map table (every wire, by header pin name), and where each instrument probe/lead lands. Build from this, not from prose.
- Safety & don’t-break-it: the specific ways this setup can hurt you or destroy a part, and how to avoid them. Read this every time.
- Project & environment setup (labs with firmware or host code): the exact CubeMX/
.iocsettings, repo paths for this lab’s firmware/host/captures, Python environment and libraries, and build scaffolding (CMake everywhere — the STM32 projects and the SBC builds alike, per the project workflow) — so nothing about how to run the lab is ambiguous. The DSP/algorithm code itself is never given (see the AI note above); only the setup is. - Background: the minimum theory, with the key equations.
- Procedure: numbered, click-by-click steps including correct instrument operation.
- Deliverable & expected results: the capture/plot/measurement to produce, the exact files to record (and where they go in the repo), and the numbers to expect.
- Analysis & reconciliation: predict by hand, then explain any gap between predicted and measured.
- Going further: optional extensions.
Bench inventory (already on hand)
Instruments
- WANPTEK 30 V / 10 A regulated DC bench power supply — adjustable voltage with a settable current limit (OCP). The primary DC source for every powered lab.
- Fluke 117 true-RMS digital multimeter — DC/AC volts, resistance, continuity (beeper), capacitance, frequency, and non-contact AC voltage (VoltAlert).
- Siglent SDS1104X-E — 100 MHz, 4-channel, 1 GSa/s digital oscilloscope with FFT math, automatic measurements, and CSV export. Ships with 10× passive probes and a ~1 kHz probe-compensation output.
- FNIRSI LC1020E LCR meter — R, L, C, ESR, and Q/D/θ at selectable test frequencies (100 Hz / 1 kHz / 10 kHz / 100 kHz).
- Saleae Logic 8 — 8-channel USB logic analyzer (Logic 2 software) with built-in protocol decoders (I²C, SPI, UART) for timing and bus analysis.
Boards & mixed-signal parts
- STM32 Nucleo-64 (NUCLEO-L476RG) — STM32L476RG Cortex-M4F @ 80 MHz with FPU, 12-bit ADC & DAC, timers, DMA, on-board ST-LINK debugger and USB virtual COM port. The real-time DSP target.
- MCP6002 dual rail-to-rail op-amp, DIP-8 (1 MHz GBW, single 1.8–6 V supply).
- MCP4725 12-bit I²C DAC breakout (rail-to-rail output, on-board EEPROM).
- ADS1115 16-bit I²C ADC breakout (4 single-ended / 2 differential inputs, programmable gain, up to 860 SPS).
- Coliao bi-directional I²C logic-level shifters (3.3 V ↔︎ 5 V).
- Breadboards + jumper-wire kit + a 37-value / 480-piece R/C/component kit with an MB102 3.3 V/5 V power-supply module.
Edge-ML accelerators (bonus module)
- Raspberry Pi 5 — quad-core CPU host for on-device inference and comparison.
- Jetson Orin Nano — CUDA GPU edge accelerator for real-time / learned signal processing.
- Audio I/O for Modules 8–9 (on hand) — a USB sound card (Wonrabai; USB Audio Class stereo codec, onboard microphone, mic and speaker headers) with two 8 Ω / 5 W speakers. Driver-free on the Pi 5 and Jetson: it appears as one ALSA card for both capture and playback (
arecord -l/aplay -lshow it;python -m sounddevicelists the same device for the host-side scripts), so every Module 8–9 audio lab has a live mic and a way to hear its output on the device itself. Lower-level alternatives, not required: an I²S or PDM MEMS mic (e.g. INMP441/SPH0645) on the 40-pin header — Such, Embedded AI Ch. 10 (“Microphones and PDM Fundamentals”) covers what a PDM stream is and how it becomes PCM — or the ADS1115 as a line-in for a mic-preamp/analog source. The STM32 has no part in this: the sound card is a Linux-side device. - Camera for Labs 8.6, 9.5, 9.6 (on hand) — an innomaker 1080p USB 2.0 UVC camera module, 130° wide-angle lens. Plug-and-play V4L2 on both boards (
v4l2-ctl --list-formats-ext,cv2.VideoCapture), and driver-free on the Mac too. The wide lens buys field of view at the cost of visible barrel distortion toward the edges — keep the subject central for detection and motion work, or calibrate and undistort first (Lab 8.6’s Going further). Lower-level alternative, not required: a CSI camera — Raspberry Pi Camera Module v2 (IMX219), natively supported by JetPack (nvarguscamerasrc); mind the cabling — the Orin Nano devkit’s CAM connectors are 22-pin FFC (the v2 ships with a 15-pin cable, so add a 15↔︎22-pin cable), and the Pi 5’s mini connector needs the Pi 5 camera cable. Avoid Camera Module 3 (IMX708) for cross-board use — stock JetPack carries no driver for it. - Optional for Lab 8.4 (vibration anomaly) — an IMU (BNO055 / ICM-20948, I²C) or a piezo disc; both are optional, and the mic “audio proxy” path is the simplest way to run that lab without them.
Global bench safety & instrument-care primer
These rules apply to every lab; individual labs add their own. Read them once, carefully, before Module 0.
- Set the power-supply current limit first. On the WANPTEK, before connecting anything: set the target voltage, briefly short the leads (or turn the current knob into CC), and set a sane current limit (start at ~100 mA for small breadboard circuits). The current limit is what protects your parts when you wire something wrong: it turns “released magic smoke” into “supply goes into constant-current and nothing happens.”
- The DMM current jacks are a dead short. The Fluke’s A input is a near-zero-ohm path. Never put the meter in current mode across a voltage source or in parallel with a component: you will blow the fuse or worse. Measure current only in series, and default the dial to volts when you’re done.
- Never measure resistance, capacitance, or continuity on a powered circuit. Ω/continuity/capacitance modes source their own small current and assume the circuit is de-energized. Power down and, for capacitors, discharge before measuring.
- Respect the 3.3 V world. The STM32 ADC/GPIO pins are not 5 V tolerant on analog inputs: never drive an STM32 input above 3.3 V (V_DDA). Use the level shifter or a divider when crossing between the 5 V and 3.3 V domains.
- Never exceed a chip’s supply on its inputs. The ADS1115 and MCP6002 inputs must stay within roughly (V−0.3 V) to (V++0.3 V). Clamp, divide, or bias signals into range before they reach an input pin.
- Common grounds. Every instrument and board sharing a signal must share a ground. The scope probe ground clip is earth-referenced: clip it only to your circuit ground, never to a node at a different potential.
- Discharge capacitors (especially electrolytics) before handling or LCR-measuring them. Observe electrolytic polarity: reversed or over-volted electrolytics can vent.
- ESD care with the bare DIP chips (MCP6002): handle by the body, ground yourself, and insert/remove only with power off.
- Double-check polarity and pin 1 on every IC and breakout before applying power. One reversed rail usually kills the part.
Toolchain & software setup
# --- Host (M-series Mac): analysis, plotting, instrument scripting ---
python3 -m venv venv && source venv/bin/activate
pip install numpy scipy matplotlib jupyter pyserial
# --- STM32 firmware (CMake-first; see the project-workflow section below) ---
# STM32CubeMX (free, ST) — owns the .ioc: pin/clock/peripheral config + HAL init-code
# generation. Generates a CMake project directly (Toolchain/IDE = CMake).
brew install --cask gcc-arm-embedded # arm-none-eabi-gcc/gdb (Arm's official build)
brew install openocd # on-board ST-LINK debug server for CLion
# STM32CubeProgrammer (free, ST) — flashing; STM32_Programmer_CLI drives the
# repo's flash-<module> CMake target.
# IDE: CLion (opens the CMake projects directly). STM32CubeIDE also works if you
# prefer it — the .ioc tables in every lab apply unchanged either way.
# Serial console over the Nucleo's ST-LINK virtual COM port (USART2 @ PA2/PA3).
# --- Logic analyzer ---
# Saleae Logic 2 (free) — capture + I2C/UART/SPI protocol decoders.
# --- Edge-ML boxes (bonus Modules 8-9) ---
# Raspberry Pi 5: numpy/scipy/sounddevice + onnxruntime / tflite-runtime.
# Jetson Orin Nano: JetPack (CUDA/cuDNN/TensorRT) + PyTorch, CuPy.
# CuPy's cupyx.scipy.signal / cupyx.scipy.ndimage mirror the SciPy API on the GPU:
# prototype with scipy.signal on the host, then swap the import on the Jetson.Optional but recommended “third opinion” for the analog labs: predict by hand → simulate in LTspice (free, runs natively on Apple silicon) → measure on the bench → reconcile the three numbers. The detailed workflows for both tools are below — read them once, then the labs only reference them.
Python & Jupyter — the MATLAB replacement
Everything the textbooks do in MATLAB, this course does in Python from the single venv above. The point of pinning the library map here is that none of your thinking time goes to tooling — the concepts and the code implementing them are the work; these are just the tools you reach for:
| Task | Library / function |
|---|---|
| FIR design (window method) | scipy.signal.firwin, windows via scipy.signal.get_window |
| IIR design (Butterworth/Chebyshev, bilinear) | scipy.signal.butter, cheby1, bilinear, iirfilter |
| Frequency response / group delay | scipy.signal.freqz, group_delay (overlay these on every measured sweep) |
| Filtering a captured record | scipy.signal.lfilter, filtfilt, convolve |
| PSD / spectrogram / STFT | scipy.signal.welch, periodogram, spectrogram, stft/istft |
| FFT | numpy.fft.rfft/rfftfreq (host reference for the CMSIS builds) |
| Matrix work (Kalman, least squares) | plain numpy arrays + numpy.linalg — G&A’s MATLAB listings port ~1:1 |
| WAV / image / video I/O | scipy.io.wavfile or soundfile; PIL.Image / opencv-python; cv2.VideoCapture |
| Serial capture from the Nucleo | pyserial (serial.Serial('/dev/tty.usbmodem*', 115200)) |
| Plots | matplotlib.pyplot |
| GPU (Jetson) | cupyx.scipy.signal / cupyx.scipy.fft / cupyx.scipy.ndimage — SciPy API, swap the import |
Notebook convention: run jupyter lab from the venv and keep one notebook per lab at labs/lab-<M>-<N>/host/analysis.ipynb — load the lab’s captures/ data, compute the predicted curves, overlay measured points, and write the reconciliation right there next to the plots. Export final figures to PNG in the same folder (the lab’s notes.md links them). Reusable pieces (the Module 9 serial harness, logging helpers) live as plain .py scripts instead, so notebooks stay disposable and scripts stay importable.
LTspice workflow — the analog “third opinion”
Used in Modules 0–1, 4, and 5.4. Learn the mechanics once (they fit in ten minutes), then every lab’s sim is a five-minute detour:
- One-time: install LTspice (free, Analog Devices, Apple-silicon native). The whole editor is keyboard-driven: F2 place component, F3 wire, F4 label net, F5 delete, G ground. Every schematic needs exactly one ground (node 0) or the sim won’t run.
- Sources: place
voltage, right-click → Advanced:SINE(offset amplitude freq)for AC sweeps and ripple,PULSE(V1 V2 tdelay trise tfall ton period)for step/square drive (mimic the probe-comp square or the DAC staircase). DC rails are just a value in the voltage field. - Op-amps (Module 4): simplest is
UniversalOpAmp2with GBW = 1 Meg, rail-to-rail limits at your measured VDD — a faithful-enough MCP6002. For letter-perfect sims, download Microchip’s MCP6002 SPICE macromodel, put the.lib/.301file next to the.asc, add.include MCP6002.lib(S key → spice directive), and place anopamp2symbol renamed to the subcircuit. - Analyses:
.tran 10mfor waveforms (right-click the plot → FFT under View for harmonic checks, e.g. Lab 4.3’s clipping);.ac dec 100 10 1Megfor Bode plots (plotV(out)/V(in); cursor readouts give the −3 dB point directly — compare to Lab 1.3/4.4 predictions). - Getting data out: click a node to plot it; File → Export data as text dumps the visible traces (tab-separated;
.acexports magnitude/phase) — load it in the lab notebook withnumpyand overlay sim, hand prediction, and bench measurement on one figure. That three-way overlay is the deliverable for the analog labs. - Where files live: one schematic per lab at
hardware/lab-<M>-<N>.asc, exports next to the lab’s other data inlabs/lab-<M>-<N>/captures/.
STM32 project workflow — CubeMX + CMake + CLion
The firmware projects are plain CMake projects, not IDE-proprietary ones: CMake is portable across IDEs and CI, CubeMX generates it natively, and the companion repo already carries the course’s CMake glue (course3/firmware/cmake/stm32_firmware.cmake, a cross-compile toolchain file, and debug/release presets in every module). CLion opens these directly. The division of labor:
STM32CubeMX (standalone) owns the
.ioc— every “CubeMX page → Setting” table in the labs is done here. When a lab says create the module project: New Project → board NUCLEO-L476RG → configure per the lab’s table → Project Manager → Toolchain/IDE = CMake → generate into the module folder (e.g.firmware/m2-timing/). CubeMX writes its ownCMakeLists.txt/CMakePresets.jsonover the repo’s; restore the repo’s versions afterwards (git checkout -- CMakeLists.txt CMakePresets.json) — everything CubeMX generated underCore/andDrivers/is picked up by glob, and the C17/18 standard, CMSIS-DSP wiring, andflash-<module>target come from the central helper. Re-generating after an.iocchange is the same step; your code insrc//include/is never touched.CLion is the editor/debugger. Open the module folder (CLion reads
CMakeLists.txt+CMakePresets.json; enable the debug and release profiles when prompted). Build normally; flash from the terminal or a run target withcmake --preset debug && cmake --build --preset debug # build cmake --build --preset debug --target flash-m2-timing # flash over ST-LINKDebugging uses the on-board ST-LINK through OpenOCD: Run → Edit Configurations → + → OpenOCD Download & Run, board config file
board/st_nucleo_l4.cfg, and the module’s executable as the target. Breakpoints, stepping, and peripheral-register views work as in CubeIDE. (Where a lab mentions CubeIDE’s SWV/ITM trace console, treat it as optional — the UART console covers the same need in every lab; SWV is a CubeIDE convenience, not a dependency.)Optimization-level experiments (Lab 2.1 Part D and the Module 6 timing runs) are the two presets:
debug=-Og -g3,release=-O2 -g. Where a lab asks for a literal-O0build, add a CLion CMake profile (or a one-off configure) passing-DCMAKE_C_FLAGS_DEBUG="-O0 -g3".
Full details (including the shared/ + host/ native-test harness that builds the DSP kernels on the Mac under sanitizers, with no board attached) are in the repo’s course3/firmware/README.md.
STM32 firmware setup essentials (do these once)
Three settings recur in every Module 5–7 lab; get them right once and the per-lab predicted numbers line up:
80 MHz system clock. New CubeMX projects often boot on the 4 MHz MSI / 16 MHz HSI. In Clock Configuration, drive the PLL from HSI16 (or MSI), select PLLCLK on the System Clock Mux, and target HCLK = 80 MHz (the L476’s maximum); CubeMX sets the flash wait-states automatically. Every cycle-budget and sample-rate figure in Modules 5–7 assumes 80 MHz.
CMSIS-DSP library. Modules 6 and 9 call
arm_fir_f32,arm_rfft_fast_f32,arm_biquad_cascade_df1_*, etc. Enable it once — in the.iocSoftware Packs → Select Components, add ARM CMSIS-DSP (or add the pack and linklibarm_cortexM4lf_math), defineARM_MATH_CM4and__FPU_PRESENT=1, and build hard-float (-mfpu=fpv4-sp-d16 -mfloat-abi=hard). Without this the DSP calls won’t link — the single most common Module 6 stall.DWT cycle counter for timing. A GPIO toggle + Saleae is fine for millisecond-scale block timing, but per-sample DSP costs are sub-µs, below the Saleae’s ~10 ns resolution floor. For cycle-accurate numbers use the Cortex-M DWT counter, and keep the GPIO only as a coarse scope cross-check:
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // enable trace DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // start cycle counter uint32_t t0 = DWT->CYCCNT; /* ... code under test ... */ uint32_t cycles = DWT->CYCCNT - t0; // / 80e6 → secondsFreeRTOS (for the RTOS-variant labs). Several labs offer an RTOS build alongside the bare-metal one, to measure what the scheduler costs. Enable it once in CubeMX: Pinout & Configuration → Middleware → FREERTOS → Interface = CMSIS_V2, which pulls in the kernel and generates
MX_FREERTOS_Init(). Create tasks withosThreadNew, and for the ISR/DMA→task handoff use a binary semaphore (osSemaphoreNew) or a message queue (osMessageQueueNew): the ISR signals viaosSemaphoreRelease/osMessageQueuePutand the task blocks onosSemaphoreAcquire/osMessageQueueGet. Critical gotcha: FreeRTOS must own SysTick, so in SYS set the HAL Timebase Source to a spare basic timer (e.g. TIM17), not SysTick — otherwise HAL and the scheduler fight over it and time bases drift. Also setconfigCHECK_FOR_STACK_OVERFLOW = 2while developing. The Rust equivalents (RTIC, Embassy) are in the Rust-on-STM32 block below.
Optional: Rust on the STM32 (second implementation track)
Many of the firmware labs offer a Rust build alongside the C/HAL one — same peripheral, same measurement, different language and memory-safety model. One-time setup:
rustup target add thumbv7em-none-eabihf # Cortex-M4F, hard-float
cargo install probe-rs-tools # flash + RTT/defmt logging over the on-board ST-LINK
# Cargo.toml deps: cortex-m, cortex-m-rt, panic-probe, defmt-rtt,
# stm32l4xx-hal (implements the embedded-hal traits), and ONE concurrency model:
# - rtic (interrupt-driven, static-priority tasks — the "bare-metal RTOS")
# - embassy-stm32 + embassy-executor (async/await, RTOS-style)probe-rs run flashes and streams logs over the same ST-LINK you already use. The embedded-hal traits expose the same ADC/DAC/I²C/timer operations as the C HAL; RTIC or Embassy stand in for FreeRTOS in the concurrency labs. Rust’s ownership model turns several classic embedded bugs (the Q15 aliasing in Lab 6.1, the torn DMA double-buffer read in Lab 9.1, the priority-inversion data race in Lab 7.2) into compile-time errors — comparing where that helps and where it just adds ceremony is itself part of the lesson.
Jetson Orin Nano setup essentials (embedded Linux — do these once)
The Jetson ports in Modules 2/3/6/7 (each lab’s Jetson Orin Nano — detailed procedure subsection) assume a Jetson Orin Nano devkit that is already flashed, on the network, and reachable over SSH from the Mac — only the bench-specific configuration below is needed. Everything here applies to the Pi 5 with the differences noted at the end.
Bench packages and permissions (one-time):
# On the Jetson (SSH in from the Mac):
sudo apt install i2c-tools libgpiod-dev gpiod python3-libgpiod \
build-essential cmake ninja-build
sudo usermod -aG i2c,gpio $USER && newgrp i2c # /dev/i2c-* and gpiochip access
# without sudo (log out/in once)
pip install smbus2 Jetson.GPIO # Python I²C + header-numbered GPIO
# Rust (optional second track): rustup default stable;
# crates: linux-embedded-hal (portable embedded-hal), rppal on the PiThe 40-pin header is the Jetson’s bench interface, physically pin-compatible with the Pi’s: pin 1 = 3.3 V, pin 2/4 = 5 V, pins 6/9/14/… = GND, pins 3/5 = I²C SDA/SCL, pins 8/10 = UART TX/RX. Three rules carried over from the STM32 world still apply — the header is a 3.3 V domain and not 5 V tolerant, GPIO drive strength is weak (the devkit routes header GPIO through level shifters — treat a few mA as the budget), and every instrument sharing a signal shares a ground. The pins this course actually uses (physical/BOARD numbering, identical on the Pi 5; odd row is the inner row on the devkit — check the silkscreen for pin 1 before wiring anything):
inner row outer row
3.3 V ● (1) (2) ● 5 V
I²C SDA ──── ● (3) (4) ● 5 V
I²C SCL ──── ● (5) (6) ● GND ◄─ instrument grounds land here
marker GPIO─ ● (7) (8) ● UART TX
GND ● (9) (10) ● UART RX
...
2nd marker ─ ● (29) (30) ● GND
Pin 7 is the course’s Jetson marker/toggle pin (the counterpart of the STM32 convention D7 = PA8) and pin 29 the second marker where a lab needs one.
I²C without sudo, by bus number. The header exposes two always-on I²C controllers — no pinmux needed. On the Orin Nano devkit pins 3/5 are bus 7 (/dev/i2c-7) and pins 27/28 are bus 1; the course wiring uses pins 3/5 throughout. Don’t memorize this — verify it once, the same way Lab 3.1 teaches on the STM32:
i2cdetect -l # list controllers
i2cdetect -y -r 7 # scan pins 3/5; your MCP4725/ADS1115 address appearsGPIO from userspace. Two paths, used deliberately in the labs:
libgpiod(C and CLI:gpiodetect,gpioinfo,gpioset) — the modern kernel character-device interface. Header pins map to gpiochip lines whose names/offsets are SoC-specific; find a pin’s line withgpioinforather than hard-coding offsets.Jetson.GPIO(Python) — accepts BOARD numbering, i.e. the physical header pin number, so scripts read the same on Jetson and Pi. This is the course default for scaffolding; the timing labs then drop tolibgpiodfrom C to measure what the abstraction costs.
UART. Header pins 8/10 appear as a /dev/ttyTHS* serial device (ls /dev/ttyTHS*; on the Orin Nano devkit typically ttyTHS1). If a getty/login console is attached to it, disable that service before using it as a data port.
Performance & timing knobs (used by every Module 6/7 timing run):
sudo nvpmodel -m 0 && sudo jetson_clocks # max power mode + pin clocks (repeatable timing)
tegrastats # live CPU/GPU/EMC load & temps
sudo apt install rt-tests && sudo cyclictest -t1 -p 90 -i 100 -m -l 100000
# scheduler-latency baseline (µs), the Linux
# counterpart of Lab 2.2's ISR jitter number
chrt -f 80 ./your_pipeline # run a measurement under SCHED_FIFO
taskset -c 3 chrt -f 80 ./your_pipeline # ... pinned to one coreThe stock JetPack kernel is not PREEMPT_RT; NVIDIA ships an optional real-time kernel package for JetPack 6 (see NVIDIA’s RT-kernel docs) — the timing labs record which kernel the numbers came from, and uname -v tells you.
Building & running C++ on the Jetson. Device-side compiled code is CMake + C++20, exactly like Modules 8–9 (template and libraries in the repo’s course3/docs/edge-setup.md); code for a lab lives in labs/lab-<M>-<N>/edge/. Two equally good dev loops — pick per task:
- CLion remote toolchain over SSH (the same workflow as Course 4’s remote RTX box): Settings → Build, Execution, Deployment → Toolchains → + Remote Host, point it at the Jetson; edit on the Mac, build/run/debug on the board.
- Terminal over SSH:
rsync/git pullthe lab folder,cmake -B build && cmake --build build -j, run underchrtas above.
GPU work (CUDA/CuPy/cuFFT) and the ML stack are unchanged from Module 8 — see docs/edge-setup.md.
Raspberry Pi 5 differences (same labs, same wiring)
The Pi 5 ports differ only in the OS-level names — the header wiring is identical:
- I²C: pins 3/5 are
/dev/i2c-1(i2cdetect -y 1); enable I²C once inraspi-configif not already on. - GPIO:
libgpiodworks the same (the Pi 5’s header lines live on the RP1’s gpiochip — again,gpioinfo, don’t hard-code); the Python equivalent ofJetson.GPIOisgpiozero/RPi.GPIO-compatible libraries, and Rustrppalgives the fast mmap’d path used in Lab 2.1. - UART: pins 8/10 are
/dev/ttyAMA0once the UART is enabled inraspi-config. - No CUDA — GPU variants of the Module 6 procedures are Jetson-only; the Pi runs the CPU builds (NEON via the compiler’s auto-vectorizer).
- Clocks: no
nvpmodel/jetson_clocks; the Pi’s governor is set withcpufreq-set(orperformancegovernor via sysfs) before timing runs.
Recommended-reading key
| Abbrev. | Book | Role |
|---|---|---|
| Lyons | Lyons, Understanding Digital Signal Processing | The DFT/FFT/FIR/IIR, number formats, Goertzel — the primary DSP reference |
| Hayes | Hayes, Statistical DSP and Modeling | Random signals, PSD, adaptive filters, spectrum estimation |
| Kuo | Kuo, Lee & Tian, Real-Time Digital Signal Processing | Real-time embedded implementation, fixed-point, DMA |
| G&A | Grewal & Andrews, Kalman Filtering: Theory and Practice with MATLAB | Recursive optimal state estimation & sensor fusion |
| G&W | Gonzalez & Woods, Digital Image Processing | 2-D sampling, spatial/frequency-domain image filtering (Module 9 image labs) |
| Zölzer | Zölzer, Digital Audio Signal Processing | Digital audio fundamentals, STFT effects, audio coding (Modules 8–9 audio labs) |
| Richards | Richards, Fundamentals of Radar Signal Processing | Matched filtering, pulse compression, CFAR detection (Labs 6.7, 6.9) |
| P&S | Proakis & Salehi, Digital Communications | Adaptive equalization, detection/decision theory (Labs 6.8, 6.9) |
| Tekalp | Tekalp, Digital Video Processing | Motion estimation, optical flow, motion-compensated filtering (Lab 9.6) |
| PEI | Scherz & Monk, Practical Electronics for Inventors | Practical components, op-amps, filters — the bench reference (early modules; see the PEI minimum below) |
| Such | Such, Embedded AI: Intelligence at the Deep Edge (No Starch, 2026 — 13 chapters, 32 numbered projects) | Project-based edge-AI companion for Module 8, cited by chapter and project number: exploratory data analysis (Ch. 5), IMU preprocessing and sensor fusion (Ch. 7–8), sensor ML and fault detection (Ch. 9), RNN noise suppression and PDM microphones (Ch. 10), model quantization (Ch. 11), hot-word detection (Ch. 12), person-detection CNN on a constrained target (Ch. 4) |
| Szeliski | Szeliski, Computer Vision: Algorithms and Applications, 2nd ed. (free PDF for personal use at szeliski.org) | Recognition & object-detection theory behind Lab 8.6; the 1st ed. covers only the classical (pre-deep) detectors |
These books are cited inline by chapter; none carries a tracked exercise set on the Books page (the Books page holds the Course 1 booklet’s set and the mathematics shelf only). The applied theory they stand behind — the DFT in practice, filter design and word lengths, multirate and sigma–delta conversion, spectrum estimation, Wiener/LMS/Kalman filtering, detection, images, audio, and the bench — is on the lab pages themselves, in each lab’s Theory section under Background, moved there from the Course 1 booklet.
The PEI minimum
PEI is a 1,000-page reference; this course needs exactly five chapters of it, read selectively, not cover to cover: Ch. 2, 3, 7, 8, and 9 (4th-edition numbering, in which Ch. 7 is Hands-on Electronics, Ch. 8 is Operational Amplifiers, and Ch. 9 is Filters).
| PEI chapter | What to read | Skip (initially) | Labs supported |
|---|---|---|---|
| Ch. 2 — Theory | Ohm’s law, voltage/current/power, series/parallel circuits, capacitive reactance, RC charging/discharging and the time constant, basic AC/complex-impedance ideas | The deeper microscopic and network-theory material | 1.3 (RC low-pass: time constant, cutoff) |
| Ch. 3 — Basic Electronic Components | Resistors — color codes, tolerance, power ratings; capacitors — dielectric types, polarity, ESR, frequency dependence, decoupling | Inductors and transformers (skim) | 0.2, 0.3 (resistor measurement, real capacitor behavior) |
| Ch. 7 — Hands-on Electronics | Breadboards, multimeters, bench power supplies, oscilloscope probes, grounding, coupling, triggering, safe measurement practices | Soldering, PCB fabrication, specialized tools | 0.1, 1.2 (supply/DMM safety, scope operation) |
| Ch. 8 — Operational Amplifiers | Ideal op-amp rules, negative feedback, the voltage follower and non-inverting amplifier, single-supply biasing, input/output impedance, common-mode range, output swing and saturation, bandwidth and gain-bandwidth product, decoupling | Specialized circuits — logarithmic, instrumentation, differentiator, precision rectifier | 4.1–4.3 (follower, non-inverting amp, clipping) |
| Ch. 9 — Filters | First-order RC low-pass response, cutoff frequency, magnitude/phase and dB plots, active low-pass filters, Sallen–Key topology, damping/Q, roll-off | High-pass, band-pass, notch, switched-capacitor, and the exotic approximation sections | 1.3, 4.4 (the anti-alias filter reused in Lab 5.4) |
Smallest practical reading order: before Module 0, Ch. 7’s DMM/supply/breadboard/safety material and Ch. 3’s resistor and capacitor sections; before Lab 1.3, the RC portions of Ch. 2 and Ch. 9; before Module 4, the core portions of Ch. 8; before Lab 4.4, the active-filter portions of Ch. 9.
Implementation tracks & cross-platform comparison
A central theme of this course is that the same signal-processing idea behaves differently depending on what it runs on. Many labs are therefore worked along three independent axes, and the value is in measuring the differences — latency, jitter, throughput, determinism, and how the code itself has to change:
- Runtime — bare-metal → RTOS → Linux. The first implementation runs on the STM32 with no operating system (a
while(1)loop or an interrupt/DMA-driven pipeline). The second adds an RTOS (FreeRTOS in C, or RTIC/Embassy in Rust) for scheduled, prioritized tasks. The third runs the same algorithm as a Linux userspace program on a Raspberry Pi 5 / Jetson, on top of a full preemptive scheduler. Each step trades determinism for convenience and throughput; you quantify exactly what. - Language — C/C++ ↔︎ Rust. Where a lab writes firmware in C with the ST HAL/LL, it can also be written in Rust (
embedded-hal+stm32l4xx-hal, RTIC or Embassy) — and on the Linux side in Rust withrppal/linux-embedded-hal. Same peripheral, same measurement; compare ergonomics, generated-code size, and which bugs the compiler catches. (Setup for both is in the toolchain section above.) - Target — STM32 (Cortex-M4F) ↔︎ Pi 5 (Cortex-A76 CPU) ↔︎ Jetson Orin Nano (CUDA GPU). The MCU owns the deterministic real-time front end; the SBCs own throughput and learned models. Not every lab maps to every target (the Pi/Jetson have no on-chip ADC/DAC and no analog front end, so the bench/analog labs stay MCU-only), but wherever it does, the comparison is explicit.
Which algorithms favor which platform. A recurring result, made concrete in Module 6 and formalized in the benchmark of Lab 8.5:
| Class | Examples | Wins on | Why |
|---|---|---|---|
| Sequential / feedback / latency-bound | IIR biquad (6.2), Goertzel (6.5), Kalman (6.6), per-sample control | STM32 bare-metal | Tight recurrences don’t parallelize; a bounded, jitter-free per-sample loop is exactly what an MCU gives and an OS scheduler erodes. Fixed-point/overflow discipline only exists here. |
| Block / batched / throughput-bound | FFT (6.3), Welch PSD (6.4), long FIR (6.1), STFT (9.3) | Jetson GPU / Pi | O(N log N) batched transforms and large blocks amortize kernel-launch and host↔︎device copy; float64 removes the quantization worries. |
| Streaming with a hard deadline | timer-triggered ADC+DMA (5.x), real-time filter (6.1), RTOS pipeline (7.2) | STM32 (RTOS if multi-task) | Deterministic sampling instant and bounded worst-case latency; Linux userspace jitters unless PREEMPT_RT, and even then loses to the MCU’s p99. |
| Learned / data-driven | keyword CNN (8.2), denoiser (8.3), anomaly AE (8.4), video CNN (9.5) | Jetson (TensorRT) / Pi | Needs the memory, the accelerators, and a training pipeline the MCU can’t host — the modern differentiator. |
Labs that carry a Cross-platform ports & language variants section spell out, for that specific algorithm, how the port is written on each target and what the measured latency/jitter/throughput gap teaches. Wherever a port genuinely runs on the SBCs, that section also carries a Jetson Orin Nano — detailed procedure subsection: numbered steps, exact 40-pin-header wiring, and the commands to run, at the same level of detail as the STM32 procedure (one-time board config in the Jetson setup essentials; Pi 5 differences are called out inline).
Module & lab map
The course is 8 core modules plus two bonus modules (edge ML, and host-in-the-loop audio/image/video). Each lab is its own page; work them in order.
Module 0 · Bench setup & safety
Learn to power, measure, and not destroy things, before any active circuit.
Module 1 · Oscilloscope & first analog signals
Module 2 · Digital I/O & timing
Module 3 · Mixed-signal I/O: I²C, DAC, ADC
Module 4 · Op-amps & analog signal conditioning
Module 5 · STM32 data acquisition
Module 6 · Real-time DSP on the STM32
- Lab 6.1 — FIR on real ADC data
- Lab 6.2 — IIR biquad stability
- Lab 6.3 — FFT spectrum analyzer
- Lab 6.4 — Noise floor / PSD
- Lab 6.5 — Goertzel tone detector
- Lab 6.6 — Real-time Kalman filter & state estimation
- Lab 6.7 — Matched-filter pulse-compression detector
- Lab 6.8 — LMS adaptive equalizer
- Lab 6.9 — CFAR detection & the ROC
Module 7 · Robust real-time firmware
Bonus Module 8 · Edge ML signal processing (Pi 5 + Jetson Orin Nano)
The modern differentiator: take the same signals and run learned DSP on edge accelerators, then compare against the classical STM32 pipeline — closing with live vision: a camera on the Jetson and a TensorRT object detector with a measured per-frame budget.
Bonus Module 9 · Host-in-the-loop audio, image & video processing
Real media, real files, verifiable results. The laptop streams a real audio/image/video file to the embedded target over a framed serial/USB link; the device processes it in real time (block by block); the result is streamed back and the host writes the processed file and checks it against a reference (SciPy/OpenCV), a reproducible, testable workflow that mirrors how embedded media-DSP firmware is actually developed and regression-tested. Covers audio, image, and video signal processing.
- Lab 9.1 — Host↔︎device streaming harness (WAV round-trip)
- Lab 9.2 — Real-time audio filtering on streamed WAV
- Lab 9.3 — STFT overlap-add audio effects
- Lab 9.4 — Image 2-D convolution on the embedded target
- Lab 9.5 — Real-time video frame pipeline (Pi 5 / Jetson)
- Lab 9.6 — Motion estimation & motion-compensated video processing
Repository structure
All lab work lives in the companion repo diivanand/diiv_website_custom_courses, which holds one top-level folder per lab course (Courses 2, 3, and 4 as peers); this course’s workspace is its course3/ folder:
diiv_website_custom_courses/
README.md
pyproject.toml # single uv Python project shared by all courses (simulate here first)
course3/ # ← this course's workspace
docs/ # course docs: reading map, Pi 5 / Jetson setup
firmware/ # STM32 CMake projects (C18), one per module (labs within a module share a project)
m2-timing/ m3-mixed/ m5-daq/ m6-dsp/ m7-rtos/ m9-media/
labs/ # one folder per lab — everything that lab produces
lab-<M>-<N>/
notes.md # bench note: setup, predicted-vs-measured, reconciliation
host/ # simulate-first Python, analysis.ipynb, exported plots
captures/ # raw instrument data: Siglent CSVs, Saleae .sal, LCR/DMM logs
edge/ # Modules 8-9 only: C++20 CMake app, device Python, models
media/ # Module 9: host streamer, reference impls, in/ and out/ WAV/PNG/MP4
hardware/ # breadboard photos, LTspice schematics (lab-<M>-<N>.asc), datasheets
course2/ # Course 2 workspace (python/, c/, rust/ — scientific Python, modern C, embedded Rust)
course4/ # Course 4 workspace (rendering & GPU engineering)
Naming convention: everything a lab produces lives in its folder — notes in labs/lab-2-1/notes.md, scripts in labs/lab-2-1/host/, raw data in labs/lab-2-1/captures/. Lab pages quote these paths relative to the repo’s course3/ folder. A lab is “done” when its notes.md has every Measured cell filled and its captures/ folder holds the raw evidence.
What you’ll be able to do at the end
- Operate a full bench (PSU, DMM, LCR meter, oscilloscope, logic analyzer) safely and correctly, and trust your measurements.
- Design and measure analog signal conditioning (buffers, gain, active anti-alias filters) around real op-amps.
- Bring up mixed-signal I/O (I²C DAC/ADC, level shifting) and reason about quantization, references, and full-scale range.
- Write interrupt- and DMA-driven real-time acquisition firmware on a Cortex-M4F and prove its timing with a logic analyzer.
- Implement and validate FIR/IIR filters, the FFT, PSD/noise-floor estimation, and Goertzel tone detection in real time, including fixed-point effects.
- Build robust firmware (watchdog, fault handling, an RTOS pipeline) and ship an end-to-end capture → process → output system.
- Deploy and benchmark learned, on-device signal processing on edge GPU/CPU accelerators against the classical embedded pipeline — including a live-camera object detector deployed through TensorRT and verified against a reference implementation.
- Process real audio, image, and video on an embedded target with a host-in-the-loop workflow: stream a file from the laptop, process it on-device in real time, return it, and verify the result against a reference implementation.
Relationship to Course 1
Course 1 is the theory this course spends. A few of the tightest links:
- Sampling & aliasing (Modules 3, 5) apply Course 1 Lessons 29 and 40 (sampling; distributions and the Dirac comb), the rigorous reason the sampling theorem is true.
- FIR/IIR filters and the z-transform (Module 6) apply Course 1 Lesson 38 (complex analysis: poles, the region of convergence, stability from pole locations).
- PSD, noise floor, and adaptive/learned processing (Modules 6, 8) apply Course 1 Parts III–IV (probability, expectation, limit theorems) and the random-signals and spectrum-estimation theory in Lab 6.4.
- Least-squares filter design and calibration apply Course 1 Lessons 6 and 42 (least squares; conditioning).