There is a specific kind of frustration reserved for the moment your temperature sensor reads 85°C when you know the room is at 22°C. Or when your motion sensor triggers on its own, twice an hour, at random. Or when your I2C bus works for five minutes and then locks up. Almost all of these have the same root cause: something about the wiring, the voltage level, or the pull-up resistors that the tutorial did not mention.
This article covers the interface types you will encounter, the common sensors you will use, and the practical details that make the difference between a project that works and a project that works for thirty seconds.
The interface zoo
Sensors communicate with your microcontroller over one of a handful of standard interfaces. Knowing which is which saves you hours of debugging.
Digital GPIO
The simplest case: a pin is either high or low. Motion sensors (PIR), limit switches, reed switches, and many hobby-grade detectors work this way. Connect the sensor's output to any GPIO, read digitalRead(pin), and you are done.
Analog input
The sensor produces a voltage proportional to the measurement. LDRs (light), thermistors (temperature), MQ-series gas sensors, and potentiometers all produce analog output. The microcontroller's ADC converts the voltage to a number (0–1023 on Uno's 10-bit ADC, 0–4095 on ESP32's 12-bit ADC). The ESP32's ADC has notorious non-linearity near the rails, which matters if you care about accuracy.
I2C (Inter-Integrated Circuit)
Two-wire bus: SDA (data) and SCL (clock). Multiple devices share the same two wires, each with a unique 7-bit address. Wildly common: BME280, MPU6050, OLED displays, RTCs, ADCs. Requires pull-up resistors on both lines (4.7 kOhm is the default, some breakout boards include them, some don't). The Uno's I2C pins are A4 (SDA) and A5 (SCL); on ESP32 the defaults are GPIO21 and GPIO22.
SPI (Serial Peripheral Interface)
Four-wire bus: MOSI (master out), MISO (master in), SCK (clock), and a dedicated chip-select (CS) per device. Faster than I2C but uses more pins. Common for SD cards, displays (ILI9341, Sharp Memory LCD), and some high-speed ADCs. Each device needs its own CS pin.
UART (serial)
Asynchronous two-wire: TX and RX. Used by GPS modules, fingerprint sensors, cellular modems, and any sensor that outputs a continuous stream. Baud rate must match on both ends. On an Arduino you get one hardware UART and can software-emulate more (with caveats); on ESP32 you get three.
1-Wire
Single-wire protocol invented by Dallas Semiconductor. DS18B20 temperature probes are the classic example. One pin can drive dozens of sensors on the same wire, each with a unique 64-bit address. Uses special timing, needs a 4.7 kOhm pull-up.
Voltage levels: the first thing to get right
An Uno R3 is 5V. An ESP32 is 3.3V. Most cheap sensor modules are rated for both, but some are not, and mixing voltage levels is the number-one cause of dead hardware in hobby projects.
Two specific dangers:
- Connecting a 5V sensor output to a 3.3V microcontroller input. If the sensor pulls the line above 3.6V, you may damage the MCU's GPIO pin. Use a level shifter (a simple resistor divider works for one-way signals; use an I2C level shifter IC for bidirectional buses).
- Powering a 3.3V-only sensor from 5V. Sensors with a 3.3V LDO on the breakout board are fine; sensors without are not. Read the datasheet.
When in doubt, power everything from 3.3V and use ESP32 or Arduino boards with 3.3V logic (Due, Nano 33 series). Most modern sensors are natively 3.3V anyway.
The common sensors, with honest assessments
DHT22 (temperature + humidity)
The classic starter sensor. 3–5V, single data pin, proprietary 1-wire protocol (not the Dallas 1-Wire). ±0.5°C accuracy, ±2% RH. About $3.
What nobody tells you: the DHT22 is slow. One reading takes 2 seconds and you cannot sample faster than every 2 seconds without getting garbage. The protocol is also quite timing-sensitive, which interferes with WiFi on an ESP8266/ESP32 (the WiFi interrupts can disrupt the DHT timing). For a better experience, upgrade to the SHT30/SHT31, which speaks I2C and costs the same.
#include "DHT.h"
#define DHT_PIN 2
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
void setup() {
Serial.begin(115200);
dht.begin();
}
void loop() {
delay(2000); // minimum spacing between reads
float h = dht.readHumidity();
float t = dht.readTemperature();
if (isnan(h) || isnan(t)) {
Serial.println("DHT read failed");
return;
}
Serial.printf("%.1f C, %.1f%% RH\n", t, h);
}BME280 / BMP280 (pressure + temperature + humidity)
The gold standard for environmental sensing. BME280 measures pressure, temperature, and humidity; BMP280 drops the humidity. I2C or SPI, 3.3V only. ±0.5°C accuracy, ±3% RH, ±1 hPa pressure. About $5 for a genuine Bosch breakout, $2–3 for a clone that may or may not actually be a BME280 (many clones are BMP280 with the humidity spec just lied about).
Default I2C address is 0x76 or 0x77 depending on the SDO pin. Multiple sensors on the same bus require different addresses or separate buses. Adafruit's library is clean and widely used.
#include <Wire.h>
#include <Adafruit_BME280.h>
Adafruit_BME280 bme;
void setup() {
Serial.begin(115200);
if (!bme.begin(0x76)) {
Serial.println("BME280 not found at 0x76");
while (1) delay(1000);
}
}
void loop() {
float t = bme.readTemperature(); // degrees Celsius
float h = bme.readHumidity(); // % RH
float p = bme.readPressure() / 100.0F; // Pa -> hPa
Serial.printf("%.2f C %.2f%% RH %.2f hPa\n", t, h, p);
delay(5000);
}MPU6050 (6-axis IMU: accelerometer + gyroscope)
Cheap, widely available, I2C, 3.3V. Three-axis accelerometer and three-axis gyroscope. About $3. The MPU9250 adds a magnetometer for nine-axis; the MPU6500 is an updated MPU6050.
The chip is genuinely capable (the on-chip DMP can do quaternion fusion), but the documentation is patchy and the calibration procedure is non-obvious. Expect to spend a day or two getting reliable orientation data out of one. For serious work, the BNO055 or BNO085 ($15) are dramatically easier because they do sensor fusion onboard.
HC-SR04 (ultrasonic distance)
The ubiquitous ranging sensor. 5V, four pins (VCC, GND, Trig, Echo). Sends an ultrasonic pulse and measures the return time. About $2. Range: 2 cm to 400 cm, accuracy ±3 mm in ideal conditions.
Gotcha: the Echo pin is 5V-referenced output. Connecting it directly to an ESP32 can damage the pin. Use a voltage divider (two resistors: 1 kOhm and 2 kOhm) between Echo and the ESP32 GPIO. The HC-SR04P is a 3.3V-tolerant variant that avoids this problem; confirm the part number before wiring.
const int TRIG_PIN = 9;
const int ECHO_PIN = 10;
void setup() {
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
}
long measureCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long us = pulseIn(ECHO_PIN, HIGH, 30000); // 30 ms timeout
if (us == 0) return -1; // no echo / out of range
return us * 0.0343 / 2; // speed of sound
}
void loop() {
long cm = measureCm();
if (cm < 0) Serial.println("out of range");
else Serial.printf("%ld cm\n", cm);
delay(200);
}PIR motion sensor (HC-SR501)
Passive infrared motion detector. 5–20V power supply, 3.3V TTL output. About $2. Two pots: sensitivity and hold time (how long the output stays HIGH after detection).
Two genuine annoyances. First, the sensor needs 30–60 seconds after power-on to stabilise — during this warm-up period it will fire falsely. Second, it detects motion via infrared and is influenced by air currents, heating vents, and sunlight shifting across a wall. Place it carefully.
DS18B20 (digital temperature probe)
Dallas 1-Wire temperature sensor. Comes as a naked TO-92 chip or more commonly as a waterproof stainless-steel probe on a cable. 3.3–5.5V, one data pin. ±0.5°C accuracy. About $2–4 for a probe.
Needs a 4.7 kOhm pull-up between the data line and VCC. Multiple probes can share a single data pin; each has a unique 64-bit address. The OneWire and DallasTemperature libraries make this painless.
#include <OneWire.h>
#include <DallasTemperature.h>
#define ONE_WIRE_PIN 4
OneWire oneWire(ONE_WIRE_PIN);
DallasTemperature sensors(&oneWire);
void setup() {
Serial.begin(115200);
sensors.begin();
Serial.printf("%d sensor(s) found on bus\n", sensors.getDeviceCount());
}
void loop() {
sensors.requestTemperatures(); // trigger conversion on all probes
int n = sensors.getDeviceCount();
for (int i = 0; i < n; i++) {
float t = sensors.getTempCByIndex(i);
Serial.printf("Sensor %d: %.2f C\n", i, t);
}
delay(1000);
}MQ-series gas sensors (MQ-2, MQ-7, MQ-135 etc.)
Cheap analog gas sensors. Detect smoke, CO, alcohol, CO2 depending on the variant. 5V, analog output, about $3. Low accuracy, high drift, notorious for false positives, needs 24–48 hours of burn-in before readings stabilise.
These are fine for a classroom demo or a hobby fire alarm. Do not put one in a production safety-critical device. If you need real gas sensing, look at CCS811 (CO2/VOC), SGP30, or MH-Z19 (dedicated NDIR CO2).
An example I2C bus
GPIO21 SDA
GPIO22 SCL]] MCU ---|SDA| Bus1[SDA line] MCU ---|SCL| Bus2[SCL line] Bus1 --- R1[4.7k pull-up
to 3.3V] Bus2 --- R2[4.7k pull-up
to 3.3V] Bus1 --- BME[BME280
addr 0x76] Bus2 --- BME Bus1 --- MPU[MPU6050
addr 0x68] Bus2 --- MPU Bus1 --- OLED[SSD1306 OLED
addr 0x3C] Bus2 --- OLED
A single I2C bus can carry many devices. Each needs a unique address. Pull-up resistors tie both lines to VCC so that any device can pull them low.
Reading sensors reliably
Debouncing mechanical inputs
Buttons and reed switches bounce. When you press a button, the contact physically bounces against the target and produces dozens of rapid state changes over 5–20 ms. Reading the pin repeatedly during that window gives you spurious events. The fix: wait 20 ms after a state change before accepting the new state as stable.
const int BTN = 2;
int lastState = HIGH;
unsigned long lastChange = 0;
const unsigned long DEBOUNCE_MS = 20;
void loop() {
int current = digitalRead(BTN);
if (current != lastState) {
lastChange = millis();
lastState = current;
}
if (millis() - lastChange > DEBOUNCE_MS && current == LOW) {
// button is stably pressed
}
}Interrupt-driven reading for edge-triggered sensors
For sensors that signal an event (motion detection, pulse counting, encoder edges), polling in the main loop can miss events if the loop is slow. Attach an interrupt to the pin and handle the event in an ISR. Keep ISRs short — set a flag and process it in the main loop. On ESP32, mark any variable shared between ISR and main loop as volatile and use IRAM_ATTR on the ISR function to keep it in IRAM.
Smoothing noisy analog readings
Raw ADC readings jump around. For slowly changing values (temperature, light level), an exponential moving average is cheap and effective:
float smoothed = 0;
const float ALPHA = 0.1; // 0 = no update, 1 = no smoothing
void loop() {
int raw = analogRead(A0);
smoothed = ALPHA * raw + (1 - ALPHA) * smoothed;
}Interrupt-driven sensor reading, visualised
Set flag = true ISR-->>MainLoop: (return) MainLoop->>MainLoop: Check flag
flag is true MainLoop->>MainLoop: Read data,
process,
clear flag MainLoop->>MainLoop: Continue loop
An interrupt service routine records the event minimally and returns; the main loop does the heavy work. Keeps ISR fast and predictable.
Frequently Asked Questions
Do I need external pull-up resistors on I2C?
Usually yes, unless your breakout board includes them. Most sensor breakouts from Adafruit and SparkFun include 10 kOhm pull-ups. Raw chips and many cheap AliExpress modules do not. If your bus does not work, add 4.7 kOhm pull-ups from SDA and SCL to VCC and try again.
Why does my ESP32 reboot randomly when using a sensor?
Four common causes: (1) drawing too much current from the 3.3V rail — add a capacitor or use external power; (2) a floating GPIO pin triggering spurious interrupts; (3) blocking too long inside an ISR and starving the WiFi stack; (4) stack overflow because you allocated a large buffer on the stack. Check the serial monitor for the panic/guru meditation output.
How do I read a sensor in the background while doing other work?
On Arduino, use millis()-based scheduling: check if (millis() - lastRead >= INTERVAL_MS) in every loop iteration. On ESP32 with FreeRTOS, create a dedicated task with xTaskCreate that reads the sensor and pushes to a queue. See our article on common project patterns for the full set of techniques.
Can I use multiple sensors on the same I2C bus?
Yes, up to the practical limit of about 10 devices per bus (electrical loading and total cable length become issues beyond that). Each device needs a unique address. If two sensors share an address (common with multiple OLED displays), use a TCA9548A I2C multiplexer to switch between them, or put them on separate I2C buses (ESP32 has two).
What is the most reliable temperature sensor?
DS18B20 for a waterproof probe in the 0–100°C range, used everywhere from beer brewing to reef tanks. Sensirion SHT30 or SHT31 for indoor air measurements with excellent humidity accuracy. BME280 if you also want barometric pressure. All three significantly outperform the DHT22 people tend to reach for first.
Share your thoughts
Worked with this in production and have a story to share, or disagree with a tradeoff? Email us at support@mybytenest.com — we read everything.