31 Signal Processing Labs
31.1 Learning Objectives
By the end of this chapter, you will be able to:
- Configure ESP32 ADC resolution: Select and program 8-bit, 10-bit, or 12-bit quantization to match sensor accuracy requirements
- Implement digital filters in firmware: Code moving average, median, and exponential moving average (EMA) low-pass filters on an ESP32
- Analyse frequency spectra with FFT: Execute Fast Fourier Transform on sampled signals to isolate frequency components from noise
- Construct multi-stage filter pipelines: Sequence anti-aliasing, median spike removal, low-pass smoothing, and downsampling into an end-to-end processing chain
- Evaluate filter trade-offs: Compare median vs moving average performance on impulsive noise and justify filter selection for a given noise profile
- Calculate ADC precision metrics: Derive LSB step size, usable quantization levels, and voltage precision from resolution and reference voltage parameters
Key Concepts
- ADC resolution must match the sensor span: A 12-bit ADC only helps if the sensor uses a meaningful portion of the reference range.
- Median filters remove spikes better than averages: Use median first when noise includes impulsive outliers, because averaging spreads spike damage into neighboring samples.
- FFT choices are design choices, not defaults: Sampling rate, FFT length, and anti-aliasing determine whether the reported peaks are physically real.
- Downsample only after filtering: If you reduce the sample rate before the signal is band-limited, high-frequency content aliases into the preserved band.
- Pipelines beat single filters: In real embedded systems, the best outcome usually comes from a sequence such as anti-aliasing, median filtering, smoothing, and then decimation.
- Lab success is measured on the target platform: CPU cost, RAM use, serial throughput, and timing jitter matter just as much as the math.
For Beginners: Signal Processing Labs
These hands-on labs let you experiment with signal processing on real hardware (the ESP32 microcontroller). You will read sensor values, apply filters to clean up noisy data, and analyze frequency patterns – all skills that are essential for building reliable IoT devices. Think of it as cooking class after learning the theory: you follow step-by-step recipes to see the concepts in action.
Related Chapters
Prerequisites:
- Signal Processing Overview - Sampling and Nyquist theorem
- Aliasing and ADC - Quantization and filtering
- Voice Compression - Audio signal processing
- Sensor Dynamics - Temporal response
- ESP32 familiarity: Basic understanding of microcontroller programming
- C programming: Variables, arrays, loops, functions
Practical:
- Hardware Prototyping - ESP32 setup
- Sensor Labs - Additional sensor exercises
31.2 Signal Processing Lab: ESP32 Wokwi Simulation
31.2.1 Interactive Lab Overview
This lab uses Wokwi ESP32 simulator - no physical hardware required! You’ll explore:
- ADC Resolution Effects: Compare 8-bit, 10-bit, and 12-bit quantization
- Filter Comparison: Test moving average, median, and low-pass filters
- FFT Frequency Analysis: Identify signal components and noise frequencies
- Multi-Stage Pipeline: Design complete signal processing chain
Time Required: 60-90 minutes (all exercises)
31.2.2 Getting Started with Wokwi
Access the Lab:
- Open Wokwi ESP32 Simulator
- Create new ESP32 project
- Copy lab code from exercises below
- Click “Run” to start simulation
Simulator Tips:
- Use Serial Monitor to view output
- Pause simulation to inspect values
- Adjust potentiometer to change analog input
- Reset to restart from beginning
31.3 Exercise 1: Understanding ADC Resolution
Objective: Compare 8-bit vs 12-bit ADC precision
Setup:
- Connect potentiometer to GPIO34 (ADC1_CH6)
- Read analog values at different resolutions
- Calculate voltage from ADC reading
Code Template:
#include <Arduino.h>
void setup() {
Serial.begin(115200);
analogReadResolution(12); // Change to 8, 10, or 12
pinMode(34, INPUT);
}
void loop() {
int adc_value = analogRead(34);
// Adjust divisor when changing resolution: 255 (8-bit), 1023 (10-bit), 4095 (12-bit)
float voltage = (adc_value / 4095.0) * 3.3; // For 12-bit
Serial.print("ADC: ");
Serial.print(adc_value);
Serial.print(" | Voltage: ");
Serial.print(voltage, 4); // 4 decimal places
Serial.println(" V");
delay(500);
}Tasks:
- Run with 8-bit resolution (0-255 range)
- Run with 12-bit resolution (0-4095 range)
- Calculate step size for each: 3.3V / (2^bits - 1)
- Observe precision difference in Serial Monitor
Expected Results:
- 8-bit: Step size = 12.9 mV, ±0.4% precision
- 12-bit: Step size = 0.8 mV, ±0.025% precision
Putting Numbers to It
ADC resolution is governed by least-significant-bit (LSB) size.
\[ \text{LSB}=\frac{V_{ref}}{2^n-1} \]
Worked example: With \(V_{ref}=3.3\) V:
\[ \begin{aligned} \text{LSB}_{8bit}&=\frac{3.3}{255}=12.94\text{ mV}\\ \text{LSB}_{12bit}&=\frac{3.3}{4095}=0.806\text{ mV} \end{aligned} \]
For a 100 mV sensor signal span:
\[ \begin{aligned} \text{usable levels}_{8bit}&=\frac{100}{12.94}\approx 7.7\\ \text{usable levels}_{12bit}&=\frac{100}{0.806}\approx 124 \end{aligned} \]
12-bit ADC gives about \(124/7.7 \approx 16\times\) finer measurement granularity on the same signal.
Interactive ADC Resolution Calculator:
Question: If your sensor accuracy is ±1%, which ADC resolution is sufficient?
31.4 Exercise 2: Comparing Filter Types
Objective: Test median vs moving average filters on noisy data
Setup:
- Simulated noisy temperature sensor
- Inject random spikes to test outlier rejection
- Compare filter outputs
Code Template:
#include <Arduino.h>
// 5-point moving average filter
float movingAverage(float newVal) {
static float buf[5] = {0};
static int idx = 0;
buf[idx] = newVal;
idx = (idx + 1) % 5;
float sum = 0;
for (int i = 0; i < 5; i++) sum += buf[i];
return sum / 5.0;
}
// 5-point median filter (rejects spikes)
float medianFilter(float newVal) {
static float buf[5] = {0};
static int idx = 0;
buf[idx] = newVal;
idx = (idx + 1) % 5;
float s[5];
memcpy(s, buf, sizeof(buf));
for (int i = 0; i < 4; i++) // bubble sort
for (int j = 0; j < 4-i; j++)
if (s[j] > s[j+1]) { float t=s[j]; s[j]=s[j+1]; s[j+1]=t; }
return s[2]; // middle value
}
void setup() { Serial.begin(115200); }
void loop() {
float base = 23.5 + random(-10,10)/100.0; // true + noise
float spike = (random(0,100) < 5) ? 50.0 : 0; // 5% spike
float raw = base + spike;
Serial.printf("Raw: %.1f | Avg: %.1f | Med: %.1f\n",
raw, movingAverage(raw), medianFilter(raw));
delay(200);
}Tasks:
- Run simulation and observe outputs
- Note how moving average reacts to spikes
- Note how median filter handles spikes
- Which filter is better for spike rejection?
Expected Results:
- Moving Average: Spikes corrupt output for 5 samples
- Median Filter: Spikes have no effect if <50% of buffer
31.5 Exercise 3: FFT Frequency Analysis
Objective: Use Fast Fourier Transform (FFT) to identify signal frequencies
Setup:
- Generate composite signal (1 Hz + 10 Hz + noise)
- Apply FFT to find frequency components
- Visualize frequency spectrum
Code Template:
#include <Arduino.h>
#include "arduinoFFT.h"
#define SAMPLES 128
#define FS 100 // 100 Hz sampling rate
double vReal[SAMPLES], vImag[SAMPLES];
ArduinoFFT<double> FFT(vReal, vImag, SAMPLES, FS);
void setup() { Serial.begin(115200); }
void loop() {
// Generate composite signal: 1 Hz + 10 Hz + noise
for (int i = 0; i < SAMPLES; i++) {
float t = i / (float)FS;
vReal[i] = sin(2*PI*1.0*t) + 0.5*sin(2*PI*10.0*t)
+ random(-10,10)/100.0;
vImag[i] = 0;
}
// FFT: window -> transform -> magnitude
FFT.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
FFT.compute(FFT_FORWARD);
FFT.complexToMagnitude();
// Print spectrum and dominant frequency
for (int i = 0; i < SAMPLES/2; i++)
Serial.printf("%.1f Hz: %.2f\n", i*FS/(double)SAMPLES, vReal[i]);
Serial.printf("Peak: %.2f Hz\n\n", FFT.majorPeak());
delay(5000);
}Tasks:
- Run FFT and observe frequency spectrum
- Identify peaks at 1 Hz and 10 Hz
- Change signal frequencies and re-run
- What happens if sampling rate is too low (aliasing)?
Expected Results:
- Peaks at 1 Hz and 10 Hz visible
- Noise spread across all frequencies
- If fs < 20 Hz, aliasing occurs (10 Hz appears as lower frequency)
31.6 Exercise 4: Designing a Multi-Stage Filter Pipeline
Objective: Combine anti-aliasing, digital filtering, and downsampling
Setup:
- Sample at 1000 Hz
- Apply low-pass filter (cutoff 100 Hz)
- Downsample to 250 Hz for transmission
Code Template:
#include <Arduino.h>
// Simple low-pass IIR filter (exponential moving average)
float lowPassFilter(float newValue, float alpha = 0.1) {
static float filtered = 0;
filtered = alpha * newValue + (1 - alpha) * filtered;
return filtered;
}
int downsampleCounter = 0;
void setup() {
Serial.begin(115200);
}
void loop() {
// Sample at 1000 Hz (1 ms period)
float rawValue = analogRead(34) * (3.3 / 4095.0);
// Apply digital low-pass filter (smoothing before downsampling)
float filtered = lowPassFilter(rawValue, 0.2); // alpha=0.2 smoothing
// Downsample 4:1 (1000 Hz → 250 Hz)
downsampleCounter++;
if (downsampleCounter >= 4) {
downsampleCounter = 0;
Serial.print("Filtered & Downsampled: ");
Serial.println(filtered, 4);
}
delayMicroseconds(1000); // 1 kHz sampling
}Tasks:
- Implement pipeline and observe output
- Try removing low-pass filter - what happens?
- Try different downsample ratios (2:1, 8:1)
- Calculate data rate reduction (1000 Hz → 250 Hz = 75% savings)
Expected Results:
- Smooth signal at 250 Hz output rate
- High-frequency noise removed
- 4× reduction in data transmission
31.7 Learning Outcomes Checklist
After completing the labs, you should be able to:
31.8 Key Takeaways from the Lab
The most effective signal processing pipeline combines multiple filter stages: median filter first (removes spikes and outliers), then low-pass or moving average (smooths remaining noise), then downsample (reduces data rate). Specific insights:
- ADC Resolution: 12-bit is sufficient for most IoT sensors (±0.025% precision)
- Filter Selection: Use median for spikes, low-pass for gradual noise
- FFT Analysis: Reveals hidden frequency components and noise sources
- Pipeline Design: Combine filters and downsampling to reduce data rate (up to 80% noise reduction)
- Practical Constraints: Balance precision, speed, and power consumption
For Kids: Meet the Sensor Squad!
The Sensor Squad went to the lab for hands-on experiments!
Sammy the Sensor was reading a noisy temperature signal. “Help! My readings are jumping between 24, 25, 99, 24, 26 degrees! That 99 is clearly wrong!”
Max the Microcontroller pulled out his filter toolkit. “First, let’s use a median filter – it sorts your last 5 readings and picks the middle one. The crazy 99 gets thrown out!” The display now showed: 24, 25, 25, 24, 25.
“Still a bit bumpy,” noted Lila the LED. Max applied a moving average: “Now I average the last 3 readings together. Smooth as butter!” The display showed: 24.7, 24.7, 24.7.
Bella the Battery was impressed: “And by cleaning the data HERE instead of sending all the noisy readings to the cloud, we save energy! Smart processing at the edge means fewer transmissions!”
Max held up his FFT analyzer: “And THIS tool shows us the hidden frequencies in the signal – like X-ray vision for data! It tells us WHERE the noise comes from so we can fix the source.”
31.9 Knowledge Check
31.10 Decision Framework: Choosing the Right Filter Pipeline
Decision Framework: Multi-Stage Filter Selection for Different Noise Types
In the labs, you implemented several filter types. In real projects, you’ll often combine multiple filters in a pipeline. This framework helps you choose the right sequence.
Filter Pipeline Design Table:
- Impulsive spikes + Gaussian noise: median filter (5-point) -> moving average (8-point) -> decimate 4:1. Use for temperature sensors with EMI spikes.
- High-frequency noise only: skip outlier removal -> low-pass IIR (
alpha = 0.2) -> decimate 2:1. Use for accelerometers with vibration noise. - 50/60 Hz power-line interference: skip outlier removal -> notch filter at 50 Hz -> keep full rate. Use for ECG or audio signals near mains wiring.
- Slow drift + spikes: median filter (3-point) -> high-pass filter for DC drift removal -> keep full rate. Use for load cells and slowly drifting analog sensors.
Example: Smart Thermostat (Temperature + Humidity)
Noise profile:
- Occasional spikes from HVAC cycling (±5°C for 1 sample)
- Slow Gaussian noise (±0.2°C standard deviation)
- Very slow real temperature changes (0.1°C/minute)
Optimal pipeline:
1. Median filter (5-point window)
→ Removes spikes without affecting true signal
2. Exponential moving average (alpha = 0.1)
→ Smooths Gaussian noise (70% reduction)
→ 0.1 coefficient gives ~10-sample effective window
3. Downsample 10:1 (1 Hz → 0.1 Hz)
→ Temperature changes slower than 0.1 Hz
→ Saves 90% transmission bandwidth
Why NOT other combinations:
- Moving average alone: Spikes corrupt 5 consecutive samples
- EMA alone: Spikes cause exponential decay artifacts
- No downsampling: Wastes energy transmitting 10 nearly-identical readings
Quick Decision Tree:
- If impulsive spikes are present, start with a median filter of 3 to 5 samples.
- If the remaining noise is broadband, use a moving average or low-pass IIR.
- If the noise is a single interfering frequency, use a notch filter.
- If the dominant problem is slow drift, add a high-pass stage.
- Downsample only when the preserved signal bandwidth is comfortably below the new Nyquist limit.
Key Takeaway: No single filter solves all problems. Real systems combine 2-4 filter stages tailored to specific noise characteristics. From Lab Exercise 4, you learned that a pipeline (median → low-pass → downsample) achieved ~80% noise reduction — far better than any single technique alone.
Concept Relationships: Signal Processing Labs
- Median filter -> Spike removal: Eliminates one-sample outliers without smearing them into neighboring readings.
- EMA -> Noise smoothing: Trades some responsiveness for cleaner trends using exponentially decaying weight on older samples.
- FFT -> Frequency analysis: Exposes periodic content that is difficult to diagnose from raw time-domain samples alone.
- Filter pipeline -> Multi-stage processing: Combining median, smoothing, and downsampling can remove noise more effectively than any one stage by itself.
Cross-module connection: Multi-Sensor Fusion shows how to combine filtered signals from multiple sensors.
31.11 See Also
- Signal Processing Overview — Nyquist theorem and sampling fundamentals
- Quantization and Filtering — ADC resolution and filter theory
- Sensor Labs — Additional hands-on sensor experiments
Common Pitfalls
1. Trusting the Simulator More Than the Timing Budget
A lab sketch that looks correct in Wokwi can still miss deadlines on real hardware if the filter, FFT, or serial output takes too long. Measure loop timing, memory use, and actual sample cadence before assuming the design scales.
2. Smoothing Noise Before Fixing Aliasing
Digital filters cannot recover a signal that was sampled too slowly or without analog anti-alias protection. If unexpected peaks appear in the FFT, check the analog front end and sample rate before piling on more software filtering.
3. Using One Filter for Every Noise Problem
Moving averages are good for broadband noise, but they are poor at rejecting spikes. Median filters reject spikes, but they do not remove narrowband interference. Match the filter to the noise pattern you actually observe.
31.12 What’s Next
- Sensor Labs: Implementation and Review: apply these same filtering and sampling ideas to broader sensor hardware exercises.
- Multi-Sensor Data Fusion: combine cleaned signals from multiple sensors into one estimate.
- Edge and Fog Computing: move parts of the filtering chain closer to the device to reduce uplink traffic.
- Signal Processing Overview: revisit the theory behind sampling, Nyquist limits, and analog-to-digital conversion.
- Quantization and Filtering: deepen the math behind the ADC and filter choices used in these labs.
- Hardware Prototyping: move from simulator workflows to physical ESP32 setups.