Building reliable I2C sensor projects on the ESP32 platform often hits a wall when moving from a single sensor to a multi-device bus. The internal pull-up resistors on the ESP32 are notoriously weak (around 45kΩ), leading to signal degradation and bus lockups when you daisy-chain environmental sensors. In this guide, we are building a robust dual-sensor environmental monitor using the ESP32-S3-DevKitC-1 (N8R8 variant), the Bosch BME688 (gas/pressure/temp/humidity), and the Sensirion SHT4x (precision temp/humidity).
We will cover the exact hardware spec sheet, provide a fully compilable Arduino C++ firmware with hardware error handling, and deeply debug the most infamous ESP32 I2C error: Error -1. If your bus is crashing, the first three things to check are: 1) missing 4.7kΩ external pull-up resistors, 2) I2C address collisions, and 3) wire capacitance exceeding 400pF (wires longer than 30cm).
Project Spec Sheet & Parts List
Before wiring, verify you have the exact board variants listed below. Substituting an original ESP32 for an ESP32-S3 changes the default I2C pin mappings and the underlying FreeRTOS I2C driver behavior.
| Component | Exact Variant / Model | Est. Price (2026) | Purpose & Notes |
|---|---|---|---|
| Microcontroller | ESP32-S3-DevKitC-1 (N8R8) | $11.00 | 8MB Flash, 8MB PSRAM. Dual-core 240MHz. Target board for this firmware. |
| Gas/Env Sensor | Bosch BME688 Breakout (Adafruit 4829) | $22.50 | I2C address 0x77. Measures VOCs, pressure, temp, humidity. Requires Bosch BME68x API. |
| Precision T/H Sensor | Sensirion SHT40 Breakout (Adafruit 4880) | $6.50 | I2C address 0x44. High-accuracy temp/humidity reference for BME688 cross-calibration. |
| Pull-up Resistors | 4.7kΩ 1/4W Carbon Film (x2) | $0.10 | Mandatory for ESP32 I2C buses running at 400kHz. Connect SDA and SCL to 3.3V. |
| Wiring | 26 AWG Silicone Jumper Wires | $8.00 | Keep I2C runs under 30cm to stay below the 400pF I2C bus capacitance limit. |
Hardware Wiring & Pin Mapping
The ESP32-S3 does not have fixed default I2C pins like the original ESP32 (which used GPIO 21/22). We are explicitly assigning GPIO 8 (SDA) and GPIO 9 (SCL) to avoid conflicts with the S3's default SPI flash pins. Both sensors share the same I2C bus, so the 4.7kΩ pull-up resistors only need to be installed once at the top of the bus.
| ESP32-S3 Pin | BME688 Pin | SHT40 Pin | Wire Color | Notes |
|---|---|---|---|---|
| 3V3 | VIN | VIN | Red | Do not use 5V; both sensors are strictly 3.3V logic. |
| GND | GND | GND | Black | Common ground is critical for I2C ACK signaling. |
| GPIO 8 (SDA) | SDA | SDA | Blue | Requires 4.7kΩ pull-up to 3V3. |
| GPIO 9 (SCL) | SCL | SCL | Yellow | Requires 4.7kΩ pull-up to 3V3. |
The I2C specification limits bus capacitance to 400pF. Every centimeter of wire and every sensor pin adds parasitic capacitance. If you use standard breadboard jumper wires longer than 30cm, the signal edges will round off, causing the ESP32 I2C peripheral to misinterpret bits and throw timeout errors. Keep wires short and twisted if possible.
Complete Compilable Firmware
This firmware targets the ESP32-S3-DevKitC-1 using the Arduino IDE (ESP32 Core v3.0.x). It utilizes the Adafruit_BME680 and Adafruit_SHT4x libraries. The code includes explicit pin definitions, hardware initialization error handling, and a non-blocking read loop.
/*
* ESP32-S3 Dual I2C Sensor Monitor
* Target Board: ESP32-S3-DevKitC-1 (N8R8)
* Core Version: ESP32 Arduino Core 3.0.x
* Libraries Required: Adafruit_BME680, Adafruit_SHT4x, Adafruit_Sensor
*/
#include
#include
#include
// --- Pin Definitions ---
#define I2C_SDA 8
#define I2C_SCL 9
#define SEALEVEL_HPA 1013.25
// --- Sensor Objects ---
Adafruit_BME680 bme;
Adafruit_SHT4x sht4 = Adafruit_SHT4x();
unsigned long lastRead = 0;
const unsigned long READ_INTERVAL = 2000; // 2 seconds
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
Serial.println("ESP32-S3 Dual Sensor Boot...");
// Initialize I2C with explicit pins and 400kHz clock
Wire.begin(I2C_SDA, I2C_SCL, 400000);
// Initialize BME688
if (!bme.begin(0x77, &Wire)) {
Serial.println("[FATAL] Could not find a valid BME680/BME688 sensor, check wiring!");
while (1) { delay(10); } // Halt execution
}
// Set up oversampling and IIR filter for BME688
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C for 150 ms
// Initialize SHT40
if (!sht4.begin(&Wire)) {
Serial.println("[FATAL] SHT4x not found. Check I2C address 0x44.");
while (1) { delay(10); }
}
sht4.setPrecision(SHT4X_HIGH_PRECISION);
sht4.setHeater(SHT4X_NO_HEATER);
Serial.println("Both sensors initialized successfully.");
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - lastRead >= READ_INTERVAL) {
lastRead = currentMillis;
// Read SHT40 (Fast, highly accurate reference)
sensors_event_t humidity, temp;
sht4.getEvent(&humidity, &temp);
// Read BME688 (Includes Gas Resistance)
if (!bme.performReading()) {
Serial.println("[ERROR] Failed to perform BME688 reading. I2C bus may be locked.");
return;
}
// Output formatted data
Serial.printf("SHT4x -> Temp: %.2f C | Hum: %.2f %%\n", temp.temperature, humidity.relative_humidity);
Serial.printf("BME688 -> Temp: %.2f C | Hum: %.2f %% | Press: %.2f hPa | Gas: %.2f KOhms\n",
bme.temperature, bme.humidity, bme.pressure / 100.0, bme.gas_resistance / 1000.0);
Serial.println("---------------------------------------------------");
}
}
Debugging Common I2C Failures
When scaling sensor projects on the ESP32, you will inevitably encounter the I2C timeout error. If your serial monitor outputs the exact string below, your I2C peripheral has failed to receive an acknowledge (ACK) bit from the sensor.
[E][Wire.cpp:500] requestFrom(): i2cWriteReadNonStop returned Error -1
This error indicates that the ESP32's I2C state machine timed out waiting for the bus to clear or the slave to respond. Here are the ranked causes and fixes, starting with the most likely:
- Missing or Incorrect Pull-Up Resistors: The ESP32-S3 internal pull-ups are roughly 45kΩ. The I2C spec requires the bus to be pulled up to VCC (3.3V) strongly enough to reach the logic-high threshold within the rise-time limit. Fix: Solder or breadboard two 4.7kΩ resistors between the SDA/SCL lines and the 3.3V rail. Do not use 10kΩ if running at 400kHz; the RC time constant will be too slow.
- I2C Address Collision or Unresponsive Device: If the BME688 and SHT4x share an address, or if a sensor is dead, the bus will hang. The BME688 defaults to 0x77 (if SDO is tied to GND) or 0x76 (SDO to VCC). The SHT4x is hardcoded to 0x44. Fix: Run Nick Gammon's
i2c_scannersketch. If you don't see 0x77 and 0x44, check your physical wiring and ensure the sensor breakout boards have voltage regulators (some raw modules require 1.8V logic, which the ESP32 will fry or fail to read). - Bus Capacitance Overload: If you are using ribbon cables or long jumper wires (>30cm), the parasitic capacitance exceeds 400pF. The signal edges become sloped, and the sensor misses the clock pulses. Fix: Shorten the wires, drop the I2C clock speed to 100kHz in the
Wire.begin()call, or use an I2C bus extender like the PCA9615.
Extending and Simplifying the Build
Depending on your end goal, you can easily modify this hardware and firmware baseline.
How to Extend: To turn this into a smart home node, add the PubSubClient library and connect to an MQTT broker (like Mosquitto or Home Assistant). Format the sensor readings into a JSON payload using ArduinoJson and publish to a topic like home/environmental/livingroom. You can also leverage the ESP32-S3's deep sleep capabilities, waking every 5 minutes via an external RTC interrupt to take a reading, push via WiFi, and sleep, reducing average current draw to under 2mA.
How to Simplify: If you do not need VOC (Volatile Organic Compound) or barometric pressure data, drop the BME688 entirely. The SHT40 alone provides superior temperature and humidity accuracy (±0.2°C) compared to the BME688's internal thermal mass compensation. Removing the BME688 saves roughly $15 in BOM costs, eliminates the need for the 3.3V/5V logic level shifting (if you were using a 5V tolerant setup), and reduces the firmware footprint by removing the complex gas heater calibration logic.
Frequently Asked Questions About Sensor Projects
What are the best sensor projects for beginners in 2026?
For beginners, the best starting point is a single-sensor I2C project using the BME280 or AHT20. These sensors are highly forgiving, have robust 3.3V/5V tolerance on most breakout boards, and do not require complex heater calibration routines. Once you master reading a single sensor and displaying it on an SSD1306 OLED, you can graduate to multi-sensor I2C buses and SPI-based sensors like the SCD41 CO2 monitor.
Why do my I2C sensor projects crash when I add a third sensor?
Adding a third sensor increases the physical length of the bus and the total parasitic capacitance. Furthermore, each sensor's internal pull-up or protection circuitry adds slight leakage. When you hit three or more devices, the standard 4.7kΩ pull-ups often fail to pull the bus high fast enough at 400kHz. The fix is to either drop the bus speed to 100kHz (Wire.setClock(100000)) or use stronger pull-ups (e.g., 2.2kΩ), provided your sensors can handle the increased current sink (typically max 3mA per I2C spec).
How do I calibrate gas sensor projects for indoor air quality?
Metal-oxide (MOx) gas sensors like the BME688 do not output specific ppm (parts per million) values for gases like CO2 or NOx out of the box. They output a raw resistance value in Ohms that changes based on the concentration of reducing or oxidizing gases. To get an actionable Indoor Air Quality (IAQ) index, you must use Bosch's BSEC (Bosch Software Environmental Cluster) library. BSEC runs a proprietary background algorithm that tracks baseline resistance over the first 4 to 24 hours of operation, dynamically calibrating the sensor to your specific room's "clean air" baseline.






