Advanced 1: Sensor Theory
Prerequisites: Lab 1 (MicroPython & Sensors)
Overview
In Lab 1, you learned to read sensors and get useful values. This module explores the physics, limitations, and engineering techniques behind sensor measurements.
You'll learn: - How optocouplers and ultrasonic sensors actually work - Why every measurement has error (and how to quantify it) - Signal conditioning techniques - Calibration methods used in industry
Part 1: Optocoupler Physics
How Reflection Sensing Works
Your line sensors are reflective optocouplers containing: 1. IR LED - emits infrared light (invisible, ~940nm wavelength) 2. Phototransistor - conducts current proportional to received light
IR Light
LED โโโโโโโโโโโโโโโโโบ Surface
โ
Reflected โ
Sensor โโโโโโโโโโโโโโโโ
Surface Reflectivity
Different surfaces reflect different amounts of IR light:
| Surface | Reflectivity | Sensor Output |
|---|---|---|
| White paper | ~80% | High (more light returns) |
| Black tape | ~5% | Low (light absorbed) |
| Shiny metal | Variable | Unpredictable (specular reflection) |
| Wood | ~40% | Medium |
The Analog Reality
Your code reads 0 or 1, but the phototransistor output is actually analog:
Phototransistor Output Voltage
3.3V โค โโโโโโโโโโโโโโโโ White surface
โ /
โ /
1.6V โค------/---------------- Threshold
โ /
โ /
0V โคโโโโดโโโโโโโโโโโโโโโโโโโ Black surface
โ
โโโโโโโโโโโโโโโโโโโโโโโโบ
Distance from surface
The GPIO pin has a threshold (typically ~1.6V). Above = 1, below = 0.
Experiment: Threshold Behavior
# Use ADC to see the analog value (if available)
from machine import Pin, ADC
import time
# Note: Line sensors may not be connected to ADC pins
# This is conceptual - check your hardware
# If you have an analog optocoupler:
sensor_adc = ADC(Pin(26)) # GP26 = ADC0
while True:
raw = sensor_adc.read_u16() # 0-65535
voltage = raw * 3.3 / 65535
digital = 1 if voltage > 1.6 else 0
print(f"Raw: {raw:5d} Voltage: {voltage:.2f}V Digital: {digital}")
time.sleep_ms(100)
Edge Cases and Problems
Problem 1: Ambient light interference - Sunlight contains IR - Fluorescent lights flicker at 100/120 Hz - Solution: Modulate IR LED, filter at receiver
Problem 2: Surface distance - Too close: Saturates sensor - Too far: No reflection detected - Optimal: 2-10mm for most sensors
Problem 3: Angle sensitivity - Best response when perpendicular to surface - Tilted sensor = reduced signal
Part 2: Ultrasonic Physics
Time-of-Flight Principle
The HC-SR04 measures distance using sound:
40 kHz
โโโโโโโโโโโโโบ โโโโโโโ
โ โ Object
โโโโโโโโโโโโโ โโโโโโโ
Echo
Distance = (Time ร Speed of Sound) / 2
Speed of Sound
Sound speed varies with conditions:
| Temperature | Speed | Error if using 343 m/s |
|---|---|---|
| 0ยฐC | 331 m/s | +3.6% |
| 20ยฐC | 343 m/s | 0% (reference) |
| 40ยฐC | 355 m/s | -3.4% |
At 1 meter distance, 3% error = 3 cm!
Beam Pattern
Ultrasonic sensors don't emit a narrow beam - it's a cone:
โฑ 15ยฐ half-angle
โฑ
[Sensor]โโโโโโโโโโโโโโโโบ
โฒ
โฒ
At 1m distance, beam is ~50cm wide!
This means: - Multiple objects may reflect - Thin objects may be missed - Corner reflections give false readings
Experiment: Beam Width Measurement
from picobot import Ultrasonic
import time
sensor = Ultrasonic()
print("Move a flat object across the beam at 50cm distance")
print("Record when the sensor starts/stops detecting it")
print()
while True:
dist = sensor.distance_cm()
if dist > 0:
bar = "#" * min(50, int(dist))
print(f"{dist:5.1f} cm |{bar}")
else:
print(" --- |")
time.sleep_ms(100)
Problematic Surfaces
| Surface | Problem |
|---|---|
| Soft fabric | Absorbs sound, weak echo |
| Angled surface | Reflects away from sensor |
| Very close (<2cm) | Echo returns before sensor ready |
| Very far (>400cm) | Echo too weak to detect |
| Mesh/grid | Sound passes through |
Multi-Path and False Echoes
Direct path (correct)
โโโโโโโโโโโโโโโโโโโโโโโโบ
Multi-path (wrong!)
โโโโโโโโ
โ Wall
โ โโโโโโโบ
โ
The sensor reports the first echo received, which may not be the closest object!
Part 3: Noise and Error
Types of Measurement Error
Systematic error (bias): - Consistent offset in one direction - Example: Sensor always reads 2cm too high - Solution: Calibration
Random error (noise): - Varies unpredictably around true value - Example: Readings fluctuate ยฑ1cm - Solution: Filtering/averaging
Gross error (outliers): - Completely wrong readings - Example: Ultrasonic timeout returns -1 - Solution: Outlier rejection
Quantifying Noise
from picobot import Ultrasonic
import time
sensor = Ultrasonic()
readings = []
print("Hold object at fixed distance. Collecting 100 samples...")
for i in range(100):
dist = sensor.distance_cm()
if dist > 0:
readings.append(dist)
time.sleep_ms(50)
# Statistics
n = len(readings)
mean = sum(readings) / n
variance = sum((x - mean) ** 2 for x in readings) / n
std_dev = variance ** 0.5
min_val = min(readings)
max_val = max(readings)
print(f"\nResults ({n} valid samples):")
print(f" Mean: {mean:.2f} cm")
print(f" Std Dev: {std_dev:.2f} cm")
print(f" Range: {min_val:.1f} - {max_val:.1f} cm")
print(f" Peak-to-peak: {max_val - min_val:.1f} cm")
Signal-to-Noise Ratio
For our purposes: $\(SNR = \frac{\text{Mean reading}}{\text{Standard deviation}}\)$
Higher SNR = cleaner measurement. Typical ultrasonic SNR is 20-50.
Part 4: Signal Conditioning
Averaging (Low-Pass Filter)
Simple averaging reduces random noise:
def average_reading(sensor, n=5):
"""Average n readings to reduce noise."""
total = 0
valid = 0
for _ in range(n):
dist = sensor.distance_cm()
if dist > 0:
total += dist
valid += 1
time.sleep_ms(10)
return total / valid if valid > 0 else -1
Trade-off: More samples = less noise, but slower response.
Moving Average
Keep a running window of recent values:
class MovingAverage:
def __init__(self, size=5):
self.size = size
self.buffer = []
def add(self, value):
self.buffer.append(value)
if len(self.buffer) > self.size:
self.buffer.pop(0)
def get(self):
if not self.buffer:
return None
return sum(self.buffer) / len(self.buffer)
# Usage
ma = MovingAverage(5)
while True:
dist = sensor.distance_cm()
if dist > 0:
ma.add(dist)
print(f"Raw: {dist:.1f} Filtered: {ma.get():.1f}")
Median Filter (Outlier Rejection)
Median is robust against outliers:
def median_reading(sensor, n=5):
"""Take median of n readings (rejects outliers)."""
readings = []
for _ in range(n):
dist = sensor.distance_cm()
if dist > 0:
readings.append(dist)
time.sleep_ms(10)
if not readings:
return -1
readings.sort()
mid = len(readings) // 2
return readings[mid]
When to use: - Averaging: Gaussian noise - Median: Outliers/spikes
Exponential Moving Average (EMA)
Weighted average favoring recent readings:
class ExponentialMA:
def __init__(self, alpha=0.3):
self.alpha = alpha # 0-1, higher = more responsive
self.value = None
def update(self, new_value):
if self.value is None:
self.value = new_value
else:
self.value = self.alpha * new_value + (1 - self.alpha) * self.value
return self.value
# Usage
ema = ExponentialMA(alpha=0.2)
while True:
dist = sensor.distance_cm()
if dist > 0:
filtered = ema.update(dist)
print(f"Raw: {dist:.1f} EMA: {filtered:.1f}")
Part 5: Calibration
Why Calibrate?
Even "accurate" sensors have: - Manufacturing variations - Temperature drift - Mounting differences - Aging effects
Calibration maps raw readings to true values.
Single-Point Calibration (Offset)
If sensor has consistent offset:
# Measure known distance, calculate offset
TRUE_DISTANCE = 50.0 # cm (measured with ruler)
measured = sensor.distance_cm()
OFFSET = TRUE_DISTANCE - measured
# Apply correction
def calibrated_distance():
raw = sensor.distance_cm()
return raw + OFFSET if raw > 0 else -1
Two-Point Calibration (Offset + Gain)
For linear sensors with offset AND scale error:
# Measure at two known distances
TRUE_1, MEASURED_1 = 20.0, 19.2 # Close
TRUE_2, MEASURED_2 = 80.0, 82.5 # Far
# Calculate gain and offset
GAIN = (TRUE_2 - TRUE_1) / (MEASURED_2 - MEASURED_1)
OFFSET = TRUE_1 - GAIN * MEASURED_1
def calibrated_distance():
raw = sensor.distance_cm()
return GAIN * raw + OFFSET if raw > 0 else -1
Lookup Table Calibration
For non-linear sensors:
# Calibration table: (measured, true) pairs
CAL_TABLE = [
(10.0, 10.2),
(20.0, 19.5),
(30.0, 29.8),
(50.0, 51.2),
(80.0, 79.0),
]
def calibrated_distance():
raw = sensor.distance_cm()
if raw < 0:
return -1
# Linear interpolation between calibration points
for i in range(len(CAL_TABLE) - 1):
m1, t1 = CAL_TABLE[i]
m2, t2 = CAL_TABLE[i + 1]
if m1 <= raw <= m2:
# Interpolate
ratio = (raw - m1) / (m2 - m1)
return t1 + ratio * (t2 - t1)
# Outside calibration range
return raw
Part 6: WS2812B Protocol (Deep Dive)
Your RGB LEDs use a precise timing protocol. The picobot library handles this, but here's what happens underneath.
The Protocol
Each bit is encoded by pulse width:
Bit 0: Bit 1:
โโโโ โโโโโโโโโโ
โ โ โ โ
โโโโโ โโโโโโโโโโโโโ โโโโโ โโโโโ
0.4ยตs 0.85ยตs 0.8ยตs 0.45ยตs
HIGH LOW HIGH LOW
Total bit time: 1.25ยตs (800 kHz)
Why Software Can't Do This
At 1.25ยตs per bit, you have ~156 CPU cycles at 125 MHz. Python can't execute fast enough!
Solutions: 1. PIO (Programmable I/O) - RP2040's hardware state machines 2. DMA - Direct memory access, no CPU involvement 3. Assembly - Hand-optimized code (still marginal)
The picobot.LEDStrip uses PIO internally.
Color Order
WS2812B expects GRB order, not RGB:
# What you write:
leds.set_pixel(0, 255, 0, 0) # "Red"
# What gets sent:
# G=0, R=255, B=0 โ appears red
The library handles this conversion.
Mini-Project: Sensor Characterization
Goal: Create a complete characterization of your ultrasonic sensor.
Part A: Accuracy vs Distance
- Set up measurements at 10, 20, 30, 50, 80, 100 cm
- Take 20 readings at each distance
- Calculate mean, std dev, min, max
- Plot accuracy vs distance
Part B: Noise Analysis
- Hold object steady at 50 cm
- Collect 200 readings
- Create histogram of values
- Calculate SNR
Part C: Build Calibration Routine
- Create two-point calibration from your data
- Implement calibrated reading function
- Verify improvement at intermediate distances
Part D: Temperature Compensation (Bonus)
- Read internal temperature sensor
- Adjust speed-of-sound calculation
- Compare accuracy with/without compensation
Deliverable
Submit a report with: - Raw data tables - Statistical analysis - Calibration coefficients - Before/after accuracy comparison - Conclusions about sensor limitations
Key Takeaways
- Sensors are analog - digital readings hide continuous reality
- Every measurement has error - systematic, random, and gross
- Noise can be reduced - averaging, median, EMA each have trade-offs
- Calibration is essential - for accuracy in real applications
- Understand the physics - helps predict failure modes
The Hardware Limits Principle
Software vs Hardware: Know the Difference
A critical skill in embedded systems is distinguishing between problems that software can solve and limits that require different hardware.
Software CAN fix: | Problem | Software Solution | |---------|-------------------| | Noisy readings | Averaging, median filter, EMA | | Temperature drift | Compensation formula | | Non-linear response | Lookup table, polynomial correction | | Timing issues | Better scheduling, interrupts | | Occasional outliers | Outlier rejection, bounds checking |
Software CANNOT fix: | Limitation | Why It's Physical | Solution | |------------|-------------------|----------| | Ultrasonic 2cm minimum range | Near-field acoustics | Different sensor type | | Optocoupler 15ยฐ viewing angle | LED/lens optics | Different sensor geometry | | ADC 12-bit resolution | Quantization is fundamental | Higher-resolution ADC | | Sensor bandwidth (speed) | Physical response time | Faster sensor | | Operating temperature range | Component ratings | Industrial-grade parts |
The Engineering Judgment: - First: understand the datasheet limits - Then: squeeze everything possible with software - Finally: if limits are still hit, select different hardware
Many students waste time trying to "fix" hardware limits with clever code. Physics always wins. The skill is knowing when you've hit the wall.
Real Example: Line Sensor Resolution
Your line sensors are digital (0 or 1). With 4 sensors spaced 10mm apart, your position resolution is ~10mm.
Can software improve this? - Partially: If you add weighted averaging between sensors (0.25, 0.5, 0.75 positions), you get ~5mm effective resolution - No further: To get 1mm resolution, you need either: - More sensors (8 sensors at 5mm spacing) - Analog sensors with ADC - Different technology (camera with line detection)
Software gave you 2x improvement. The next 5x requires hardware change.
Further Reading
- Optocoupler Design Guide - Vishay application note
- Ultrasonic Ranging - MaxBotix tutorials
- Signal Conditioning Basics - Analog Devices