Learning Goals 5 min
Cluster D ends with both protocols on the same board: an OLED on I²C showing numbers and labels, a 74HC595 + LED bar on SPI showing a quick at-a-glance level. Two protocols, one project, no pin conflicts. By the end of this lesson you will:
- Wire an SSD1306 OLED (I²C on A4/A5) and a 74HC595 + 8 LED bar (SPI on D11/D13 + D10 latch) to the same UNO, sharing nothing but power and ground.
- Build a finished "dashboard" sketch where one sensor (your choice — pot, LDR, temperature) drives both displays simultaneously: precise number on the OLED, coarse bar on the LEDs.
- Add a second sensor / second visualisation on the same dashboard, demonstrating that you can run multiple peripherals on multiple buses without them interfering with each other.
Warm-Up 10 min
Time to combine the last two lessons' rigs on a single breadboard. Lay out:
- OLED — A4 (SDA), A5 (SCL), 5V, GND.
- 74HC595 — D11 (data), D13 (clock), D10 (latch), 5V, GND, plus 8 LEDs with 220 Ω resistors.
- One sensor on A0 (a pot to start; swap for an LDR or TMP36 later).
Pin map sanity check
Three separate "territories" on the Arduino:
| Pins | Purpose |
|---|---|
| A4, A5 | I²C bus → OLED |
| D11, D12, D13 | SPI bus → 74HC595 (we don't use CIPO/D12 here, but it's reserved by the SPI peripheral) |
| D10 | 74HC595 latch (acts as "CS" even though the chip doesn't have a real CS pin) |
| A0 | Sensor input |
Nothing overlaps. Once you've wired the build, run yesterday's I²C scanner — confirm the OLED still appears at 0x3C. Then load yesterday's shift-register counter and confirm the LEDs still cycle. Both must work in isolation before you combine the code.
New Concept · Two buses, one loop 20 min
Why this works — the two protocols don't share resources
I²C lives on A4 / A5 with the Wire peripheral. SPI lives on D11–D13 with the SPI peripheral. They're completely separate hardware blocks inside the AVR. You can "SPI.transfer()" and "Wire.write()" in the same loop without any worry about collisions.
The dashboard pattern
One reading drives multiple outputs. We'll structure the sketch as:
- Read the sensor (or sensors).
- Compute derived values (smoothed value, bar level, threshold flags).
- Drive each output in turn: OLED redraw, then LED bar update.
- Sleep for the frame interval (~50 ms = 20 fps is smooth and gentle).
What goes on each display
| Output | What it shows | Why it's the right tool |
|---|---|---|
| OLED | Exact number, units, header label, second-line trend | Precision, text. Slow to read but rich. |
| LED bar | Coarse level (0–8 LEDs lit) | Glanceable, visible across a room. No detail. |
This is exactly how dashboards in cars, aeroplanes and industrial control rooms are designed: precise gauges for the operator to read; warning lights and bars for "at-a-glance". We're building a tiny version of the same idea.
Avoiding flicker
The OLED is the slower output — its display() push takes ~20 ms. The LED bar is essentially instant. Update the OLED last so the flicker (if any) is at the slow output, not on the LEDs. In practice with a 50 ms loop, neither flickers visibly.
Worked Example · The full dashboard 30 min
Step 1 — wire everything as in §1
OLED on I²C (A4/A5), 74HC595 + 8 LEDs on SPI (D10/D11/D13), pot wiper on A0. Common 5V and GND between all devices.
Step 2 — the sketch
// L03-22 · Multi-display dashboard
// Sensor on A0 (pot/LDR/TMP36) -> OLED (precise number) + 8-LED bar (coarse level)
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int SENSOR_PIN = A0;
const int LATCH_PIN = 10;
const int SCREEN_W = 128;
const int SCREEN_H = 64;
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, -1);
// --- smoothing ---
const int WINDOW = 8;
int buf[WINDOW];
int bufIndex = 0;
long bufSum = 0;
int smoothed(int newValue) {
bufSum -= buf[bufIndex];
buf[bufIndex] = newValue;
bufSum += newValue;
bufIndex = (bufIndex + 1) % WINDOW;
return bufSum / WINDOW;
}
void writeBar(byte pattern) {
digitalWrite(LATCH_PIN, LOW);
SPI.transfer(pattern);
digitalWrite(LATCH_PIN, HIGH);
}
byte barPattern(int lit) {
byte p = 0;
for (int i = 0; i < lit; i++) p |= (1 << i);
return p;
}
void setup() {
Serial.begin(9600);
pinMode(LATCH_PIN, OUTPUT);
SPI.begin();
Wire.begin();
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("# OLED begin failed");
while (true) ;
}
display.setTextColor(SSD1306_WHITE);
// pre-fill smoothing buffer
int first = analogRead(SENSOR_PIN);
for (int i = 0; i < WINDOW; i++) buf[i] = first;
bufSum = (long)first * WINDOW;
writeBar(0);
}
void loop() {
int raw = analogRead(SENSOR_PIN);
int value = smoothed(raw);
int leds = map(value, 0, 1023, 0, 8); // 0..8 LEDs lit
int pct = map(value, 0, 1023, 0, 100); // 0..100%
// 1. LED bar (instant)
writeBar(barPattern(leds));
// 2. OLED (~20 ms)
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print("LEVEL");
display.setTextSize(3);
display.setCursor(0, 14);
display.print(pct);
display.print("%");
display.drawRect(0, 50, 128, 12, SSD1306_WHITE);
display.fillRect(2, 52, map(value, 0, 1023, 0, 124), 8, SSD1306_WHITE);
display.display();
delay(50);
}Step 3 — upload and test
Turn the pot. The OLED's number changes smoothly from 0% to 100%, the bottom bar fills up, and the LEDs light up 0–8 segments in step. All three respond to the same sensor reading. No flicker, no lag.
Step 4 — add a second sensor
Wire a second pot (or an LDR voltage divider) to A1. Read it as a "trend" reading and display the comparison:
int trendRaw = analogRead(A1);
int trend = map(trendRaw, 0, 1023, 0, 100);
int diff = pct - trend;
const char* dir = diff > 5 ? "^" : diff < -5 ? "v" : "-";
// Add to the OLED block:
display.setTextSize(1);
display.setCursor(0, 42);
display.print("trend ");
display.print(trend);
display.print("% ");
display.print(dir);Now you have a two-sensor dashboard: the LEDs show one reading at a glance, the OLED shows both with a trend arrow comparing them.
Step 5 — add a danger flag
Light a 9th LED (Q7 of the shift register, already wired) only when the value is above a threshold:
byte pattern = barPattern(leds);
if (pct > 80) pattern |= 0b10000000; // also light bit 7 (top LED) for danger
writeBar(pattern);That dedicates one LED as the "warning light" — visually distinct from the level bar even though they share the same chip. Standard dashboard idiom.
Step 6 — speed comparison
Time one full loop iteration:
unsigned long t0 = micros();
// ... entire loop body ...
unsigned long elapsed = micros() - t0;
Serial.print("loop took ");
Serial.print(elapsed);
Serial.println(" us");Typical: ~20–30 ms per iteration, dominated by the OLED's display() push. The SPI bar write takes microseconds. This is why you spread work across the loop — OLED can't refresh faster than ~50 fps, but the LEDs can refresh thousands of times per second.
Basic 5 min
Goal: Replace the pot with a TMP36 temperature sensor (L02-13 wiring). Change the OLED label to "TEMP" and the unit to "°C". Adjust the map ranges for sensible thresholds (say 15 °C to 35 °C → 0 to 100% of dashboard scale).
Challenge 1 5 min
Goal: Add a button on D2 that switches between two display modes: "LEVEL" (current value, current bar) and "HISTORY" (last 30 seconds' min and max printed on the OLED, bar still shows current).
Challenge 2 5 min
Goal: Cascade a second 74HC595 (16 LEDs total) and display the value with twice the resolution on the bar (0–16 LEDs instead of 0–8). OLED unchanged.
Challenge 3 · Fix the two-display dashboard 10 min
A classmate shortened the Worked Example. Their 74HC595 latch wire goes to the pin in LATCH_PIN. It uploads, but both displays misbehave.
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const int SENSOR_PIN = A0;
const int LATCH_PIN = 13;
const int SCREEN_W = 128;
const int SCREEN_H = 64;
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, -1);
void writeBar(byte pattern) {
digitalWrite(LATCH_PIN, LOW);
SPI.transfer(pattern);
digitalWrite(LATCH_PIN, HIGH);
}
byte barPattern(int lit) {
byte p = 0;
for (int i = 0; i <= lit; i++) {
p |= (1 << i);
}
return p;
}
void setup() {
pinMode(LATCH_PIN, OUTPUT);
SPI.begin();
Wire.begin();
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.setTextColor(SSD1306_WHITE);
writeBar(0);
}
void loop() {
int value = analogRead(SENSOR_PIN);
int leds = map(value, 0, 1023, 0, 8);
int pct = map(value, 0, 1023, 0, 100);
byte pattern = barPattern(leds);
if (pct > 80) {
pattern = 0b10000000;
}
writeBar(pattern);
display.clearDisplay();
display.setTextSize(3);
display.setCursor(0, 14);
display.print(pct);
display.print("%");
delay(50);
}Match each symptom to its cause and write the fix:
- Symptom A: the OLED never shows the number.
- Symptom B: the LED bar shows random patterns and flickers.
- Symptom C: with the pot at zero, one LED is still lit.
- Symptom D: above 80%, the whole bar vanishes and only the top LED stays on.
It works if the OLED shows 0% to 100% and the bar lights 0 to 8 LEDs. Above 80% the top LED must stay on with the rest of the bar.
Recap 5 min
Multi-protocol projects work because I²C and SPI live on entirely separate hardware peripherals — same loop, same sketch, no conflict. The dashboard pattern: one sensor reading, multiple display channels (precise OLED + glanceable LEDs). Update the slow display last so any flicker hides on the slow channel. Cluster D recap: UART for slow point-to-point, I²C for many slow devices, SPI for a few fast devices, shift registers for cheap pin expansion. Next cluster: putting wires away and going wireless with Bluetooth.
- Multi-bus project
- A project that uses two or more communication protocols on the same microcontroller. Common combination: I²C for sensors + SPI for displays + UART for a radio.
- Dashboard pattern
- One input feeding multiple outputs designed for different reading distances and reading speeds. Big-picture (LEDs) + detail (numbers) + warning (single bright indicator).
- At-a-glance vs precise
- The two reading modes a dashboard supports. At-a-glance = coarse, fast, no thinking; precise = exact value, slower, needs focus. Good dashboards have both.
- Warning light
- A dedicated indicator that lights only when a threshold is crossed. Stays off most of the time, draws the eye when active. Universal idiom.
- Splash screen
- A short branded display shown at power-up before the live UI. Confirms the device booted and gives a moment for hardware to stabilise. Standard polish for a "ship-ready" product.
- Slow output / fast output
- Different output channels run at different update rates. OLED at 20 fps; LEDs at 200+ fps. Design the loop so the slow output happens last and doesn't block the fast one.
- Sensor smoothing
- Running average (or low-pass filter) applied to raw analog readings so the display isn't twitchy. Same pattern from L02-08 and L03-04 — wins again here.
Extra Mission 5 min
Part 1 — Design a dashboard for a real place
Pick a place that needs a dashboard. Is the classroom too warm? Is the reading corner bright enough? Is the garden soil dry? Design the finished box on paper.
Your design must include:
- What is measured, by which sensor, in which units.
- What the OLED shows: a drawing with the label, the number and the units.
- What the LED bar means, with a printed scale such as 0, 25, 50, 75 and 100.
- The warning threshold, and which LED lights for it.
- A drawing of the box: OLED at the top, LED bar below, sensor sticking out.
Part 2 — Make it
Build your dashboard with the OLED on I²C, the 74HC595 bar on SPI and your sensor. Set the map ranges for your sensor's real units. Add the warning LED and a 2-second start-up splash with the project name.
It works if the OLED and the bar always agree. The warning LED must light exactly at your threshold.
Bring back next class: your design, your uploaded sketch and photos at a low, middle and high reading. Also bring an HC-05 module and a phone with a Bluetooth serial terminal app for L03-23.