A sensor is a device that converts a physical quantity into an electrical signal that we can read.
So far, we have learned to use GPIO pins and the ADC from a purely electronic point of view. We know how to read a 1 or a 0, and we know how to read a voltage from 0V to 3.3V.
But in the real world, we are not interested in voltage. We want to know if there is someone in the room (Digital) or how much light there is (Analog).
In this post, we are going to connect real sensors and, most importantly, learn to process their data so that it makes sense within our program.
Digital sensors: all or nothing
A digital sensor is, in essence, an automatic switch. It only has two possible states: Activated or Deactivated.
Typical examples:
- PIR (HC-SR501): Detects infrared motion.
- IR Obstacle Sensor: Detects if something reflects light nearby.
- Tilt Sensor: A metal ball that closes the circuit if moved.
- Hall Effect Sensor: Detects magnetic fields.
Voltage levels: 3.3V and 5V
Beware of Arduino sensors! Many cheap sensor modules are designed for 5V. If a digital sensor “sends” 5V on its output pin when activated, and you connect it directly to your ESP32 or Raspberry Pi Pico (which operate at 3.3V), you can burn the pin.
- PIR HC-SR501: Runs on 5V, but its output signal is usually 3.3V (compatible).
- Industrial sensors: Often use 12V or 24V (require optocouplers).
Example: PIR motion sensor
Let’s use a PIR HC-SR501 sensor. It has 3 pins: VCC (5V), GND, and OUT. When it detects motion, it sets the OUT pin HIGH (3.3V) for a period.
from machine import Pin
import time
# Configure the sensor pin as INPUT
# No pull-up needed because the sensor already sends active voltage
pir = Pin(14, Pin.IN)
# LED to indicate detection
led = Pin(2, Pin.OUT)
print("Starting surveillance...")
time.sleep(2) # The PIR needs time to "calibrate" on startup
while True:
state = pir.value()
if state == 1:
print("Motion detected!")
led.value(1)
else:
led.value(0)
time.sleep(0.1)
For this type of sensor that keeps the signal active for a few seconds, polling (the while loop) works well. For very fast sensors (like a Hall effect sensor when passing a wheel), we should use Interrupts (IRQ) as we saw in previous chapters.
Analog sensors: a continuous spectrum
Here things get interesting. We want to measure quantities that vary smoothly: light intensity, temperature, soil moisture, applied force.
Most of these basic sensors are Resistive. This means that their electrical resistance varies according to the physical quantity.
- LDR (Photoresistor): Less light = More resistance.
- NTC Thermistor: More heat = Less resistance.
- Potentiometer: Varies with rotation.
The voltage divider
The ADC on our microcontroller only knows how to measure voltage, not resistance. You cannot connect an LDR directly between the pin and GND. It won’t work.
We need to convert that resistance variation into a voltage variation. For this, we use the most famous circuit in electronics: the Voltage Divider.
We connect the sensor in series with a fixed resistor (usually 10kΩ).
- VCC (3.3V) -> Pin 1 of the LDR.
- Midpoint -> Pin 2 of LDR + 10k Resistor + ADC Pin.
- GND -> The other end of the 10k resistor.
Example: twilight switch with LDR
Let’s read an LDR. Remember that the ADC in MicroPython returns a value from 0 to 65535 (u16).
from machine import Pin, ADC
import time
# ADC configuration
# On ESP32: Pin 34 (ADC1) is input-only, ideal for sensors
# On Pico: Pin 26 (ADC0)
light_sensor = ADC(Pin(34))
# Attenuation (ESP32 only): To read the full 0-3.3V range
# light_sensor.atten(ADC.ATTN_11DB)
while True:
# 1. Raw reading
raw_reading = light_sensor.read_u16()
# 2. Convert to Voltage (Optional, for debug)
voltage = (raw_reading / 65535) * 3.3
# 3. Control logic
# Experimentally, we see that ambient light gives about 40000
# and covering it drops to 10000 (or rises, depending on your LDR/Resistor wiring)
print(f"RAW Value: {raw_reading} | Voltage: {voltage:.2f}V")
if raw_reading < 20000: # Adjust this threshold based on your resistor
print("It's dark -> Turn on lights")
else:
print("There is light -> Turn off lights")
time.sleep(0.5)
Mapping values with a map function
Often we don’t want to work with strange numbers like 45320, but with a percentage 0-100%. Arduino has the map() function, but Python doesn’t.
Let’s create our own mapping function to normalize sensors.
def map_value(x, in_min, in_max, out_min, out_max):
# Rule of three to scale the value
value = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
# Clamp to prevent going out of range
return max(min(value, out_max), out_min)
# Example usage with a potentiometer
# Suppose our pot doesn't perfectly reach 0 or 65535 due to tolerances
raw_value = light_sensor.read_u16()
# Convert from 1000-60000 to 0-100%
percentage = map_value(raw_value, 1000, 60000, 0, 100)
print(f"Light: {int(percentage)} %")
Noise filtering with averaging
Analog sensors often have “noise”. If you read the sensor very fast, you’ll see the values fluctuate: 34050, 34100, 33950. This can cause your system to flicker erratically.
The simplest software solution is to do a Simple Average.
def read_average(adc_pin, samples=10):
total = 0
for _ in range(samples):
total += adc_pin.read_u16()
time.sleep_ms(5) # Small pause between readings
return total // samples
# Usage
stable_reading = read_average(light_sensor)
Before connecting a sensor
Integrating sensors is not just about connecting wires. It requires:
- Knowing if it is Digital (States) or Analog (Values).
- Ensuring proper voltages (watch out for 5V).
- Using Voltage Dividers for resistive sensors.
- Processing the data (mapping and filtering) to make it useful.
With these fundamentals, you can now connect almost any simple sensor you find on the market.