Built-in project · Arduino Uno R3

Indoor and outdoor thermometer on two OLEDs, on an Arduino Uno

Two BME280s and two SSD1306 OLEDs share their addresses, so a TCA9548A puts each pair on its own channel: each screen shows its own weather. The whole build, an Arduino Uno and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 30 minutes to build and run. Runs in your browser. Free, and no account needed.

Build it as the Weather Station Kit: the parts in numbered bags and a booklet that checks every step. Retails for about $103, free on Mokxi.

The circuit runs here once the page has loaded.

What you'll learn

  • Why identical I2C addresses clash on one bus
  • Switching between devices with a TCA9548A multiplexer
  • Reading a BME280 for temperature, humidity and pressure
  • Sharing one display object between two screens
  • Tracking a low and high value since start-up
  • Difficulty: Intermediate
  • Time: About 30 minutes
  • Board: Arduino Uno R3
  • Parts: 7

How it works

Both BME280 sensors answer at 0x76 and both OLED screens answer at 0x3C. On one I2C bus they would collide. A TCA9548A multiplexer at 0x70 gives each pair its own channel: indoor on channel 0, outdoor on channel 1.

Before it talks to a pair, the sketch calls mux.selectChannel, so only that pair is connected to the Uno. One display object serves both screens. Its frame is redrawn and sent to each screen in turn, which saves the Uno's limited RAM.

Every 2 seconds each screen shows its own temperature, humidity and pressure, plus the lowest and highest temperature since power-up. The Serial Monitor prints both readings too.

Parts list

7 parts, plus the jumper wires. They retail for about $52; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
I2C multiplexer, TCA9548A pin SCLArduino Uno R3 pin SCL
I2C multiplexer, TCA9548A pin SDAArduino Uno R3 pin SDA
I2C multiplexer, TCA9548A pin VINArduino Uno R3 pin 5V
Pressure, humidity and temperature, BME280 (1) pin VINArduino Uno R3 pin 5V
OLED display, 128x64 (1) pin VCCArduino Uno R3 pin 5V
Pressure, humidity and temperature, BME280 (2) pin VINArduino Uno R3 pin 5V
OLED display, 128x64 (2) pin VCCArduino Uno R3 pin 5V
I2C multiplexer, TCA9548A pin GNDArduino Uno R3 pin GND
Pressure, humidity and temperature, BME280 (1) pin GNDArduino Uno R3 pin GND
OLED display, 128x64 (1) pin GNDArduino Uno R3 pin GND
Pressure, humidity and temperature, BME280 (2) pin GNDArduino Uno R3 pin GND
OLED display, 128x64 (2) pin GNDArduino Uno R3 pin GND
Pressure, humidity and temperature, BME280 (1) pin SDAI2C multiplexer, TCA9548A pin SD0
OLED display, 128x64 (1) pin SDAI2C multiplexer, TCA9548A pin SD0
Pressure, humidity and temperature, BME280 (1) pin SCLI2C multiplexer, TCA9548A pin SC0
OLED display, 128x64 (1) pin SCLI2C multiplexer, TCA9548A pin SC0
Pressure, humidity and temperature, BME280 (2) pin SDAI2C multiplexer, TCA9548A pin SD1
OLED display, 128x64 (2) pin SDAI2C multiplexer, TCA9548A pin SD1
Pressure, humidity and temperature, BME280 (2) pin SCLI2C multiplexer, TCA9548A pin SC1
OLED display, 128x64 (2) pin SCLI2C multiplexer, TCA9548A pin SC1

The note on the canvas

Word for word, as it sits beside the circuit in the editor.

Try it
1. Press Run. The left screen shows INDOOR, the right one OUTDOOR, both at 22.0 C.
2. Click the outdoor BME280 and drag its temperature below zero. Only the OUTDOOR screen changes, and its Lo value follows.
3. Change the indoor humidity: only the INDOOR screen changes.
4. The serial monitor prints both readings every two seconds.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Indoor and outdoor weather on two OLEDs, through a TCA9548A.
//
// Two BME280s and two SSD1306 OLEDs, and every one of them has the same
// I2C address as its twin: the BME280s answer at 0x76 and the screens at
// 0x3C. On one bus they would talk over each other. A TCA9548A I2C
// multiplexer (at 0x70) keeps them apart: the indoor sensor and screen are
// on its channel 0, the outdoor pair on channel 1, and the sketch selects a
// channel before it talks to the pair on it.
//
//   TCA9548A  SDA -> SDA (A4)  SCL -> SCL (A5)  VIN -> 5V  GND -> GND
//   channel 0 (SD0, SC0): indoor BME280 and indoor OLED
//   channel 1 (SD1, SC1): outdoor BME280 and outdoor OLED
//
// One display object drives both screens. Its 1 KB frame is redrawn and
// sent for each screen in turn, which leaves room in the Uno's 2 KB of RAM
// that a second frame would take. Each screen still needs its own begin(),
// with its channel selected, to be set up.
//
// Every two seconds each screen shows its own temperature, humidity and
// pressure, with the lowest and highest temperature since power-up, and the
// serial monitor prints both.
//
// Things to try. Drag the outdoor sensor's temperature below zero and watch
// only the outdoor screen change. Change TEMP_IN_F to true for Fahrenheit.

#include <Wire.h>
#include <TCA9548.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_BME280.h>

const uint8_t MUX_ADDRESS = 0x70;
const uint8_t BME_ADDRESS = 0x76;
const uint8_t OLED_ADDRESS = 0x3C;
const uint8_t INDOOR = 0;   // the multiplexer channel of the indoor pair
const uint8_t OUTDOOR = 1;  // and of the outdoor pair
const unsigned long INTERVAL_MS = 2000;
const bool TEMP_IN_F = false;

TCA9548 mux(MUX_ADDRESS);
Adafruit_SSD1306 display(128, 64, &Wire, -1);
Adafruit_BME280 bmeIn;
Adafruit_BME280 bmeOut;

struct Reading {
  float temp;
  float humidity;
  float pressure;
  float low;
  float high;
  bool ok;
};

Reading indoor = {0, 0, 0, 1000, -1000, false};
Reading outdoor = {0, 0, 0, 1000, -1000, false};
unsigned long lastRead = 0;

float shown(float c) {
  return TEMP_IN_F ? c * 9.0 / 5.0 + 32.0 : c;
}

// Set up the screen and the sensor on one channel.
bool startPair(uint8_t channel, Adafruit_BME280 &bme, const char *name) {
  mux.selectChannel(channel);
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    Serial.print(name);
    Serial.println(F(": no OLED at 0x3C"));
  }
  bool found = bme.begin(BME_ADDRESS);
  if (!found) {
    Serial.print(name);
    Serial.println(F(": no BME280 at 0x76"));
  }
  return found;
}

void readPair(uint8_t channel, Adafruit_BME280 &bme, Reading &r) {
  mux.selectChannel(channel);
  r.temp = bme.readTemperature();
  r.humidity = bme.readHumidity();
  r.pressure = bme.readPressure() / 100.0;
  if (r.temp < r.low) r.low = r.temp;
  if (r.temp > r.high) r.high = r.temp;
}

void drawPair(uint8_t channel, const char *title, const Reading &r) {
  mux.selectChannel(channel);
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print(title);
  if (!r.ok) {
    display.setCursor(0, 24);
    display.print(F("No BME280 found"));
    display.display();
    return;
  }
  display.setTextSize(2);
  display.setCursor(0, 14);
  display.print(shown(r.temp), 1);
  display.print(TEMP_IN_F ? F(" F") : F(" C"));
  display.setTextSize(1);
  display.setCursor(0, 36);
  display.print(F("Humidity "));
  display.print(r.humidity, 0);
  display.print(F(" %"));
  display.setCursor(0, 46);
  display.print(r.pressure, 1);
  display.print(F(" hPa"));
  display.setCursor(0, 56);
  display.print(F("Lo "));
  display.print(shown(r.low), 1);
  display.print(F("  Hi "));
  display.print(shown(r.high), 1);
  display.display();
}

void printReading(const char *name, const Reading &r) {
  Serial.print(name);
  if (!r.ok) {
    Serial.print(F(" no sensor"));
    return;
  }
  Serial.print(' ');
  Serial.print(r.temp, 1);
  Serial.print(F(" C "));
  Serial.print(r.humidity, 0);
  Serial.print(F(" % "));
  Serial.print(r.pressure, 0);
  Serial.print(F(" hPa"));
}

void setup() {
  Serial.begin(9600);
  Wire.begin();
  if (!mux.begin()) {
    Serial.println(F("No TCA9548A at 0x70: check SDA, SCL and its power"));
    while (true);
  }
  indoor.ok = startPair(INDOOR, bmeIn, "indoor");
  outdoor.ok = startPair(OUTDOOR, bmeOut, "outdoor");
  Serial.println(F("two screens ready"));
  lastRead = millis() - INTERVAL_MS;
}

void loop() {
  if (millis() - lastRead < INTERVAL_MS) return;
  lastRead = millis();
  if (indoor.ok) readPair(INDOOR, bmeIn, indoor);
  if (outdoor.ok) readPair(OUTDOOR, bmeOut, outdoor);
  drawPair(INDOOR, "INDOOR", indoor);
  drawPair(OUTDOOR, "OUTDOOR", outdoor);
  printReading("indoor", indoor);
  Serial.print(F("  "));
  printReading("outdoor", outdoor);
  Serial.println();
}

Code walkthrough

The key functions of the sketch, and what each one does.

startPair sketch.ino

This sets up one channel. It selects the channel, starts the OLED at OLED_ADDRESS, then starts the BME280 at BME_ADDRESS. It prints a message if either is missing, and returns whether the sensor was found. Each screen needs its own begin with its own channel selected.

// Set up the screen and the sensor on one channel.
bool startPair(uint8_t channel, Adafruit_BME280 &bme, const char *name) {
  mux.selectChannel(channel);
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    Serial.print(name);
    Serial.println(F(": no OLED at 0x3C"));
  }
  bool found = bme.begin(BME_ADDRESS);
  if (!found) {
    Serial.print(name);
    Serial.println(F(": no BME280 at 0x76"));
  }
  return found;
}

readPair sketch.ino

This selects a channel and reads that channel's sensor. It stores temperature, humidity, and pressure converted to hPa. It also updates the lowest and highest temperature seen so far, kept in the reading's low and high.

void readPair(uint8_t channel, Adafruit_BME280 &bme, Reading &r) {
  mux.selectChannel(channel);
  r.temp = bme.readTemperature();
  r.humidity = bme.readHumidity();
  r.pressure = bme.readPressure() / 100.0;
  if (r.temp < r.low) r.low = r.temp;
  if (r.temp > r.high) r.high = r.temp;
}

drawPair sketch.ino

This selects a channel, clears the shared display, and draws the title, the temperature in large text, humidity, pressure, and the low and high values. If the sensor was not found, it shows a No BME280 found message instead.

void drawPair(uint8_t channel, const char *title, const Reading &r) {
  mux.selectChannel(channel);
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print(title);
  if (!r.ok) {
    display.setCursor(0, 24);
    display.print(F("No BME280 found"));
    display.display();
    return;
  }
  display.setTextSize(2);
  display.setCursor(0, 14);
  display.print(shown(r.temp), 1);
  display.print(TEMP_IN_F ? F(" F") : F(" C"));
  display.setTextSize(1);
  display.setCursor(0, 36);
  display.print(F("Humidity "));
  display.print(r.humidity, 0);
  display.print(F(" %"));
  display.setCursor(0, 46);
  display.print(r.pressure, 1);
  display.print(F(" hPa"));
  display.setCursor(0, 56);
  display.print(F("Lo "));
  display.print(shown(r.low), 1);
  // ... 4 more lines in the editor

loop sketch.ino

Using millis, it waits until INTERVAL_MS has passed. Then it reads both sensors, draws both screens, and prints both readings to the Serial Monitor on one line.

void loop() {
  if (millis() - lastRead < INTERVAL_MS) return;
  lastRead = millis();
  if (indoor.ok) readPair(INDOOR, bmeIn, indoor);
  if (outdoor.ok) readPair(OUTDOOR, bmeOut, outdoor);
  drawPair(INDOOR, "INDOOR", indoor);
  drawPair(OUTDOOR, "OUTDOOR", outdoor);
  printReading("indoor", indoor);
  Serial.print(F("  "));
  printReading("outdoor", outdoor);
  Serial.println();
}

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • Both screens show the same numbers, or one stays blank This project
    Each sensor and screen must be wired to the same channel pair. Indoor goes on SD0 and SC0, outdoor on SD1 and SC1.
  • The Serial Monitor says No TCA9548A at 0x70 This project
    Check the multiplexer's SDA, SCL, VIN and GND wires to the Uno.
  • Temperatures look wrong in Celsius or Fahrenheit This project
    TEMP_IN_F changes only the screens to Fahrenheit. The Serial Monitor always prints Celsius.
  • A second sensor with the same address still does not respond, or both answer together. I2C multiplexer
    Select one channel by writing a single byte to the multiplexer, with bit n set for channel n (1 << n). Talk to the sensor, then change channels. Only enable one channel at a time.
  • The multiplexer itself is not found. I2C multiplexer
    Its address is 0x70 plus the A0 to A2 bits, so 0x70 to 0x77. Tie A0, A1, A2 to GND or VCC, tie RESET high, and run an I2C scanner.
  • The sketch cannot find the sensor, or reads zeros. Pressure
    The I2C address is 0x76 when SDO is tied to GND and 0x77 when tied to VCC. Run an I2C scanner, and make sure the library address matches. Wire SDA and SCL correctly.
  • Humidity always reads zero or looks wrong. Pressure
    A BMP280 looks the same but has no humidity sensor. Check the chip ID register: 0x60 is a BME280 and 0x58 is a BMP280. Use the right library for yours.
  • The OLED stays blank. OLED display
    Check SDA and SCL are not swapped, and power it with 3.3 V or 5 V as its board allows. Most use address 0x3C, some 0x3D. Call display.begin() with the matching address.

Questions and answers

Why do I need a TCA9548A for two BME280s or two OLEDs?

Every SSD1306 module answers at 0x3C (some at 0x3D), and the BME280 at 0x76 or 0x77. Two at one address answer every byte together. The TCA9548A at 0x70 joins one channel at a time to SDA and SCL, so each pair gets a bus of its own.

Can I skip the multiplexer for just two BME280s?

Yes. A BME280 breakout moves to 0x77 with its SDO pin, so two can share a bus. The OLEDs are what need the multiplexer here, and it takes up to eight of each.

How does one display object drive two screens?

The 128x64 frame is 1 KB of the Uno's 2 KB of RAM, so the sketch keeps one and redraws it for each screen: select the channel, clear, draw, display(). Each screen still gets its own begin() with its channel selected.

How many screens or sensors can I add?

Eight channels on one TCA9548A, and eight TCA9548As (0x70 to 0x77, set with A0 to A2) on one bus. On an Uno, RAM and flash run out before the channels do.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.