IoT 📖 11 min read

ESP32 BME280 Weather Station: Temperature, Humidity and Pressure

A BME280 breakout and a classic ESP32 dev board make a small weather logger that reports over USB. You wire the sensor to the I2C pins, confirm its address with a bus scan, then run a sketch that takes one reading at a time in the sensor's low-power forced mode. Each reading prints temperature, relative humidity, station pressure and sea-level pressure in a format the Arduino Serial Plotter can graph, and the last step saves the readings to a file on your computer with arduino-cli.

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.

Wiring diagram: BME280 VIN to ESP32 3V3, GND to GND, SCL to GPIO22 and SDA to GPIO21.
The four wires: VIN to 3V3, GND to GND, SCL to GPIO22 and SDA to GPIO21, the Arduino core's default I2C pins on a generic ESP32.
BME280 pin ESP32 pin Notes
VIN or VCC3V33.3 V suits both bare and regulated breakouts
GNDGND
SCL (sometimes SCK)GPIO22I2C clock
SDA (sometimes SDI)GPIO21I2C data
CSB (6-pin boards)3V3 or unconnectedMust be high for I2C; many boards pull it up already
SDO (6-pin boards)GND or 3V3GND 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:

Terminal window with the commands from the section Install the ESP32 core and the sensor library
Install the ESP32 core and the BME280 library at pinned versions, then find the board's port.
  1. 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
  2. Open Tools → Board → Boards Manager, search for esp32 and install esp32 by Espressif Systems, version 3.3.12.
  3. Select Tools → Board → esp32 → ESP32 Dev Module, plug in the board, and pick its port under Tools → Port.
  4. 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:

PowerShell / Bash
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.

Editor window showing I2CScan.ino from this section
The I2C scan prints the sensor's address: 0x76 or 0x77.
I2CScan.ino
#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):

PowerShell / Bash
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.

Editor window showing WeatherStation.ino from this section
The top of the weather sketch. Set BME280_ADDR to the address your scan printed.
WeatherStation.ino
#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:

PowerShell / Bash
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's seaLevelForAltitude(). 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.

PowerShell
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:

Bash
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, 0x57 or 0x58: 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.txt and the I2CScan folder 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