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.
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.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × I2C multiplexer, TCA9548AIn Weather Station Kit.
- 2 × Pressure, humidity and temperature, BME280In Weather Station Kit.
- 2 × OLED display, 128x64In Fight Stick Kit, Pocket Pet Kit, Slot Machine Kit.
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 pin | Connects to |
|---|---|
| I2C multiplexer, TCA9548A pin SCL | Arduino Uno R3 pin SCL |
| I2C multiplexer, TCA9548A pin SDA | Arduino Uno R3 pin SDA |
| I2C multiplexer, TCA9548A pin VIN | Arduino Uno R3 pin 5V |
| Pressure, humidity and temperature, BME280 (1) pin VIN | Arduino Uno R3 pin 5V |
| OLED display, 128x64 (1) pin VCC | Arduino Uno R3 pin 5V |
| Pressure, humidity and temperature, BME280 (2) pin VIN | Arduino Uno R3 pin 5V |
| OLED display, 128x64 (2) pin VCC | Arduino Uno R3 pin 5V |
| I2C multiplexer, TCA9548A pin GND | Arduino Uno R3 pin GND |
| Pressure, humidity and temperature, BME280 (1) pin GND | Arduino Uno R3 pin GND |
| OLED display, 128x64 (1) pin GND | Arduino Uno R3 pin GND |
| Pressure, humidity and temperature, BME280 (2) pin GND | Arduino Uno R3 pin GND |
| OLED display, 128x64 (2) pin GND | Arduino Uno R3 pin GND |
| Pressure, humidity and temperature, BME280 (1) pin SDA | I2C multiplexer, TCA9548A pin SD0 |
| OLED display, 128x64 (1) pin SDA | I2C multiplexer, TCA9548A pin SD0 |
| Pressure, humidity and temperature, BME280 (1) pin SCL | I2C multiplexer, TCA9548A pin SC0 |
| OLED display, 128x64 (1) pin SCL | I2C multiplexer, TCA9548A pin SC0 |
| Pressure, humidity and temperature, BME280 (2) pin SDA | I2C multiplexer, TCA9548A pin SD1 |
| OLED display, 128x64 (2) pin SDA | I2C multiplexer, TCA9548A pin SD1 |
| Pressure, humidity and temperature, BME280 (2) pin SCL | I2C multiplexer, TCA9548A pin SC1 |
| OLED display, 128x64 (2) pin SCL | I2C 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.
// 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 editorloop 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_Fchanges 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.