Skip to content

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:

\[v_{sound} = 331.3 + 0.606 \times T_{celsius} \text{ m/s}\]
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

\[SNR = \frac{\text{Signal amplitude}}{\text{Noise amplitude}}\]

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

  1. Set up measurements at 10, 20, 30, 50, 80, 100 cm
  2. Take 20 readings at each distance
  3. Calculate mean, std dev, min, max
  4. Plot accuracy vs distance

Part B: Noise Analysis

  1. Hold object steady at 50 cm
  2. Collect 200 readings
  3. Create histogram of values
  4. Calculate SNR

Part C: Build Calibration Routine

  1. Create two-point calibration from your data
  2. Implement calibrated reading function
  3. Verify improvement at intermediate distances

Part D: Temperature Compensation (Bonus)

  1. Read internal temperature sensor
  2. Adjust speed-of-sound calculation
  3. 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

  1. Sensors are analog - digital readings hide continuous reality
  2. Every measurement has error - systematic, random, and gross
  3. Noise can be reduced - averaging, median, EMA each have trade-offs
  4. Calibration is essential - for accuracy in real applications
  5. 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


โ† Advanced Track Overview | Next: Real-Time Concepts โ†’