Versions this guide targets
- Arduino-ESP32 core 3.3.12 (built on ESP-IDF 5.5.5), the current stable release on GitHub. A 4.0.0 release candidate exists, but the stable Boards Manager index used below installs 3.3.x.
- Adafruit BME280 Library 2.3.0, which depends on Adafruit Unified Sensor and Adafruit BusIO (repository).
- Arduino IDE 2.x or Arduino CLI 1.5. Every step below shows the IDE route first and the CLI commands after it.
Parts
- A classic ESP32 development board (the kind Arduino lists as "ESP32 Dev Module"). ESP32-S2, S3 and C3 boards work too, but their default I2C pins differ, so check your board's pinout.
- A BME280 breakout board. Check the chip: the similar-looking BMP280 measures temperature and pressure only, and the Adafruit library rejects it (see troubleshooting).
- Four jumper wires and a breadboard.
- A USB cable that carries data, not just power.
Wire the sensor to I2C
On a generic ESP32 the Arduino core's default I2C pins are GPIO21 for SDA and GPIO22 for SCL (Arduino-ESP32 I2C API). Unplug the USB cable while you wire.
| BME280 pin | ESP32 pin | Notes |
|---|---|---|
| VIN or VCC | 3V3 | 3.3 V suits both bare and regulated breakouts |
| GND | GND | |
| SCL (sometimes SCK) | GPIO22 | I2C clock |
| SDA (sometimes SDI) | GPIO21 | I2C data |
| CSB (6-pin boards) | 3V3 or unconnected | Must be high for I2C; many boards pull it up already |
| SDO (6-pin boards) | GND or 3V3 | GND gives address 0x76, 3V3 gives 0x77 |
The CSB and SDO behaviour comes from Bosch's BME280 datasheet. Four-pin breakouts set both on the board, which is why some answer at 0x76 and others at 0x77. The scan in step 1 tells you which one you have.
Install the ESP32 core and the sensor library
In Arduino IDE 2, follow Espressif's installation guide:
- Open File → Preferences (Arduino IDE → Settings on macOS) and add this URL to Additional boards manager URLs:
https://espressif.github.io/arduino-esp32/package_esp32_index.json - Open Tools → Board → Boards Manager, search for
esp32and install esp32 by Espressif Systems, version 3.3.12. - Select Tools → Board → esp32 → ESP32 Dev Module, plug in the board, and pick its port under Tools → Port.
- Open Sketch → Include Library → Manage Libraries, search for
Adafruit BME280, install version 2.3.0, and choose to install all dependencies when asked.
If no port appears, try another cable first. If the cable is fine, install the driver for the board's USB-to-serial chip (CP210x and CH340 are common; the chip name is usually printed next to the USB socket).
With Arduino CLI the same setup is four commands. Pinning the versions keeps the build matching this guide:
arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
arduino-cli core install esp32:esp32@3.3.12 --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
arduino-cli lib install "Adafruit BME280 Library@2.3.0"
arduino-cli board list
Check: arduino-cli core list shows esp32:esp32 at 3.3.12, and arduino-cli board list shows a serial port (for example COM5 on Windows or /dev/ttyUSB0 on Linux). The board's FQBN is esp32:esp32:esp32.
Step 1: Find the sensor on the I2C bus
This sketch asks every 7-bit address whether a device is there. A device that acknowledges makes endTransmission() return 0.
#include <Wire.h>
void setup() {
Serial.begin(115200);
delay(500);
Wire.begin(21, 22); // SDA, SCL
Serial.println("Scanning I2C bus...");
int found = 0;
for (uint8_t address = 1; address < 127; address++) {
Wire.beginTransmission(address);
if (Wire.endTransmission() == 0) {
Serial.printf("Device found at 0x%02X\n", address);
found++;
}
}
Serial.printf("Scan finished, %d device(s) found\n", found);
}
void loop() {
}
Upload it with the IDE's Upload button. From the CLI, create the sketch folder with arduino-cli sketch new I2CScan, paste the code into I2CScan/I2CScan.ino, then build and upload (use your own port):
arduino-cli compile --fqbn esp32:esp32:esp32 I2CScan
arduino-cli upload -p COM5 --fqbn esp32:esp32:esp32 I2CScan
Some boards stall at Connecting... during upload. Hold the BOOT button until writing starts.
Check: open the Serial Monitor at 115200 baud and press the board's EN (reset) button, because the scan runs only once at boot. You want exactly one line such as Device found at 0x76. Note that address for the next step. If the scan finds nothing, go to troubleshooting before moving on.
Step 2: Upload the weather sketch
The sketch uses the settings Bosch's datasheet recommends for weather monitoring, which the Adafruit library's advancedsettings example also shows: forced mode, 1× oversampling on all three channels and the IIR filter off. In forced mode the sensor takes one measurement when asked and then sleeps, so it isn't running continuously between readings.
BME280_ADDR to the address your scan printed.#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// Classic ESP32 dev board I2C pins (the core's defaults).
constexpr int SDA_PIN = 21;
constexpr int SCL_PIN = 22;
// The address your I2C scan printed: 0x76 or 0x77.
constexpr uint8_t BME280_ADDR = 0x76;
// Height above sea level in metres. Only used to convert the measured
// (station) pressure into the sea-level figure that weather services publish.
constexpr float STATION_ALTITUDE_M = 0.0F;
// Correction in degrees C after comparing with a reference thermometer (step 4).
constexpr float TEMP_OFFSET_C = 0.0F;
// Time between readings. The datasheet suggests one reading per minute
// (60000 ms) for weather monitoring; 10000 ms is quicker while testing.
constexpr unsigned long INTERVAL_MS = 10000UL;
Adafruit_BME280 bme;
unsigned long lastReadMs = 0;
void setup() {
Serial.begin(115200);
delay(500);
Wire.begin(SDA_PIN, SCL_PIN);
if (!bme.begin(BME280_ADDR, &Wire)) {
Serial.printf("No BME280 at 0x%02X (sensor ID read: 0x%02X)\n",
BME280_ADDR, (unsigned int)bme.sensorID());
while (true) {
delay(1000);
}
}
// Weather-monitoring mode: forced, 1x oversampling, filter off.
bme.setSampling(Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLING_X1, // temperature
Adafruit_BME280::SAMPLING_X1, // pressure
Adafruit_BME280::SAMPLING_X1, // humidity
Adafruit_BME280::FILTER_OFF);
bme.setTemperatureCompensation(TEMP_OFFSET_C);
lastReadMs = millis() - INTERVAL_MS; // take the first reading straight away
}
void loop() {
if (millis() - lastReadMs < INTERVAL_MS) {
return;
}
lastReadMs = millis();
if (!bme.takeForcedMeasurement()) {
Serial.println("BME280 measurement timed out");
return;
}
float tempC = bme.readTemperature();
float humidity = bme.readHumidity();
float stationHpa = bme.readPressure() / 100.0F;
float seaLevelHpa = bme.seaLevelForAltitude(STATION_ALTITUDE_M, stationHpa);
// label:value pairs separated by commas, which the Serial Plotter can graph
Serial.printf("temp_c:%.2f,humidity_pct:%.1f,station_hpa:%.2f,sea_level_hpa:%.2f\n",
tempC, humidity, stationHpa, seaLevelHpa);
}
Before uploading, set BME280_ADDR to the address from step 1 and STATION_ALTITUDE_M to your altitude (a map service or survey marker will give you a value close enough). Upload with the IDE, or from the CLI after creating the sketch folder with arduino-cli sketch new WeatherStation:
arduino-cli compile --fqbn esp32:esp32:esp32 WeatherStation
arduino-cli upload -p COM5 --fqbn esp32:esp32:esp32 WeatherStation
Step 3: Read and sanity-check the output
Open the Serial Monitor at 115200 baud. A new line arrives every 10 seconds with four fields:
temp_c: air temperature at the sensor in °C.humidity_pct: relative humidity in percent. It should stay between 0 and 100 and rise when you breathe near the sensor.station_hpa: the pressure the sensor actually measures. Near sea level expect roughly 1000 hPa. The figure drops by about 1 hPa for every 8 m you climb, so it is noticeably lower at altitude.sea_level_hpa: station pressure converted with your altitude using the library'sseaLevelForAltitude(). This is the number to compare with a local weather report.
Check: cup the sensor in your hand for a few seconds. Temperature and humidity should climb on the next readings and settle back afterwards. Pressure should barely move.
To graph the readings, close the Serial Monitor and open Tools → Serial Plotter. The plotter scales every series on one vertical axis, so temperature and humidity look flat next to pressure values around 1000. Remove the two pressure fields from the printf format string for a while if you want to watch small temperature changes.
Step 4: Calibrate against a reference
Put the sensor next to a thermometer you trust, away from the ESP32 board, and let both settle for several readings. If the BME280 reads consistently high or low, set TEMP_OFFSET_C to the difference (for example -1.2F if it reads 1.2 °C high) and upload again. The library adds this offset inside its compensation maths, so the corrected temperature is also used for the humidity and pressure calculations.
For pressure, compare sea_level_hpa with the sea-level pressure from a nearby weather station at the same time. A steady offset usually means STATION_ALTITUDE_M is off, so correct the altitude rather than the reading.
Step 5: Log readings to a file
Arduino CLI's monitor command can stream the serial output while PowerShell copies it into a file. Close the IDE's Serial Monitor and Serial Plotter first, because only one program can hold the port open.
arduino-cli monitor -p COM5 --config baudrate=115200 --quiet --timestamp | Tee-Object -FilePath weather-log.txt
On Linux or macOS, use the Unix port name and tee:
arduino-cli monitor -p /dev/ttyUSB0 --config baudrate=115200 --quiet --timestamp | tee weather-log.txt
--quiet leaves only the board's output, and --timestamp adds the time each line arrived. Press Ctrl+C to stop. For long runs, change INTERVAL_MS to 60000UL to match the datasheet's one-reading-per-minute weather setting.
Check: after a minute, open weather-log.txt. Each line should hold a timestamp followed by the four label:value fields.
Where to put the sensor
- Keep the breakout a short cable away from the ESP32. The board warms up while it runs, and a sensor sitting right next to it reads that heat as room temperature.
- Keep it out of direct sun and away from radiators, laptops and air vents.
- A breadboard build is for indoor use. Outdoors, the sensor needs a ventilated shield that blocks sun and rain while letting air through, and the electronics need a weatherproof enclosure.
- Don't seal the sensor inside a closed box. It would then measure the air in the box, not the room.
Troubleshooting
The Adafruit library's bme280test example documents what the sensor ID values mean. The weather sketch prints that ID when begin() fails:
sensor ID read: 0x00: nothing answered at that address. Run the scan again and use the address it reports.0x56,0x57or0x58: the chip is a BMP280, which has no humidity sensor. Use a real BME280, or Adafruit's separate BMP280 library if temperature and pressure are enough.0x61: a BME680, which needs its own library.0xFF: usually a wrong address, or an older BMP180/BMP085.
Other common problems:
- The scan finds no devices. Check that SDA goes to GPIO21 and SCL to GPIO22 (swapping them is the usual mistake), that VIN is on 3V3, and that GND is connected. On a six-pin module, make sure CSB isn't pulled low.
- No serial port appears. Swap to a cable you know carries data, then install the USB-to-serial driver for your board's chip.
- Upload fails after
Connecting.... Hold BOOT while the upload starts, as Espressif's installation guide notes. - The monitor shows random characters. Set the baud rate to 115200 to match
Serial.begin(). BME280 measurement timed out. The library waits up to two seconds for a forced measurement. A loose SDA or SCL wire is the likely cause.- The log command reports the port is busy. Close the IDE's Serial Monitor and Serial Plotter, then run it again.
Cleanup
- Stop the logger with Ctrl+C and unplug the board before changing any wiring.
- Delete
weather-log.txtand theI2CScanfolder once you no longer need them. - To remove the libraries, uninstall them in Library Manager, or run
arduino-cli lib uninstall "Adafruit BME280 Library"(then the same command for Adafruit Unified Sensor and Adafruit BusIO if nothing else uses them). - Uploading any other sketch replaces this one. The ESP32 core can stay installed for your next project.
Where to go next
- TinyML Gesture Recognition on Nano 33 BLE Sense or ESP32 reuses this ESP32 I2C wiring with an MPU6050 accelerometer and trains a gesture model in Edge Impulse.
- WLED Smart LED Controller puts a second ESP32 to work driving an addressable LED strip.
- Raspberry Pi Setup Guide covers a headless Pi, which can run arduino-cli and keep the USB log going without your PC.



